openvibers/openvibe.shared Commits
Avatars show the account's current picture (the Network's /avatar/<username> when a record has none or a picture fails), and the launcher panel fits a phone (#106)
Changes
@@@ -4,7 +4,19 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semvebreaking change to any exported module, browser global or served file name is a new major.A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).--## 4.12.0 — 2026-10-26++## 4.12.0 — 2026-10-25++++- **Avatars show the picture the account has now.** The owner, 2026-10-25: their picture showed on Live but the++ navbar on openvibe.network drew their initial. A user record can be older than its picture (a site's session, or a++ remembered account, from before the picture was set).++ - The navbar (`navbar.js`) and the footer's account row (`footer.js`) ask the Network's `/avatar/<username>` when a++ record has no openvibe.media picture. That route redirects to the current picture, or answers an initial.++ - A picture that fails to load asks `/avatar/<username>` once (`data-retry-src`) before falling back to the initial.++ That also covers an image that failed before the handler was attached.++ - On openvibe.network itself the request is same-origin.++- **The launcher fits a phone.** Its panel was `min(760px, 100vw - 24px)` wide plus its padding and border, so on a page++ without a global `box-sizing: border-box` it ran 10 px past the screen and the page scrolled sideways. It is now++ `border-box`.- **Every theme in light and dark.** The owner, 2026-10-25: "each theme should have a light counterpart instead of … amixed bag", with an easy light and dark switch in the account menu.@@@ -291,8 +291,10 @@function accountHTML(user, service) {const name = esc(user.display_name || user.username || 'you');// An openvibe.media object picture comes in sizes (Media's /o/:id?w=&fmt=): 64 px as WebP for the 24 px avatar.-- const sized = /^https:\/\/openvibe\.media\/o\/med_[0-9A-Za-z]{10,40}$/.test(user.avatar_url || '') ? `${user.avatar_url}?w=64&fmt=webp` : user.avatar_url;-- const avatar = user.avatar_url++ // Without one in the record, the Network's /avatar/<username>: the picture the account has now (navbar.js, the same rule).++ const media = /^https:\/\/openvibe\.media\/o\/med_[0-9A-Za-z]{10,40}$/.test(user.avatar_url || '');++ const sized = media ? `${user.avatar_url}?w=64&fmt=webp` : (user.username && !user.is_anon ? `${NETWORK_URL}/avatar/${encodeURIComponent(user.username)}?s=32` : '');++ const avatar = sized? `<img src="${esc(sized)}" alt="">`: `<span>${esc(String(name).charAt(0).toUpperCase())}</span>`;const quick = [@@@ -104,7 +104,7 @@.ovnav-drawer-net a.ovnav-net { display: flex !important; }@media (max-width: 860px) { .openvibe-navbar .ovnav-burger { display: inline-flex; } .openvibe-navbar .openvibe-navbar-links { display: none; } .openvibe-navbar .ovnav-chip .ovnav-ic + .ovnav-chip-v:empty { display: none; } }@media (max-width: 420px) { .openvibe-navbar .ovnav-chip { padding: 0 8px; height: 30px; font-size: 12px; } }-- .ovnav-launcher { position: absolute; top: calc(100% + 6px); left: 12px; width: min(760px, calc(100vw - 24px)); background: var(--bg-elevated, var(--bg-secondary, #111826)); border: 1px solid var(--border, rgba(255,255,255,.12)); border-radius: 16px; box-shadow: 0 24px 60px rgba(0,0,0,.5); padding: 0 12px 12px; z-index: 1000; opacity: 0; transform: translateY(-6px) scale(.98); transform-origin: top left; pointer-events: none; transition: opacity .16s, transform .2s cubic-bezier(.2,1.2,.3,1); }++ .ovnav-launcher { position: absolute; top: calc(100% + 6px); left: 12px; box-sizing: border-box; width: min(760px, calc(100vw - 24px)); background: var(--bg-elevated, var(--bg-secondary, #111826)); border: 1px solid var(--border, rgba(255,255,255,.12)); border-radius: 16px; box-shadow: 0 24px 60px rgba(0,0,0,.5); padding: 0 12px 12px; z-index: 1000; opacity: 0; transform: translateY(-6px) scale(.98); transform-origin: top left; pointer-events: none; transition: opacity .16s, transform .2s cubic-bezier(.2,1.2,.3,1); }.ovnav-launcher.open { opacity: 1; transform: none; pointer-events: auto; }.ovnav-launcher .ovl-top { position: sticky; top: 0; z-index: 1; margin: 0 -12px; padding: 12px 12px 2px; background: inherit; }.ovnav-launcher .ovl-q { width: 100%; box-sizing: border-box; padding: 9px 12px; border-radius: 10px; border: 1px solid var(--border, rgba(255,255,255,.12)); background: var(--bg-primary, #0a0f18); color: var(--text-primary, #e6edf7); font: 500 14px/1.2 inherit; outline: none; }@@@ -1350,26 +1350,42 @@return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg.replace(/\s+/g, ' ').trim())}`;}++ // The account's picture now (the Network's /avatar/<username>): a record can predate its picture (CHANGELOG 4.12.0).++ function currentAvatarUrl(user, size) {++ if (!user || user.is_anon || user.id === 'anon' || !user.username) return null;++ const network = /(^|\.)openvibe\.network$/.test(currentHost()) ? '' : 'https://openvibe.network';++ return `${network}/avatar/${encodeURIComponent(user.username)}?s=${size}`;++ }++function avatarSrc(user, size = 64) {const url = user?.avatar_url;-- return /^https:\/\/openvibe\.media\/o\/med_\w+$/.test(url || '') ? `${url}?w=${size}&fmt=webp` : url || makeAvatarPlaceholder(user, size);++ if (/^https:\/\/openvibe\.media\/o\/med_\w+$/.test(url || '')) return `${url}?w=${size}&fmt=webp`;++ if (/^https:\/\/openvibe\.media\//.test(url || '')) return url;++ return currentAvatarUrl(user, size) || makeAvatarPlaceholder(user, size);}function avatarImg(user, size = 64, className = 'openvibe-navbar-avatar', id = '') {const fallback = makeAvatarPlaceholder(user, size);++ const src = avatarSrc(user, size);++ const current = currentAvatarUrl(user, size);++ const retry = current && current !== src ? ` data-retry-src="${escapeAttr(current)}"` : '';const idAttr = id ? ` id="${escapeAttr(id)}"` : '';const alt = escapeAttr(user?.display_name || user?.username || 'Avatar');-- return `<img class="${escapeAttr(className)}" src="${escapeAttr(avatarSrc(user, size))}" data-fallback-src="${escapeAttr(fallback)}" alt="${alt}"${idAttr}>`;++ return `<img class="${escapeAttr(className)}" src="${escapeAttr(src)}"${retry} data-fallback-src="${escapeAttr(fallback)}" alt="${alt}"${idAttr}>`;}++ // A picture that does not load: the current one once (data-retry-src), then the initial.function attachAvatarFallbacks(rootEl) {rootEl?.querySelectorAll('img[data-fallback-src]').forEach((img) => {-- img.addEventListener('error', () => {++ const onError = () => {++ const retry = img.dataset.retrySrc;++ if (retry) { delete img.dataset.retrySrc; img.src = retry; return; }++ img.removeEventListener('error', onError);const fallback = img.dataset.fallbackSrc;-- if (fallback && img.src !== fallback) {-- img.src = fallback;-- }-- }, { once: true });++ if (fallback && img.src !== fallback) img.src = fallback;++ };++ img.addEventListener('error', onError);++ if (img.complete && img.naturalWidth === 0 && img.getAttribute('src')) onError();});}@@@ -166,6 +166,9 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,for (const [avatar, want] of [['https://openvibe.media/o/med_01KZK5GRQ088Y3CW1TMEF4Z4RN', 'src="https://openvibe.media/o/med_01KZK5GRQ088Y3CW1TMEF4Z4RN?w=64&fmt=webp"'],['https://openvibe.media/f/b.png', 'src="https://openvibe.media/f/b.png"'],++ // No picture in the record (or one that is not on openvibe.media): the account's current one, from the Network.++ [null, 'src="https://openvibe.network/avatar/vibe?s=64"'],++ ['https://example.com/me.png', 'src="https://openvibe.network/avatar/vibe?s=64"'],]) {const { navbar } = loadNavbar({hostname: 'net.openvibe.tools', href: 'https://net.openvibe.tools/', cookie: 'ov_token=valid-jwt',@@@ -176,8 +179,29 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,assert.ok(navbar.getElement().innerHTML.includes(want), `${avatar}: ${want}`);}-- // The footer's 24 px signed-in avatar asks for 64 px WebP of an object picture too.-- assert.match(require('fs').readFileSync(require('path').join(__dirname, '..', 'footer.js'), 'utf8'), /\? `\$\{user\.avatar_url\}\?w=64&fmt=webp` : user\.avatar_url;/, 'the footer sizes an object avatar');++ // A picture that fails to load asks the Network for the current one once, then shows the initial.++ {++ const { navbar } = loadNavbar({++ hostname: 'net.openvibe.tools', href: 'https://net.openvibe.tools/', cookie: 'ov_token=valid-jwt',++ fetch: async (url) => (url === 'https://openvibe.network/api/auth/me' ? jsonResponse(200, { user: { ...USER, avatar_url: 'https://openvibe.media/o/med_01KZK5GRQ088Y3CW1TMEF4Z4RN' }, preferences: {} }) : jsonResponse(404, {})),++ });++ navbar.init({ service: 'net', apiBase: 'https://openvibe.network', token: 'valid-jwt' });++ await navbar.refreshAuth();++ assert.ok(navbar.getElement().innerHTML.includes('data-retry-src="https://openvibe.network/avatar/vibe?s=64"'), 'a media picture carries the current-picture retry');++ }++ // On the Network itself the current picture is same-origin.++ {++ const { navbar } = loadNavbar({ hostname: 'openvibe.network', href: 'https://openvibe.network/updates', cookie: 'ov_token=valid-jwt',++ fetch: async (url) => (/\/api\/auth\/me$/.test(url) ? jsonResponse(200, { user: USER, preferences: {} }) : jsonResponse(404, {})) });++ navbar.init({ service: 'network', apiBase: 'https://openvibe.network', token: 'valid-jwt' });++ await navbar.refreshAuth();++ assert.ok(navbar.getElement().innerHTML.includes('src="/avatar/vibe?s=64"'), 'same-origin /avatar on openvibe.network');++ }++++ // The footer's 24 px signed-in avatar asks for 64 px WebP of an object picture, and the current picture without one.++ const footerSrc = require('fs').readFileSync(require('path').join(__dirname, '..', 'footer.js'), 'utf8');++ assert.match(footerSrc, /media \? `\$\{user\.avatar_url\}\?w=64&fmt=webp`/, 'the footer sizes an object avatar');++ assert.match(footerSrc, /\$\{NETWORK_URL\}\/avatar\/\$\{encodeURIComponent\(user\.username\)\}/, 'the footer asks for the current picture');// ── 6. No token, no sessionUrl → signed out, zero fetches ─{
@@ -4,7 +4,19 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
@@ -4,7 +4,19 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
-## 4.12.0 — 2026-10-26
+## 4.12.0 — 2026-10-25
+
+- **Avatars show the picture the account has now.** The owner, 2026-10-25: their picture showed on Live but the
+ navbar on openvibe.network drew their initial. A user record can be older than its picture (a site's session, or a
+ remembered account, from before the picture was set).
+ - The navbar (`navbar.js`) and the footer's account row (`footer.js`) ask the Network's `/avatar/<username>` when a
+ record has no openvibe.media picture. That route redirects to the current picture, or answers an initial.
+ - A picture that fails to load asks `/avatar/<username>` once (`data-retry-src`) before falling back to the initial.
+ That also covers an image that failed before the handler was attached.
+ - On openvibe.network itself the request is same-origin.
+- **The launcher fits a phone.** Its panel was `min(760px, 100vw - 24px)` wide plus its padding and border, so on a page
+ without a global `box-sizing: border-box` it ran 10 px past the screen and the page scrolled sideways. It is now
+ `border-box`.
- **Every theme in light and dark.** The owner, 2026-10-25: "each theme should have a light counterpart instead of … a
- **Every theme in light and dark.** The owner, 2026-10-25: "each theme should have a light counterpart instead of … a
mixed bag", with an easy light and dark switch in the account menu.
mixed bag", with an easy light and dark switch in the account menu.
@@ -291,8 +291,10 @@
@@ -291,8 +291,10 @@
function accountHTML(user, service) {
function accountHTML(user, service) {
const name = esc(user.display_name || user.username || 'you');
const name = esc(user.display_name || user.username || 'you');
// An openvibe.media object picture comes in sizes (Media's /o/:id?w=&fmt=): 64 px as WebP for the 24 px avatar.
// An openvibe.media object picture comes in sizes (Media's /o/:id?w=&fmt=): 64 px as WebP for the 24 px avatar.
- const sized = /^https:\/\/openvibe\.media\/o\/med_[0-9A-Za-z]{10,40}$/.test(user.avatar_url || '') ? `${user.avatar_url}?w=64&fmt=webp` : user.avatar_url;
- const avatar = user.avatar_url
+ // Without one in the record, the Network's /avatar/<username>: the picture the account has now (navbar.js, the same rule).
+ const media = /^https:\/\/openvibe\.media\/o\/med_[0-9A-Za-z]{10,40}$/.test(user.avatar_url || '');
+ const sized = media ? `${user.avatar_url}?w=64&fmt=webp` : (user.username && !user.is_anon ? `${NETWORK_URL}/avatar/${encodeURIComponent(user.username)}?s=32` : '');
+ const avatar = sized
? `<img src="${esc(sized)}" alt="">`
? `<img src="${esc(sized)}" alt="">`
: `<span>${esc(String(name).charAt(0).toUpperCase())}</span>`;
: `<span>${esc(String(name).charAt(0).toUpperCase())}</span>`;
const quick = [
const quick = [
@@ -104,7 +104,7 @@
@@ -104,7 +104,7 @@
.ovnav-drawer-net a.ovnav-net { display: flex !important; }
.ovnav-drawer-net a.ovnav-net { display: flex !important; }
@media (max-width: 860px) { .openvibe-navbar .ovnav-burger { display: inline-flex; } .openvibe-navbar .openvibe-navbar-links { display: none; } .openvibe-navbar .ovnav-chip .ovnav-ic + .ovnav-chip-v:empty { display: none; } }
@media (max-width: 860px) { .openvibe-navbar .ovnav-burger { display: inline-flex; } .openvibe-navbar .openvibe-navbar-links { display: none; } .openvibe-navbar .ovnav-chip .ovnav-ic + .ovnav-chip-v:empty { display: none; } }
@media (max-width: 420px) { .openvibe-navbar .ovnav-chip { padding: 0 8px; height: 30px; font-size: 12px; } }
@media (max-width: 420px) { .openvibe-navbar .ovnav-chip { padding: 0 8px; height: 30px; font-size: 12px; } }
- .ovnav-launcher { position: absolute; top: calc(100% + 6px); left: 12px; width: min(760px, calc(100vw - 24px)); background: var(--bg-elevated, var(--bg-secondary, #111826)); border: 1px solid var(--border, rgba(255,255,255,.12)); border-radius: 16px; box-shadow: 0 24px 60px rgba(0,0,0,.5); padding: 0 12px 12px; z-index: 1000; opacity: 0; transform: translateY(-6px) scale(.98); transform-origin: top left; pointer-events: none; transition: opacity .16s, transform .2s cubic-bezier(.2,1.2,.3,1); }
+ .ovnav-launcher { position: absolute; top: calc(100% + 6px); left: 12px; box-sizing: border-box; width: min(760px, calc(100vw - 24px)); background: var(--bg-elevated, var(--bg-secondary, #111826)); border: 1px solid var(--border, rgba(255,255,255,.12)); border-radius: 16px; box-shadow: 0 24px 60px rgba(0,0,0,.5); padding: 0 12px 12px; z-index: 1000; opacity: 0; transform: translateY(-6px) scale(.98); transform-origin: top left; pointer-events: none; transition: opacity .16s, transform .2s cubic-bezier(.2,1.2,.3,1); }
.ovnav-launcher.open { opacity: 1; transform: none; pointer-events: auto; }
.ovnav-launcher.open { opacity: 1; transform: none; pointer-events: auto; }
.ovnav-launcher .ovl-top { position: sticky; top: 0; z-index: 1; margin: 0 -12px; padding: 12px 12px 2px; background: inherit; }
.ovnav-launcher .ovl-top { position: sticky; top: 0; z-index: 1; margin: 0 -12px; padding: 12px 12px 2px; background: inherit; }
.ovnav-launcher .ovl-q { width: 100%; box-sizing: border-box; padding: 9px 12px; border-radius: 10px; border: 1px solid var(--border, rgba(255,255,255,.12)); background: var(--bg-primary, #0a0f18); color: var(--text-primary, #e6edf7); font: 500 14px/1.2 inherit; outline: none; }
.ovnav-launcher .ovl-q { width: 100%; box-sizing: border-box; padding: 9px 12px; border-radius: 10px; border: 1px solid var(--border, rgba(255,255,255,.12)); background: var(--bg-primary, #0a0f18); color: var(--text-primary, #e6edf7); font: 500 14px/1.2 inherit; outline: none; }
@@ -1350,26 +1350,42 @@
@@ -1350,26 +1350,42 @@
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg.replace(/\s+/g, ' ').trim())}`;
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg.replace(/\s+/g, ' ').trim())}`;
}
}
+ // The account's picture now (the Network's /avatar/<username>): a record can predate its picture (CHANGELOG 4.12.0).
+ function currentAvatarUrl(user, size) {
+ if (!user || user.is_anon || user.id === 'anon' || !user.username) return null;
+ const network = /(^|\.)openvibe\.network$/.test(currentHost()) ? '' : 'https://openvibe.network';
+ return `${network}/avatar/${encodeURIComponent(user.username)}?s=${size}`;
+ }
+
function avatarSrc(user, size = 64) {
function avatarSrc(user, size = 64) {
const url = user?.avatar_url;
const url = user?.avatar_url;
- return /^https:\/\/openvibe\.media\/o\/med_\w+$/.test(url || '') ? `${url}?w=${size}&fmt=webp` : url || makeAvatarPlaceholder(user, size);
+ if (/^https:\/\/openvibe\.media\/o\/med_\w+$/.test(url || '')) return `${url}?w=${size}&fmt=webp`;
+ if (/^https:\/\/openvibe\.media\//.test(url || '')) return url;
+ return currentAvatarUrl(user, size) || makeAvatarPlaceholder(user, size);
}
}
function avatarImg(user, size = 64, className = 'openvibe-navbar-avatar', id = '') {
function avatarImg(user, size = 64, className = 'openvibe-navbar-avatar', id = '') {
const fallback = makeAvatarPlaceholder(user, size);
const fallback = makeAvatarPlaceholder(user, size);
+ const src = avatarSrc(user, size);
+ const current = currentAvatarUrl(user, size);
+ const retry = current && current !== src ? ` data-retry-src="${escapeAttr(current)}"` : '';
const idAttr = id ? ` id="${escapeAttr(id)}"` : '';
const idAttr = id ? ` id="${escapeAttr(id)}"` : '';
const alt = escapeAttr(user?.display_name || user?.username || 'Avatar');
const alt = escapeAttr(user?.display_name || user?.username || 'Avatar');
- return `<img class="${escapeAttr(className)}" src="${escapeAttr(avatarSrc(user, size))}" data-fallback-src="${escapeAttr(fallback)}" alt="${alt}"${idAttr}>`;
+ return `<img class="${escapeAttr(className)}" src="${escapeAttr(src)}"${retry} data-fallback-src="${escapeAttr(fallback)}" alt="${alt}"${idAttr}>`;
}
}
+ // A picture that does not load: the current one once (data-retry-src), then the initial.
function attachAvatarFallbacks(rootEl) {
function attachAvatarFallbacks(rootEl) {
rootEl?.querySelectorAll('img[data-fallback-src]').forEach((img) => {
rootEl?.querySelectorAll('img[data-fallback-src]').forEach((img) => {
- img.addEventListener('error', () => {
+ const onError = () => {
+ const retry = img.dataset.retrySrc;
+ if (retry) { delete img.dataset.retrySrc; img.src = retry; return; }
+ img.removeEventListener('error', onError);
const fallback = img.dataset.fallbackSrc;
const fallback = img.dataset.fallbackSrc;
- if (fallback && img.src !== fallback) {
- img.src = fallback;
- }
- }, { once: true });
+ if (fallback && img.src !== fallback) img.src = fallback;
+ };
+ img.addEventListener('error', onError);
+ if (img.complete && img.naturalWidth === 0 && img.getAttribute('src')) onError();
});
});
}
}
@@ -166,6 +166,9 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,
@@ -166,6 +166,9 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,
for (const [avatar, want] of [
for (const [avatar, want] of [
['https://openvibe.media/o/med_01KZK5GRQ088Y3CW1TMEF4Z4RN', 'src="https://openvibe.media/o/med_01KZK5GRQ088Y3CW1TMEF4Z4RN?w=64&fmt=webp"'],
['https://openvibe.media/o/med_01KZK5GRQ088Y3CW1TMEF4Z4RN', 'src="https://openvibe.media/o/med_01KZK5GRQ088Y3CW1TMEF4Z4RN?w=64&fmt=webp"'],
['https://openvibe.media/f/b.png', 'src="https://openvibe.media/f/b.png"'],
['https://openvibe.media/f/b.png', 'src="https://openvibe.media/f/b.png"'],
+ // No picture in the record (or one that is not on openvibe.media): the account's current one, from the Network.
+ [null, 'src="https://openvibe.network/avatar/vibe?s=64"'],
+ ['https://example.com/me.png', 'src="https://openvibe.network/avatar/vibe?s=64"'],
]) {
]) {
const { navbar } = loadNavbar({
const { navbar } = loadNavbar({
hostname: 'net.openvibe.tools', href: 'https://net.openvibe.tools/', cookie: 'ov_token=valid-jwt',
hostname: 'net.openvibe.tools', href: 'https://net.openvibe.tools/', cookie: 'ov_token=valid-jwt',
@@ -176,8 +179,29 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,
@@ -176,8 +179,29 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,
assert.ok(navbar.getElement().innerHTML.includes(want), `${avatar}: ${want}`);
assert.ok(navbar.getElement().innerHTML.includes(want), `${avatar}: ${want}`);
}
}
- // The footer's 24 px signed-in avatar asks for 64 px WebP of an object picture too.
- assert.match(require('fs').readFileSync(require('path').join(__dirname, '..', 'footer.js'), 'utf8'), /\? `\$\{user\.avatar_url\}\?w=64&fmt=webp` : user\.avatar_url;/, 'the footer sizes an object avatar');
+ // A picture that fails to load asks the Network for the current one once, then shows the initial.
+ {
+ const { navbar } = loadNavbar({
+ hostname: 'net.openvibe.tools', href: 'https://net.openvibe.tools/', cookie: 'ov_token=valid-jwt',
+ fetch: async (url) => (url === 'https://openvibe.network/api/auth/me' ? jsonResponse(200, { user: { ...USER, avatar_url: 'https://openvibe.media/o/med_01KZK5GRQ088Y3CW1TMEF4Z4RN' }, preferences: {} }) : jsonResponse(404, {})),
+ });
+ navbar.init({ service: 'net', apiBase: 'https://openvibe.network', token: 'valid-jwt' });
+ await navbar.refreshAuth();
+ assert.ok(navbar.getElement().innerHTML.includes('data-retry-src="https://openvibe.network/avatar/vibe?s=64"'), 'a media picture carries the current-picture retry');
+ }
+ // On the Network itself the current picture is same-origin.
+ {
+ const { navbar } = loadNavbar({ hostname: 'openvibe.network', href: 'https://openvibe.network/updates', cookie: 'ov_token=valid-jwt',
+ fetch: async (url) => (/\/api\/auth\/me$/.test(url) ? jsonResponse(200, { user: USER, preferences: {} }) : jsonResponse(404, {})) });
+ navbar.init({ service: 'network', apiBase: 'https://openvibe.network', token: 'valid-jwt' });
+ await navbar.refreshAuth();
+ assert.ok(navbar.getElement().innerHTML.includes('src="/avatar/vibe?s=64"'), 'same-origin /avatar on openvibe.network');
+ }
+
+ // The footer's 24 px signed-in avatar asks for 64 px WebP of an object picture, and the current picture without one.
+ const footerSrc = require('fs').readFileSync(require('path').join(__dirname, '..', 'footer.js'), 'utf8');
+ assert.match(footerSrc, /media \? `\$\{user\.avatar_url\}\?w=64&fmt=webp`/, 'the footer sizes an object avatar');
+ assert.match(footerSrc, /\$\{NETWORK_URL\}\/avatar\/\$\{encodeURIComponent\(user\.username\)\}/, 'the footer asks for the current picture');
// ── 6. No token, no sessionUrl → signed out, zero fetches ─
// ── 6. No token, no sessionUrl → signed out, zero fetches ─
{
{