← All stories

TRUNK · admin-shell-access

Admin Shell access & navigation

Persona: Organizer Stage: Foundation Surface: admin.vxge-aperture.porivo.com Branches that root here: 57 (estimated)

An organizer arrives at Voyage with one of three intentions in mind: they're standing up a new event, they're configuring an event already in flight, or they're closing one out. Whatever the intention, the first 30 seconds determine whether they trust the product. The shell has to authenticate them, recognize their workspace, orient them within the surface, and make their next action discoverable — without requiring a manual or a tour.

Happy path

The organizer types admin.vxge-aperture.porivo.com or follows a deep link from an email or chat message. They land on whichever page they intended (the deep link target, or the home dashboard if they came in cold). They are recognized as a logged-in member of their workspace and can see the workspace name in the chrome. They scan the page and within ~5 seconds locate either the action they came to perform or the navigation entry that leads to it.

  1. Navigate to admin.vxge-aperture.porivo.com (or a deep link below it).

    First-time visit: the browser has no Voyage cookies. Returning visit within token TTL: silent auth refreshes. Returning visit past TTL: redirected to Auth0 with the deep-link path captured in state.

  2. Auth0 redirects with a freshly-issued ID token; the shell exchanges it for a workspace team token.

    The workspace team token (WORKSPACE_TEAM_TOKEN cookie or storage state) is the short-lived credential that scopes API calls to the user's tenant. Token TTL is ~5 minutes; the shell silently refreshes ahead of expiry without a flash of "you've been logged out."

  3. The shell renders with three orienting signals: workspace name in the header, the user's identity in the upper right, and a primary nav listing the major sections (Events, Designs, Reports, Integrations, Settings).

    If the user followed a deep link, the matching nav item is highlighted and the page below the chrome is the requested view. If the user came in cold, the home dashboard surfaces a card grid: in-flight events, recent reports, recent activity.

  4. The user finds their next action.

    Three patterns the shell supports: (a) "I have an event ID and I'm jumping to it" — search box at the top by name or ID; (b) "I'm starting something new" — visible Create event button in the header or home card grid; (c) "I'm reviewing recent work" — recent-activity feed in the home dashboard.

Failure modes

Surface-level failures: anything that prevents the shell from rendering or the user from being recognized. Branch stories don't re-test these — they assume the shell renders correctly and the user is authenticated.

Auth0 returns an error during initial login

Trigger: the Auth0 callback returns ?error=... instead of an authorization code (e.g., user denied consent, MFA failed, account disabled).

The shell catches the callback, displays a clean error page (not the bare Auth0 default) explaining what happened in plain language: "We couldn't sign you in to Voyage. Auth0 reported: <error description>." A primary action invites them to retry; a secondary action links to support. The page tells them whether the issue is on Voyage's side, Auth0's side, or their account's side, so they know whether retrying will help.

Recovery: Retry button re-initiates the Auth0 round-trip with the same deep-link state preserved. If retry fails three times, the page surfaces a "Contact support" link with the user's session ID pre-filled.

Deep link refers to a tenant the user can't access

Trigger: user clicks a link to /admin/events/xyz for an event in a workspace they aren't a member of (cross-tenant link, role downgraded, etc.).

The shell resolves auth, then tries the workspace lookup, gets denied. It does NOT show "Event not found" (which would lie about the existence of the event); it shows "You don't have access to this workspace." with the user's currently-recognized workspace listed below. If the user has membership in another workspace, the page offers a switcher; otherwise it offers "Contact your administrator" with a copy-the-event-link button so they can request access.

Recovery: Workspace switcher (if applicable), or a clean way to request access without leaking that the event exists.

Workspace team token mint fails (downstream service down)

Trigger: Auth0 succeeded, but the /api/auth/cli-token exchange returned 503 because the auth worker is down.

The shell shows a warm error page: "Voyage is having trouble starting your session. This usually clears in a minute. We'll keep trying." It auto-retries with exponential backoff (3 attempts over ~30s) before surfacing the manual retry button. Critically, it does NOT lock the user out — they stay on a recognizably-Voyage-branded page (not a generic "Application error"), so they understand it's not their connection.

Recovery: Auto-retry succeeds silently, or the manual retry surfaces. Status page link in the footer if the issue persists.

Token TTL expires while the user is mid-task

Trigger: user has an unsaved form open, walks away for 10 minutes, comes back. The team token has expired.

The shell silently refreshes the token in the background using the Auth0 refresh-token flow. The user does not see a redirect, does not lose their form input, and does not get a "you've been logged out" toast unless the refresh genuinely fails (e.g., they were logged out remotely or their refresh token expired). If refresh fails: the shell preserves their unsaved input in localStorage scoped to the form's path and re-prompts auth; on successful re-auth the form is restored exactly as they left it.

Recovery: Silent refresh is invisible. Hard refresh failures preserve form state across the re-login.

User has zero workspaces / zero permissions in any workspace

Trigger: user successfully authenticates via Auth0 but isn't a member of any Voyage workspace yet.

