Six audit documents capture the 2026-05-06 review pass (comprehensive, frontend, missing-features, permissions, reliability) along with the Documenso integration audit + locked build plan that drove the bulk of subsequent feature work. Adds `docs/admin-ux-backlog.md` as a living tracker for the autonomous push — every item marked DONE or REMAINING with file pointers and scope estimates so future sessions can pick up where this one stopped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
406 lines
16 KiB
Markdown
406 lines
16 KiB
Markdown
# 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).
|