Skip to main content

Admin Dashboard IA — Expected Output Spec (Cross-cutting)

This doc is the Layer-3 acceptance spec for the admin dashboard at /admin/[token]. It defines exactly what every plan × role combination should see — what tabs, what sub-sections, what buttons, what copy. It is the test target every Playwright spec under e2e/admin-*.spec.ts must match.

Scope: all plans, all roles, all touchpoints inside /admin/[token]. Authority: the RBAC capability list in src/lib/rbac.ts and plan predicates in src/lib/tier-config.ts are the implementation-level sources of truth. This spec is the customer-facing contract derived from them.


1. The shell — what every admin loads

When a magic-link lands on /admin/[token] and auth validates:

┌──────────────────────────────────────────────────────────────────────────┐
│ 🪶 ChurchWiseAI [tabs] ⚙️ 🔔 [Upgrade?] │
│ ──── │
└──────────────────────────────────────────────────────────────────────────┘

[active tab content renders here]

Shell elements (from left to right):

ElementComponent / SourceCapabilityPlan predicateFallback
Logo + "ChurchWiseAI" wordmarkAdminHeaderalways shownalways
Top-level tabsALL_TABS filtered by canSeeTabLocal() + plan filtersper tab (see §2)per tabhidden tabs absent from DOM
Gear icon (opens Settings slide-over)SettingsSlideOver triggerroleCanSeeAnySettings(role) && canSeeTabLocal('settings')alwayshidden
Bell icon (notifications)NotificationBell (P2)planned; P1 renders icon but no panelalwaysP1: icon only, no-op
Upgrade buttonUpgradeCTAalways when applicableupgrade_path_exists(plan) starter+hidden if suite or no path

Header height: 56px sticky on desktop, 56px sticky on mobile.

Tab underline: active tab gets 2px --sacred-gold underline. Inactive tabs are --stone-500.

Role lockout: if member has zero capabilities (orphan member, group deleted mid-session), shell still renders but all tabs absent; center area shows: "Your account has no active permissions — ask your admin."


2. The 4 top-level tabs — per-plan visibility

Universal nav shape: Home · Inbox · Train AI · Website. Each tab gated on capability + plan predicate.

2.1 Visibility matrix per plan

PlanHomeInboxTrain AIWebsiteNotes
cwa_starter_chatNo Website; Inbox has no Calls chip
cwa_pro_chatSame, + Pro-tier Train AI features
cwa_starter_voiceNo Visitors chip in Inbox
cwa_pro_voiceSame, + Pro voice features
cwa_starter_bothAll Inbox chips
cwa_pro_bothAll Inbox chips, Pro everywhere
cwa_suite_bothWebsite bundled in Suite
cwa_bundleAlias of suite_both
cwa_pro_websitePro Website IS a chatbot customer
legacy ps_pro_websiteSame as cwa_pro_website
legacy pro_websiteSame
legacy ps_premiumDirectory listing only

2.2 Visibility matrix per role (template groups)

TemplateHomeInboxTrain AIWebsiteSettings gear
admin✅ full✅ full✅ full✅ (if plan)✅ all 5 sub-tabs incl. Billing
pastor✅ full✅ full✅ full✅ (if plan, no publish-destructive)✅ 4 sub-tabs (no Billing)
office_admin✅ full✅ full✅ 3 sub-sections (Agents, FAQs, Safety)✅ sections + preview (no design, no publish)✅ 4 sub-tabs
care_team✅ no-financial✅ prayer+visitor+callback chips, no Calls/Safety❌ absent❌ absent✅ Account only
prayer_team✅ stripped (overview + metrics + share_link)✅ Prayer chip only❌ absent❌ absent✅ Account only
treasurer✅ financial only❌ absent❌ absent❌ absent✅ Billing only
volunteer_coordinator✅ no-financial✅ visitor + callback chips only, no Prayer/Calls/Safety❌ absent❌ absent✅ Account only
worship_team✅ view-only stripped❌ absent✅ Pastor Pulse only❌ absent✅ Account only
usher_team✅ share_link + overview✅ Visitor chip only❌ absent❌ absent✅ Account only
kids_ministry✅ no-financial✅ visitor + callback chips only❌ absent❌ absent✅ Account only
youth_ministry✅ no-financial✅ visitor + callback chips only❌ absent❌ absent✅ Account only
tech_team✅ share_link + overview❌ absent✅ Simulator only✅ sections + design + preview + media (no publish)✅ Integrations + Sharing

