[C01] Application Structure: Shell, Tenant + Workspace Selection, Notifications
Summary
The application shell — the durable chrome around every screen in admin-portal: cascading sidebar (Tenant → Workspace → Mode nav → Entity), topbar with breadcrumb / Jump-to / notifications, sidebar collapse, and the concept of a workspace landing. C01 owns the structure; it does not own the data shown inside any workspace landing.
Scope discipline (2026-04-27): C01 is structural only.
-
Events workspace landing content (Resume / Active / Recent / Glance / Attention) → E05 Events Control Centre
-
Memberships workspace landing → owned by an
M0xuse case when designed (mirror E05 pattern as guidance until then) -
Tenant admin workspace landing → T01
-
Super Tenant workspace landing → S01
-
Single-event drill-down → E01 Event Overview
-
Pre-auth public landing → C03 Public Landing
Actor & Context
Actors:
-
Staff user — has access to one or more tenants. May see Events, Memberships depending on each tenant’s
enabledModules; Affiliates is shown disabled with a "Coming soon" tooltip platform-wide. -
Tenant admin — staff user with the tenant-admin role at one or more tenants (sourced from
OrgPermission.role, see Feature #534). Additionally sees the Tenant admin workspace. -
Super admin — staff user with the global
isSuperAdminJWT claim (US #536). Additionally sees the Super Tenant row pinned at the top of the tenant switcher.
Frequency: every login. The shell is the constant frame around all other portal work.
Precondition: user has authenticated via OIDC (OIDC ⇄ admin-service Token Exchange); admin-portal session holds a valid backend JWT (Session-Held JWT & JSESSIONID); the JWT carries the user’s identity, current orgId, accessible tenants (linkedOrgs), per-tenant roles (post-#534), and the isSuperAdmin flag (post-#536).
Entry point: authenticated landing — direct URL https://admin.event.idealogic.co.za/, or post-OIDC-callback redirect from the gateway. Pre-auth predecessor is the public landing (C03).
Main Flow
-
Bootstrap. SPA
APP_INITIALIZERcallsGET /api/session/currenton the gateway. Response: user,currentTenantId, available tenants,isSuperAdmin, last-context (from localStorage). -
Restore last context. SPA navigates to
lastTenant + lastWorkspace(from localStorage) or to a default workspace (Eventsif enabled at this tenant) on first visit. -
Render shell. Sidebar: tenant switcher → workspace switcher → mode nav → user/collapse footer. Topbar: breadcrumb, Jump-to (⌘K), notifications bell.
-
Defer to workspace landing. Render the active workspace’s landing inside the shell. Specific content is owned by the workspace use case (E05 / M0x / T01 / S01). C01’s job ends at "the right landing component is mounted in the right slot."
-
The user proceeds inside the workspace. Switching tenant, switching workspace, opening notifications, collapsing the sidebar, or invoking ⌘K all stay within shell concerns.
Alternative Flows
-
AF-1 — Switch tenant. User clicks the tenant chip. Dropdown: search field, Super Tenant (pinned, super admins only), then My tenants (alphabetical or recency-sorted). Selecting a tenant calls
POST /api/session/tenant(re-issues the admin-service JWT with the new tenant claim — see OIDC Token Exchange § Runtime Tenant Switch + US #537). On success, SPA reloads tenant-scoped state and lands on the workspace last used in that tenant. -
AF-2 — Switch workspace. User clicks the workspace chip below the tenant chip. Menu lists Events, Memberships, Affiliates (always shown; disabled with
cursor: not-allowed+ aSoonpill badge + a "Coming soon" tooltip), and Tenant admin (only if user hasTENANT_ADMINrole for current tenant — separated by divider). Selecting an enabled workspace navigates to its landing. -
AF-3 — Super Tenant selection. Super admin clicks Super Tenant. Application rescopes: tenant chip shows globe icon + "Super Tenant · Global · cross-tenant"; workspace switcher narrows to the Super admin workspace only ("Super Tenant only has the Super admin workspace"); nav shows the super-admin items (Tenants / Global users / Role templates / Global modes / Regions / Master data / System settings / Audit log). Returning to a regular tenant uses the same switcher.
-
AF-4 — Open notifications. User clicks the bell in the topbar. A 380 px panel drops below the bell with: header (title, "N unread" subtitle, "Mark all read" button, settings icon), three tabs (All / Unread / Mentions, with unread count badge on the Unread tab), and a scrollable list of notification rows. Each row: workspace-tinted icon, title, body (truncated), source ("Tour du Worcester 2025 · Comments"), relative timestamp, unread-dot rail on the left. Click a row to navigate to its source. Empty state: "You’re all caught up." See Notifications (designed).
-
AF-5 — Collapse sidebar. User clicks the collapse toggle in the sidebar footer. Sidebar animates from 232 px to 52 px (180 ms ease) showing icons only. Hover surfaces tooltips. Collapse state persists in localStorage (per user, per browser).
-
AF-6 — Jump-to command palette. User presses ⌘K (or Ctrl-K) or clicks the topbar "Jump to…" pill. Palette opens with fuzzy search over tenants, events in current tenant, members in current tenant, recent screens. Selecting a result navigates directly. Internals out of C01 scope; flag as a sibling C-screen when it grows.
-
AF-7 — Single-tenant fast path. User has access to exactly one tenant and is not a super admin. The tenant chip is informational; clicking it does nothing or shows a static dropdown with just the one entry. No degraded layout.
-
AF-8 — User loses access to current tenant mid-session. A tenant switch returns 403; SPA shows a toast and reverts to the previous tenant; tenant list is re-fetched (
GET /api/session/tenants).
Acceptance Criteria
-
After OIDC login, the SPA renders the shell + the right workspace landing within 1.5s of
/api/session/currentreturning. -
The tenant chip shows the current tenant’s name truncated to fit; full name visible on hover.
-
Super Tenant row appears in the tenant switcher only when JWT carries
isSuperAdmin: true. Otherwise the row is absent (not hidden via CSS — not even rendered). -
Tenant admin workspace appears in the workspace switcher only when the user holds
TENANT_ADMINrole on the current tenant. -
Affiliates workspace always appears in the workspace switcher in a disabled state, with a "Coming soon" tooltip and
Soonpill badge. Backend not yet implemented — this is a portal-level teaser, not a feature flag. -
Switching tenant re-issues the admin-service JWT via token exchange (server-authoritative); subsequent admin-service calls show the new tenant in the JWT claim.
-
Sidebar collapse / expand animates smoothly (180 ms ease); collapse state persists across reloads.
-
Bell icon shows red unread-count badge when notifications are unread; badge caps at "9+". Badge is absent when
unreadCount === 0. -
Notifications panel opens below the bell on click, closes on outside-click or Esc.
-
Workspace accent colours are used for chips/badges only and never as dominant UI:
Workspace Accent Events
indigo
#4f46e5Memberships
teal
#0891b2Affiliates
amber
#ca8a04Tenant admin
burnt amber
#b45309Super admin
near-black
#0a0a0a -
All shell elements meet WCAG AA contrast on the Linear/Vercel-style cool-neutral palette.
API Surface
C01 needs only the shell-level endpoints. Workspace-specific data (events, members, glance metrics, attention items) belongs to the relevant workspace use case (E05 etc.).
| Call | Purpose |
|---|---|
|
Bootstrap. |
|
Refresh available-tenants list. Proxies to admin-service |
|
Switch current tenant. Re-mints admin-service JWT via |
|
List notifications for the current user, scoped to the current tenant by default. Pagination + filter ( |
|
Read-state mutations. Per-user, server-persisted. |
Notification-specific endpoints are still candidates — see Notifications (designed); final shape decided when WS5 starts. Existing alternatives (extend CommunicationLogResource, or define a new NotificationResource) need a small spike at WS5 kickoff.
Out of Scope
-
Workspace landing content (lives in the relevant workspace use case).
-
Single-event detail (E01-E04, M01-M02, etc.).
-
Public landing page (C03).
-
Internal command-palette mechanics.
-
Notification creation — that lives in admin-service (publishers like CommunicationLogService, PersonMergeService etc. emit; notification-fan-out lives in the Async Signal & Sweep Framework, Feature #403).
Notifications (designed)
The bell in the topbar opens a 380 px notifications panel. Designed in the 2026-04-27 Claude Design pass; reverse-engineered into this spec.
Bell
-
Topbar right, next to the Jump-to pill.
-
bellicon, 15 px, in a 28×28 px button. -
Active state: button background + border becomes
bgMuted/border. -
Unread-count badge: top-right of the icon; red
#ef4444background, white text, ≥15 px tall, rounded-full. Caps at "9+". Absent whenunreadCount === 0. White 1.5 px border to lift it off the topbar.
Panel
-
Anchored absolute below the bell,
top: 38px,right: -4px. -
380 × ≤440 px, panel background, 1 px border, 10 px radius, soft shadow. -
z-index: 30so it overlays the body content.
Header
-
Title "Notifications" + "N unread" muted subtitle when applicable.
-
Right side: ghost button "Mark all read" with check icon; quiet settings icon.
-
Tab strip below: All · Unread · Mentions. Active tab marked with bottom indigo accent border. Unread tab carries a small unread count next to its label.
Notification row
| Element | Detail |
|---|---|
Unread rail |
6 px column on the left; renders an indigo |
Workspace icon |
26×26 px rounded square. Background = workspace accent at 9% alpha; foreground = workspace accent. Icon picked per type: |
Title |
Bold, single line, ellipsis on overflow. |
Body |
Short, muted text. Single line of context — quote, count, summary. |
Source |
Smaller still, faint colour. e.g. "Tour du Worcester 2025 · Comments". Click target. |
Timestamp |
Right-aligned, faint, relative ("2m", "14m", "1h", "Yesterday", "2 days ago"). |
Read-state background |
Unread rows have a subtle indigo-tinted background ( |
Notification types (sample, from the design)
The 2026-04-27 design seeds the panel with seven sample notifications across these types:
| Type | Example | Source pattern |
|---|---|---|
|
"Zanele mentioned you" — "@chris can you confirm the start groups for the 109 km?" |
|
|
"Participant import completed" — 1,204 rows · 12 warnings · 0 errors |
|
|
"Role assigned: Results officer" — Pieter de Villiers granted Results officer |
|
|
"Event status changed: Setup → Registration open" |
|
|
"Bulk email sent — 12,480 recipients" — open-rate % |
|
|
"83 renewals processed" |
|
|
"Results published" |
|
Type-set is open — new types are added by adding a new icon mapping. Treat the list as illustrative, not exhaustive.
Tenant scope
-
Within a regular tenant: notifications scope to the current tenant. Cross-tenant notifications are not shown.
-
On Super Tenant: the panel still shows the user’s notifications across the tenants they have access to, with a small per-row tenant badge (the design’s
onSuperTenantprop wires this — adds the tenant chip on each row when active). Defer Super-Tenant-specific tenant badge implementation until Super Tenant becomes a real workflow.
Read state
Server-persisted (cross-device). POST /api/notifications/{id}/read on click; POST /api/notifications/read-all on the "Mark all read" button. Optimistic UI with rollback on 4xx/5xx.
Open follow-ups (design)
-
Settings cog in the panel header — currently inert. Decide v1 scope: silence-by-type? quiet hours? defer to a second design pass.
-
Empty-state per tab: the design has one ("You’re all caught up") for the All tab. Tab-specific copy ("No unread", "No mentions") could be small wins.
-
Long-list pagination: panel caps at 440 px height. "View all" link to a dedicated
/notificationspage is a candidate; not designed yet.
Open Questions
Resolved 2026-04-27 (round 2)
| Question | Resolution |
|---|---|
Affiliates state |
Designed: disabled row, |
Memberships landing |
Mirror Events landing pattern (E05). Not a design priority right now. |
Public landing scope |
Extracted to C03 — confirmed in design. |
Super-admin claim |
|
Last-context persistence |
Browser localStorage (per-user, per-browser). |
Tenant glance composite |
Belongs to E05, not C01. Endpoint design follows the gateway-side composite recommendation. |
Notifications structure |
Designed: bell with unread-count badge, 380 px panel, tabs (All/Unread/Mentions), per-row icon/title/body/source/timestamp, read-state row tinting, mark-all-read. |
Still open
-
Recent events on tenant switch. When the user switches tenant, jump straight to the last-used entity in the destination tenant, or drop them at the workspace overview? Belongs to E05 (workspace overview), not C01.
-
Bulk-action placement. Where do bulk actions live on list pages (participants, members, results)? Out of C01 scope; affects E02 / M02 / E04.
-
Secondary entity drill-down pattern. A Race nested under an Event — does the title panel nest, swap, or breadcrumb-only? Out of C01 scope; affects E03 / E04.
-
Single-tenant fast path UI. Confirm the tenant chip stays present-but-static for single-tenant users. Alternative: hide the chevron + dropdown affordance.
-
Notifications: settings cog scope. What goes in the settings panel? Defer to a follow-up design pass.
Forward-Engineering Backlog
All items now ADO-tracked under Epic #533 (Admin Portal):
| Concern | Detail | ADO |
|---|---|---|
|
Define |
Feature #534 |
|
Boolean flag on |
US #536 |
|
Optional field; when present + valid, mint with this |
US #537 |
|
Returns user’s accessible orgs |
US #538 |
|
Enum-set column ( |
Open ticket — file under Epic #533 when first needed by a screen. |
Notifications backend |
|
Open ticket — file under Epic #533 when WS5 nears. |
Tenant glance composite (gateway) |
Owned by E05 (workspace landing). Not C01. |
Belongs to E05’s API surface. |
Browser-local persistence |
Last-context + sidebar-collapse — localStorage. No backend ticket. |
n/a |
Reference Implementation
| File | Role |
|---|---|
|
Cascading sidebar — single component parameterised by tenant / workspace / mode |
|
Topbar with breadcrumb / Jump-to / notifications bell |
|
Notifications panel (380 px dropdown) |
|
Reads |
|
Reads |
|
Wraps localStorage for last tenant + workspace + entity |
|
Workspace accent colour CSS custom properties + dev banner |
Notes for Implementation
-
Place shell components under
admin-portal-app/src/app/core/shell/. Workspace landings (E05 etc.) live underfeatures/<workspace>/. -
The cascading sidebar is one component parameterised by
tenant,workspace,mode. Mode-specific nav arrays are TypeScript consts, not Angular-route-derived. -
TenantContextServiceis the canonical client of/api/session/*endpoints. Sidebar binds to its observables. -
Sidebar collapse — Signal-based; persisted in localStorage on change; read at app init.
-
Last-context — write
lastContext = { tenantId, workspace, entityType, entityId, label, timestamp }on every entity-screen navigation, keyed by user. Read on bootstrap. Workspace landings (E05 etc.) read this for their Resume bars. -
Workspace accent colours — define as CSS custom properties at the root, e.g.
--ems-events: #4f46e5. Tag/Dot components consume the property. -
NotificationsPanelComponentopens with a Material-CDK overlay or Angular CDK overlay; not a custom positioning solution.
Change History
| Date | Change |
|---|---|
2026-04-27 (rev 3 — handoff-ready) |
Stripped Events-specific landing content (Resume / Active / Glance / Attention) — moved to E05. Notifications section rewritten as "designed" (bell with unread-count badge, 380 px panel with tabs, per-row anatomy, sample types, read-state semantics) reverse-engineered from the 2026-04-27 design pass. Public landing extraction confirmed → C03. Affiliates "Coming soon" tooltip implementation matches design. Forward-engineering items linked to ADO Epic #533 (Features #534/#535, USs #536–538). Status promoted to handoff-ready: structural shell is settled. |
2026-04-27 (rev 2) |
User decisions absorbed: Affiliates "Coming soon" tooltip; Memberships landing mirrors Events; public-landing extracts to C03; super-admin = JWT flag (not org); last-context = localStorage v1; tenant glance = gateway composite. |
2026-04-27 |
Initial reverse-engineered draft from Claude Design canvas. Status: in-design. |