openvibers/openvibe.network Commits
Network: a profile shows the community badge its person wears before their name (the Workshop's network.badge) (#106)
Changes
@@@ -271,6 +271,9 @@ function renderPage(u, inv) {const fxClass = fx && NAME_FX_RE.test(String((fx.art || {}).token || '')) ? fx.art.token : null;const hat = worn('live.hat');const hatGlyph = hat && (hat.art || {}).emoji ? String(hat.art.emoji).slice(0, 8) : null;++ // A community badge they wear (the Workshop, network.badge): its reviewed image before their name.++ const badge = worn('network.badge');++ const badgeId = badge && /^med_[0-9A-HJKMNP-TV-Z]{26}$/.test(String((badge.art || {}).media_id || '')) ? badge.art.media_id : null;const color = COLOR_RE.test(String(u.profile_color || '')) ? u.profile_color : '#3b82f6';const since = monthOf(u.created_at);const bio = isPublic ? String(u.bio || '').trim().slice(0, 500) : '';@@@ -324,6 +327,7 @@ a{color:var(--accent)}.pf-avatar{width:112px;height:112px;border-radius:50%;object-fit:cover;border:4px solid var(--bg-secondary);box-shadow:0 0 0 2px var(--pf-color);background:var(--bg-card);flex:none}.pf-who{flex:1;min-width:200px;padding-top:56px}.pf-who h1{margin:0;font-size:clamp(1.5rem,4vw,2.1rem);line-height:1.15;overflow-wrap:anywhere}++.pf-badge{width:.85em;height:.85em;margin-right:.25em;vertical-align:-.08em;border-radius:6px;object-fit:contain}.pf-handle{margin:.2em 0 0;color:var(--text-secondary)}.pf-bio{margin:0;padding:0 24px 18px;color:var(--text-primary);line-height:1.6;white-space:pre-line;overflow-wrap:anywhere;max-width:72ch}.pf-stats{list-style:none;display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0 24px 22px}@@@ -369,7 +373,7 @@ ${require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links<div class="pf-banner" aria-hidden="true"></div><div class="pf-head"><div class="pf-pic"><img class="pf-avatar" src="${esc(avatarUrl(u.username))}" alt="" width="112" height="112">${hatGlyph ? `<span class="pf-hat" title="${esc(hat.name)}" aria-hidden="true">${esc(hatGlyph)}</span>` : ''}</div>--<div class="pf-who"><h1>${fxClass ? `<span class="ov-fx ${fxClass}" title="${esc(fx.name)}">${esc(name)}</span>` : esc(name)}</h1><p class="pf-handle">@${esc(u.username)}</p></div>++<div class="pf-who"><h1>${badgeId ? `<img class="pf-badge" src="https://openvibe.media/o/${badgeId}" alt="" title="${esc(badge.name)}" width="32" height="32">` : ''}${fxClass ? `<span class="ov-fx ${fxClass}" title="${esc(fx.name)}">${esc(name)}</span>` : esc(name)}</h1><p class="pf-handle">@${esc(u.username)}</p></div></div>${bio ? `<p class="pf-bio">${esc(bio)}</p>` : ''}${isPublic ? `<ul class="pf-stats">${stats}</ul>` : '<p class="pf-private">This profile is private.</p>'}@@@ -16,17 +16,19 @@ const publicCors = require('../server/public-cors');const A = 'usr_01JZ0000000000000000000AAA';const KINDS = [-- { id: 'live.name_effect', name: 'Name effect' }, { id: 'live.hat', name: 'Hat' }, { id: 'live.particle', name: 'Particle' },++ { id: 'live.name_effect', name: 'Name effect' }, { id: 'live.hat', name: 'Hat' }, { id: 'live.particle', name: 'Particle' }, { id: 'network.badge', name: 'Badge' },];const DEFS = {itd_01JZ00000000000000000001R1: { id: 'itd_01JZ00000000000000000001R1', kind: 'live.hat', name: 'Royal Crown', rarity: 'legendary', art: { emoji: '👑' } },itd_01JZ00000000000000000001R2: { id: 'itd_01JZ00000000000000000001R2', kind: 'live.name_effect', name: 'Rainbow', rarity: 'epic', art: { emoji: '🌈', token: 'name-fx-rainbow' } },++ itd_01JZ00000000000000000001B1: { id: 'itd_01JZ00000000000000000001B1', kind: 'network.badge', name: 'OG Viewer', rarity: 'uncommon', art: { media_id: 'med_01JZ00000000000000000000B1' } },itd_01JZ00000000000000000001R3: { id: 'itd_01JZ00000000000000000001R3', kind: 'live.particle', name: 'Sparkles <b>', rarity: 'common', art: { emoji: '✨' } },};const INSTANCES = [{ id: 'inv_01JZ0000000000000000000001', definition_id: 'itd_01JZ00000000000000000001R1', state: 'owned', acquired_at: '2026-10-02T00:00:00Z' },{ id: 'inv_01JZ0000000000000000000002', definition_id: 'itd_01JZ00000000000000000001R2', state: 'owned', acquired_at: '2026-10-03T00:00:00Z' },{ id: 'inv_01JZ0000000000000000000003', definition_id: 'itd_01JZ00000000000000000001R3', state: 'owned', acquired_at: '2026-10-04T00:00:00Z' },++ { id: 'inv_01JZ0000000000000000000004', definition_id: 'itd_01JZ00000000000000000001B1', state: 'owned', acquired_at: '2026-10-05T00:00:00Z' },];(async () => {@@@ -59,7 +61,7 @@ const INSTANCES = [const json = (s, o) => { res.writeHead(s, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(o)); };if (down) return json(503, { code: 'down' });if (u.pathname === '/api/v1/kinds') return json(200, { kinds: KINDS });-- if (u.pathname === `/api/v1/people/${A}/equipped`) return json(200, { subject: A, slots: { 'live.hat:hat': { instance_id: INSTANCES[0].id, definition_id: INSTANCES[0].definition_id }, 'live.name_effect:name_effect': { instance_id: INSTANCES[1].id, definition_id: INSTANCES[1].definition_id, token: 'name-fx-rainbow' } } });++ if (u.pathname === `/api/v1/people/${A}/equipped`) return json(200, { subject: A, slots: { 'live.hat:hat': { instance_id: INSTANCES[0].id, definition_id: INSTANCES[0].definition_id }, 'live.name_effect:name_effect': { instance_id: INSTANCES[1].id, definition_id: INSTANCES[1].definition_id, token: 'name-fx-rainbow' }, 'network.badge:badge': { instance_id: INSTANCES[3].id, definition_id: INSTANCES[3].definition_id, media_id: 'med_01JZ00000000000000000000B1' } } });if (u.pathname === `/api/v1/people/${A}/items`) return json(200, { subject: A, instances: INSTANCES, definitions: DEFS, next_cursor: null });if (u.pathname === `/api/v1/profiles/${A}/badges`) return json(200, { subject: A, badges: [{ id: 'first-follow', name: 'First Follow', awarded_at: '2026-10-02T10:00:00Z' }, { id: 'thread-starter', name: 'Thread <Starter>', awarded_at: '2026-10-05T10:00:00Z' }] });return json(404, { code: 'inventory.unknown_subject' });@@@ -81,7 +83,7 @@ const INSTANCES = [// A public profile.let r = await get('/@ana');assert.strictEqual(r.status, 200);-- assert.ok(r.text.includes('<h1><span class="ov-fx name-fx-rainbow" title="Rainbow">Ana</span></h1>') && r.text.includes('@Ana'), 'her name wears its effect');++ assert.ok(r.text.includes('<h1><img class="pf-badge" src="https://openvibe.media/o/med_01JZ00000000000000000000B1" alt="" title="OG Viewer" width="32" height="32"><span class="ov-fx name-fx-rainbow" title="Rainbow">Ana</span></h1>') && r.text.includes('@Ana'), 'her community badge, then her name in its effect');assert.ok(r.text.includes('<link rel="stylesheet" href="/shared/items.css">'), 'with the shared effects stylesheet');assert.ok(/<span class="pf-hat" title="Royal Crown" aria-hidden="true">👑<\/span>/.test(r.text), 'her hat sits on her picture');assert.ok(!r.text.includes('<script>alert(1)</script>') && r.text.includes('I stream <script>alert(1)</script> & paint'), 'the bio is escaped');@@@ -89,7 +91,7 @@ const INSTANCES = [assert.ok(r.text.includes('id="pf-wearing"') && /pf-worn r-legendary[\s\S]*Royal Crown/.test(r.text), 'the crown is in the showcase');assert.ok(r.text.includes('Member since') && r.text.includes('October 2026'));assert.ok(/<span class="pf-stat-k">Rarest<\/span><span class="pf-stat-v">Legendary<\/span>/.test(r.text));-- assert.ok(r.text.includes('<span class="pf-count">3</span>'), 'three items');++ assert.ok(r.text.includes('<span class="pf-count">4</span>'), 'four items');assert.ok(r.text.includes('"@type":"ProfilePage"') && r.text.includes('<link rel="canonical" href="https://openvibe.network/@Ana">'));assert.ok(r.text.includes('--pf-color:#ff0066'));assert.ok(r.text.includes('https://inventory.openvibe.network/items/itd_01JZ00000000000000000001R1'), 'items link to their Inventory page');@@@ -108,10 +110,10 @@ const INSTANCES = [r = await get('/api/v1/profiles/Ana');assert.strictEqual(r.status, 200);const p = JSON.parse(r.text).profile;-- assert.deepStrictEqual([p.username, p.private, p.color, p.member_since, p.items.count, p.showcase.map((s) => s.name)], ['Ana', false, '#ff0066', '2026-10-01', 3, ['Rainbow', 'Royal Crown']]);++ assert.deepStrictEqual([p.username, p.private, p.color, p.member_since, p.items.count, p.showcase.map((s) => s.name)], ['Ana', false, '#ff0066', '2026-10-01', 4, ['Rainbow', 'Royal Crown', 'OG Viewer']]);assert.strictEqual(p.avatar_url, 'https://openvibe.network/avatar/Ana?s=160');assert.strictEqual(p.inventory_url, `https://inventory.openvibe.network/u/${A}`);-- assert.deepStrictEqual(p.items.by_kind.map((k) => k.kind).sort(), ['live.hat', 'live.name_effect', 'live.particle']);++ assert.deepStrictEqual(p.items.by_kind.map((k) => k.kind).sort(), ['live.hat', 'live.name_effect', 'live.particle', 'network.badge']);assert.deepStrictEqual(p.badges.map((b) => b.id), ['thread-starter', 'first-follow']);// A hidden profile: name and picture, nothing else, not indexed.
@@ -271,6 +271,9 @@ function renderPage(u, inv) {
@@ -271,6 +271,9 @@ function renderPage(u, inv) {
const fxClass = fx && NAME_FX_RE.test(String((fx.art || {}).token || '')) ? fx.art.token : null;
const fxClass = fx && NAME_FX_RE.test(String((fx.art || {}).token || '')) ? fx.art.token : null;
const hat = worn('live.hat');
const hat = worn('live.hat');
const hatGlyph = hat && (hat.art || {}).emoji ? String(hat.art.emoji).slice(0, 8) : null;
const hatGlyph = hat && (hat.art || {}).emoji ? String(hat.art.emoji).slice(0, 8) : null;
+ // A community badge they wear (the Workshop, network.badge): its reviewed image before their name.
+ const badge = worn('network.badge');
+ const badgeId = badge && /^med_[0-9A-HJKMNP-TV-Z]{26}$/.test(String((badge.art || {}).media_id || '')) ? badge.art.media_id : null;
const color = COLOR_RE.test(String(u.profile_color || '')) ? u.profile_color : '#3b82f6';
const color = COLOR_RE.test(String(u.profile_color || '')) ? u.profile_color : '#3b82f6';
const since = monthOf(u.created_at);
const since = monthOf(u.created_at);
const bio = isPublic ? String(u.bio || '').trim().slice(0, 500) : '';
const bio = isPublic ? String(u.bio || '').trim().slice(0, 500) : '';
@@ -324,6 +327,7 @@ a{color:var(--accent)}
@@ -324,6 +327,7 @@ a{color:var(--accent)}
.pf-avatar{width:112px;height:112px;border-radius:50%;object-fit:cover;border:4px solid var(--bg-secondary);box-shadow:0 0 0 2px var(--pf-color);background:var(--bg-card);flex:none}
.pf-avatar{width:112px;height:112px;border-radius:50%;object-fit:cover;border:4px solid var(--bg-secondary);box-shadow:0 0 0 2px var(--pf-color);background:var(--bg-card);flex:none}
.pf-who{flex:1;min-width:200px;padding-top:56px}
.pf-who{flex:1;min-width:200px;padding-top:56px}
.pf-who h1{margin:0;font-size:clamp(1.5rem,4vw,2.1rem);line-height:1.15;overflow-wrap:anywhere}
.pf-who h1{margin:0;font-size:clamp(1.5rem,4vw,2.1rem);line-height:1.15;overflow-wrap:anywhere}
+.pf-badge{width:.85em;height:.85em;margin-right:.25em;vertical-align:-.08em;border-radius:6px;object-fit:contain}
.pf-handle{margin:.2em 0 0;color:var(--text-secondary)}
.pf-handle{margin:.2em 0 0;color:var(--text-secondary)}
.pf-bio{margin:0;padding:0 24px 18px;color:var(--text-primary);line-height:1.6;white-space:pre-line;overflow-wrap:anywhere;max-width:72ch}
.pf-bio{margin:0;padding:0 24px 18px;color:var(--text-primary);line-height:1.6;white-space:pre-line;overflow-wrap:anywhere;max-width:72ch}
.pf-stats{list-style:none;display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0 24px 22px}
.pf-stats{list-style:none;display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0 24px 22px}
@@ -369,7 +373,7 @@ ${require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links
@@ -369,7 +373,7 @@ ${require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links
<div class="pf-banner" aria-hidden="true"></div>
<div class="pf-banner" aria-hidden="true"></div>
<div class="pf-head">
<div class="pf-head">
<div class="pf-pic"><img class="pf-avatar" src="${esc(avatarUrl(u.username))}" alt="" width="112" height="112">${hatGlyph ? `<span class="pf-hat" title="${esc(hat.name)}" aria-hidden="true">${esc(hatGlyph)}</span>` : ''}</div>
<div class="pf-pic"><img class="pf-avatar" src="${esc(avatarUrl(u.username))}" alt="" width="112" height="112">${hatGlyph ? `<span class="pf-hat" title="${esc(hat.name)}" aria-hidden="true">${esc(hatGlyph)}</span>` : ''}</div>
-<div class="pf-who"><h1>${fxClass ? `<span class="ov-fx ${fxClass}" title="${esc(fx.name)}">${esc(name)}</span>` : esc(name)}</h1><p class="pf-handle">@${esc(u.username)}</p></div>
+<div class="pf-who"><h1>${badgeId ? `<img class="pf-badge" src="https://openvibe.media/o/${badgeId}" alt="" title="${esc(badge.name)}" width="32" height="32">` : ''}${fxClass ? `<span class="ov-fx ${fxClass}" title="${esc(fx.name)}">${esc(name)}</span>` : esc(name)}</h1><p class="pf-handle">@${esc(u.username)}</p></div>
</div>
</div>
${bio ? `<p class="pf-bio">${esc(bio)}</p>` : ''}
${bio ? `<p class="pf-bio">${esc(bio)}</p>` : ''}
${isPublic ? `<ul class="pf-stats">${stats}</ul>` : '<p class="pf-private">This profile is private.</p>'}
${isPublic ? `<ul class="pf-stats">${stats}</ul>` : '<p class="pf-private">This profile is private.</p>'}
@@ -16,17 +16,19 @@ const publicCors = require('../server/public-cors');
@@ -16,17 +16,19 @@ const publicCors = require('../server/public-cors');
const A = 'usr_01JZ0000000000000000000AAA';
const A = 'usr_01JZ0000000000000000000AAA';
const KINDS = [
const KINDS = [
- { id: 'live.name_effect', name: 'Name effect' }, { id: 'live.hat', name: 'Hat' }, { id: 'live.particle', name: 'Particle' },
+ { id: 'live.name_effect', name: 'Name effect' }, { id: 'live.hat', name: 'Hat' }, { id: 'live.particle', name: 'Particle' }, { id: 'network.badge', name: 'Badge' },
];
];
const DEFS = {
const DEFS = {
itd_01JZ00000000000000000001R1: { id: 'itd_01JZ00000000000000000001R1', kind: 'live.hat', name: 'Royal Crown', rarity: 'legendary', art: { emoji: '👑' } },
itd_01JZ00000000000000000001R1: { id: 'itd_01JZ00000000000000000001R1', kind: 'live.hat', name: 'Royal Crown', rarity: 'legendary', art: { emoji: '👑' } },
itd_01JZ00000000000000000001R2: { id: 'itd_01JZ00000000000000000001R2', kind: 'live.name_effect', name: 'Rainbow', rarity: 'epic', art: { emoji: '🌈', token: 'name-fx-rainbow' } },
itd_01JZ00000000000000000001R2: { id: 'itd_01JZ00000000000000000001R2', kind: 'live.name_effect', name: 'Rainbow', rarity: 'epic', art: { emoji: '🌈', token: 'name-fx-rainbow' } },
+ itd_01JZ00000000000000000001B1: { id: 'itd_01JZ00000000000000000001B1', kind: 'network.badge', name: 'OG Viewer', rarity: 'uncommon', art: { media_id: 'med_01JZ00000000000000000000B1' } },
itd_01JZ00000000000000000001R3: { id: 'itd_01JZ00000000000000000001R3', kind: 'live.particle', name: 'Sparkles <b>', rarity: 'common', art: { emoji: '✨' } },
itd_01JZ00000000000000000001R3: { id: 'itd_01JZ00000000000000000001R3', kind: 'live.particle', name: 'Sparkles <b>', rarity: 'common', art: { emoji: '✨' } },
};
};
const INSTANCES = [
const INSTANCES = [
{ id: 'inv_01JZ0000000000000000000001', definition_id: 'itd_01JZ00000000000000000001R1', state: 'owned', acquired_at: '2026-10-02T00:00:00Z' },
{ id: 'inv_01JZ0000000000000000000001', definition_id: 'itd_01JZ00000000000000000001R1', state: 'owned', acquired_at: '2026-10-02T00:00:00Z' },
{ id: 'inv_01JZ0000000000000000000002', definition_id: 'itd_01JZ00000000000000000001R2', state: 'owned', acquired_at: '2026-10-03T00:00:00Z' },
{ id: 'inv_01JZ0000000000000000000002', definition_id: 'itd_01JZ00000000000000000001R2', state: 'owned', acquired_at: '2026-10-03T00:00:00Z' },
{ id: 'inv_01JZ0000000000000000000003', definition_id: 'itd_01JZ00000000000000000001R3', state: 'owned', acquired_at: '2026-10-04T00:00:00Z' },
{ id: 'inv_01JZ0000000000000000000003', definition_id: 'itd_01JZ00000000000000000001R3', state: 'owned', acquired_at: '2026-10-04T00:00:00Z' },
+ { id: 'inv_01JZ0000000000000000000004', definition_id: 'itd_01JZ00000000000000000001B1', state: 'owned', acquired_at: '2026-10-05T00:00:00Z' },
];
];
(async () => {
(async () => {
@@ -59,7 +61,7 @@ const INSTANCES = [
@@ -59,7 +61,7 @@ const INSTANCES = [
const json = (s, o) => { res.writeHead(s, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(o)); };
const json = (s, o) => { res.writeHead(s, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(o)); };
if (down) return json(503, { code: 'down' });
if (down) return json(503, { code: 'down' });
if (u.pathname === '/api/v1/kinds') return json(200, { kinds: KINDS });
if (u.pathname === '/api/v1/kinds') return json(200, { kinds: KINDS });
- if (u.pathname === `/api/v1/people/${A}/equipped`) return json(200, { subject: A, slots: { 'live.hat:hat': { instance_id: INSTANCES[0].id, definition_id: INSTANCES[0].definition_id }, 'live.name_effect:name_effect': { instance_id: INSTANCES[1].id, definition_id: INSTANCES[1].definition_id, token: 'name-fx-rainbow' } } });
+ if (u.pathname === `/api/v1/people/${A}/equipped`) return json(200, { subject: A, slots: { 'live.hat:hat': { instance_id: INSTANCES[0].id, definition_id: INSTANCES[0].definition_id }, 'live.name_effect:name_effect': { instance_id: INSTANCES[1].id, definition_id: INSTANCES[1].definition_id, token: 'name-fx-rainbow' }, 'network.badge:badge': { instance_id: INSTANCES[3].id, definition_id: INSTANCES[3].definition_id, media_id: 'med_01JZ00000000000000000000B1' } } });
if (u.pathname === `/api/v1/people/${A}/items`) return json(200, { subject: A, instances: INSTANCES, definitions: DEFS, next_cursor: null });
if (u.pathname === `/api/v1/people/${A}/items`) return json(200, { subject: A, instances: INSTANCES, definitions: DEFS, next_cursor: null });
if (u.pathname === `/api/v1/profiles/${A}/badges`) return json(200, { subject: A, badges: [{ id: 'first-follow', name: 'First Follow', awarded_at: '2026-10-02T10:00:00Z' }, { id: 'thread-starter', name: 'Thread <Starter>', awarded_at: '2026-10-05T10:00:00Z' }] });
if (u.pathname === `/api/v1/profiles/${A}/badges`) return json(200, { subject: A, badges: [{ id: 'first-follow', name: 'First Follow', awarded_at: '2026-10-02T10:00:00Z' }, { id: 'thread-starter', name: 'Thread <Starter>', awarded_at: '2026-10-05T10:00:00Z' }] });
return json(404, { code: 'inventory.unknown_subject' });
return json(404, { code: 'inventory.unknown_subject' });
@@ -81,7 +83,7 @@ const INSTANCES = [
@@ -81,7 +83,7 @@ const INSTANCES = [
// A public profile.
// A public profile.
let r = await get('/@ana');
let r = await get('/@ana');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
- assert.ok(r.text.includes('<h1><span class="ov-fx name-fx-rainbow" title="Rainbow">Ana</span></h1>') && r.text.includes('@Ana'), 'her name wears its effect');
+ assert.ok(r.text.includes('<h1><img class="pf-badge" src="https://openvibe.media/o/med_01JZ00000000000000000000B1" alt="" title="OG Viewer" width="32" height="32"><span class="ov-fx name-fx-rainbow" title="Rainbow">Ana</span></h1>') && r.text.includes('@Ana'), 'her community badge, then her name in its effect');
assert.ok(r.text.includes('<link rel="stylesheet" href="/shared/items.css">'), 'with the shared effects stylesheet');
assert.ok(r.text.includes('<link rel="stylesheet" href="/shared/items.css">'), 'with the shared effects stylesheet');
assert.ok(/<span class="pf-hat" title="Royal Crown" aria-hidden="true">👑<\/span>/.test(r.text), 'her hat sits on her picture');
assert.ok(/<span class="pf-hat" title="Royal Crown" aria-hidden="true">👑<\/span>/.test(r.text), 'her hat sits on her picture');
assert.ok(!r.text.includes('<script>alert(1)</script>') && r.text.includes('I stream <script>alert(1)</script> & paint'), 'the bio is escaped');
assert.ok(!r.text.includes('<script>alert(1)</script>') && r.text.includes('I stream <script>alert(1)</script> & paint'), 'the bio is escaped');
@@ -89,7 +91,7 @@ const INSTANCES = [
@@ -89,7 +91,7 @@ const INSTANCES = [
assert.ok(r.text.includes('id="pf-wearing"') && /pf-worn r-legendary[\s\S]*Royal Crown/.test(r.text), 'the crown is in the showcase');
assert.ok(r.text.includes('id="pf-wearing"') && /pf-worn r-legendary[\s\S]*Royal Crown/.test(r.text), 'the crown is in the showcase');
assert.ok(r.text.includes('Member since') && r.text.includes('October 2026'));
assert.ok(r.text.includes('Member since') && r.text.includes('October 2026'));
assert.ok(/<span class="pf-stat-k">Rarest<\/span><span class="pf-stat-v">Legendary<\/span>/.test(r.text));
assert.ok(/<span class="pf-stat-k">Rarest<\/span><span class="pf-stat-v">Legendary<\/span>/.test(r.text));
- assert.ok(r.text.includes('<span class="pf-count">3</span>'), 'three items');
+ assert.ok(r.text.includes('<span class="pf-count">4</span>'), 'four items');
assert.ok(r.text.includes('"@type":"ProfilePage"') && r.text.includes('<link rel="canonical" href="https://openvibe.network/@Ana">'));
assert.ok(r.text.includes('"@type":"ProfilePage"') && r.text.includes('<link rel="canonical" href="https://openvibe.network/@Ana">'));
assert.ok(r.text.includes('--pf-color:#ff0066'));
assert.ok(r.text.includes('--pf-color:#ff0066'));
assert.ok(r.text.includes('https://inventory.openvibe.network/items/itd_01JZ00000000000000000001R1'), 'items link to their Inventory page');
assert.ok(r.text.includes('https://inventory.openvibe.network/items/itd_01JZ00000000000000000001R1'), 'items link to their Inventory page');
@@ -108,10 +110,10 @@ const INSTANCES = [
@@ -108,10 +110,10 @@ const INSTANCES = [
r = await get('/api/v1/profiles/Ana');
r = await get('/api/v1/profiles/Ana');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
const p = JSON.parse(r.text).profile;
const p = JSON.parse(r.text).profile;
- assert.deepStrictEqual([p.username, p.private, p.color, p.member_since, p.items.count, p.showcase.map((s) => s.name)], ['Ana', false, '#ff0066', '2026-10-01', 3, ['Rainbow', 'Royal Crown']]);
+ assert.deepStrictEqual([p.username, p.private, p.color, p.member_since, p.items.count, p.showcase.map((s) => s.name)], ['Ana', false, '#ff0066', '2026-10-01', 4, ['Rainbow', 'Royal Crown', 'OG Viewer']]);
assert.strictEqual(p.avatar_url, 'https://openvibe.network/avatar/Ana?s=160');
assert.strictEqual(p.avatar_url, 'https://openvibe.network/avatar/Ana?s=160');
assert.strictEqual(p.inventory_url, `https://inventory.openvibe.network/u/${A}`);
assert.strictEqual(p.inventory_url, `https://inventory.openvibe.network/u/${A}`);
- assert.deepStrictEqual(p.items.by_kind.map((k) => k.kind).sort(), ['live.hat', 'live.name_effect', 'live.particle']);
+ assert.deepStrictEqual(p.items.by_kind.map((k) => k.kind).sort(), ['live.hat', 'live.name_effect', 'live.particle', 'network.badge']);
assert.deepStrictEqual(p.badges.map((b) => b.id), ['thread-starter', 'first-follow']);
assert.deepStrictEqual(p.badges.map((b) => b.id), ['thread-starter', 'first-follow']);
// A hidden profile: name and picture, nothing else, not indexed.
// A hidden profile: name and picture, nothing else, not indexed.