Cell-by-cell source of truth: knowledge/architecture/admin-nav-capability-map.md §7 (plan × tab, 91 cells) and §8 (role × cap, 650 cells).


3. Home tab — expected outputs

3.1 Default layout (populated state, any plan + admin role)

Left column (~62%):

  • Welcome card. Welcome back, {member_name}. (Playfair 30px). {church_name} on next line (body 18px). — if memberName is null, renders Welcome back.
  • Share links card. Conditional content:
    • If hasProWebsite(plan): shows • {slug}.john316.church with [↗ Preview] [📋 Copy link] buttons.
    • If hasChat(plan): shows • Chat widget: [Copy code] button.
    • If hasVoice(plan): shows • Voice: +1 (XXX) XXX-XXXX with [📋 Copy number].
    • If none of the above (directory-only plan): card absent.
  • This week stats strip. Mini stat cards in a row, 1–4 depending on plan:
    • Always (if home:metrics:view): N calls if hasVoice(plan).
    • Always (if home:metrics:view): N prayers if hasChat(plan) || hasVoice(plan).
    • Always (if home:metrics:view): N visitors if hasChat(plan).
    • If hasVoice(plan): N callbacks.
    • If home:metrics:financial:view present AND plan has giving data: $X given this week.
  • Example conversation card (fresh accounts only, last 24h, 0 real requests): pre-seeded "EXAMPLE" card with removable ribbon.
  • Contextual CTA (ends column): "Your agents are ready. Share your link to see activity here."

Right column (~38%):

  • Setup checklist rail. Plan-aware step list (see §3.2). Progress bar at top: {N} of {M} done · {pct}%. Dismissible via [Hide this checklist] button → collapses to Setup {pct}% ▸ badge. State persists in localStorage key cwai-setup-rail-collapsed.
  • Treasurer variant: right column instead shows a Billing at a glance card with Next invoice: {date} · ${amount} and [Manage billing →] (opens Settings slide-over → Billing sub-tab).
  • Prayer_team variant: right column instead shows Recent prayer requests (3 most recent, each links to Inbox Prayer chip).

3.2 Setup checklist steps per plan

PlanUniversal stepsPlan-specific steps
cwa_starter_chat / cwa_pro_chatChurch name · Pastor tone · Theology tradition · Write 3 FAQs · Send share link · Invite teamUpload chatbot logo · Configure widget colors
cwa_starter_voice / cwa_pro_voicesame 6 +Record greeting · Set business hours · Test call
*_both (starter/pro/suite/bundle)same 6 +All 5 plan-specific from chat + voice
cwa_pro_websitesame 6 +Pick a theme · Upload hero photo · Add service times · Publish your site
Suite with Websitesame 6 +All from both + Website set

3.3 Empty-state copy (0 activity, fresh account)

Plan channelEmpty-state bodyCTA
chat-only"No activity yet. Share your chatbot link to start engaging visitors."[📋 Copy share link]
voice-only"Your voice line is live. Share your number: +1 XXX-XXX-XXXX."[📋 Copy number]
both"Your agents are ready. Share your links to start meeting people."[View share links] (scrolls to Share card)
Pro Website"Your site preview is up. Publish when you're ready to share."[Go to Website] (switches tabs)

4. Inbox tab — expected outputs

4.1 Filter chip row (per plan × role)

Filter chips rendered in this fixed order when present: All · Calls · Prayer Requests · Visitors · Callbacks · Safety. All is present if the user has access to at least 2 other chips.