Instead of dumping them into an empty shell or a 403, the page explains: "You're signed in, but you don't have access to a Voyage workspace yet." It surfaces three paths: request access to an existing workspace (link to a request form), self-serve a new workspace (if entitled), or contact support. The user's email is shown so they know which identity is being asked about — important when they have multiple Google accounts.

Recovery: The user can act on one of the three paths without losing context.

The shell loads but a major section's data fails to fetch

Trigger: the home dashboard's "Recent events" card calls /v1/admin/events and gets 500.

The card itself shows a contained error state — "Couldn't load recent events. Try again." with a refresh icon — but the rest of the shell remains usable. The user can still navigate to other sections, search, create events directly via the header button. Failures are isolated to the failing card; one bad backend doesn't black-hole the whole shell.

Recovery: Per-card retry; the rest of the surface stays alive.

Slow network (3G simulation)

Trigger: user on a poor connection. Initial bundle takes 8s, individual API calls take 3s each.

The shell paints chrome (header, nav) immediately from cached or critical-path assets. Each section card shows a skeleton loading state with an explicit "Loading..." for screen readers. No card is silently empty for >500ms without showing it's loading. Once data arrives, skeletons replace cleanly without layout shift (CLS).

Recovery: Slow but never broken; the user can begin reading the chrome and primary nav before the cards finish.

Edge cases

User is in two workspaces and switches mid-session

Workspace switcher in the header. Switching is a hard navigation (not a soft state change), because tenant context cascades through every API call and trying to mutate it in place creates ghosts in cached data. The user's URL updates to reflect the new workspace; if they were on a deep link not valid in the new workspace, they land on the new workspace's home dashboard instead of a 404.

Browser back button after a workspace switch

Back returns the user to the prior workspace's last page. The history is preserved per workspace; the URL is canonical (workspace slug in path or subdomain), so the back-button behavior matches the user's mental model.

Clipboard-paste of an event ID into the search box

The search box accepts both event names and IDs. Pasting an ID matches it directly. Names are fuzzy-matched. Results render under the search box without a full-page navigation; clicking a result deep-links to that event.

Multiple browser tabs open to different workspaces

Each tab maintains its own workspace context. The shell does not require single-tab usage. localStorage workspace state is scoped per tab via sessionStorage where it would conflict.

Print or screenshot a page

The shell's print stylesheet hides the chrome and prints just the main content. Useful when an organizer wants to share a guest list or report URL with a colleague — they can print to PDF.

Page evaluation

At each surface within the Admin Shell, an agent (or a curious human auditor) should be able to answer these questions in the affirmative. The questions are deliberately the same as a human would ask while running a UX review.

