You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/docs/superpowers/specs/2026-07-17-golive-cluster-d...

11 KiB

HQ Console — Go-Live Cluster Spec (Due Dates, Import UI, Reminders/Documents Pages, Shell & Profile, Client Support Data)

STATUS: DELIVERED 2026-07-17. Successor slice to the delivered D16 funnel (2026-07-17-quote-to-close-funnel-design.md). The Postgres switch (D15) is deliberately NOT in this slice — it is the dedicated task queued immediately after it, before the real-data import commit.

1. Overview & goals

Six workstreams that take the console from "feature-complete" to "safe to go live on":

WS What Why now
A Invoice due dates — dated default terms + per-document override Overdue chase currently counts from issue date (A7); the client email should say the honest thing
B APEX import web UI (owner-only) The 300-client cutover is one-shot; CLI-only staging invites a mistake
C Standalone Reminders page Queue volume outgrows the dashboard card at 300 live clients
D Documents list page + honest pagination on GET /documents Documents are only reachable via client ledger/pipeline today; the endpoint returns ALL rows (rule-8 violation at scale)
E Shell & profile: collapsible sidebar, self-service profile (name + own password), "My Day" dashboard mode, minimal employee extras (phone/title) Self password change doesn't exist; the old app's most-used surface was the per-person workbench
F Client support data from APEX: AnyDesk/OS/DB-password (encrypted), district + sector, WhatsApp + secretary contacts AnyDesk was referenced 44× in the old app — support opens it daily; district/sector are the team's main list filters
G Editable module→clients roster + one-click Convert & send from list views Founder call: both directions (client→modules, module→clients) must be editable, and proforma→invoice must be one click from anywhere
H Section-per-module quotation PDF Founder call: a quote spanning all 5 modules prints each module as its own titled section (name, inclusions, price) with clean page breaks, totals + terms on the last page

Hard rules apply throughout (paise, one billing engine, dated config, immutable documents, portable repos, audited mutations in-txn, no silent caps, strict TS + green suite).

2. Assumptions & non-goals

  • A1 billing.payment_terms_days is a flat operational setting (like quote.followup.policy): the dated fact is the due date stamped on each document at issue, so history never rewrites when terms change.
  • A2 Due dates apply to INVOICE only (quotes/proformas are not payable paper).
  • A3 DB passwords encrypt with the same AES-256-GCM + HQ_SECRET_KEY used for the Gmail refresh token (crypto.ts). No HQ_SECRET_KEY ⇒ the field refuses to save, loudly.
  • A4 Import UI accepts the same two CSVs the CLI does; parsing/verification/commit logic is reused, not reimplemented (import-apex.ts functions get routes, not a rewrite).
  • A5 Roster editing (WS-G) reuses the SAME audited repo calls Client 360 uses (updateClientModule, assignModule) with the same gating — no second write path, so the two views can never disagree.
  • A6 The sectioned layout (WS-H) applies to QUOTATION documents only; invoices and proformas keep the compact billing table (they are payment paper, not proposals). It is rendered inside the ONE shared documentHtml renderer, so the preview-fidelity guarantee (screen HTML ≡ PDF HTML, byte-identical) is preserved by the existing golden test.
  • Non-goals: Postgres switch (next task); support ticket desk; WhatsApp sending; HR fields (DOB/joining/leave/payroll — permanently out per D16); bulk actions; kanban board.

3. Data model (all additive; no new tables)

Change Where Notes
document.due_date TEXT NULL SCHEMA + migrate() (PRAGMA-guarded) A column, not payload JSON — the scan needs portable SQL
setting billing.payment_terms_days seed '15' Default terms; owner-editable
staff_user.phone TEXT NULL, staff_user.title TEXT NULL additive Shown on Employees + profile; nothing routes on them
client.anydesk TEXT NULL, client.os TEXT NULL, client.db_password_enc TEXT NULL additive db_password_enc stored encrypted; NEVER returned by list/get payloads
client.district TEXT NULL, client.sector TEXT NULL additive Filterable on the client book; mapped from the APEX CSV at import
contacts JSON none WhatsApp + secretary captured as typed contact rows (role: 'whatsapp' | 'secretary') — UI labels only

4. API ([N] new · [C] changed)

Route Gate Notes
[C] POST /documents (draft) auth accepts optional dueDate on INVOICE drafts
[C] issueDocument stamps due_date = doc_date + terms when null and type is INVOICE (covers convert-and-send + recurring generation for free)
[N] PATCH /me auth (self) { displayName?, phone? } — audited, role untouchable
[N] POST /me/password auth (self) requires currentPassword; purges the user's OTHER sessions (keeps the current one)
[N] POST /import/stage owner body: { clientsCsv, invoicesCsv } text; runs existing staging; audited
[N] GET /import/status owner staging rows + per-row problems + verification report
[N] POST /import/commit owner refuses while ANY problem exists (existing guard); audited
[C] GET /documents auth gains page/pageSize + returns total (rule 8); keeps type/status/clientId filters
[C] PATCH /clients/:id auth accepts anydesk/os/district/sector; dbPassword (plaintext in, encrypted at rest) — owner/manager only for dbPassword
[N] POST /clients/:id/reveal-db-password owner/manager returns the decrypted value once; every reveal writes an audit row

