Contract-freeze · UX approval

Portals directory · Approval Frames

A directory in the FuzeFront shell's left menu where a master-admin sees the portals they can manage and opens a chosen portal in a new browser tab at that portal's own host URL. Management then happens on that host via the existing per-host tenant routing — there is no in-app portal switcher and no context switch. Every portal carries an identity tier: soft (shares the root Authentik) or hard (its own Authentik instance, e.g. Mendys Robotics). These frames freeze the flow, its states, and the build inventory. Walk each frame; approve per flow.

Soft shares the root Authentik Hard its own Authentik instance
Portals directory · route /portals · page PortalsDirectory (shell page, no new package)
Frame (a) · /portals

Portals

The directory: each portal shows name, primary domain, status, and a soft/hard tier badge, with an "Open portal ↗" action that opens the portal's own host in a new tab. Searchable, scannable.

Frame (b) · /portals

States

Loading, empty (no portals yet), error + retry, suspended (no launch), and the fail-closed permission-denied / read-only case — no manage authority ⇒ zero launch affordances.

Build inventory

Approving this flow approves its component plan and its contract binding — implementation cannot quietly invent a different architecture. Mirrored in manifest.json → build. PortalsDirectory is a shell page in frontend/src — NOT a new npm package.

PortalsDirectory (page)

route /portals · page in frontend/src · flag fuzefront.platform.portals-directory (default OFF) · approved: false
data: adminPortals list — GET /api/v1/admin/portals (S1 adds identity_mode + launchUrl) · REAL
launch: <a target="_blank" rel="noopener noreferrer" href="{launchUrl}"> — new tab, portal's own host
PortalsDirectoryShellPortalsListPortalRowPortalCard PortalTierBadgePortalStatusBadgeOpenPortalLink PermissionDeniedNoticePortalsListLoadingPortalsListEmpty PortalsListError

npm packages

  • None new. PortalsDirectory is a shell page in frontend/src.
  • Data client: @fuzefront/portal-client (existing, adminPortals list).

DS primitives (compose from tokens; flag missing)

  • StatusPill — semantic lifecycle pill (reused; not in DS today)
  • Badge — the soft/hard tier badge
  • DataList / DataTable — the scannable directory
  • SearchField — filter by name / domain
  • ExternalLink — new-tab anchor with the ↗ affordance + rel="noopener noreferrer"
● Awaiting approval — flow portals-directory

Design system: @fuzefront/design-system (fuse-seam, tokens only — all raw literals confined to tokens.css).
Data (real): the adminPortals list (GET /api/v1/admin/portals, backend/src/routes/adminPortals.ts), extended in S1 to return identity_mode (soft|hard) and launchUrl per portal. The directory renders those directly — identity_mode → the tier badge, launchUrl → the "Open portal" href. The client never composes a host from client-held data; launchUrl is authoritative.
No switcher: there is deliberately no in-app portal context switch. Choosing a portal is a plain external navigation to its own host in a new tab; per-host tenant routing takes over there.
Fail-closed: the list is Permit-ReBAC-scoped to portals the caller may manage; a caller without manage authority sees the permission-denied / read-only state with no launch affordance emitted, and a suspended portal is never launchable.
DS primitives above are not all in @fuzefront/design-system today — frontend-engineer (sole editor of design-system/) adds any missing ones as foundation PRs; the frames compose them from tokens only in the meantime.
Approve this flow by setting approved: true on it in manifest.json, or reply @claude approve portals-directory / @claude reject: <reason>.

Review · portals-directory approve or reject each flow ↓
portals-directoryapproved · izzywdev