From e77bfd36fb259fa7d8be819445ea76d9fdc2211a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 08:47:43 +0000 Subject: [PATCH] feat(member-tools): nudge publisher/adtech org admins to populate brand.json properties Closes #4280 https://claude.ai/code/session_01BYkdrK3XyCwvkc6XAmLz74 --- .../member-dash-brand-properties-nudge.md | 4 ++ server/public/dashboard.html | 40 ++++++++++++++++--- 2 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 .changeset/member-dash-brand-properties-nudge.md diff --git a/.changeset/member-dash-brand-properties-nudge.md b/.changeset/member-dash-brand-properties-nudge.md new file mode 100644 index 0000000000..63bc9046a2 --- /dev/null +++ b/.changeset/member-dash-brand-properties-nudge.md @@ -0,0 +1,4 @@ +--- +--- + +Add a setup-alert nudge on the member dashboard for publisher/adtech org admins when brand.json has zero properties. Dismisses with a 7-day localStorage snooze so it resurfaces if properties stay empty. diff --git a/server/public/dashboard.html b/server/public/dashboard.html index 887e37f999..97a66f73ca 100644 --- a/server/public/dashboard.html +++ b/server/public/dashboard.html @@ -2071,6 +2071,11 @@

Join the Agentic Advertising Organization

// Get dismissed alerts from session storage const dismissedAlerts = JSON.parse(sessionStorage.getItem('dismissedSetupAlerts') || '[]'); + // Brand-properties nudge uses a localStorage timestamp for 7-day snooze (keyed per org) + const brandPropsDismissedAt = localStorage.getItem(`propertyNudgeDismissedAt_${org.id}`); + const brandPropsSnoozed = brandPropsDismissedAt && + (Date.now() - parseInt(brandPropsDismissedAt, 10)) < (7 * 24 * 60 * 60 * 1000); + // Fetch member profile to check for missing items try { const response = await fetch(`/api/member-profiles/org/${org.id}`); @@ -2130,6 +2135,25 @@

Join the Agentic Advertising Organization

}); } + // Nudge publisher/adtech org admins to populate brand.json properties when empty. + // Empty properties = downstream discovery gap (brand→property catalog, ad-agents.json delegation). + if (!isPersonalAccount + && ['admin', 'owner'].includes(org.role) + && ['publisher', 'adtech'].includes(org.billing?.company_type) + && profile.primary_brand_domain + && profile.resolved_brand + && (profile.resolved_brand.property_count ?? 0) === 0 + && !brandPropsSnoozed) { + alerts.push({ + id: 'missing-brand-properties', + icon: '🏗️', + title: 'List your owned properties', + message: `List the publishers and properties ${org.name ?? 'your organization'} owns or represents — this powers brand-aware discovery and ad-agents.json delegation.`, + actionLabel: 'Add properties', + actionUrl: `/brand-builder?domain=${encodeURIComponent(profile.primary_brand_domain)}#properties` + }); + } + // Check for pending join requests (admins only) try { const pendingResponse = await fetch(`/api/organizations/${org.id}/join-requests`); @@ -2189,14 +2213,20 @@

${escapeHtml(alert.title)}

} function dismissSetupAlert(alertId) { - const dismissedAlerts = JSON.parse(sessionStorage.getItem('dismissedSetupAlerts') || '[]'); - if (!dismissedAlerts.includes(alertId)) { - dismissedAlerts.push(alertId); - sessionStorage.setItem('dismissedSetupAlerts', JSON.stringify(dismissedAlerts)); + if (alertId === 'missing-brand-properties') { + // currentOrg is set to org right before renderSetupAlerts is called; org switches rebuild + // the alerts DOM entirely, so currentOrg.id always matches the rendered card's org here. + localStorage.setItem(`propertyNudgeDismissedAt_${currentOrg?.id}`, Date.now().toString()); + } else { + const dismissedAlerts = JSON.parse(sessionStorage.getItem('dismissedSetupAlerts') || '[]'); + if (!dismissedAlerts.includes(alertId)) { + dismissedAlerts.push(alertId); + sessionStorage.setItem('dismissedSetupAlerts', JSON.stringify(dismissedAlerts)); + } } // Remove the alert from the DOM - const alertEl = document.querySelector(`.setup-alert[data-alert-id="${alertId}"]`); + const alertEl = document.querySelector(`.setup-alert[data-alert-id="${CSS.escape(alertId)}"]`); if (alertEl) { alertEl.remove(); }