Admin Shell Pre-Refactor Audit
Purpose: Grounding document for Lanes B–E of the admin-shell vertical-aware refactor (2026-04-30). Every claim is cited to file:line. Judgments are marked confirmed (code read) or assumed (inferred from context). Inconsistencies are called out explicitly.
1. Entry Point & Server Component
File: src/app/admin/[token]/page.tsx
Boundary: Server component (export default async function AdminPage)
1.1 Vertical Resolution
The page performs vertical resolution in four ordered steps (lines 192–259):
getVerticalByHostname(host)— checks the incomingHostheader againstregistry.tsprofiles (line 195).- If hostname matches
funeral, callsresolveFuneralToken(token)fromfuneral-queries.ts, then runsadaptFuneralToChurchShape()to synthesise aPremiumChurch-shaped object (lines 213–218). - Otherwise calls
resolveToken(token)frompremium-queries.ts— the original church-only path (lines 220–222). - Vertical conflict check: if
hostnameProfile.key !== planVertical, returns 404 (lines 244–246).
Tab list source of truth: The tab list is NOT hardcoded in page.tsx. It is declared per-vertical in src/lib/verticals/church.ts and src/lib/verticals/funeral.ts as CHURCH_TABS and FUNERAL_TABS respectively, then exposed via VerticalProfile.tabs. The client component AdminDashboard.tsx reads verticalProfile.tabs and uses them when non-empty; it falls back to ALL_TABS (the hardcoded church-only list) when the array is empty. See §3.3.
1.2 Data Fetched Server-Side (page.tsx lines 286–336)
All queries run in Promise.all before render:
| Query | Table(s) / Helper | Notes |
|---|---|---|
getVoiceAgentSafe(church.id) | church_voice_agents | confirmed — returns null safely |
getDashboardMetrics(church.id) | voice_call_logs, voice_prayer_requests, voice_callback_requests, moderation_violations | confirmed |
getRecentActivity(church.id, role) | voice_call_logs, voice_prayer_requests, voice_visitor_contacts, voice_callback_requests | confirmed |
getCallLogsByChurch(church.id, 0) | voice_call_logs | confirmed |
getRequestsByType(church.id, 'prayer', 0, role) | voice_prayer_requests | confirmed — legacy prop, not rendered in active UI (Slice 2) |
getRequestsByType(church.id, 'visitor', 0, role) | voice_visitor_contacts | confirmed — legacy prop |
getRequestsByType(church.id, 'callback', 0, role) | voice_callback_requests | confirmed — legacy prop |
getCareMemberCount(church.id) | congregation_care_members | confirmed |
getToolUsageCounts(church.id) | voice_callback_requests, voice_visitor_contacts, voice_prayer_requests, voice_call_logs | confirmed |
getAgentConversationCounts(church.id) | chatbot_conversations or similar | confirmed — tool-queries.ts |
organization_settings select | organization_settings | agent_config, agent_tool_config columns |
| FAQ count | unified_rag_content | filtered by organization_id = chatbot_agent_id, content_type='faq', curation_status='approved' |
| Document count | church_document_uploads | filtered by organization_id = chatbot_agent_id |
| Theology lens count | church_theological_lenses | filtered by church_id, confidence='confirmed' |
| Backup owner check | church_admin_identities | wrapped in try/catch (table may not exist) |
| RBAC group/cap lookup | church_team_members, church_custom_groups | wrapped in try/catch (columns may not exist pre-migration) |
getInboxStream(...) | All of the above via premium-queries.ts | Server-side unified inbox (Slice 2); redacts confidential content per capabilities |
1.3 adaptFuneralToChurchShape (lines 63–155) — confirmed
This function maps PremiumFuneralHome + FuneralHome into the PremiumChurch + Church shapes expected by AdminDashboard. Key synthetic mappings:
premium.church_id←premiumFh.funeral_home_idpremium.plan←premiumFh.plan(fwa_* key preserved — no normalization)premium.status←premiumFh.status(cast; 'paused' exists on funeral but not church enum)premium.chatbot_enabled←false(hardcoded — funeral has no chatbot yet)premium.care_enabled←false(hardcoded)- All church-specific fields (
custom_staff,custom_ministries,events,sermons,beliefs,what_to_expect, etc.) ← safe empty defaults
Funeral adapter concern: Every data prop that page.tsx prefetches uses church.id (the synthesised funeralHome.id). Because voice_callback_requests, voice_call_logs, and voice_prayer_requests use church_id for funeral rows (see §7), this works today — but only because the funeral home's Supabase UUID happens to be stored as church_id in those tables. If the adapter maps church.id ← funeralHome.id, and that UUID was inserted into voice tables as church_id when the funeral voice agent provisioned, the queries return correct data. This assumption is fragile: if the UUID in voice tables ever differs from funeralHome.id, the adapter silently returns empty data. assumed — not verified against live data.
2. Layout & Routing
File: src/app/admin/[token]/layout.tsx
- Trivial passthrough (
<>{children}</>) — no auth guards, no providers.confirmed
File: src/middleware.ts
funeralwiseai.com/admin/*→NextResponse.next()(lines 147–149) — no rewrite.confirmedchurchwiseai.com/admin/*→ falls through all hostname blocks, reaches token resolver normally.confirmed
Legacy funeral route: src/app/admin/funeral/[token]/page.tsx exists and calls redirect() to /admin/[token]. confirmed (file exists at that path). VerticalProfile.legacyAdminPath = '/admin/funeral/[token]' declared in funeral.ts:237.
3. Client Wrapper: AdminDashboard.tsx
File: src/app/admin/[token]/components/AdminDashboard.tsx
Boundary: 'use client' (line 1) — entire component tree below is client-side
3.1 ALL_TABS — Church-specific hardcoded list (lines 151–209)
This list is used when verticalProfile.tabs is empty (i.e., when no vertical or church vertical):
key | label | Notes |
|---|---|---|
overview | Home | Always visible |
inbox | Inbox | Unified Calls+Requests+Care (Slice 2, 2026-04-18) |
training | Train AI | Renamed in Slice 2.5 |
social | Social | Coming Soon — ShareWiseAI placeholder |
website | Website | Pro Website / PewSearch listing |
upgrade | Subscription | Billing management |
Settings is NOT in ALL_TABS — it was removed to a slide-over in Slice 1 (2026-04-18). Still reachable via gear icon and hash routing.
Legacy keys retained in Tab union (AdminDashboard.rbac.ts line 25): calls, requests, care — these redirect into inbox via hash routing. Not rendered in the nav.
3.2 Tab Filtering Logic (lines 300–365)
Two mutually exclusive code paths:
Path A — Vertical has declared tabs (verticalDefinedTabs.length > 0):
- Uses
verticalProfile.tabs(from VerticalProfile for funeral/vet). - Filters by
canSeeTabLocal(t.key). - Injects icon from
TAB_ICON_MAPby key (no icon inTabSpec).
Path B — No vertical tabs (church default / empty verticalProfile.tabs):
- Uses
ALL_TABS, filters out:- Tabs that fail
canSeeTabLocal socialtab (always hidden — ShareWiseAI not launched)websitetab forplanTier === 'starter' && !hasWebsiteAccess
- Tabs that fail
Key finding: Church vertical's CHURCH_TABS in church.ts declares 6 tabs but VerticalProvider CHURCH_DEFAULTS sets tabs: [] (line 104 of VerticalProvider.tsx). This means the church vertical always hits Path B (ALL_TABS), not Path A. Only the funeral vertical (4 tabs) hits Path A. This is intentional per the code comment at lines 303–305.
3.3 Tab-Reordering (lines 353–365)
After filtering, tabs are reordered so the "priority" tab appears second (after Home):
- Pro Website plans →
websiteis priority - All others with inbox access →
inboxis priority
3.4 Settings: Slide-Over (not a tab)
Settings moved out of the tab bar in Slice 1. It is rendered as <SettingsSlideOver> outside the tab content area. Opened via gear icon in the dashboard header. Hash anchors (#notifications, #hours, etc.) map to { tab: 'settings', subTab: ... } in HASH_TO_TAB and open the slide-over. confirmed
4. Tab-by-Tab Inventory
4.1 overview — Home Tab
File: src/app/admin/[token]/components/DashboardOverview.tsx
Boundary: 'use client' (line 1)
RBAC gate: Always visible (empty capability array — CAP_TABS.overview = [])
Vertical specificity: Church-specific — confirmed
Components used:
src/components/admin/SetupChecklistRail.tsxsrc/app/admin/[token]/components/WebsiteOnlyOverview.tsxuseIsChurchVertical()hook fromVerticalProvider.tsx
Church-specific UI elements (all confirmed):
- Setup checklist steps reference "congregation care", "prayer requests", "sermon topic" explicitly (DashboardOverview props:
careEnabled,hasFaqs,hasSermonTopic,hasTheologyLens,hasMinistries, etc.) - Header copy: "Your church dashboard — see prayer requests, visitor contacts…" (ALL_TABS line 157)
hasTheologyLensprop — theology lens is church-onlycareEnabledprop — congregation care is church-only- Activity feed items use terminology like "prayer request", "pastoral callback" — not funeral-aware
Funeral adapter handling: DashboardOverview receives care_enabled: false and chatbot_enabled: false from the adapter (both hardcoded). The component renders using useIsChurchVertical() — for a funeral visitor, this returns false and the component adjusts some copy. However the setup checklist rail, metric cards, and activity copy still use church-centric labels. This creates visual misfit for funeral admins. confirmed (DashboardOverview line 67 imports useIsChurchVertical).
4.2 inbox — Unified Inbox Tab (Slice 2, 2026-04-18)
File: src/app/admin/[token]/components/InboxTab.tsx
Boundary: 'use client' (line 1)
RBAC gate: Any of ['inbox:calls:read', 'inbox:prayer:read', 'inbox:visitor:read', 'inbox:callback:read', 'inbox:safety:read'] — confirmed (CAP_TABS.inbox, AdminDashboard.rbac.ts lines 64–70)
Vertical specificity: Partially vertical-aware — confirmed
Data source: initialInbox prop — server-prefetched by getInboxStream() in premium-queries.ts. Uses VerticalProvider for terminology.
Components used:
src/lib/inbox-stream.ts(chip computation)src/components/admin/VerticalProvider.tsx(terminology hooks)
InboxItem types: call | prayer | visitor | callback | safety | at_need | arrangement (types.ts line 60–70).
prayerandvisitortypes are purely church-centric.at_needandarrangementtypes are purely funeral-centric.safetytype is shared.
Funeral adapter handling: getInboxStream() uses premium-queries.ts which queries voice_prayer_requests, voice_visitor_contacts, voice_call_logs, voice_callback_requests all by church_id. For a funeral adapter call, this means prayer requests and visitor contacts would show church-centric items even if the funeral vertical has none (they would be empty). The funeral InboxFeedOpts from funeral.ts skips voice_prayer_requests and voice_visitor_contacts in funeralInboxFeedQuery — but the server page.tsx calls getInboxStream() (church query), not funeralInboxFeedQuery(). This is a gap: the funeral vertical's inbox prefetch in page.tsx still uses the church-path getInboxStream(), not the vertical-specific inboxFeedQuery. confirmed
INCONSISTENCY TO RESOLVE: page.tsx calls getInboxStream() unconditionally (line 431), regardless of vertical. The funeral VerticalProfile.inboxFeedQuery is declared but never called from page.tsx. This means funeral inbox data flows through the church query path on the server side.
4.3 training — Train AI Tab (Slice 2.5, 2026-04-18)
File: src/app/admin/[token]/components/TrainAITab.tsx
Boundary: 'use client' (line 1)
RBAC gate: Any of 8 train:* capabilities — confirmed (CAP_TABS.training, rbac.ts lines 88–97)
Vertical specificity: Church-specific — confirmed
Sub-sections (SECTIONS array, TrainAITab.tsx lines 101–174):
| Sub-section key | Label | Capability | Church-specific? |
|---|---|---|---|
church-knowledge | Church Knowledge | train:church_knowledge:edit | Yes — label, description |
theology | Theology & Tradition | train:theology:edit | Yes — church tradition lens |
agents | Agent Personality | train:agents:edit | Partially — contains voice greeting, pastor name |
faqs | FAQs | train:faqs:edit | Neutral label, shared concept |
safety | Safety | train:safety:edit | Shared |
simulator | Simulator | train:simulator:use | Neutral |
pastor-pulse | This Week | train:pastor_pulse:edit | Church-specific — sermon topic, theme verse |
Components used:
src/components/admin/training/ChurchKnowledgePanel.tsxsrc/components/admin/training/ThisWeekPanel.tsxsrc/components/admin/FAQManagement.tsxsrc/app/admin/[token]/components/TheologySettings.tsxsrc/components/admin/SafetyCompliance.tsxsrc/components/admin/SimulatorPanel.tsxAgentSettingsPanel(imported fromTrainingTab.tsx)
Funeral adapter handling: The funeral VerticalProfile.tabs uses train:safety:edit as the capability for the training tab (funeral.ts line 46). In the shared TrainAITab, this gates the entire tab. However, the sub-sections inside TrainAITab (church-knowledge, theology, pastor-pulse) remain visually church-specific. The SECTIONS array in TrainAITab.tsx is hardcoded and not vertical-aware. A funeral admin sees "Church Knowledge", "Theology & Tradition", and "Pastor Pulse" — all wrong labels. confirmed
INCONSISTENCY TO RESOLVE: The funeral VerticalProfile declares the training tab component path as '@/app/admin/[token]/components/TrainAITab', but AdminDashboard.tsx mounts <TrainAITab> from a hardcoded switch (line 1146–1176), not via dynamic import from componentPath. The componentPath field in TabSpec is currently documentation-only, not used for actual rendering. Both the funeral profile and the switch render the same (church-specific) TrainAITab. confirmed
4.4 social — Social Tab
File: Inline in src/app/admin/[token]/components/AdminDashboard.tsx (lines 1178–1240)
Boundary: 'use client' (enclosing component)
RBAC gate: audit:view cap only (admin-only — effectively hidden for all roles). confirmed (rbac.ts line 100). Belt-and-suspenders: t.key === 'social' is also filtered out in Path B (line 319). confirmed
Vertical specificity: Church-specific (hardcoded "ShareWise AI" copy and church social media framing). confirmed
Funeral adapter handling: Tab is hidden for all funeral users by double gate (RBAC + Path A filter). The funeral FUNERAL_TABS does not include a social key. confirmed
4.5 website — Website Tab
Files:
- Read-only stub:
src/app/admin/[token]/components/WebsiteTab.tsx - Full editor:
src/app/admin/[token]/components/WebsiteTabEditor.tsxBoundary:'use client'(line 1 of both) RBAC gate:['website:sections:edit', 'website:design:edit', 'website:publish']—confirmed(rbac.ts lines 101–105) Vertical specificity: Church-specific —confirmed
WebsiteTab.tsx (read-only stub):
- Hardcodes PewSearch listing URL (
https://pewsearch.com/churches/${church.slug}) — church-specific.confirmed(WebsiteTab.tsx line 53) - "Pro: Premium Listing ($4.95 value) included free" — church-specific copy.
confirmed - Falls back to church denomination
tier === 'pro' || tier === 'suite'for premium listing.confirmed
WebsiteTabEditor.tsx:
- Full Pro Website editor with sections: hero, staff, ministries, events, sermons, about, giving. All church-specific labels.
confirmed(section-registry.tsx imports confirm church-centric section types)
Funeral adapter handling: Funeral FUNERAL_TABS does not include website tab (4 tabs: overview, inbox, training, settings). The tab is never shown for funeral users. confirmed
4.6 upgrade — Subscription Tab
File: src/app/admin/[token]/components/UpgradeTab.tsx
Boundary: 'use client' (line 1)
RBAC gate: billing:view cap — confirmed (rbac.ts line 116)
Vertical specificity: Church-specific — confirmed
Hardcoded church plan keys and pricing (TIER_PRICES lines 54–69):
- References
cwa_*plan key labels, Chat/Voice/Bundle tiers - Hardcoded pricing: Starter $14.95/$49.95/$54.95, Pro $34.95/$99.95/$119.95, Suite $59.95/$139.95
Churchicon (Lucide) used for the Starter tier display
Funeral adapter handling: Funeral FUNERAL_TABS does not include upgrade tab. confirmed
4.7 settings — Settings Slide-Over
Files:
src/app/admin/[token]/components/SettingsPanel.tsx— outer 4-section nav (lazy-loads SettingsTab)src/app/admin/[token]/components/SettingsSlideOver.tsx— slide-over containersrc/app/admin/[token]/components/SettingsTab.tsx— sub-tab content Boundary:'use client'(SettingsPanel line 1) RBAC gate: Any of 8settings:*capabilities —confirmed(rbac.ts lines 104–116) Vertical specificity: Church-specific —confirmed
SettingsSubTab type (SettingsTab.tsx line 229):
'church-info' | 'hours' | 'notifications' | 'integrations' | 'team' | 'agent-tools' | 'sharing'
SettingsPanel top-level sections:
- Account →
church-info,hours,sharing— church-specific labels (e.g., "Church Info", office hours concept) - Team →
team— role labels come fromgetRoleLabel()which uses church roles (Pastor, Office Admin, etc.) - Notifications → voice/chatbot notification routing
- Integrations → PCO, Cal.com integrations — church-specific
ROLE_SETTINGS constant (premium-shared.ts lines 245–255): Lists settings sections by role — all church role names. confirmed
Funeral adapter handling: Funeral FUNERAL_TABS includes settings with capability 'settings:church_profile:edit' (funeral.ts line 57). This IS a real capability in CAP_TABS (settings maps to settings:church_profile:edit among others — rbac.ts line 107). So the settings tab IS visible to funeral admins. When they open Settings, they see "Church Info" section labels, role labels like "Pastor / Admin", church-specific hours fields, and PCO integration options — all wrong for a funeral home. confirmed
5. ROLE_TABS vs CAP_TABS — Two RBAC Systems
The codebase has two parallel tab-gating systems:
5.1 Legacy: ROLE_TABS (premium-shared.ts lines 232–242)
admin: ['overview', 'calls', 'requests', 'care', 'training', 'social', 'website', 'settings', 'upgrade']
office_admin: ['overview', 'calls', 'requests', 'care', 'training', 'social', 'website', 'settings']
prayer_team: ['overview', 'requests']
care_team: ['overview', 'requests', 'care']
treasurer: ['overview']
volunteer_coordinator:['overview', 'requests']
worship_leader: ['overview']
spiritual_leader: ['overview', 'training']
care_leader: ['overview', 'training']
Still contains legacy keys calls, requests, care. These map to inbox in Slice 2 via canSeeTabLocal() (AdminDashboard.tsx lines 282–287).
5.2 Model C: CAP_TABS (AdminDashboard.rbac.ts lines 57–117)
overview: []
inbox: [inbox:calls:read, inbox:prayer:read, inbox:visitor:read, inbox:callback:read, inbox:safety:read]
calls: [inbox:calls:read] (legacy redirect)
requests: [inbox:prayer:read, ...] (legacy redirect)
care: [inbox:prayer:read, ...] (legacy redirect)
training: [train:church_knowledge:edit, train:theology:edit, train:agents:edit, ...]
social: [audit:view]
website: [website:sections:edit, website:design:edit, website:publish]
settings: [settings:church_profile:edit, settings:hours:edit, settings:notifications:edit, ...]
upgrade: [billing:view]
5.3 Fallback Logic (AdminDashboard.tsx lines 280–288)
const canSeeTabLocal = (tab: Tab): boolean => {
if (hasAnyCaps) return canSeeTab(tab, capsSet);
if (tab === 'inbox') {
return ['calls', 'requests', 'care'].some(legacy => legacyVisibleTabKeys.includes(legacy));
}
return legacyVisibleTabKeys.includes(tab);
};
Pre-migration members (no group/direct caps) fall back to ROLE_TABS. Post-migration members use CAP_TABS. confirmed
INCONSISTENCY TO RESOLVE: ROLE_TABS still lists legacy keys (calls, requests, care). CAP_TABS lists inbox as the unified key. The fallback logic bridges them (line 282–287), but ROLE_TABS and CAP_TABS can never be used as a 1:1 source of truth for the same tab without the bridging logic. Any Lane that tries to simplify tab config should be aware the bridge is load-bearing.
6. VerticalProfile — Tab Declaration vs AdminDashboard Mount
6.1 How tabs are declared
Each vertical declares its tabs in its profile file:
CHURCH_TABSinsrc/lib/verticals/church.ts— 6 tabsFUNERAL_TABSinsrc/lib/verticals/funeral.ts— 4 tabs
TabSpec.componentPath is declared (documentation + future lazy-load intention) but never used for dynamic import. AdminDashboard.tsx uses a hardcoded switch for all tab content rendering (lines 1075–1289). confirmed
6.2 CHURCH_TABS vs ALL_TABS — Duplication
CHURCH_TABS in church.ts and ALL_TABS in AdminDashboard.tsx declare the same 6 tabs with the same keys, labels, and tooltips. They are not in sync via a shared import. Changes to one must be made to the other manually.
INCONSISTENCY TO RESOLVE: CHURCH_TABS and ALL_TABS duplicate the tab manifest for the church vertical. The tabs: [] in VerticalProvider.CHURCH_DEFAULTS means the church vertical always falls back to ALL_TABS in AdminDashboard.tsx — CHURCH_TABS is defined in church.ts but never exercised for the church vertical's tab rendering. The church vertical is gated by Path B (ALL_TABS), while funeral is gated by Path A (verticalProfile.tabs). This asymmetry means the church vertical bypasses VerticalProfile-driven tab rendering entirely.
7. Shared Tables — vertical / source / tenant_id Column Status
The CLAUDE.md notes that voice_prayer_requests, voice_callback_requests, voice_visitor_contacts, and voice_call_logs have a source column (voice, pewsearch, chat). Status of additional discriminator columns:
| Column | Status |
|---|---|
source | Exists (confirmed by CLAUDE.md + voice-queries code that selects it) |
vertical | Does NOT exist — no query in codebase selects it from these tables. confirmed by grep (no .vertical or .eq('vertical' in church.ts or funeral.ts) |
tenant_id | Does NOT exist — documented explicitly in funeral.ts comments (lines 81–84, 116–118, 142–145): "tenant_id column does not exist on voice_callback_requests/voice_call_logs/moderation_violations yet. TODO Phase 2." confirmed |
category | Not found in query selects on these tables — status assumed not present |
urgency | Exists on voice_callback_requests — church.ts and funeral.ts both select it. confirmed |
All four tables are currently discriminated only by church_id for both church and funeral verticals. The funeral vertical stores its tenant UUID as church_id in these tables (the adapter maps funeral_home_id → church.id). Multi-tenant cross-vertical data isolation relies entirely on distinct UUID spaces. No vertical or tenant_id column exists for server-side filtering by vertical.
This is the founding constraint for Lanes B–E: the "adapter approach — keep shared tables, discriminate via category/urgency/vertical columns" (founder call #1) requires adding a vertical (or tenant_id) column to these tables as a Phase 2 migration before vertical-aware filtering can be implemented server-side.
8. VerticalProvider and Client-Side Terminology Propagation
File: src/components/admin/VerticalProvider.tsx
The provider wraps AdminDashboard's entire subtree (AdminDashboard.tsx line 724). Every client component can call:
useVertical()— fullClientVerticalProfileuseVisitorLabel()— e.g., "visitor" vs "family"useCallbackLabel()— e.g., "pastoral callback" vs "at-need callback"useDirectorLabel()— e.g., "pastor" vs "director"useOrganizationLabel()— e.g., "church" vs "funeral home"useIsChurchVertical()— boolean, used in DashboardOverviewuseIsFuneralVertical()— boolean
Components that currently use these hooks: confirmed
DashboardOverview.tsxusesuseIsChurchVertical()(line 67)
Components that do NOT use these hooks but should: assumed
TrainAITab.tsx(hardcoded "Church Knowledge", "Pastor Pulse" section labels)SettingsPanel.tsx(hardcoded "Church Info" section label, church role labels viagetRoleLabel())InboxTab.tsx(uses terminology for chip labels — needs audit)
9. Legacy / Retired Components (Retained for Rollback)
Per the comment in AdminDashboard.tsx (lines 57–62), these components exist in the codebase but are NOT imported by the active dashboard:
| Component | Retired in | Notes |
|---|---|---|
CallHistory.tsx | Slice 2 (2026-04-18) | Was Calls tab — collapsed into InboxTab |
RequestManager.tsx | Slice 2 | Was Requests tab — collapsed into InboxTab |
CareTab.tsx | Slice 2 | Was Care tab — collapsed into InboxTab |
TrainingTab.tsx | Slice 2.5 | Was Training tab — replaced by TrainAITab. Still imported for AgentSettingsPanel export |
TrainingTab.tsx is a special case: it is retired as a tab but its exported AgentSettingsPanel component is still imported by TrainAITab.tsx (TrainAITab.tsx line 43: import { AgentSettingsPanel } from './TrainingTab'). confirmed
10. Top 3 Surprises for Lanes B–E
Surprise 1: CHURCH_TABS is declared but never used for the church vertical's tab rendering
src/lib/verticals/church.ts defines CHURCH_TABS with 6 tabs. VerticalProvider.CHURCH_DEFAULTS sets tabs: []. Because AdminDashboard.tsx uses the fallback path (ALL_TABS) whenever verticalProfile.tabs is empty, the church vertical bypasses the VerticalProfile-driven tab system entirely. Any Lane that expects to modify church tabs via CHURCH_TABS will be surprised that changes have no effect. The right place to change church tab behavior is ALL_TABS in AdminDashboard.tsx. To make CHURCH_TABS authoritative, CHURCH_DEFAULTS.tabs would need to be populated (which would also change the VerticalProvider serialization contract).
Surprise 2: page.tsx calls getInboxStream() (church query) for ALL verticals — funeral's inboxFeedQuery is never invoked
The funeral VerticalProfile declares inboxFeedQuery: funeralInboxFeedQuery (funeral.ts line 258). But page.tsx hardcodes getInboxStream(...) (line 431) for both church and funeral. The funeral-specific query that excludes prayer requests and visitor contacts is declared but dead code. A funeral admin's Inbox is currently populated by the church-path inbox stream. If a funeral tenant has no data in voice_prayer_requests (expected), this produces an empty Inbox — but the bug becomes critical if funeral data is ever inserted into these tables via a church_id that matches another church's UUID.
Surprise 3: The settings slide-over is fully church-specific and is shown to funeral admins
The funeral FUNERAL_TABS includes settings at line 54 with capability: 'settings:church_profile:edit'. This tab is visible to funeral admins, opens the <SettingsSlideOver>, and renders <SettingsPanel> with sub-sections "Church Info", "Team", "Notifications", "Integrations" — all church-centric labels. The team tab renders role labels via getRoleLabel() which returns "Prayer Team", "Care Team", "Worship Leader", etc. for a funeral home's staff. The "Church Info" form has fields for denomination, PewSearch listing opt-in, etc. This is the most immediately jarring vertical bleed for a funeral admin after logging in.
11. Full Component Tree Summary
src/app/admin/[token]/page.tsx (server — entry, vertical resolution, data fetch)
└── AdminDashboard.tsx (client — tab routing, vertical context, layout)
├── VerticalProvider (client context — terminology, tabs)
├── DashboardOverview.tsx (client — 'overview' tab)
│ ├── SetupChecklistRail.tsx
│ └── WebsiteOnlyOverview.tsx
├── InboxTab.tsx (client — 'inbox' tab)
├── TrainAITab.tsx (client — 'training' tab)
│ ├── ChurchKnowledgePanel.tsx
│ ├── ThisWeekPanel.tsx
│ ├── FAQManagement.tsx
│ ├── TheologySettings.tsx
│ ├── SafetyCompliance.tsx
│ ├── SimulatorPanel.tsx
│ └── AgentSettingsPanel (from TrainingTab.tsx)
├── [social content inline] (client — 'social' tab, hardcoded JSX)
├── WebsiteTabEditor.tsx or (client — 'website' tab)
│ WebsiteTab.tsx
├── UpgradeTab.tsx (client — 'upgrade' tab)
├── SettingsSlideOver.tsx (client — 'settings' slide-over)
│ └── SettingsPanel.tsx
│ └── SettingsTab.tsx (lazy)
└── CancelledTombstone.tsx (client — shown instead of dashboard when cancelled)
12. Files Requiring Attention in Refactor
| File | Issue |
|---|---|
src/app/admin/[token]/page.tsx | getInboxStream() called for all verticals — should call vertical.inboxFeedQuery() |
src/app/admin/[token]/components/AdminDashboard.tsx | ALL_TABS duplicates CHURCH_TABS; tab-mount switch is not driven by componentPath |
src/app/admin/[token]/components/TrainAITab.tsx | SECTIONS array hardcoded with church labels; no vertical awareness |
src/app/admin/[token]/components/SettingsPanel.tsx | "Church Info" label; getRoleLabel() returns church roles |
src/app/admin/[token]/components/DashboardOverview.tsx | Setup checklist uses church-specific checks (theology lens, sermon topic, care members) |
src/lib/verticals/church.ts | CHURCH_TABS defined but never consumed for church tab rendering |
src/lib/verticals/funeral.ts | funeralInboxFeedQuery declared but never called by page.tsx; TODO Phase 2 tenant_id migrations documented in comments |
src/components/admin/VerticalProvider.tsx | CHURCH_DEFAULTS.tabs = [] prevents church vertical from using VerticalProfile-driven tabs |
DB tables: voice_* | No vertical or tenant_id column — requires Phase 2 migration before true per-vertical filtering works |