From 02bc75d6915d4d4c51f2eceac7838c77a905e7c3 Mon Sep 17 00:00:00 2001 From: Thomas Joise Date: Fri, 17 Jul 2026 10:53:01 +0530 Subject: [PATCH] =?UTF-8?q?docs(spec):=20go-live=20cluster=20design=20?= =?UTF-8?q?=E2=80=94=20due=20dates,=20import=20UI,=20reminders/documents?= =?UTF-8?q?=20pages,=20shell=20&=20profile,=20client=20support=20data=20(8?= =?UTF-8?q?-phase=20roadmap)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../specs/2026-07-17-golive-cluster-design.md | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-17-golive-cluster-design.md diff --git a/docs/superpowers/specs/2026-07-17-golive-cluster-design.md b/docs/superpowers/specs/2026-07-17-golive-cluster-design.md new file mode 100644 index 0000000..df1ab74 --- /dev/null +++ b/docs/superpowers/specs/2026-07-17-golive-cluster-design.md @@ -0,0 +1,114 @@ +# HQ Console — Go-Live Cluster Spec (Due Dates, Import UI, Reminders/Documents Pages, Shell & Profile, Client Support Data) + +> **STATUS: DRAFT — awaiting founder review.** 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 | + +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). +- **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. + +## 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. **Docs refresh** — STATUS/README/CLAUDE/D-number record. + +**Total: 8 phases.** After this slice: the **Postgres switch**, then the real cutover.