# Missing-Features Audit — 2026-05-06 Focused pass on **features that look done in the UI but aren't fully wired through the service layer**, plus **admin settings exposed to users that no code reads**. Companion to `docs/audit-comprehensive-2026-05-06.md` — the three "coming soon" stubs already documented there (client Files tab, client reservations history, berth tabs), the import-worker stub, the two interest-form TODOs, and the EOI "Price: TBD" finding are NOT re-flagged here. Hard cap: 12 findings. Severity tiers below. --- ## VISIBLE-BROKEN (admin sees a control, click is a no-op or wrong) ### V1. 6 of 8 admin-editable email subject overrides are silently ignored at send time **Files:** - `src/components/admin/email-templates-admin.tsx:24-72` (UI) - `src/lib/email/template-catalog.ts:16-25` (catalog of 8 keys) - `src/lib/services/portal-auth.service.ts:120-127, 332-339` (the only consumers of `loadSubjectOverride`) The `/admin/email-templates` page lets an admin override the subject line on **eight** transactional templates: `portal_activation`, `portal_reset`, `portal_invite_resend`, `crm_invite`, `inquiry_client_confirmation`, `inquiry_sales_notification`, `residential_inquiry_client_confirmation`, `residential_inquiry_sales_alert`. The save endpoint persists each one to `system_settings` (`email_template__subject`). Only **two** of those eight are ever read at send time — `portal_activation` and `portal_reset` in `portal-auth.service.ts`. A repo-wide search for `loadSubjectOverride` / `settingKeyForSubject` returns no other consumers. The other six templates use their hardcoded subject regardless of the admin override. **Impact:** sales/ops teams will customize an inquiry confirmation subject, hit Save, see the "Overridden" badge, and silently ship the default subject to every prospect. **Fix:** small per template — call `loadSubjectOverride(portId, key)` in each sender (`crm-invite.service.ts`, the inquiry sender, the residential inquiry sender, the portal-invite-resend path) and pass the result through as the email subject. **Scope:** small (5 callsites + tests). --- ### V2. Branding admin (logo / app name / primary color / email header & footer HTML) saves to settings but no code reads them **Files:** - `src/app/(dashboard)/[portSlug]/admin/branding/page.tsx:7-46` — UI with five fields. - `src/lib/services/port-config.ts:240-272` — `getPortBrandingConfig()` resolves the five `branding_*` settings into a typed config. - Repo-wide: `getPortBrandingConfig` has **zero callers** outside its declaration. The five `SETTING_KEYS.branding*` constants are only read inside `getPortBrandingConfig` itself. The admin panel is functional end-to-end (write hits the settings API, "Reset to default" works), and the email-templates module hardcodes `s3.portnimara.com/...` for the logo URL plus a fixed table layout. None of the email-rendering helpers (`renderEmail`, the template modules in `src/lib/email/templates/`) call `getPortBrandingConfig`, and the `` component sources its logo + colors from constants too. **Impact:** every multi-tenant assumption made about branding is broken. A second port wired into this CRM will see Port Nimara's logo - colors in every transactional email and on the auth pages, even after their admin "configures branding" successfully. **Fix:** plumb `getPortBrandingConfig(portId)` through the email renderer (header/footer HTML + primary button color), and through `` via a server-fetched prop. **Scope:** medium (touches every transactional email + auth shell). --- ### V3. Reminder admin page configures defaults that no service applies **Files:** - `src/app/(dashboard)/[portSlug]/admin/reminders/page.tsx:7-50` — UI for default-enabled, default-days, digest-enabled, digest-time, digest-timezone. - `src/lib/services/port-config.ts:284-306` — `getPortReminderConfig()` defines the schema. - Repo-wide: the keys (`reminder_default_*`, `reminder_digest_*`) and `getPortReminderConfig` have **zero callers**. Same pattern as V2. The admin sets "enable reminders by default on new interests" → toggles to true → save succeeds → newly-created interests still default to `reminderEnabled=false`. The digest-time + timezone fields go nowhere — there is no scheduler that batches pending reminders into a daily digest. **Impact:** the entire reminder UX is decorative. Sales reps think they configured a daily digest at 09:00 Europe/Warsaw, get fire-as-they-hit notifications instead. **Fix:** wire `getPortReminderConfig` into (a) the interest-create service (defaults), (b) the maintenance/notifications worker that fires reminders (digest batching + delivery window). The `digest` behavior didn't exist before this audit — needs a new scheduled job. **Scope:** medium (defaults are small, digest job is new code). --- ### V4. Portal dashboard "My Memberships" tile has no link, no destination page, and isn't reachable from nav **Files:** - `src/app/(portal)/portal/dashboard/page.tsx:58-63` — `` — note no `href` prop. - `src/components/portal/portal-nav.tsx:8-15` — six nav entries, no memberships. - Filesystem: `src/app/(portal)/portal/memberships/` does not exist. The dashboard shows a count of "memberships" (companies the portal user belongs to) but the tile is non-clickable and there is no `/portal/memberships` route. A user with 3 memberships sees the tile, clicks → nothing happens. **Impact:** dead-end on the portal home for any client tied to a company (the residential and yacht-ownership use-cases). **Fix:** ship `/portal/memberships/page.tsx` listing the companies returned by the existing `companyMemberships` query (already aggregated in `getPortalDashboard`), and add it to `PortalNav`. Or pull the tile if memberships isn't a portal feature. **Scope:** small. --- ### V5. Company detail page Documents tab is a "Coming soon" stub **File:** `src/components/companies/company-tabs.tsx:230-234` ```ts { id: 'documents', label: 'Documents', content: , }, ``` Visible alongside the working Notes / Activity / Addresses / Members tabs on every company detail page. NOT covered by the existing audit doc's H7 (which lists clients, client reservations, and berths). **Impact:** the same UX problem H7 calls out for clients. **Fix:** mirror what client-Files-tab needs — query `documents` joined to a polymorphic billing-entity = company link, render a list, ship a download button. Or hide the tab. **Scope:** small to medium. --- ## HALF-WIRED (the page works but the surrounding promise overstates it) ### V6. "Onboarding" admin page is a static checklist, not the wizard the page itself promises **File:** `src/app/(dashboard)/[portSlug]/admin/onboarding/page.tsx` The page renders 8 stepwise links and explicitly says (lines 71-72, 98-110): "The future onboarding wizard will track progress per port…", "What this page will become", "The wizard will record completion per port in `system_settings`, gate the public marketing-site cutover…". The admin landing card describes it as the "Initial-setup wizard for fresh ports" — admins clicking through expect a wizard, get a static table of contents. **Impact:** the only "fresh port" workflow doesn't exist; cutover gating logic mentioned in the page body is also unimplemented. **Fix:** either (a) build the wizard with progress in `system_settings` - banner integration, or (b) re-label both this page and the admin landing card to "Setup checklist" so expectations match reality. **Scope:** large for the wizard; tiny for the relabel. --- ### V7. Backup & Restore admin page is informational only — admin landing card promises actions **Files:** - `src/app/(dashboard)/[portSlug]/admin/backup/page.tsx` - `src/app/(dashboard)/[portSlug]/admin/page.tsx:148` — landing card description: "Database snapshots and on-demand exports." The landing card sells "on-demand exports". The actual page renders a two-card explainer: "Current backup posture" (read-only) and "What this page will become" (the entire interactive surface — list snapshots, "Take backup now" button, per-port logical export, restore preview, GDPR per-client export). None of those exist. **Impact:** the "Backup & Restore" tile is functionally a docs page. Compliance officers / users expecting a self-serve GDPR export button have to file a support ticket. **Fix:** match the language on the landing card to the page reality ("Backup posture" → docs only) until the snapshot/export buttons ship. The maintenance worker already runs `database-backup` (per `docs/audit-comprehensive-2026-05-06.md` C1 — though that worker isn't imported), so wiring "Take backup now" against the existing job is small once C1 is fixed. **Scope:** small (doc tweak) or medium (button + per-port export endpoint). --- ### V8. Inquiry inbox is read-only — no "Convert to Client" / "Mark resolved" / "Assign" actions **File:** `src/components/admin/inquiry-inbox.tsx` (entire file, 207 lines, ends at the View payload toggle) The inbox lists website-form submissions (berth_inquiry, residence_inquiry, contact_form) with filter chips and a "View payload" expand. There is no action to: - create a client/interest from the submission, - assign the inquiry to a sales rep, - mark it resolved / triaged, - reply directly, - archive or trash the row, - export. The `website_submissions` table appears to be permanent — every inquiry ever received remains in the inbox forever, with no triage state. Sales has to manually copy the email into a new client form and back-reference the original submission. **Impact:** the inquiry-to-pipeline conversion step isn't supported in the CRM. The marketing-site cutover (per the user's `project_email_ownership_at_cutover.md` memory) will increase volume on this surface and make the missing triage UX painful. **Fix:** add a per-submission "Convert" action that prefills the client + interest forms with the payload, plus a `triage_state` column (open / converted / dismissed) and a default filter that hides non-open rows. **Scope:** medium. --- ## MOBILE PARITY ### V9. Mobile More-sheet is missing several real top-nav destinations **File:** `src/components/layout/mobile/more-sheet.tsx:38-50` `MORE_ITEMS` lists 11 entries. The dashboard route directory has at least these top-level segments not represented anywhere in the mobile bottom-tabs OR more-sheet: - `residential` — exists at `/[portSlug]/residential/...` - `notifications` — exists at `/[portSlug]/notifications/...` - `berth-reservations` — exists at `/[portSlug]/berth-reservations/...` - `documents` — exists as a top-level page (separate from the bottom tab `documents`, which IS in mobile-bottom-tabs) - `website-analytics` — exists at `/[portSlug]/website-analytics/...` A mobile-only user has no path to any of them. The Documents bottom tab does cover the doc list, but residential is an entire feature domain (per the `(dashboard)/.../residential` directory) with no mobile entry point. **Impact:** anyone using the mobile chrome to triage on the go can't reach residential clients/interests, alerts (`alerts` IS in the sheet), or notifications. **Fix:** add the missing segments to `MORE_ITEMS`. If the grid feels too dense, reorganize into sections. **Scope:** small. --- ### V10. Portal has no "Profile" / "Change password" surface **Files:** - `src/components/portal/portal-nav.tsx:8-15` — six tabs, no profile. - Filesystem: no `src/app/(portal)/portal/profile/` directory. A portal user who wants to change their email, phone, mailing address, or password has no UI. The portal sign-in flow goes through the better-auth session but the app exposes zero account-management controls. The "Need assistance?" card on the dashboard tells the user to contact the port team — which is the explicit answer for data edits, but does not cover password changes (a security expectation, not a per-port-staff burden). **Impact:** every portal user who forgets their password (after already activating) has to use `/portal/forgot-password` even if they remember the old one. There's no proactive password rotation. A user who changes their phone number has to email the port to update it. **Fix:** ship `/portal/profile` with at minimum: read-only PII view + "Change password" form (re-uses the existing reset-password endpoint or a new `change-password` endpoint that takes the current pw). Phone/address editing is a longer fix because of the audit-trail implications. **Scope:** small for password; medium with PII edits. --- ### V11. Portal invoices page lists invoices but offers no view/download — even though documents do **File:** `src/app/(portal)/portal/invoices/page.tsx:53-99` Each invoice row shows number, status, due/paid dates, amount, and a small payment-status caption. There is no link, no PDF view, no download. By contrast, the portal Documents page (peer route) ends each row with a `` that fetches a signed S3 URL. Compare to admin/CRM where invoices have a full PDF render flow (invoice service generates the PDF + signed URL). **Impact:** a portal user can see they owe money and cannot retrieve the actual invoice document. They have to email the port to ask for a PDF copy. **Fix:** add an invoice-PDF endpoint under `/api/portal/invoices/[id]/ download` mirroring the documents one, and a download button on each row. The invoice PDF generator already exists (`src/lib/services/ invoices.ts`). **Scope:** small. --- ## DEV-NOTES (legitimately staged-for-later, calling out so they're not forgotten) ### V12. Email-templates admin only edits subject lines — body editing is a documented "next iteration" **Files:** - `src/components/admin/email-templates-admin.tsx:78-79` — "Customize the subject line of transactional emails per port. Body editing is the next iteration; for now the layout and HTML stay locked to the default template." - `src/lib/email/template-catalog.ts:5-9` — same statement in the catalog header. The page is honest about the limitation, so this isn't a "broken" finding. But it's a notable shipped-without-the-killer-feature gap: the multi-tenant promise of per-port email customization can't deliver the body changes that ports actually want (logo placement, signature, language). Combined with V2 (branding HTML fragments aren't read at all), there is currently NO way for a non-super-admin per-port admin to customize the email body in any way. **Impact:** confined to admin expectations — most ports will assume "Email templates" = "edit the email", click in, see only a subject field, and request the missing body editor. **Fix:** scope a body-editing flow that reuses the `merge_fields.ts` token catalog (the validator already exists for document templates) for safety. Until that's built, V2 + this finding together mean a "rebrand the emails" task is single-tenant only. **Scope:** large (HTML editor + token validator + per-port override storage + render-side composition). --- ## Summary 12 findings, four severity tiers: - **Visible-broken (V1-V5):** five admin/portal controls produce no effect. V1 (email overrides) and V2 (branding) are the highest impact — both silently break the multi-tenant promise. - **Half-wired (V6-V8):** three pages where the surrounding wrapper oversells what's there. V8 (inquiry inbox) is the largest scope. - **Mobile parity (V9-V11):** mobile users can't reach several real features; portal users have no profile/password surface and can't download invoices. - **Dev-notes (V12):** documented limitations called out for the roadmap. The two highest-leverage quick wins are **V1** (wire 6 missing template subject overrides — a few hours) and **V11** (portal invoice download — small, fixes a real customer pain point).