# 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_<key>_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 `<BrandedAuthShell>` 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
`<BrandedAuthShell>` 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` — `<PortalCard
title="My Memberships" ... icon={Building2} />` — 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: <EmptyState title="Documents" description="Coming soon" />,
},
```

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 `<DocumentDownloadButton documentId={doc.id} />` 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).
