[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 M0x use 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 isSuperAdmin JWT 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

  1. Bootstrap. SPA APP_INITIALIZER calls GET /api/session/current on the gateway. Response: user, currentTenantId, available tenants, isSuperAdmin, last-context (from localStorage).

  2. Restore last context. SPA navigates to lastTenant + lastWorkspace (from localStorage) or to a default workspace (Events if enabled at this tenant) on first visit.

  3. Render shell. Sidebar: tenant switcher → workspace switcher → mode nav → user/collapse footer. Topbar: breadcrumb, Jump-to (⌘K), notifications bell.

  4. 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."

  5. 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 + a Soon pill badge + a "Coming soon" tooltip), and Tenant admin (only if user has TENANT_ADMIN role 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/current returning.

  • 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_ADMIN role on the current tenant.

  • Affiliates workspace always appears in the workspace switcher in a disabled state, with a "Coming soon" tooltip and Soon pill 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 #4f46e5

    Memberships

    teal #0891b2

    Affiliates

    amber #ca8a04

    Tenant admin

    burnt amber #b45309

    Super 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

GET /api/session/current (gateway)

Bootstrap. { user, currentTenantId, availableTenants[], isSuperAdmin, lastContext }. Built on the session-held JWT.

GET /api/session/tenants (gateway)

Refresh available-tenants list. Proxies to admin-service GET /api/org-permissions/current-user (US #538).

POST /api/session/tenant (gateway)

Switch current tenant. Re-mints admin-service JWT via POST /auth/token-exchange/oauth2 with the requested orgId (US #537).

GET /api/notifications (admin-service)

List notifications for the current user, scoped to the current tenant by default. Pagination + filter (unread, mentions).

POST /api/notifications/{id}/read and POST /api/notifications/read-all (admin-service)

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.

  • bell icon, 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 #ef4444 background, white text, ≥15 px tall, rounded-full. Caps at "9+". Absent when unreadCount === 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: 30 so 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 Dot (6 px) when row is unread, empty otherwise. Reads as a soft visual rail of unread items.

Workspace icon

26×26 px rounded square. Background = workspace accent at 9% alpha; foreground = workspace accent. Icon picked per type: user (mention), file (import), shield (role), event (status), arrowRight (comm), swap (membership), trophy (results-published).

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 (#fafbff); read rows are transparent.

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

mention

"Zanele mentioned you" — "@chris can you confirm the start groups for the 109 km?"

<event> · Comments

import

"Participant import completed" — 1,204 rows · 12 warnings · 0 errors

<event> · Imports

role

"Role assigned: Results officer" — Pieter de Villiers granted Results officer

Tenant admin · Users & roles

status

"Event status changed: Setup → Registration open"

<event>

comm

"Bulk email sent — 12,480 recipients" — open-rate %

<event> · Communications

membership

"83 renewals processed"

Memberships · Renewals

status (results)

"Results published"

<event>

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 onSuperTenant prop 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)

  1. Settings cog in the panel header — currently inert. Decide v1 scope: silence-by-type? quiet hours? defer to a second design pass.

  2. 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.

  3. Long-list pagination: panel caps at 440 px height. "View all" link to a dedicated /notifications page is a candidate; not designed yet.

Open Questions

Resolved 2026-04-27 (round 2)

Question Resolution

Affiliates state

Designed: disabled row, Soon pill badge, cursor: not-allowed, "Coming soon" tooltip on hover.

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

isSuperAdmin boolean JWT claim — US #536 captures the implementation.

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

  1. 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.

  2. Bulk-action placement. Where do bulk actions live on list pages (participants, members, results)? Out of C01 scope; affects E02 / M02 / E04.

  3. 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.

  4. Single-tenant fast path UI. Confirm the tenant chip stays present-but-static for single-tenant users. Alternative: hide the chevron + dropdown affordance.

  5. 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

OrgPermission.role enum + JWT claim shape

Define TENANT_ADMIN, EVENT_MANAGER, RESULTS_OFFICER, MEMBERSHIP_ADMIN, STAFF aligned with Spring Security GrantedAuthorities. Mint per-tenant roles into JWT. Drives isTenantAdmin derivation.

Feature #534

isSuperAdmin JWT claim

Boolean flag on OrgUser; minted into JWT by NimbusTokenProvider. Backward-compatible w.r.t. registration-portal.

US #536

requestedOrgId on OAuth2TokenExchangeRequestDTO

Optional field; when present + valid, mint with this orgId. Falls back to registrationSystemId when absent.

US #537

GET /api/org-permissions/current-user endpoint

Returns user’s accessible orgs [{orgId, organisationName, role, accessLevel, enabledModules}]. Cached in Hazelcast.

US #538

Organisation.enabledModules

Enum-set column (EVENT, MEMBERSHIP). Affiliates is platform-level disabled in v1; not yet a tenant flag.

Open ticket — file under Epic #533 when first needed by a screen.

Notifications backend

NotificationResource (or extension of CommunicationLogResource). Best implemented as a reactor on Async Signal & Sweep Framework (Feature #403) once that lands.

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

admin-portal-app/src/app/core/shell/sidebar.component.ts

Cascading sidebar — single component parameterised by tenant / workspace / mode

admin-portal-app/src/app/core/shell/topbar.component.ts

Topbar with breadcrumb / Jump-to / notifications bell

admin-portal-app/src/app/core/shell/notifications-panel.component.ts

Notifications panel (380 px dropdown)

admin-portal-app/src/app/core/services/tenant-context.service.ts

Reads /api/session/current, drives switcher

admin-portal-app/src/app/core/services/notification.service.ts

Reads /api/notifications, manages read state

admin-portal-app/src/app/core/services/last-context.service.ts

Wraps localStorage for last tenant + workspace + entity

@ems/shared-ui design tokens module

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 under features/<workspace>/.

  • The cascading sidebar is one component parameterised by tenant, workspace, mode. Mode-specific nav arrays are TypeScript consts, not Angular-route-derived.

  • TenantContextService is 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.

  • NotificationsPanelComponent opens 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. OrgPermission distinction clarified vs LinkedOrg.accessLevel. Notifications promoted from "later" to a Claude Design ask in the next pass.

2026-04-27

Initial reverse-engineered draft from Claude Design canvas. Status: in-design.