docs: comprehensive audits + Documenso build plan + admin UX backlog
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>
This commit is contained in:
405
docs/audit-missing-features-2026-05-06.md
Normal file
405
docs/audit-missing-features-2026-05-06.md
Normal file
@@ -0,0 +1,405 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user