PlanRoleChips rendered
cwa_starter_chatadminAll · Prayer Requests · Visitors · Safety
cwa_pro_chatadminAll · Prayer Requests · Visitors · Safety
cwa_starter_voiceadminAll · Calls · Prayer Requests · Callbacks · Safety
cwa_pro_voiceadminAll · Calls · Prayer Requests · Callbacks · Safety
cwa_starter_both / cwa_pro_bothadminAll · Calls · Prayer Requests · Visitors · Callbacks · Safety
cwa_suite_both / cwa_bundleadminAll · Calls · Prayer Requests · Visitors · Callbacks · Safety
cwa_pro_websiteadminAll · Prayer Requests · Visitors · Safety (Pro Website has chat only)
anyprayer_teamPrayer Requests (ONLY; no All chip)
anycare_teamAll · Prayer Requests · Visitors · Callbacks (no Calls, no Safety)
anyvolunteer_coordinatorAll · Visitors · Callbacks
anyusher_teamVisitors (ONLY)
anykids_ministry / youth_ministryAll · Visitors · Callbacks
anytreasurerInbox tab absent; direct URL redirects to Home with toast "Your role doesn't have Inbox access — ask your admin."

4.2 Card content per item type

Call card:

  • Icon: Lucide Phone in --navy
  • Title: Call — {formatted_phone} · {duration}
  • Body: AI summary (first 80 chars + ellipsis)
  • Actions (conditional):
    • [View transcript] — requires inbox:calls:transcript
    • [Log pastoral follow-up] — always present (writes to voice_callback_requests)
    • [Delete] — requires inbox:calls:delete (admin only)

Prayer card:

  • Icon: Lucide Heart in --sacred-gold
  • Title: Prayer request — {submitter_name} OR Prayer request — anonymous
  • Body: prayer text. Redacted to "Confidential — contact the pastor" if is_confidential AND user lacks inbox:prayer:read:confidential.
  • Actions:
    • [Reply] — writes to voice_prayer_requests.reply_text
    • [Mark prayed for] — requires inbox:prayer:update, stamps prayed_by_member_id
    • [Assign to prayer team] — requires inbox:prayer:assign (admin + office_admin + pastor)
    • [Send care message] — requires inbox:prayer:care_message (admin + office_admin + pastor + care_team)

Visitor card:

  • Icon: Lucide UserPlus in --success
  • Title: Visitor contact — {name || "anonymous"}
  • Body: message text
  • Actions: [Send welcome], [Schedule follow-up], [Mark first visit], [Send care message] (requires inbox:prayer:care_message)

Callback card:

  • Icon: Lucide PhoneOutgoing in --navy
  • Title: Callback requested — {caller_name}
  • Body: reason text. Redacted to "Pastoral inquiry" if user lacks inbox:callback:read:reason.
  • Actions: [Call back] (opens tel: link), [Mark resolved], [Send care message] (requires inbox:prayer:care_message)

Safety card:

  • Icon: Lucide ShieldAlert in --danger
  • Title: Safety flag — {category} (crisis / DV / threat / abuse)
  • Body: one-line summary only (full transcript link)
  • Actions: [View context], [Mark resolved] (requires inbox:safety:resolve)

4.3 Empty-state per plan

Plan channelEmpty bodyCTA
chat-only"No chatbot activity yet. Share your link."[📋 Copy share link]
voice-only"No calls yet. Share your phone number."[📋 Copy number]
both"No activity yet. Share your links."[View share links]
Pro Website"No chatbot activity yet. Share your site."[📋 Copy site link]

Empty state renders an 80px illustrated Lucide icon centered above body text (Phone for voice-only, MessageSquare for chat-only, Inbox for both).

4.4 Day dividers

Items grouped by day, most-recent first. Divider text uppercase, --stone-500, --tracking-wider, --text-xs: TODAY, YESTERDAY, MONDAY APR 14, etc.

4.5 Filter popover (Inbox sprint PR 1/5, 2026-04-19)

A Filters button to the right of the "Inbox" headline opens a popover that narrows the merged stream by time range, full-text search, per-type flags, and read state. Filter state is entirely client-side — the server still returns the full stream.

Button state:

  • Unpressed: [Filters] (stone-700, stone-200 ring).
  • Any filter active: [Filters 2] — navy ring + navy badge with the count of active filter groups.

