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...

144 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# HQ Console — Go-Live Cluster Spec (Due Dates, Import UI, Reminders/Documents Pages, Shell & Profile, Client Support Data)
> **STATUS: DELIVERED 2026-07-17; reviewed same day — all 8 review findings fixed
> (`fix(review)` commit), suite at 345 tests.** 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.
>
> Post-review deltas vs the text below: the clients CSV also carries optional
> `anydesk,os,district,sector` columns (§3's "mapped from the APEX CSV" promise now holds —
> A4's "same two CSVs" means same files, with four optional extra headers), and the
> district/sector book filters match partial text (LIKE) rather than exact values.
## 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: <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.