Skip to content

feat(team): show brand-registry hierarchy classification + report-wrong link#3450

Merged
bokelley merged 7 commits into
mainfrom
bokelley/show-org-brand-hierarchy
Apr 28, 2026
Merged

feat(team): show brand-registry hierarchy classification + report-wrong link#3450
bokelley merged 7 commits into
mainfrom
bokelley/show-org-brand-hierarchy

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

PR #3430 added the subsidiary auto-provisioning toggle but only surfaced what's below the org (children). Two things owners actually care about were hidden:

  1. Parent — if the registry classified them as a subsidiary of another brand. Without this, an analyticsiq.com-style org couldn't tell they were classified under Alliant.
  2. Classification confidence + last-validated date — context owners need to judge whether the data is trustworthy enough to flip the toggle.

This PR closes that "black box" gap.

What

Backend: GET /api/organizations/:orgId/domains returns hierarchy_classification: { self, parent } | null. Sourced from the brands table for the org's primary verified domain plus a self-join for the parent.

Frontend: new "Brand registry classification" section on team.html above the auto-provision toggles. Renders parent → you → children with confidence badge + last-validated dates. Each entry has a "Report wrong" mailto:hello@agenticadvertising.org link that prefills subject/body, routing corrections through the existing support channel without new infra.

The inferred-subsidiaries panel inside the auto-provision toggle is now a thin pointer back to the hierarchy section (no duplication).

Verification

  • Typecheck clean.
  • 76 integration tests pass on the impacted suite (3 new for hierarchy_classification: parent + self surfaced, parent null when no house_domain, classification null when no brands row).
  • Live UI smoke via Playwright in dev mode (seeded a brand row + subsidiary in the dev DB): hierarchy section renders correctly with parent → self → children layout, confidence badge, and working "Report wrong" links. Screenshot captured. Verification depends on PR refactor(auth): universal resolveUserOrgMembership helper for org auth-check pattern #3448 (universal dev-mode bypass) since /api/organizations/:orgId/domains would otherwise 403 in dev — that PR is open for review.

Out of scope (future PRs if signal emerges)

🤖 Generated with Claude Code

bokelley and others added 6 commits April 28, 2026 11:23
…h-check pattern

Replaces 14 copy-pasted call sites across organizations.ts and member-profiles.ts
that did the WorkOS-membership-then-resolveUserRole auth check inline. Two
problems with the old pattern:

- Every admin-only org endpoint 403'd in dev mode because WorkOS doesn't know
  about DEV_USERS. The dev-mode bypass that solved this lived in exactly one
  place (GET /api/me/member-profile), and most other routes didn't have it.
- 14 subtly different versions of the same auth check, each with its own
  error message and role logic.

The helper does it once. In dev mode it reads from local
organization_memberships (seeded by dev-setup at boot). In prod it defers
to WorkOS as source of truth.

Verified end-to-end: every admin endpoint the team page hits now responds 200
to a dev-mode admin user where most previously returned 403. The single
remaining 404 in dev (api/billing) is unrelated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…membership' into bokelley/show-org-brand-hierarchy
…ng link

PR #3430's subsidiary toggle only showed children of the org. Two things
owners actually need:

- Parent (if classified as a subsidiary of another brand). Without this,
  an analyticsiq.com-style org can't tell they're classified under Alliant.
- Classification confidence + last-validated date — context owners need to
  judge whether the data is trustworthy enough to flip the auto-provision
  toggle.

GET /api/organizations/:orgId/domains now returns hierarchy_classification
with self + parent. team.html renders a "Brand registry classification"
section above the toggles showing parent → you → children, each with a
"Report wrong" mailto link that routes corrections into the existing
support flow without new infra.