Popover contents (top-to-bottom):

  • Header: Filters + [Clear all] (disabled when no filters active).
  • Search input with Lucide Search icon. Clear × appears when input has value.
  • Time range pill group — Today, Past 7 days, Past 30 days, All time, Custom range. Custom reveals two date inputs.
  • Status fieldset — single checkbox "Unread only" (always visible; every row type carries read state).
  • Show only fieldset — per-type flags, each auto-hidden when the matching chip isn't available to the viewer:
    • "Confidential prayer requests" (when Prayer chip is available)
    • "Visitors who requested follow-up" (when Visitor chip is available)
    • "Urgent callbacks" (when Callback chip is available)

Layering order in InboxTab:

mergedItems
→ applyInboxFilters(items, filters) // this popover
→ filterStreamByChip(items, activeChip) // the pill chip row
→ groupByDay(items)

Dismissal: ESC, click-outside (overlay), or toggling the Filters button.

Acceptance criteria (PR 1/5):

  • AC 40: Filters button opens a popover anchored to the button. aria-haspopup="dialog", aria-expanded reflects open state.
  • AC 41: Time range pills are mutually exclusive (aria-pressed flips). Custom reveals From/To inputs.
  • AC 42: Search filter narrows the stream in real time. Searching "confidential" does NOT surface prayer rows whose body has been redacted to the fixed "Confidential — contact the pastor" string.
  • AC 43: Clear all resets every filter group and disables itself.
  • AC 44: Per-type flag auto-hides when the corresponding chip isn't available to the viewer.

4.6 Per-card read/unread state (Inbox sprint PR 3/5, 2026-04-19)

Every inbox card carries a click-to-toggle read-state dot in its top-right corner. Read state is per-viewer — what Sarah has seen doesn't clear it for Pastor Jim.

Visual treatment:

  • Unread card: white bg, font-semibold title, gold dot (--sacred-gold) top-right.
  • Read card: bg-stone-50/60, font-medium stone-700 title, stone-400 check icon top-right.
  • Items with no state row yet (is_read === undefined) render as unread — the pastor hasn't acknowledged them.

Persistence: Click fires POST /api/inbox/mark-read with {item_type, item_id, read: boolean}. Optimistic update flips instantly; fetch settles. On error, the local override reverts and the dot swaps back.

Viewer identity: memberId for team members, premium.id for primary admins. Both are stable UUIDs in disjoint spaces, stored in inbox_item_state.read_by as a JSONB array of {member_id, read_at} objects.

Acceptance criteria (PR 3/5):

  • AC 45: Every card renders a read-toggle button with aria-pressed, aria-label, and data-testid="inbox-item-read-toggle".
  • AC 46: Clicking the toggle flips the card visual + fires POST /api/inbox/mark-read with the right payload. On error the UI reverts.
  • AC 47: Filter popover "Unread only" hides cards with is_read === true; is_read === undefined counts as unread (fresh accounts start fully-unread).
  • AC 48: Re-clicking a previously-read dot marks it unread and preserves the original read_at timestamp per the RPC's idempotency guard.

4.7 Assignee picker (Inbox sprint PR 4/5, 2026-04-19)

Every inbox card renders an inline "Assigned: …" / "Unassigned" chip below the action row. Clicking the chip opens a team-member picker. Team-wide field: one row in inbox_item_state.assigned_to per item.

Visual treatment:

  • Unassigned: stone chip [👤 Unassigned].
  • Assigned: navy-tint chip [👤 Assigned: Sarah Chen].
  • Read-only (viewer lacks inbox:item:assign): flat stone chip with no dropdown affordance. Still shows current assignee name for awareness.

Picker contents:

  • Scrollable list of active team members (church_team_members.is_active=true, limit 50).
  • Empty-team copy: "No team members yet. Invite someone from Settings → Team."
  • Footer "Unassign" button appears only when the item is currently assigned.

Persistence: POST /api/inbox/assign with {item_type, item_id, assigned_to: uuid | null}. Gated on inbox:item:assign (admin + office_admin by default). Cross-church assignment is rejected server-side.