PageDiscoverabilityError UXLayoutOrientation
/login "Sign in" button is the visual focus, no scrolling required, contrast ≥4.5:1. Auth errors render inline below the form, not as a toast. Each error explains what to fix. Centered card, no horizontal scroll at any viewport ≥320px. Voyage logo and tagline above the form so the user knows which product they're signing into.
/admin (home) "Create event" CTA visible above the fold on a 1280×800 viewport. Per-card error states are isolated; the rest of the page is interactive. Card grid reflows to single column at <640px without truncation. Workspace name in header. Page title is "Voyage" or workspace name, not the literal "/admin/index".
/admin/* (any deep link) Active nav item is highlighted. Breadcrumb shows the path. If the deep-link target 404s within the workspace, the page says "Event not found in <workspace>" and offers Search. Main content area scrolls independently of the nav (sticky shell). document.title reflects the current page, not the workspace name alone.
/admin/error Primary action is "Try again," secondary is "Contact support" or "Status page." Error description is human-readable, not a stack trace. Page does not blame the user; tone is "we couldn't" not "you failed". User identity still visible if the error happened post-auth.

Color contrast

All body text against the page background is ≥4.5:1. Pill labels (persona / stage indicators) are ≥3:1 against their pill background. Disabled buttons are visibly disabled, not just slightly faded — disabled + cursor:not-allowed + aria-disabled, with contrast still ≥3:1 so the user knows there's a button there even if they can't click it.

Keyboard navigation

Tab order follows the visual reading order. The skip-to-main link is the first focusable element. Modal dialogs trap focus while open and return focus to the trigger button on close. Esc closes modals.

Screen reader

The header is a <header>, the nav is <nav aria-label="primary">, the main content is a <main>. Card grids on the home dashboard are wrapped in landmarks with descriptive labels. Loading skeletons announce "Loading recent events" via aria-live polite, not assertive (so they don't interrupt screen readers).

Acceptance signals

  • URL is the deep-link target (or /admin if no deep link).
  • Workspace team token cookie or storage state is present and unexpired.
  • Header chrome is rendered: workspace name visible, user identity visible, primary nav visible.
  • document.title is set to a per-page descriptive label (not blank, not "Vite app", not the same across all pages).
  • The primary action of the destination page is visible above the fold at 1280×800.
  • No console errors at severity ≥ warn from Voyage origins (third-party SDK warnings allowed but flagged).
  • Time to interactive (TTI) ≤ 2s on broadband, ≤ 8s on simulated 3G.
  • No layout shifts (CLS) ≥ 0.1 during the first 5 seconds.

Stable test attributes

This trunk's contract — the named data-test attributes the Aperture admin shell MUST expose. Every branch rooted in Admin Shell can rely on these existing without re-declaring them. Removing one is a loosening of every branch that inherits this trunk; the linter blocks merge accordingly.

Visibility teeth. Each attribute below must be present in the document AND effectively visible per the runtime predicate (no display:none, visibility:hidden, opacity:0, zero bounding rect, off-screen positioning, or aria-hidden="true" on it or any ancestor). Where the row's purpose implies interactivity, it must additionally satisfy effectively interactive. Hiding without removal is treated identically to removal — both require the loosening token. See PREDICATES.md for the full runtime definition.

data-testWherePurpose
workspace-nameAdmin shell headerShows the active workspace's display name
user-identityAdmin shell header upper-rightShows the signed-in user's email or name
workspace-switcherAdmin shell headerPresent only if user is in >1 workspace
primary-navAdmin shell sidebar / topbarnav[aria-label="primary"] with Events / Designs / Reports / Integrations / Settings
nav-activeInside primary-navThe currently-active nav item
global-searchAdmin shell headerEvent-name and event-id search box
home-card-eventsPage /adminRecent events card
home-card-reportsPage /adminRecent reports card
home-card-activityPage /adminRecent activity feed card
home-card-errorInside any home-card-*Per-card error state with retry; one per card that failed
home-card-skeletonInside any home-card-*Loading skeleton; aria-live="polite"
auth-error-pagePage /admin/errorWarm error page after Auth0 returns an error
auth-error-retryInside auth-error-pagePrimary CTA — retries Auth0 with deep-link state preserved
auth-error-supportInside auth-error-pageSecondary CTA with prefilled session id
no-access-pagePage /admin/no-accessCross-tenant deep-link landing — does not leak event existence
no-access-switcherInside no-access-pageWorkspace switcher; absent if user has only one workspace
session-recovering-bannerTop of any admin pageVisible during cli-token retry storm; clears on success
request-access-pagePage /admin/request-accessShown when the user has zero workspace memberships
request-access-emailInside request-access-pageDisplays the signed-in email so the user knows which identity is being asked about
skip-to-mainFirst focusable element of every admin pageSkip link for keyboard users

Branches inheriting this trunk write probes that depend on workspace-name, primary-nav, etc. Adding a 58th branch tomorrow does not require it to re-list these attributes; they're inherited.

Agent test plan

This trunk's setup is consumed as a precondition by every branch story rooted in Admin Shell. The agent harness chains these steps before running the branch's specific assertions, so branches never re-implement auth.

Trunk setup steps (expand)
1. Preflight `fastr-auth.sh preflight workspace-aperture` — confirm CLI-token endpoint exists.
   FAIL CLOSED: if preflight is non-zero, abort the run with "auth surface broken, fix workspace config first."
2. Wrap the run in `fastr-auth.sh workspace-aperture --` so AUTH0_TOKEN, WORKSPACE_TEAM_TOKEN, WORKSPACE_BASE_URL are injected.
3. Open Playwright context. setExtraHTTPHeaders Authorization Bearer ${AUTH0_TOKEN}.
   addCookies __fastr_workspace_token = ${WORKSPACE_TEAM_TOKEN} on ${WORKSPACE_BASE_URL} hostname.
4. page.goto(${WORKSPACE_BASE_URL}). Wait for header chrome to render: ['header', 'nav[aria-label="primary"]', '[data-test=workspace-name]'].
5. Capture page-state snapshot: URL, document.title, console errors, screenshot.
6. Assert authMode = 'silent-refresh' or 'interactive' — never 'bypassed'.
7. If any of the assertions in §acceptance fail, the trunk fails closed and no branch attempts to run.
Failure-mode probes (expand)
For each failure mode in §failure-modes, the harness has a corresponding probe:
- Auth0 error: inject ?error=consent_required into the callback, assert error page renders, retry button works.
- Cross-tenant deep link: navigate to /admin/events/<known-other-workspace-event-id>, assert "no access" page (not "not found").
- Token mint fails: stub the cli-token endpoint to return 503 once, assert auto-retry → success.
- Token TTL expires: clock.tick past expiry, assert silent refresh, no redirect.
- Zero workspaces: stub the workspaces endpoint to return [], assert the "you don't have access yet" page.
- Section data fails: stub /v1/admin/events to return 500, assert per-card error + rest of shell still navigable.
- Slow network: throttle network to 3G profile, assert skeletons render within 500ms, TTI < 8s.
Page-evaluation assertions (expand)
For each row in §page-evaluation, the harness:
- Computes axe-core results, asserts no violations of impact 'serious' or 'critical'.
- Computes color contrast for body text and pill labels via the PostCSS calc + WCAG formula.
- Tabs through the focusable elements and asserts the order matches the visual reading order top-to-bottom.
- Triggers Esc on each modal observed and asserts focus returns to the trigger.
- Crawls headings (h1..h6) and asserts no skip levels.
- Reads document.title at each navigation and asserts it's per-page-distinct.