Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/brand-viewer-agent-sections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Brand viewer (`/brand/view/<domain>`): show explicit "Agents this brand operates" and "Agents authorized to sell this brand's inventory" sections with empty states. Both sections always render, sourcing from `/api/registry/operator` and `/api/registry/publisher`. When empty, hint text explains what's missing (no member claim, no `adagents.json`, etc.) so a viewer can distinguish "nothing set up" from "registry isn't surfacing it."
173 changes: 173 additions & 0 deletions server/public/brand-viewer.html
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,69 @@
color: var(--color-text-heading);
}

/* Agents list (operated / authorized) */
.agents-list {
list-style: none;
margin: 0;
padding: 0;
}
.agents-list__item {
padding: var(--space-3) 0;
border-bottom: 1px solid var(--color-border);
}
.agents-list__item:last-child {
border-bottom: none;
}
.agents-list__name {
font-weight: var(--font-semibold);
color: var(--color-text-heading);
margin-bottom: var(--space-1);
}
.agents-list__url {
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--color-text-secondary);
word-break: break-all;
}
.agents-list__url a {
color: var(--color-brand);
text-decoration: none;
}
.agents-list__type {
display: inline-block;
margin-top: var(--space-1);
padding: var(--space-0_5) var(--space-2);
background: var(--color-bg-subtle);
border-radius: var(--radius-full);
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.agents-list__scope {
margin-top: var(--space-1);
font-size: var(--text-xs);
color: var(--color-text-muted);
}

/* Empty state for agent sections */
.empty-state {
padding: var(--space-2) 0;
}
.empty-state p {
margin: 0 0 var(--space-2) 0;
color: var(--color-text-secondary);
}
.empty-state__hint {
font-size: var(--text-sm);
color: var(--color-text-muted);
}
.empty-state code {
background: var(--color-bg-subtle);
padding: var(--space-0_5) var(--space-1);
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: var(--text-xs);
}

/* Properties table */
.properties-table {
width: 100%;
Expand Down Expand Up @@ -1380,6 +1443,28 @@ <h3>Edit brand details</h3>
</div>
</div>

<!-- Agents this brand operates -->
<div id="agents-operated-section" class="viewer-section">
<div class="section-heading">
<span class="section-icon">&#129302;</span>
Agents this brand operates
</div>
<div id="agents-operated-content" class="card">
<div class="loading">Loading…</div>
</div>
</div>

<!-- Agents authorized to sell this brand's inventory -->
<div id="agents-authorized-section" class="viewer-section">
<div class="section-heading">
<span class="section-icon">&#128274;</span>
Agents authorized to sell this brand's inventory
</div>
<div id="agents-authorized-content" class="card">
<div class="loading">Loading…</div>
</div>
</div>

<!-- Collections -->
<div id="collections-section" class="viewer-section hidden">
<div class="section-heading">
Expand Down Expand Up @@ -2355,6 +2440,9 @@ <h3>Upload logo</h3>
}
}

// Agent sections render regardless of brand.json state — empty states explain what's missing.
loadAgents(domain);

// Always try loading revision history (works for community brands even without brand.json)
const hasCommunityData = await loadRevisionHistory(domain);

Expand Down Expand Up @@ -2847,6 +2935,91 @@ <h3>Upload logo</h3>
</tbody>`;
}

async function loadAgents(domain) {
const [operatorResp, publisherResp] = await Promise.allSettled([
fetch(`/api/registry/operator?domain=${encodeURIComponent(domain)}`),
fetch(`/api/registry/publisher?domain=${encodeURIComponent(domain)}`),
]);

let operator = null;
if (operatorResp.status === 'fulfilled' && operatorResp.value.ok) {
try { operator = await operatorResp.value.json(); } catch {}
}

let publisher = null;
if (publisherResp.status === 'fulfilled' && publisherResp.value.ok) {
try { publisher = await publisherResp.value.json(); } catch {}
}

renderAgentsOperated(operator);
renderAgentsAuthorized(publisher);
}

function renderAgentsOperated(operator) {
const el = document.getElementById('agents-operated-content');
const member = operator?.member;
const agents = operator?.agents || [];

if (!member) {
el.innerHTML = `
<div class="empty-state">
<p><strong>No operator on record.</strong></p>
<p class="empty-state__hint">No member has claimed this domain. To list operated agents here, an authenticated member of the controlling organization needs to claim this domain in their member profile.</p>
</div>`;
return;
}

const memberLink = `<a href="/members/${escapeAttr(member.slug)}">${escapeHtml(member.display_name)}</a>`;

if (agents.length === 0) {
el.innerHTML = `
<div class="empty-state">
<p>Operated by ${memberLink}.</p>
<p class="empty-state__hint">${escapeHtml(member.display_name)} has not registered any agents for this brand.</p>
</div>`;
return;
}

el.innerHTML = `
<div style="margin-bottom:var(--space-3);color:var(--color-text-secondary);">Operated by ${memberLink}</div>
<ul class="agents-list">
${agents.map(a => `
<li class="agents-list__item">
<div class="agents-list__name">${escapeHtml(a.name || a.url)}</div>
<div class="agents-list__url">${isSafeUrl(a.url) ? `<a href="${escapeAttr(a.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(a.url)}</a>` : escapeHtml(a.url)}</div>
${a.type && a.type !== 'unknown' ? `<span class="agents-list__type">${escapeHtml(a.type)}</span>` : ''}
</li>`).join('')}
</ul>`;
}

function renderAgentsAuthorized(publisher) {
const el = document.getElementById('agents-authorized-content');
const adagentsValid = publisher?.adagents_valid;
const authorized = publisher?.authorized_agents || [];

if (authorized.length === 0) {
const hint = adagentsValid
? `An <code>adagents.json</code> is published for this domain but lists no agents.`
: `No <code>adagents.json</code> found at this domain. To authorize a sales agent, publish <code>https://&lt;domain&gt;/adagents.json</code> listing each agent's URL.`;
el.innerHTML = `
<div class="empty-state">
<p><strong>No authorized agents.</strong></p>
<p class="empty-state__hint">${hint}</p>
</div>`;
return;
}

el.innerHTML = `
<ul class="agents-list">
${authorized.map(a => `
<li class="agents-list__item">
<div class="agents-list__url">${isSafeUrl(a.url) ? `<a href="${escapeAttr(a.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(a.url)}</a>` : escapeHtml(a.url)}</div>
${a.source ? `<span class="agents-list__type">${escapeHtml(a.source)}</span>` : ''}
${Array.isArray(a.authorized_for) && a.authorized_for.length > 0 ? `<div class="agents-list__scope">Authorized for: ${a.authorized_for.map(s => escapeHtml(typeof s === 'string' ? s : JSON.stringify(s))).join(', ')}</div>` : ''}
</li>`).join('')}
</ul>`;
}

function renderCollections(collections) {
const section = document.getElementById('collections-section');
const content = document.getElementById('collections-content');
Expand Down
Loading