Dismissal: ESC or click-outside.

Acceptance criteria (PR 4/5):

  • AC 49: Cards render the picker chip when viewer has inbox:item:assign OR the item is already assigned (label visible to anyone who can read the card).
  • AC 50: Viewer without the cap sees a flat chip with no dropdown; attempting to POST /api/inbox/assign returns 403.
  • AC 51: Picking a team member fires POST /api/inbox/assign with assigned_to=<uuid>; optimistic UI updates immediately; errors revert the chip.
  • AC 52: "Unassign" footer sends assigned_to=null and the chip snaps back to "Unassigned".
  • AC 53: assigned_to UUID must reference an active team member of THIS church; cross-church assignment returns 400.

5. Train AI tab — expected outputs

5.1 Layout — 2-column left-rail pattern

Left rail ~220px (section list). Right main ~1220px (sub-section content). No live preview column.

5.2 Sub-sections per capability

Sub-sections rendered in left rail only if user has the matching train:* capability:

Sub-sectionCapabilityFirst-visible actions
Church Knowledgetrain:church_knowledge:editDocument list + [+ Upload document] button (requires train:documents:upload)
Theology & Traditiontrain:theology:editTheoLens picker (17 radio options) + vocabulary boost list
Agent Personalitytrain:agents:editTone radio (Warm/Professional/Casual) + pastor voice textarea + greeting script textarea + escalation rules
FAQstrain:faqs:editQ&A list + [+ Add FAQ] + priority slider. Warning banner if >3 rows share same answer text (DB trigger protects).
Safety Rulestrain:safety:editCrisis keyword list + escalation contacts + moderation sensitivity slider
Chat Simulatortrain:simulator:useSplit pane: test prompt input (left) + diagnostic panel (right, P2 feature). P1 renders shell + "Coming in P2" placeholder.
Pastor Pulsetrain:pastor_pulse:editThis week's sermon topic + theme verse (used by AI context)

5.3 Tab absent when

  • Member has zero train:* caps (care_team, prayer_team, treasurer, usher_team, kids, youth all fall here).
  • Plan has neither chat nor voice (no paying plan today fits — Pro Website IS chat).

5.4 Pro Website note banner

On Agent Personality sub-section for plans with Pro Website, a banner renders: "The tone you set here also drives your Pro Website hero greeting."


6. Website tab — expected outputs

6.1 Visibility

  • Visible only if hasProWebsite(plan) returns true.
  • Visible only if user has ANY website:* capability (admin, pastor, office_admin, tech_team).

6.2 Desktop layout — 3-column

Left rail ~18% (section list: Hero · About · Services · Beliefs · Staff · Contact · + Add section · Theme controls). Center ~60% (live preview of /s/{slug}). Right ~22% (publish panel: Draft saved HH:MM + [Publish changes] gold button + [View live →] + "N unsaved sections" counter).

6.3 Save discipline

  • Publish ≠ Save. Section edits persist as draft. [Publish changes] is a separate action.
  • Publish button disabled (--stone-400 + no hover) when no unsaved changes.
  • Preview top-right shows amber pill "Draft — not yet public" when unsaved changes exist.
  • Post-publish: panel shows Published HH:MM · View live →.

6.4 Capability granularity

ActionCapabilityRole examples
Section editwebsite:sections:editadmin, pastor, office_admin, tech_team
Design (theme, colors, fonts)website:design:editadmin, pastor, tech_team (NOT office_admin)
Publishwebsite:publishadmin, pastor only
Previewwebsite:previewall Website-capable roles
Media uploadwebsite:media:uploadadmin, pastor, office_admin, tech_team

6.5 Mobile state

Viewport <640px: Website tab shows "Edit on desktop" notice with Lucide Monitor icon + [View live site →] + [Copy share link] buttons. No editor on mobile (P2 decision).


7. Settings slide-over — expected outputs

Opens from header gear icon. Slides from right on desktop (480px wide); bottom sheet on mobile (full-width slide-up).

7.1 Sub-tab visibility per role

