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):
| Element | Component / Source | Capability | Plan predicate | Fallback |
|---|---|---|---|---|
| Logo + "ChurchWiseAI" wordmark | AdminHeader | always shown | always | — |
| Top-level tabs | ALL_TABS filtered by canSeeTabLocal() + plan filters | per tab (see §2) | per tab | hidden tabs absent from DOM |
| Gear icon (opens Settings slide-over) | SettingsSlideOver trigger | roleCanSeeAnySettings(role) && canSeeTabLocal('settings') | always | hidden |
| Bell icon (notifications) | NotificationBell (P2) | planned; P1 renders icon but no panel | always | P1: icon only, no-op |
| Upgrade button | UpgradeCTA | always when applicable | upgrade_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
| Plan | Home | Inbox | Train AI | Website | Notes |
|---|---|---|---|---|---|
cwa_starter_chat | ✅ | ✅ | ✅ | ❌ | No Website; Inbox has no Calls chip |
cwa_pro_chat | ✅ | ✅ | ✅ | ❌ | Same, + Pro-tier Train AI features |
cwa_starter_voice | ✅ | ✅ | ✅ | ❌ | No Visitors chip in Inbox |
cwa_pro_voice | ✅ | ✅ | ✅ | ❌ | Same, + Pro voice features |
cwa_starter_both | ✅ | ✅ | ✅ | ❌ | All Inbox chips |
cwa_pro_both | ✅ | ✅ | ✅ | ❌ | All Inbox chips, Pro everywhere |
cwa_suite_both | ✅ | ✅ | ✅ | ✅ | Website bundled in Suite |
cwa_bundle | ✅ | ✅ | ✅ | ✅ | Alias of suite_both |
cwa_pro_website | ✅ | ✅ | ✅ | ✅ | Pro Website IS a chatbot customer |
legacy ps_pro_website | ✅ | ✅ | ✅ | ✅ | Same as cwa_pro_website |
legacy pro_website | ✅ | ✅ | ✅ | ✅ | Same |
legacy ps_premium | ✅ | ✅ | ✅ | ❌ | Directory listing only |
2.2 Visibility matrix per role (template groups)
| Template | Home | Inbox | Train AI | Website | Settings 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). — ifmemberNameis null, rendersWelcome back. - Share links card. Conditional content:
- If
hasProWebsite(plan): shows• {slug}.john316.churchwith[↗ Preview][📋 Copy link]buttons. - If
hasChat(plan): shows• Chat widget: [Copy code]button. - If
hasVoice(plan): shows• Voice: +1 (XXX) XXX-XXXXwith[📋 Copy number]. - If none of the above (directory-only plan): card absent.
- If
- This week stats strip. Mini stat cards in a row, 1–4 depending on plan:
- Always (if
home:metrics:view):N callsifhasVoice(plan). - Always (if
home:metrics:view):N prayersifhasChat(plan) || hasVoice(plan). - Always (if
home:metrics:view):N visitorsifhasChat(plan). - If
hasVoice(plan):N callbacks. - If
home:metrics:financial:viewpresent AND plan has giving data:$X given this week.
- Always (if
- 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 toSetup {pct}% ▸badge. State persists inlocalStoragekeycwai-setup-rail-collapsed. - Treasurer variant: right column instead shows a
Billing at a glancecard withNext 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
| Plan | Universal steps | Plan-specific steps |
|---|---|---|
cwa_starter_chat / cwa_pro_chat | Church name · Pastor tone · Theology tradition · Write 3 FAQs · Send share link · Invite team | Upload chatbot logo · Configure widget colors |
cwa_starter_voice / cwa_pro_voice | same 6 + | Record greeting · Set business hours · Test call |
*_both (starter/pro/suite/bundle) | same 6 + | All 5 plan-specific from chat + voice |
cwa_pro_website | same 6 + | Pick a theme · Upload hero photo · Add service times · Publish your site |
| Suite with Website | same 6 + | All from both + Website set |
3.3 Empty-state copy (0 activity, fresh account)
| Plan channel | Empty-state body | CTA |
|---|---|---|
| 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.
| Plan | Role | Chips rendered |
|---|---|---|
cwa_starter_chat | admin | All · Prayer Requests · Visitors · Safety |
cwa_pro_chat | admin | All · Prayer Requests · Visitors · Safety |
cwa_starter_voice | admin | All · Calls · Prayer Requests · Callbacks · Safety |
cwa_pro_voice | admin | All · Calls · Prayer Requests · Callbacks · Safety |
cwa_starter_both / cwa_pro_both | admin | All · Calls · Prayer Requests · Visitors · Callbacks · Safety |
cwa_suite_both / cwa_bundle | admin | All · Calls · Prayer Requests · Visitors · Callbacks · Safety |
cwa_pro_website | admin | All · Prayer Requests · Visitors · Safety (Pro Website has chat only) |
| any | prayer_team | Prayer Requests (ONLY; no All chip) |
| any | care_team | All · Prayer Requests · Visitors · Callbacks (no Calls, no Safety) |
| any | volunteer_coordinator | All · Visitors · Callbacks |
| any | usher_team | Visitors (ONLY) |
| any | kids_ministry / youth_ministry | All · Visitors · Callbacks |
| any | treasurer | Inbox 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
Phonein--navy - Title:
Call — {formatted_phone} · {duration} - Body: AI summary (first 80 chars + ellipsis)
- Actions (conditional):
[View transcript]— requiresinbox:calls:transcript[Log pastoral follow-up]— always present (writes tovoice_callback_requests)[Delete]— requiresinbox:calls:delete(admin only)
Prayer card:
- Icon: Lucide
Heartin--sacred-gold - Title:
Prayer request — {submitter_name}ORPrayer request — anonymous - Body: prayer text. Redacted to
"Confidential — contact the pastor"ifis_confidentialAND user lacksinbox:prayer:read:confidential. - Actions:
[Reply]— writes tovoice_prayer_requests.reply_text[Mark prayed for]— requiresinbox:prayer:update, stampsprayed_by_member_id[Assign to prayer team]— requiresinbox:prayer:assign(admin + office_admin + pastor)[Send care message]— requiresinbox:prayer:care_message(admin + office_admin + pastor + care_team)
Visitor card:
- Icon: Lucide
UserPlusin--success - Title:
Visitor contact — {name || "anonymous"} - Body: message text
- Actions:
[Send welcome],[Schedule follow-up],[Mark first visit],[Send care message](requiresinbox:prayer:care_message)
Callback card:
- Icon: Lucide
PhoneOutgoingin--navy - Title:
Callback requested — {caller_name} - Body: reason text. Redacted to
"Pastoral inquiry"if user lacksinbox:callback:read:reason. - Actions:
[Call back](opens tel: link),[Mark resolved],[Send care message](requiresinbox:prayer:care_message)
Safety card:
- Icon: Lucide
ShieldAlertin--danger - Title:
Safety flag — {category}(crisis / DV / threat / abuse) - Body: one-line summary only (full transcript link)
- Actions:
[View context],[Mark resolved](requiresinbox:safety:resolve)
4.3 Empty-state per plan
| Plan channel | Empty body | CTA |
|---|---|---|
| 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
Searchicon. Clear×appears when input has value. - Time range pill group —
Today,Past 7 days,Past 30 days,All time,Custom range.Customreveals 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-expandedreflects open state. - AC 41: Time range pills are mutually exclusive (
aria-pressedflips). 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-semiboldtitle, gold dot (--sacred-gold) top-right. - Read card:
bg-stone-50/60,font-mediumstone-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, anddata-testid="inbox-item-read-toggle". - AC 46: Clicking the toggle flips the card visual + fires
POST /api/inbox/mark-readwith the right payload. On error the UI reverts. - AC 47: Filter popover "Unread only" hides cards with
is_read === true;is_read === undefinedcounts as unread (fresh accounts start fully-unread). - AC 48: Re-clicking a previously-read dot marks it unread and preserves the original
read_attimestamp 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:assignOR 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/assignreturns 403. - AC 51: Picking a team member fires
POST /api/inbox/assignwithassigned_to=<uuid>; optimistic UI updates immediately; errors revert the chip. - AC 52: "Unassign" footer sends
assigned_to=nulland the chip snaps back to "Unassigned". - AC 53:
assigned_toUUID 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-section | Capability | First-visible actions |
|---|---|---|
| Church Knowledge | train:church_knowledge:edit | Document list + [+ Upload document] button (requires train:documents:upload) |
| Theology & Tradition | train:theology:edit | TheoLens picker (17 radio options) + vocabulary boost list |
| Agent Personality | train:agents:edit | Tone radio (Warm/Professional/Casual) + pastor voice textarea + greeting script textarea + escalation rules |
| FAQs | train:faqs:edit | Q&A list + [+ Add FAQ] + priority slider. Warning banner if >3 rows share same answer text (DB trigger protects). |
| Safety Rules | train:safety:edit | Crisis keyword list + escalation contacts + moderation sensitivity slider |
| Chat Simulator | train:simulator:use | Split pane: test prompt input (left) + diagnostic panel (right, P2 feature). P1 renders shell + "Coming in P2" placeholder. |
| Pastor Pulse | train:pastor_pulse:edit | This 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
| Action | Capability | Role examples |
|---|---|---|
| Section edit | website:sections:edit | admin, pastor, office_admin, tech_team |
| Design (theme, colors, fonts) | website:design:edit | admin, pastor, tech_team (NOT office_admin) |
| Publish | website:publish | admin, pastor only |
| Preview | website:preview | all Website-capable roles |
| Media upload | website:media:upload | admin, 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
| Role | Account | Team | Notifications | Integrations | Billing |
|---|---|---|---|---|---|
| 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. AmberUnsaved changemicro-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?"
7.3 Hash deep-links
All legacy hash routes preserved, opening slide-over to matching sub-tab:
| Hash | Opens | Sub-tab |
|---|---|---|
#basic / #contact / #photo-setup | slide-over | Account |
#hours / #availability | slide-over | Account (scrolled to Hours) |
#team | slide-over | Team |
#notifications | slide-over | Notifications |
#integrations | slide-over | Integrations |
#sharing | slide-over | Account (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
| Plan | Tabs rendered |
|---|---|
| chat-only | Home · Inbox · Train AI (3) |
| voice-only | Home · Inbox · Train AI (3) |
| both | Home · Inbox · Train AI (3) |
| Pro Website / Suite-with-website | Home · 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 plan | Upgrade CTA |
|---|---|
cwa_starter_chat | Upgrade to Pro — add voice |
cwa_starter_voice | Upgrade to Pro — add chat |
cwa_starter_both | Upgrade to Pro |
cwa_pro_chat | Upgrade — add voice |
cwa_pro_voice | Upgrade — add chat |
cwa_pro_both | Upgrade to Suite — add website |
cwa_pro_website | Upgrade — add voice + chat agents |
| Suite / Bundle | hidden |
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
- Given role with zero
inbox:*caps, when page loads, then Inbox tab is ABSENT from DOM. - Given
cwa_starter_chatplan + admin role, when page loads, then nav is exactlyHome · Inbox · Train AI. - Given
cwa_pro_websiteplan + admin role, when page loads, then nav is exactlyHome · Inbox · Train AI · Website. - Given
treasurerrole on any plan, when page loads, then nav is exactlyHome(no other tabs). - Given
prayer_teamrole, when page loads, then Inbox tab present but only shows Prayer Requests chip.
10.2 Capability fallback
- Given member with empty
group_idsANDcapabilities, when page loads, thenlegacyRoleToCapabilities(role)is used for tab gating. - Given member with only
role='admin'and empty group_ids, when page loads, then nav shows the same tabs as a cap-driven admin.
10.3 Share links & empty states
- Given
cwa_pro_bothplan, when Home loads, then Share card shows Chat widget code AND Voice number. - Given
cwa_starter_chaton day 0, 0 requests, when Home loads, then example conversation card renders withEXAMPLEribbon. - Given user dismisses example card, when they reload, then card stays dismissed (localStorage).
10.4 Inbox chips
- Given voice-only plan, when Inbox opens, then Visitors chip is absent.
- Given chat-only plan, when Inbox opens, then Calls and Callbacks chips are absent.
- Given confidential prayer AND user lacks
inbox:prayer:read:confidential, when card renders, then body text is"Confidential — contact the pastor". - Given user clicks Safety chip without
inbox:safety:read, chip is absent from DOM entirely.
10.5 Train AI
- Given
office_adminrole, when Train AI opens, then left rail shows exactly Agent Personality, FAQs, Safety Rules (no Church Knowledge, no Theology, no Simulator, no Pastor Pulse). - 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
- Given chat-only plan, when page loads, then Website tab is ABSENT.
- Given user edits a section without saving, when preview updates, then right panel shows
"1 unsaved section"and Publish button is disabled. - Given user clicks Publish then confirms, when publish completes, then right panel shows
"Published HH:MM · View live →". - Given mobile viewport, when Website tab opens, then
"Edit on desktop"notice renders instead of the editor.
10.7 Settings slide-over
- Given no settings caps AND no billing:view, when page loads, then gear icon is ABSENT from header.
- Given treasurer role, when gear opens, then only Billing sub-tab is visible.
- Given URL hash
#notifications, when page loads, then gear opens slide-over directly to Notifications. - Given mobile viewport, when gear tapped, then slide-over fills screen as bottom sheet.
- Given text field dirty, when user closes slide-over, then confirm-before-close modal fires.
- Given radio changed, when change detected, then autosaves within 300ms AND timestamp renders.
10.8 Mobile bottom nav
- Given chat-only plan + viewport <640px, when nav renders, then exactly 3 tabs (Home, Inbox, Train AI).
- Given Pro Website plan + viewport <640px, when nav renders, then exactly 4 tabs.
- Given viewport <360px + Pro Website plan, when nav renders, then 3 tabs + overflow menu containing Website.
- Given user taps active tab, when tap fires, then scroll-to-top within active tab.
10.9 Upgrade CTA
- Given
cwa_suite_bothplan, when page loads, then Upgrade CTA is ABSENT everywhere. - Given
cwa_starter_chatplan + desktop, when page loads, then header shows pillUpgrade to Pro — add voice. - Given starter plan + mobile, when page loads, then Upgrade CTA is a dismissible toast on Home, never in bottom nav.
10.10 Edge cases
- 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."
- Given
status='cancelled'subscription, when page loads: (KNOWN GAP FA-046) — customer still sees full dashboard. Planned fix: middleware redirect to Resubscribe page. - 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.tschanges, update §2.2 and the cap-map doc in the SAME PR. - When plan predicates in
tier-config.tschange, 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.
12. Supersession and hop-links
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.