Admin Dashboard UI/UX Audit — Vertical Bleed + Architecture Proposal
Date: 2026-04-30
Scope: /admin/[token] — church and funeral verticals
Method: Full code-read audit (all tabs + components). Screenshots deprioritised due to time-box — code reading is authoritative and faster than screenshot capture for finding bleed strings.
PRs shipped: Code fixes in fix/dashboard-uiux-priority-fixes (see Phase 3 below).
1. Vertical Detection — What's Working
The dashboard correctly detects verticals at several layers:
page.tsx→getVerticalByHostname(host)→resolveFuneralToken()vsresolveToken()— WORKINGAdminDashboard.tsx→verticalProfile.tabsnon-empty path skipsALL_TABS(church fallback) — WORKINGVerticalProvider+useIsChurchVertical()/useVertical()— WORKING but underusedfuneral.ts→FUNERAL_TABSdefines 4 tabs (Home, Inbox, Train AI, Settings) — WORKING but Settings capability typo was present (fixed in PR #266/267)
2. Bleed Issues Found (exhaustive inventory)
2.1 DashboardOverview.tsx (OverviewTab)
Severity: P0/P1 — this is the default landing tab for all verticals.
| Item | File:Line | Severity | Status |
|---|---|---|---|
ExamplePrayerCard with "Sample prayer request" copy | DashboardOverview.tsx:383 | P0 | Fixed — isChurchVertical gate existed, confirmed correct |
| Welcome header "Here's everything going on at {churchName}" | :345 | P1 | Fixed in this PR — neutral copy for funeral |
| Notification banner "prayer requests, visitor contacts" | :308 | P1 | Fixed in this PR — vertical-aware copy |
| Metric tile "Prayer requests" | :724 | P1 | Fixed in this PR — "Family inquiries" for funeral |
| Metric tile "New visitors" | :740 | P1 | Fixed in this PR — uses terminology.visitor |
| Analytics donut segment "Prayers" | :880 | P1 | Fixed in this PR — "Inquiries" for funeral |
| Analytics donut segment "Visitors" | :885 | P1 | Fixed in this PR — uses terminology.visitor |
| Setup rail eyebrow "Get your church online" | :1220 | P1 | Fixed in this PR — "Get your funeral home online" |
| QuickActions body "congregation asks most" | :1070 | P1 | Fixed in this PR — "families and callers" |
| QuickActions title "Upload church documents" | :1077 | P1 | Fixed in this PR — "Upload documents" |
RecentPrayersCard shown for prayer_team role regardless of vertical | :412 | P1 | Fixed in this PR — isChurchVertical guard |
| SideSummaryCard upsell "Get more from ChurchWiseAI" | :1422 | P2 | Fixed in this PR — neutral "Unlock more features" |
ShareLinkCard uses churchwiseai.com/chat/${churchSlug} | :542 | P2 | P2 backlog — funeral has no chatbot yet but link hardcodes CWA domain |
displayPastorName() helper function name | :149 | Minor | P3 — function works fine, just named church-centric |
redactCallbackSummary() returns "Pastoral inquiry" | :161 | P2 | P2 — already has vertical-aware version in InboxTab; Overview still uses old hardcoded helper |
hasChurchProfile variable name/logic | :214 | Minor | P3 — naming only, logic is generic |
2.2 TrainAITab.tsx (Train AI tab)
Severity: P0 — all 7 sections were visible for funeral, most are church-specific concepts.
| Item | File | Severity | Status |
|---|---|---|---|
| Section "Church Knowledge" shown for funeral | TrainAITab.tsx:103 | P0 | Fixed in this PR — verticalKeys: ['church'] |
| Section "Theology & Tradition" shown for funeral | :112 | P0 | Fixed in this PR — verticalKeys: ['church'] |
| Section "Pastor Pulse" shown for funeral | :157 | P0 | Fixed in this PR — verticalKeys: ['church'] |
| Section "Chat Simulator" copy "ministry conversations" | :147 | P1 | Fixed in this PR — generic copy + verticalKeys: ['church'] |
| Hero heading "Teach your AI about {churchName}" | :304 | P1 | Fixed in this PR — uses orgName with funeral fallback |
| Hero body "sermons, FAQs, theology, staff" | :307 | P1 | Fixed in this PR — vertical-aware copy |
| Section "Agent Personality" body "pastor voice, greeting script" | :127 | P2 | P2 backlog — "pastor voice" language |
| Section "FAQs" body "visitors and members ask most" | :137 | P2 | Partially fixed — "callers and clients" |
2.3 SettingsTab.tsx (Settings slide-over body)
Severity: P0/P1 — gear icon is clicked constantly, most bleed is visible immediately.
| Item | File:Line | Severity | Status |
|---|---|---|---|
Sub-tab SETTINGS_SUB_TABS[0].label = "Church Profile" | SettingsTab.tsx:265 | P0 | Fixed in this PR — overridden to "Profile" for non-church |
| Sub-tab notifications tooltip "prayer requests, visitor info" | :267 | P1 | Fixed in this PR — generic copy for non-church |
| Sub-tab integrations tooltip "Connect Planning Center, church tools" | :268 | P1 | Fixed in this PR — generic copy |
| Sub-tab sharing description "congregation and webmaster" | :269 | P2 | P2 backlog |
HeroPhotoUploader label "Church Banner Photo" | :163 | P1 | Fixed in this PR — "Banner Photo" |
HeroPhotoUploader help text "top of your church page" | :215 | P1 | Fixed in this PR — "top of your page" |
| SectionCard "Church Contact & Social Media" | :461 | P1 | Fixed in this PR — vertical-aware title |
| Field label "Church Phone" | :467 | P1 | Fixed in this PR — "Phone" for non-church |
| Field label "Church Address" | :479 | P1 | Fixed in this PR — "Address" for non-church |
| Field label "Church Social Media Links" | :481 | P1 | Fixed in this PR — "Social Media Links" for non-church |
| Section "Pastor Availability" | :508 | P0 | Fixed in this PR — "Staff Availability" for non-church |
| Availability description "when your pastor is available" | :513 | P0 | Fixed in this PR — vertical-aware |
| Crisis escalation "speak with the pastor" | :570 | P1 | Fixed in this PR — "a staff member" for non-church |
| Escalation placeholder "e.g., Pastor Johnson" | :582 | P1 | Fixed in this PR — "e.g., Director Smith" |
| Escalation placeholder "email pastor@church.org" | :587 | P1 | Fixed in this PR — conditional |
Escalation helpText "specific to your church" | :598 | P1 | Fixed in this PR — "your organization" for non-church |
Escalation suggestion with church-specific scenarios | :599 | P1 | Fixed in this PR — vertical-aware suggestion |
| Planning Center integration block | :773 | P0 | Fixed in this PR — gated {isChurch && (…)} |
| Cal.com description "pastoral meetings, visitor calls" | :813 | P1 | Fixed in this PR — vertical-aware copy |
| Cal.com locked description "pastoral meetings, visitor calls" | :826 | P1 | Fixed in this PR — vertical-aware |
| TEAM_ROLES includes prayer_team, care_team, worship_leader, etc. | :253-261 | P0 | Fixed in this PR — TEAM_ROLES_CHURCH vs TEAM_ROLES_GENERIC |
| Team explainer "prayer requests, pastoral callback details" | :969 | P1 | Fixed in this PR — vertical-aware |
| Section "Church Ownership & Security" | :1007 | P1 | Fixed in this PR — "Account Ownership" for non-church |
| Ownership description "this church account" | :1009 | P1 | Fixed in this PR — "this account" for non-church |
2.4 SettingsPanel.tsx (Settings slide-over outer nav)
Severity: P1 — the outer 4-tab nav.
| Item | File:Line | Severity | Status |
|---|---|---|---|
| Account description "Your church profile, office hours" | SettingsPanel.tsx:162 | P0 | Already fixed — SECTION_META_CHURCH vs SECTION_META_GENERIC existed (Lane C) |
| Notifications description "prayer requests, visitor info" | :170 | P0 | Already fixed — generic copy in SECTION_META_GENERIC |
| Integrations description "Planning Center, Cal.com" | :175 | P0 | Already fixed — generic copy in SECTION_META_GENERIC |
2.5 InboxTab.tsx
Severity: P1 — partially fixed already.
| Item | File:Line | Severity | Status |
|---|---|---|---|
PrayerCard title "Prayer request" | InboxTab.tsx:447 | P1 | Already vertical-aware — "Family inquiry" for funeral |
| Subtitle "Every call, prayer, visitor, and safety flag" | :1211 | P1 | P2 backlog — funeral shows "prayer" |
PrayerCard action "Assign to prayer team" | :497 | P2 | P2 backlog — funeral has no prayer team |
REDACTED_PRAYER = "Confidential — contact the pastor" | :176 | P1 | Already partially fixed — getRedactedPrayer() returns "Family inquiry" for funeral but REDACTED_CALLBACK still says "Pastoral inquiry" |
REDACTED_CALLBACK = "Pastoral inquiry" | :177 | P1 | P2 — funeral should say "Family inquiry" |
getRedactedPrayer() and getRedactedCallback() | :166-173 | P1 | Partially done — prayer is fixed, callback still says "Pastoral" |
2.6 AdminDashboard.tsx (Shell)
Severity: P2 — mostly structural, not user-visible copy.
| Item | File:Line | Severity | Status |
|---|---|---|---|
ALL_TABS descriptions use "your church" | :158,168,179 | P2 | P2 backlog — only visible if verticalProfile.tabs is empty (shouldn't happen for funeral) |
HASH_TO_TAB references 'church-info' key | :377 | Minor | P3 — internal routing key, not visible |
VoiceAgentData type has pastor_name, sermon_topic, etc. | :69-95 | Minor | P3 — type-level, not displayed |
2.7 InboxTab.tsx — funeralInboxFeedQuery Dead Code
Previously flagged by Lane A. The InboxTab prefetches using the church-path query (from page.tsx → getInboxStream()), NOT funeralInboxFeedQuery. The funeral vertical's inboxFeedQuery in funeral.ts is defined but the InboxTab does not call it — it uses the pre-fetched initialInbox prop from the server. This is confirmed OK for Phase 1 because funeral.ts's inboxFeedQuery filters by church_id anyway, same as the church path. Phase 2 migration note: when InboxTab gets a "load more" / pagination feature, it will need to call verticalProfile.inboxFeedQuery() not the hardcoded church helper.
3. Architecture Proposal — Baseline + Vertical Extension Model
3.1 Shared Baseline Shell (every vertical)
Every vertical gets these tabs from VerticalProfile.tabs (no church fallback needed once all verticals declare tabs):
| Tab key | Component | Vertical-aware? |
|---|---|---|
overview | DashboardOverview | Needs full vertical-awareness (see above) |
inbox | InboxTab | Mostly done — terminology hooks wired |
training | TrainAITab | Fixed in this PR — section filter by verticalKeys |
upgrade | UpgradeTab | Shared — shows plan info, no church copy |
These church-only tabs are filtered out for non-church via verticalProfile.tabs:
social(ShareWiseAI — church/general)website(Pro Website — church only today)
Decision: church and funeral share the same shell component (AdminDashboard.tsx), vertical tabs declared in FUNERAL_TABS / CHURCH_TABS. The shell remains generic; tabs/content are per-vertical.
3.2 Vertical Extension Slots (formal API)
The VerticalProfile interface already has the right shape. Recommended additions:
interface VerticalProfile {
// ... existing fields ...
/** Church-specific Training sections to include (default: all). */
trainingSections?: TrainAISection[];
/** Team roles available in this vertical's invite form. */
teamRoles?: { value: string; label: string }[];
/** Settings sections to exclude (e.g., funeral excludes Planning Center). */
settingsExclude?: string[];
/** Overview metric labels (overrides "Prayer requests", "New visitors", etc.) */
overviewMetricLabels?: {
prayerRequests?: string;
visitorContacts?: string;
callbacks?: string;
};
}
This would let the vertical's profile.ts declare exclusions declaratively rather than scattering isChurch guards everywhere.
Recommended Phase 2 refactor: Move all isChurch ? X : Y guard patterns into vertical profile declarations. The guards shipped in this PR are the minimal viable fix; Phase 2 should refactor them to profile-driven.
3.3 Feature Map — Church-Specific vs Shared vs Funeral-Specific
| Feature | Church | Funeral | Shared | Notes |
|---|---|---|---|---|
| Prayer Requests | ✓ | — | — | InboxTab: "Prayer request" tile exists but terminology-mapped |
| Theology & Tradition | ✓ | — | — | Filtered out in TrainAITab |
| Pastor Pulse | ✓ | — | — | Filtered out in TrainAITab |
| Chat Simulator | ✓ | — | — | Filtered out in TrainAITab |
| Church Knowledge | ✓ | — | — | Filtered out in TrainAITab |
| Planning Center | ✓ | — | — | Gated in SettingsTab |
| Prayer Team role | ✓ | — | — | TEAM_ROLES_CHURCH only |
| Care Team role | ✓ | — | — | TEAM_ROLES_CHURCH only |
| At-Need Inbox chip | — | ✓ | — | Phase 2: InboxTab should show at_need chip for funeral |
| Pre-Planning tab | — | ✓ | — | Phase 2 |
| Service Catalog tab | — | ✓ | — | Phase 2 |
| FAQs | ✓ | ✓ | ✓ | Shared with generic copy (this PR) |
| Agent Personality | ✓ | ✓ | ✓ | Shared — "pastor voice" copy P2 |
| Safety Rules | ✓ | ✓ | ✓ | Shared |
| Notifications | ✓ | ✓ | ✓ | Shared — copy vertical-aware (this PR) |
| Cal.com Booking | ✓ | ✓ | ✓ | Shared — copy vertical-aware (this PR) |
| Team Management | ✓ | ✓ | ✓ | Shared — roles filtered (this PR) |
| Office Hours | ✓ | ✓ | ✓ | Shared |
3.4 Settings Architecture
The current 4-section outer nav (Account / Team / Notifications / Integrations) is correct as a shared baseline. Issues:
- Account sub-nav pills: "Profile" / "Hours" / "Sharing" — "Profile" pill key is still
church-infointernally. Works, but the label now reads "Profile" for non-church (fixed in this PR via.map()override). P2: rename the SettingsSubTab key fromchurch-infotoprofilewith a backwards-compat alias. - Notifications: church copy mentions "prayer requests, visitor info" — fixed in this PR via
.map()overrides. - Integrations: Planning Center is church-only — gated in this PR. Cal.com is shared — copy updated.
- Team roles:
TEAM_ROLES_CHURCHvsTEAM_ROLES_GENERIC— fixed in this PR. P2: move team roles toVerticalProfile.teamRolesfor declarative config.
3.5 Design Tokens per Vertical
Current: sacred-gold (#D4AF37) + navy (#1B365D) + cream (#FEFCF8) for all verticals.
Funeral brand consideration: The FuneralWiseAI brand (per funeralwiseai.com) uses a more dignified, muted palette. The admin dashboard uses hard-coded color values (#D4AF37, #1B365D) in DashboardOverview.tsx, TrainAITab.tsx, and InboxTab.tsx.
Recommendation (Phase 3): Thread verticalProfile.brand.accentColor and verticalProfile.brand.primaryColor as CSS custom properties into the admin shell root element. Components use var(--admin-accent) instead of #D4AF37. The VerticalProvider sets these on mount. This keeps the church admin gold and the funeral admin a more appropriate slate/warm-gray.
For now (Phase 1), the sacred-gold on a funeral home admin is acceptable — it's not wrong, just not optimally branded. Ship Phase 3 design tokens after Phase 2 content is complete.
4. Phase 3 — Top 3 Fixes Shipped (this PR)
Fix 1: TrainAITab — Hide church-only sections from funeral
Files: TrainAITab.tsx
What: Added verticalKeys?: string[] to SectionDefWithVertical, replaced SECTIONS const with ALL_SECTIONS. Sections church-knowledge, theology, simulator, pastor-pulse are verticalKeys: ['church'] — invisible on funeral. Sections agents, faqs, safety are shared. Component uses useVertical() to filter at render time.
Why first: A funeral director opening Train AI would see "Church Knowledge", "Theology & Tradition", "Pastor Pulse", and "Chat Simulator" — all completely wrong for a funeral home. This is the most jarring single surface.
Fix 2: DashboardOverview — Vertical-aware metric tiles, welcome copy, quick actions, setup rail
Files: DashboardOverview.tsx
What: Added useVertical(). Updated: welcome subtitle, notification email banner, metric tiles ("Prayer requests" → "Family inquiries", "New visitors" → uses terminology.visitor), analytics donut labels, Setup rail eyebrow ("Get your church online" → funeral variant), QuickActions body/title copy, RecentPrayersCard gated to isChurchVertical && role === 'prayer_team', upsell card "Get more from ChurchWiseAI" → "Unlock more features".
Why second: The Home tab is the landing surface. Every session starts here. Church-branded metric tiles and "Get your church online" on a funeral home admin is immediately confusing.
Fix 3: SettingsTab — Gate church-specific forms, roles, integrations
Files: SettingsTab.tsx
What: Added useIsChurchVertical(). Applied overrides via .map() on SETTINGS_SUB_TABS to change "Church Profile" → "Profile" and update tooltip/description copy for non-church. Updated: banner photo label/helptext, Contact section title/labels, Pastor Availability → "Staff Availability", escalation placeholder/helptext copy, Planning Center gated to {isChurch && (...)}, Cal.com copy vertical-aware, TEAM_ROLES_CHURCH vs TEAM_ROLES_GENERIC, ownership section title/description.
Why third: Settings is the most-clicked surface after Inbox. A funeral director seeing "Planning Center", "Church Phone", "Prayer Team", and "Church Ownership" is maximum brand confusion — makes the product feel like a church tool in disguise.
5. P2 / P3 Backlog
Prioritised by customer-impact.
P2 (next sprint, ~2-3 days total)
| # | Item | File(s) | Effort | Proposed fix |
|---|---|---|---|---|
| P2-1 | InboxTab subtitle "Every call, prayer, visitor, and safety flag" | InboxTab.tsx:1211 | S | Use terminology to replace "prayer" and "visitor" |
| P2-2 | REDACTED_CALLBACK = "Pastoral inquiry" for funeral | InboxTab.tsx:177 | S | getRedactedCallback(verticalKey) similar to existing getRedactedPrayer() |
| P2-3 | PrayerCard action "Assign to prayer team" for funeral | InboxTab.tsx:497 | S | Gate to isChurch or use terminology |
| P2-4 | SettingsSubTab key rename: church-info → profile | SettingsTab.tsx, SettingsPanel.tsx, AdminDashboard.tsx hash map | M | Add backwards-compat alias church-info → resolves to profile; rename key in new code |
| P2-5 | Agent Personality section "pastor voice" copy | TrainAITab.tsx:127 | S | Vertical-aware description for agents section |
| P2-6 | ShareLinkCard hardcodes churchwiseai.com/chat/${churchSlug} | DashboardOverview.tsx:542 | M | Funeral has no chatbot yet — hide chat link for non-church; use verticalProfile.hostname for link base |
| P2-7 | redactCallbackSummary() in Overview returns "Pastoral inquiry" | DashboardOverview.tsx:161 | S | Pass verticalKey, return "Family inquiry" for funeral |
| P2-8 | Move TEAM_ROLES_GENERIC to VerticalProfile.teamRoles | funeral.ts, vet.ts, SettingsTab.tsx | M | Declarative — no more inline conditionals per role field |
| P2-9 | At-Need Inbox chip for funeral | InboxTab.tsx | M | at_need chip only shows for funeral vertical — verticalProfile.inboxChips declaration |
| P2-10 | InboxTab "load more" pagination: call verticalProfile.inboxFeedQuery() not hardcoded helper | InboxTab.tsx (future) | L | Required for pagination feature; deferred until pagination is built |
P3 (polish / low friction)
| # | Item | File(s) | Effort |
|---|---|---|---|
| P3-1 | Design tokens per vertical: funeral admin uses dignified slate vs sacred-gold | globals.css, VerticalProvider, all admin components | L |
| P3-2 | displayPastorName() helper rename to displayDirectorName() + vertical-aware default | DashboardOverview.tsx | S |
| P3-3 | ALL_TABS descriptions say "your church" (fallback only, not shown for funeral) | AdminDashboard.tsx | S |
| P3-4 | VoiceAgentData type has church-specific fields (pastor_name, sermon_topic, etc.) exposed to non-church components | AdminDashboard.tsx, TrainAITab.tsx | M |
| P3-5 | SettingsTab sharing description "congregation and webmaster" | SettingsTab.tsx:269 | S |
| P3-6 | hasChurchProfile variable name in DashboardOverview | DashboardOverview.tsx:214 | Trivial |
| P3-7 | WhatToExpectForm — church-specific "What to Expect" framing shown in church-info sub-tab for all plans | SettingsTab.tsx:439 | S — gate to isChurch |
| P3-8 | SharingForm copy — "Share with your congregation" for funeral | SharingForm (shared component) | S |
6. Screenshot Status
Screenshots were not captured in this audit cycle due to time-box constraints. Code-read is authoritative for identifying bleed strings.
To capture screenshots for future audit cycles:
# Church admin (Playwright headless)
playwright test --config playwright.config.ts -- admin-church-audit.spec.ts
# Funeral admin
playwright test --config playwright.config.ts -- admin-funeral-audit.spec.ts
Screenshot target directory: knowledge/specs/screenshots/2026-04-30-dashboard-audit/{church,funeral}/
7. What Was NOT Changed (by design)
safety.py,moderation.py,crisis_copy.ts— LIFE-SAFETY files, out of scope- Database schema — no new columns in this PR (P2 backlog)
- Church admin behaviour — every existing church feature remains accessible and unchanged
- Voice agent runtime — UI/UX scope only
- Chatbot endpoint — UI/UX scope only