RoleAccountTeamNotificationsIntegrationsBilling
admin
pastor
office_admin
care_team
prayer_team
treasurer
volunteer_coordinator
worship_team
usher_team
kids_ministry
youth_ministry
tech_team

Gear icon itself absent if user has zero settings:* AND no billing:view.

7.2 Save discipline (universal rule)

  • Radio / toggle / dropdown — autosave + inline ● Saved HH:MM AM/PM (sacred-gold dot + stone-500 text). Never silent.
  • Text field / textarea — explicit [Save] button, hidden until dirty. Amber Unsaved change micro-label in field corner. On save: button disappears, label swaps to ● Saved HH:MM AM/PM.
  • Close slide-over with dirty text field: confirmation modal "Save your changes before closing?"

All legacy hash routes preserved, opening slide-over to matching sub-tab:

HashOpensSub-tab
#basic / #contact / #photo-setupslide-overAccount
#hours / #availabilityslide-overAccount (scrolled to Hours)
#teamslide-overTeam
#notificationsslide-overNotifications
#integrationsslide-overIntegrations
#sharingslide-overAccount (scrolled to Sharing)

8. Mobile bottom nav — expected outputs

Viewport <640px. Sticky bottom, 72px + env(safe-area-inset-bottom) padding.

8.1 Tab count per plan

PlanTabs rendered
chat-onlyHome · Inbox · Train AI (3)
voice-onlyHome · Inbox · Train AI (3)
bothHome · Inbox · Train AI (3)
Pro Website / Suite-with-websiteHome · Inbox · Train AI · Website (4)

8.2 Below 360px viewport

Fall back to 3-tab nav + overflow menu for any 4th tab (Website). Overflow = Lucide MoreHorizontal icon.

8.3 Header on mobile

56px sticky top. Logo left, ⚙️ 🔔 right (gear + bell, 20px each, --stone-700, 16px gap). Bell badge if unread >0: 16px circle, --sacred-gold bg, white number.


9. Upgrade CTA — expected outputs

9.1 Placement

  • Desktop header: pill button right-most, only if upgrade_path_exists(plan) is true. Starter → Pro, Pro → Suite, Chat-only → Bundle, Voice-only → Bundle.
  • Mobile: contextual toast on Home, dismissible. NEVER in bottom nav.
  • Suite customers: no Upgrade CTA anywhere.

9.2 Copy per plan

Current planUpgrade CTA
cwa_starter_chatUpgrade to Pro — add voice
cwa_starter_voiceUpgrade to Pro — add chat
cwa_starter_bothUpgrade to Pro
cwa_pro_chatUpgrade — add voice
cwa_pro_voiceUpgrade — add chat
cwa_pro_bothUpgrade to Suite — add website
cwa_pro_websiteUpgrade — add voice + chat agents
Suite / Bundlehidden

10. Acceptance criteria (Given/When/Then)

Testable against every Playwright admin spec. Derived from design spec §8 + cap-map §7-8 + sprint plan cross-cutting rules.

10.1 Tab visibility

  1. Given role with zero inbox:* caps, when page loads, then Inbox tab is ABSENT from DOM.
  2. Given cwa_starter_chat plan + admin role, when page loads, then nav is exactly Home · Inbox · Train AI.
  3. Given cwa_pro_website plan + admin role, when page loads, then nav is exactly Home · Inbox · Train AI · Website.
  4. Given treasurer role on any plan, when page loads, then nav is exactly Home (no other tabs).
  5. Given prayer_team role, when page loads, then Inbox tab present but only shows Prayer Requests chip.

10.2 Capability fallback

  1. Given member with empty group_ids AND capabilities, when page loads, then legacyRoleToCapabilities(role) is used for tab gating.
  2. Given member with only role='admin' and empty group_ids, when page loads, then nav shows the same tabs as a cap-driven admin.
  1. Given cwa_pro_both plan, when Home loads, then Share card shows Chat widget code AND Voice number.
  2. Given cwa_starter_chat on day 0, 0 requests, when Home loads, then example conversation card renders with EXAMPLE ribbon.
  3. Given user dismisses example card, when they reload, then card stays dismissed (localStorage).

