Skip to main content

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):

  1. getVerticalByHostname(host) — checks the incoming Host header against registry.ts profiles (line 195).
  2. If hostname matches funeral, calls resolveFuneralToken(token) from funeral-queries.ts, then runs adaptFuneralToChurchShape() to synthesise a PremiumChurch-shaped object (lines 213–218).
  3. Otherwise calls resolveToken(token) from premium-queries.ts — the original church-only path (lines 220–222).
  4. 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:

QueryTable(s) / HelperNotes
getVoiceAgentSafe(church.id)church_voice_agentsconfirmed — returns null safely
getDashboardMetrics(church.id)voice_call_logs, voice_prayer_requests, voice_callback_requests, moderation_violationsconfirmed
getRecentActivity(church.id, role)voice_call_logs, voice_prayer_requests, voice_visitor_contacts, voice_callback_requestsconfirmed
getCallLogsByChurch(church.id, 0)voice_call_logsconfirmed
getRequestsByType(church.id, 'prayer', 0, role)voice_prayer_requestsconfirmed — legacy prop, not rendered in active UI (Slice 2)
getRequestsByType(church.id, 'visitor', 0, role)voice_visitor_contactsconfirmed — legacy prop
getRequestsByType(church.id, 'callback', 0, role)voice_callback_requestsconfirmed — legacy prop
getCareMemberCount(church.id)congregation_care_membersconfirmed
getToolUsageCounts(church.id)voice_callback_requests, voice_visitor_contacts, voice_prayer_requests, voice_call_logsconfirmed
getAgentConversationCounts(church.id)chatbot_conversations or similarconfirmed — tool-queries.ts
organization_settings selectorganization_settingsagent_config, agent_tool_config columns
FAQ countunified_rag_contentfiltered by organization_id = chatbot_agent_id, content_type='faq', curation_status='approved'
Document countchurch_document_uploadsfiltered by organization_id = chatbot_agent_id
Theology lens countchurch_theological_lensesfiltered by church_id, confidence='confirmed'
Backup owner checkchurch_admin_identitieswrapped in try/catch (table may not exist)
RBAC group/cap lookupchurch_team_members, church_custom_groupswrapped in try/catch (columns may not exist pre-migration)
getInboxStream(...)All of the above via premium-queries.tsServer-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_idpremiumFh.funeral_home_id
  • premium.planpremiumFh.plan (fwa_* key preserved — no normalization)
  • premium.statuspremiumFh.status (cast; 'paused' exists on funeral but not church enum)
  • premium.chatbot_enabledfalse (hardcoded — funeral has no chatbot yet)
  • premium.care_enabledfalse (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. confirmed
  • churchwiseai.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):

keylabelNotes
overviewHomeAlways visible
inboxInboxUnified Calls+Requests+Care (Slice 2, 2026-04-18)
trainingTrain AIRenamed in Slice 2.5
socialSocialComing Soon — ShareWiseAI placeholder
websiteWebsitePro Website / PewSearch listing
upgradeSubscriptionBilling 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_MAP by key (no icon in TabSpec).

Path B — No vertical tabs (church default / empty verticalProfile.tabs):

  • Uses ALL_TABS, filters out:
    • Tabs that fail canSeeTabLocal
    • social tab (always hidden — ShareWiseAI not launched)
    • website tab for planTier === 'starter' && !hasWebsiteAccess

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 → website is priority
  • All others with inbox access → inbox is 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.tsx
  • src/app/admin/[token]/components/WebsiteOnlyOverview.tsx
  • useIsChurchVertical() hook from VerticalProvider.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)
  • hasTheologyLens prop — theology lens is church-only
  • careEnabled prop — 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).

  • prayer and visitor types are purely church-centric.
  • at_need and arrangement types are purely funeral-centric.
  • safety type 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 keyLabelCapabilityChurch-specific?
church-knowledgeChurch Knowledgetrain:church_knowledge:editYes — label, description
theologyTheology & Traditiontrain:theology:editYes — church tradition lens
agentsAgent Personalitytrain:agents:editPartially — contains voice greeting, pastor name
faqsFAQstrain:faqs:editNeutral label, shared concept
safetySafetytrain:safety:editShared
simulatorSimulatortrain:simulator:useNeutral
pastor-pulseThis Weektrain:pastor_pulse:editChurch-specific — sermon topic, theme verse

Components used:

  • src/components/admin/training/ChurchKnowledgePanel.tsx
  • src/components/admin/training/ThisWeekPanel.tsx
  • src/components/admin/FAQManagement.tsx
  • src/app/admin/[token]/components/TheologySettings.tsx
  • src/components/admin/SafetyCompliance.tsx
  • src/components/admin/SimulatorPanel.tsx
  • AgentSettingsPanel (imported from TrainingTab.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.tsx Boundary: '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
  • Church icon (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 container
  • src/app/admin/[token]/components/SettingsTab.tsx — sub-tab content Boundary: 'use client' (SettingsPanel line 1) RBAC gate: Any of 8 settings:* 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:

  • Accountchurch-info, hours, sharing — church-specific labels (e.g., "Church Info", office hours concept)
  • Teamteam — role labels come from getRoleLabel() 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_TABS in src/lib/verticals/church.ts — 6 tabs
  • FUNERAL_TABS in src/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:

ColumnStatus
sourceExists (confirmed by CLAUDE.md + voice-queries code that selects it)
verticalDoes 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_idDoes 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
categoryNot found in query selects on these tables — status assumed not present
urgencyExists 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_idchurch.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() — full ClientVerticalProfile
  • useVisitorLabel() — 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 DashboardOverview
  • useIsFuneralVertical() — boolean

Components that currently use these hooks: confirmed

  • DashboardOverview.tsx uses useIsChurchVertical() (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 via getRoleLabel())
  • 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:

ComponentRetired inNotes
CallHistory.tsxSlice 2 (2026-04-18)Was Calls tab — collapsed into InboxTab
RequestManager.tsxSlice 2Was Requests tab — collapsed into InboxTab
CareTab.tsxSlice 2Was Care tab — collapsed into InboxTab
TrainingTab.tsxSlice 2.5Was 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

FileIssue
src/app/admin/[token]/page.tsxgetInboxStream() called for all verticals — should call vertical.inboxFeedQuery()
src/app/admin/[token]/components/AdminDashboard.tsxALL_TABS duplicates CHURCH_TABS; tab-mount switch is not driven by componentPath
src/app/admin/[token]/components/TrainAITab.tsxSECTIONS 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.tsxSetup checklist uses church-specific checks (theology lens, sermon topic, care members)
src/lib/verticals/church.tsCHURCH_TABS defined but never consumed for church tab rendering
src/lib/verticals/funeral.tsfuneralInboxFeedQuery declared but never called by page.tsx; TODO Phase 2 tenant_id migrations documented in comments
src/components/admin/VerticalProvider.tsxCHURCH_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