5. Screens

  • /import (ADMIN, owner-only): two CSV pickers → staging tables with per-row problem chips → verification summary → Commit enabled only at zero problems, behind a confirm that states "APEX goes read-only after cutover". Re-stage freely.
  • /reminders (WORK): FilterChips Queued · Sent · Failed · Dismissed + Mine/All (managerial), table (kind · client · doc · due · status), Send/Preview/Dismiss reusing the dashboard components, pager. Dashboard keeps its short queue and links here.
  • /documents (WORK): type chips (All/QT/PI/INV/CN/RCT) + status filter, mono numbers, amount column, pager; row → /documents/:id.
  • /profile (user-chip menu): email + role read-only; edit display name/phone; change own password (current password required).
  • Sidebar collapse: footer ⟨/⟩ toggle to an icon rail, remembered in localStorage; the <900px hamburger behavior is unchanged.
  • "My Day" dashboard: staff land on mine (my pipeline due, my queue, my follow-ups); owner/manager get a Mine/Everyone toggle. Composition of existing scoped queries only.
  • Client 360 support card: AnyDesk (copy button) · OS · district · sector · DB password as •••• with owner/manager Reveal (audited) · contacts list gains WhatsApp/secretary typed rows.
  • Composer: Due date field on INVOICE drafts, auto-filled from terms, editable.
  • Invoice PDF: prints "Due date: " when present.
  • Module roster — full edit (WS-G): each row gains inline controls for kind, edition, next renewal and lifecycle status (saving through updateClientModule, audited); an ✕ unassigns (sets the link inactive); an Assign client… type-ahead (existing client search) adds a client to the module via assignModule. Same gating as Client 360.
  • One-click convert everywhere (WS-G): issued proformas show a Convert & send quick action on the Documents page rows and Pipeline rows — the same POST /documents/:id/convert-and-send behind the same confirm dialog.
  • Quotation PDF — section per module (WS-H): page 1 = letterhead + client + a one-line summary of the modules quoted; then one titled section per line — module name (+ pack/ edition), its "what's included" block, its line price — with page-break-inside: avoid and a break before each section so no module splits mid-section; final page = totals, GST split, terms, jurisdiction, signature. Modules without content print a tight section.

6. Due-date mechanics (WS-A detail)

  • Scan anchor becomes COALESCE(due_date, doc_date); milestone math (d7/d15/d30, F7 highest-only) is unchanged.
  • reminderContext: ctx.dueDate when present; template renders "was due on {dueDate} and is now {N} day(s) overdue", falling back to today's "past due" wording for legacy invoices without one.
  • Editing terms changes future stamps only — issued documents are immutable (rule 4).

7. Security notes (WS-E/F)

  • db_password_enc: AES-256-GCM, never in GET /clients/GET /clients/:id payloads, reveal-only endpoint, one audit row per reveal (action='reveal_db_password').
  • POST /me/password cannot touch role or other users; rate of change limited by the current-password requirement.
  • Import routes are owner-only and audited (stage/commit actions).

8. Build order (TDD; each phase independently shippable, suite green before landing)

  1. Due dates — column + terms setting + issue-stamp + scan COALESCE + template + composer field + PDF line. (Accept: stamp on all three issue paths; overdue math honors due_date; legacy invoices unaffected.)
  2. Documents pagination + page — API page/total, /documents screen + nav. (Accept: total honest; filters compose; 300-row dataset paginates.)
  3. Reminders page/reminders screen + nav; dashboard links to it. (Accept: status chips map to ?status=; staff see own only.)
  4. Profile + self password/me routes + page + user-chip menu. (Accept: wrong current password rejected; other sessions purged, current kept; role untouchable.)
  5. Sidebar collapse + My Day — shell toggle + dashboard mine/everyone. (Accept: collapse persists; staff default to mine.)
  6. Client support fields — columns + encrypted password + reveal + Client 360 card + district/sector filters + contact types. (Accept: reveal audited; password absent from all list payloads; saving without HQ_SECRET_KEY fails loudly.)
  7. Import UI — routes over existing importer + /import screen. (Accept: commit refuses with problems; re-stage idempotent; series seeding preserved.)
  8. Editable roster + quick convert — inline edit/assign/unassign on the module roster; Convert & send on Documents + Pipeline rows. (Accept: roster edits round-trip through the same audited repo fns as Client 360; unassign hides the row and shrinks totals; quick convert = exactly one invoice + one email.)
  9. Sectioned quotation PDF — section-per-module layout in documentHtml for QUOTATION. (Accept: 5-module quote renders 5 titled sections + totals page; no section splits across pages; preview-fidelity golden test still passes; INVOICE/PI layout unchanged.)
  10. Docs refresh — STATUS/README/CLAUDE/D-number record.

Total: 10 phases. After this slice: the Postgres switch, then the real cutover.