10.4 Inbox chips

  1. Given voice-only plan, when Inbox opens, then Visitors chip is absent.
  2. Given chat-only plan, when Inbox opens, then Calls and Callbacks chips are absent.
  3. Given confidential prayer AND user lacks inbox:prayer:read:confidential, when card renders, then body text is "Confidential — contact the pastor".
  4. Given user clicks Safety chip without inbox:safety:read, chip is absent from DOM entirely.

10.5 Train AI

  1. Given office_admin role, when Train AI opens, then left rail shows exactly Agent Personality, FAQs, Safety Rules (no Church Knowledge, no Theology, no Simulator, no Pastor Pulse).
  2. Given Pro Website plan, when Agent Personality sub-section opens, then the banner "The tone you set here also drives your Pro Website hero greeting." is visible.

10.6 Website

  1. Given chat-only plan, when page loads, then Website tab is ABSENT.
  2. Given user edits a section without saving, when preview updates, then right panel shows "1 unsaved section" and Publish button is disabled.
  3. Given user clicks Publish then confirms, when publish completes, then right panel shows "Published HH:MM · View live →".
  4. Given mobile viewport, when Website tab opens, then "Edit on desktop" notice renders instead of the editor.

10.7 Settings slide-over

  1. Given no settings caps AND no billing:view, when page loads, then gear icon is ABSENT from header.
  2. Given treasurer role, when gear opens, then only Billing sub-tab is visible.
  3. Given URL hash #notifications, when page loads, then gear opens slide-over directly to Notifications.
  4. Given mobile viewport, when gear tapped, then slide-over fills screen as bottom sheet.
  5. Given text field dirty, when user closes slide-over, then confirm-before-close modal fires.
  6. Given radio changed, when change detected, then autosaves within 300ms AND timestamp renders.

10.8 Mobile bottom nav

  1. Given chat-only plan + viewport <640px, when nav renders, then exactly 3 tabs (Home, Inbox, Train AI).
  2. Given Pro Website plan + viewport <640px, when nav renders, then exactly 4 tabs.
  3. Given viewport <360px + Pro Website plan, when nav renders, then 3 tabs + overflow menu containing Website.
  4. Given user taps active tab, when tap fires, then scroll-to-top within active tab.

10.9 Upgrade CTA

  1. Given cwa_suite_both plan, when page loads, then Upgrade CTA is ABSENT everywhere.
  2. Given cwa_starter_chat plan + desktop, when page loads, then header shows pill Upgrade to Pro — add voice.
  3. Given starter plan + mobile, when page loads, then Upgrade CTA is a dismissible toast on Home, never in bottom nav.

10.10 Edge cases

  1. Given user's group deleted mid-session, when they navigate to a now-gated tab, then page shows "Your account has no active permissions — ask your admin."
  2. Given status='cancelled' subscription, when page loads: (KNOWN GAP FA-046) — customer still sees full dashboard. Planned fix: middleware redirect to Resubscribe page.
  3. Given cross-church token attempt (admin of church A opens church B's /admin/[token]), when middleware runs, then 403 redirect.

11. Change-management rules

  • When the cap list in rbac.ts changes, update §2.2 and the cap-map doc in the SAME PR.
  • When plan predicates in tier-config.ts change, update §2.1 in the SAME PR.
  • When the design system adds a new Lucide icon to admin chrome, reference it in the relevant section here.
  • When a new admin feature ships, add its expected outputs to the relevant section BEFORE writing Playwright tests.

Per-tier specs (starter-chat.md, pro-both.md, cwa-pro-website.md, etc.) hold expected outputs for all customer-facing touchpoints EXCEPT the admin dashboard, which lives in this cross-cutting spec. Their admin-dashboard sub-sections should read:

"Admin dashboard expected outputs live in admin-ia-2026-04-18.md. See that spec for tab visibility, slide-over behavior, and per-role gating for this plan."

Update batch pending — tracked in FOUNDER_ACTIONS.md.


End of spec. This document is the test target for every e2e/admin-*.spec.ts file and the expected-output contract for every admin dashboard feature built in Slices 1–6 of the P1 sprint.