Verified end-to-end via Playwright in dev mode (depends on the universal
dev-mode bypass from #3448): seeded a brand row + subsidiary, the
hierarchy diagram renders correctly, "Report wrong" links work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Security review found a HIGH ship-blocker on PR #3448:

- Add a hard prod-boot guard that crashes startup if DEV_USER_EMAIL +
  DEV_USER_ID are set in a production-shaped environment (NODE_ENV=production
  OR FLY_APP_NAME present). Override via ALLOW_DEV_MODE_IN_PROD=true for
  the rare one-off. Pre-PR the dev-mode bypass affected one read endpoint;
  post-PR it widens to 23 mutating routes (delete org, ban member, transfer
  ownership, etc), so a stray secret-flip would now grant owner-level
  takeover instead of read access.

Code review found 4 more sites that match the auth-check pattern but
weren't refactored in the original commit:

- POST /api/organizations/:orgId/members (line 2177)
- POST /api/organizations/:orgId/invitations (line 2360)
- DELETE /api/organizations/:orgId/invitations/:invitationId (line 2469)
- POST /api/organizations/:orgId/invitations/:invitationId/resend (line 2536)
- POST /api/organizations/:orgId/members/by-email (line 3092 — the
  hybrid AAO-admin / static-API-key / org-role path; refactored carefully
  to preserve the conditional-skip for static admin keys)
- PATCH/DELETE /api/organizations/:orgId/members/:membershipId (line 3291)

All 6 now route through resolveUserOrgMembership.

billing-public.ts and certification.ts have hand-rolled dev-mode bypasses
(isDevUserInvoice, isOrgMember). Filed as separate followup — different
patterns that need their own analysis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nd_json visual distinction

Code-review FBM and product feedback on PR #3450:

- SQL bug (code review): the ORDER BY scalar subquery for is_primary did
  not filter by workos_organization_id. If the same domain is verified
  across multiple tenants, the wrong org's primary flag could win the
  LIMIT 1. Added the tenant filter.
- Self-loop guard (code review): the LEFT JOIN brands parent ON
  parent.domain = db.house_domain didn't guard against db.house_domain ==
  db.domain. A malformed registry row pointing at itself would render
  "you are a child of yourself". Added parent.domain != db.domain to
  the join.
- Dropped data.org_name from the mailto body — the /domains endpoint
  doesn't return it, so the line always rendered as "Org: " blank.
- New `source` field on hierarchy_classification.self / .parent /
  inferred_subsidiaries (product). brand_json self-asserted gets a green
  "Asserted" badge; LLM-inferred gets a neutral "Inferred" badge.
- For brand_json self-asserted parent relationships, render a small
  guidance hint pointing the admin at their own brand.json instead of the
  mailto support flow — fix is one PR away from where they look, not
  three weeks away after a triage review.
- Confidence badge now suppressed for brand_json (the "confidence" frame
  is wrong for a self-assertion).

Tests: 3 new cases (parent-row-doesn't-exist via LEFT JOIN miss,
self-loop guard, source field surfacing for brand_json). 12/12 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley

Copy link
Copy Markdown
Contributor Author

Pushed round-2 fixups addressing all three reviews:

Code-review FBM (resolved):

  • Tenant-scoped ORDER BY (organizations.ts:639-644). The is_primary scalar subquery now filters AND od.workos_organization_id = $1. Without this, if the same domain were verified across multiple tenants, the wrong org's primary flag could win the LIMIT 1.
  • Self-loop guard on the parent LEFT JOIN. parent.domain != db.domain — a malformed brand row pointing house_domain at its own domain would otherwise render "you are a child of yourself."
  • Dropped data.org_name from the mailto body. The endpoint doesn't return it; the line always rendered as Org: blank.

Product feedback (resolved):

  • Source field added to API + UI. brand_json self-asserted gets a green "Asserted" badge; LLM-inferred (enriched) gets a neutral "Inferred" badge.
  • For brand_json-asserted parents, render a small guidance hint pointing the admin at their own brand.json instead of the mailto support flow. Fix is one PR away from where they look, not three weeks away after triage.
  • Confidence badge suppressed for brand_json — "confidence" is the wrong frame for a self-assertion.
  • Skipped: the reasoning field per your guidance (heuristic-leaking, invites debate).

Deferred to follow-up issues (not in this PR):

  • Click-tracking POST on "Report wrong" so we have a triage queue + can detect "10 people reported the same thing." Worth doing when click volume justifies a real form anyway.
  • Stale-entry visual muting (>90d). Quick win once we have signal.
  • /member-profile deep link to "your hierarchy is shown on /team." One-line addition, doing in the next member-profile-touching PR.

Tests: 3 new cases (parent-row-doesn't-exist via LEFT JOIN miss, self-loop guard data-quality regression, source field surfacing). 12/12 pass on this file's suite.

@bokelley

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough round-2 summary. The tenant-scoped ORDER BY fix and self-loop guard look like the right calls — good catches from the review. The deferred items (click-tracking, stale muting, member-profile deep link) are well-scoped follow-ups; leaving them off this PR makes sense.

No action needed from triage. Ready for human review when you are.


Generated by Claude Code

@bokelley bokelley merged commit c006c0c into main Apr 28, 2026
12 checks passed
@bokelley bokelley deleted the bokelley/show-org-brand-hierarchy branch April 28, 2026 16:59
bokelley added a commit that referenced this pull request Apr 28, 2026
…#3469)

Two polish items deferred from the PR #3450 review (product expert
recommendations):

- Click-tracking endpoint POST /api/organizations/:orgId/brand-classification-report
  records {orgId, userId, kind, subject_domain} to registry_audit_log on
  every Report-wrong click. The mailto: opens regardless via the existing
  anchor href; sendBeacon doesn't block navigation. Gives us a triage
  queue + lets us detect when N different members all flag the same
  domain. Member-or-above can flag (broader than admin/owner since the
  action is informational and the corrective action is admin-side).
- One-line deep link from /member-profile's Brand identity section to
  the /team page where the registry hierarchy is now shown. Closes the
  "I came to member-profile to check if my company is classified right"
  bounce-off case.

1 new integration test (happy path + invalid-kind 400 + invalid-domain
400). 13/13 pass on the toggles suite.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant