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.
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.
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.
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)PortalsDirectoryShellPortalsListPortalRowPortalCard
PortalTierBadgePortalStatusBadgeOpenPortalLink
PermissionDeniedNoticePortalsListLoadingPortalsListEmpty
PortalsListError
PortalsDirectory is a shell page in frontend/src.@fuzefront/portal-client (existing, adminPortals list).StatusPill — semantic lifecycle pill (reused; not in DS today)Badge — the soft/hard tier badgeDataList / DataTable — the scannable directorySearchField — filter by name / domainExternalLink — new-tab anchor with the ↗ affordance + rel="noopener noreferrer"
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>.