docs: record the review pass — 345 tests, all 8 findings closed

STATUS: count + review summary, D18 sentence in the intro, honest migrate()
column list, routes table gains /reminders /documents /profile /import and the
collapse/user-menu shell description. README + CLAUDE: 345. D18 record in
06-DECISIONS gains the review outcome. Spec banner notes the post-review
deltas (CSV support columns, LIKE filters).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 5 days ago
parent 94018b9ae4
commit 19fe037bb4

@ -43,7 +43,7 @@ distilled to what applies to HQ. A change that breaks one does not land.
| Append-only audit | Every mutation calls `writeAudit(db, userId, action, entity, id, before, after)` in the **same transaction** as the change. The audit log is append-only — never update or delete rows. Previews and the public share route write nothing. | `audit.ts` | | Append-only audit | Every mutation calls `writeAudit(db, userId, action, entity, id, before, after)` in the **same transaction** as the change. The audit log is append-only — never update or delete rows. Previews and the public share route write nothing. | `audit.ts` |
| Client id is the future tenant id | `client.id` is a client-generated UUIDv7 that becomes `tenant_id` when the cloud tier stands up — same rows, promoted once. Never re-key clients. | `repos-clients.ts` | | Client id is the future tenant id | `client.id` is a client-generated UUIDv7 that becomes `tenant_id` when the cloud tier stands up — same rows, promoted once. Never re-key clients. | `repos-clients.ts` |
| No silent caps | Every list that can grow paginates or warns; never silently truncate. | reports, directory queries | | No silent caps | Every list that can grow paginates or warns; never silently truncate. | reports, directory queries |
| Strict TS, green before landing | Strict TypeScript everywhere; `npm run typecheck` and `npm test` both clean before anything lands. Money math is locked by tests — a rounding change without a test change does not land. | `tsc`, vitest (344 tests) | | Strict TS, green before landing | Strict TypeScript everywhere; `npm run typecheck` and `npm test` both clean before anything lands. Money math is locked by tests — a rounding change without a test change does not land. | `tsc`, vitest (345 tests) |
## Running it ## Running it
@ -63,7 +63,7 @@ cd apps/hq-web && npm run dev
- Env: `HQ_DATA_DIR` (DB location), `SIMS_PIN_PEPPER` (set once, never change), `HQ_SECRET_KEY` - Env: `HQ_DATA_DIR` (DB location), `SIMS_PIN_PEPPER` (set once, never change), `HQ_SECRET_KEY`
(before Gmail), `GOOGLE_*` (Gmail), `AWS_*` (Cost Explorer), `HQ_PORT` (default 5182). (before Gmail), `GOOGLE_*` (Gmail), `AWS_*` (Cost Explorer), `HQ_PORT` (default 5182).
- Health check: `GET /api/health``{ ok: true }`. - Health check: `GET /api/health``{ ok: true }`.
- Verified state: `npm install` + typecheck clean, 344 tests pass. Exercise real flows end - Verified state: `npm install` + typecheck clean, 345 tests pass. Exercise real flows end
to end before claiming done. to end before claiming done.
## Docs (kept in `docs/`) ## Docs (kept in `docs/`)

@ -34,7 +34,7 @@ document and conversation we've had with them.
## Build state (start here) ## Build state (start here)
Working codebase, not a plan. `npm install` + typecheck are clean and **344 tests pass**. Working codebase, not a plan. `npm install` + typecheck are clean and **345 tests pass**.
Current state and run-vs-pending detail live in **[STATUS.md](STATUS.md)**; the go-live Current state and run-vs-pending detail live in **[STATUS.md](STATUS.md)**; the go-live
runbook (server, HTTPS, backups, Gmail/AWS/import wiring, the Postgres switch) is in runbook (server, HTTPS, backups, Gmail/AWS/import wiring, the Postgres switch) is in
**[docs/DEPLOY-HQ.md](docs/DEPLOY-HQ.md)**. **[docs/DEPLOY-HQ.md](docs/DEPLOY-HQ.md)**.

@ -13,7 +13,11 @@ append-only audit trail, an APEX cutover importer, and a background scheduler.
The 2026-07-17 quote-to-close slice (D16) added employee management with The 2026-07-17 quote-to-close slice (D16) added employee management with
owner / manager / staff roles, account-owner routing on clients, a cross-client owner / manager / staff roles, account-owner routing on clients, a cross-client
pipeline chase-list with derived stages, and escalating quote follow-up pipeline chase-list with derived stages, and escalating quote follow-up
reminders driven by the dated `reminder_schedule` table. reminders driven by the dated `reminder_schedule` table. The same-day D18
go-live cluster added invoice due dates, the Documents and Reminders pages,
self-service profiles, My Day, client support-access data, the APEX import web
UI, the editable module roster with one-click Convert & send, and the
section-per-module quotation PDF — then closed all 8 findings from its review.
## Verification state ## Verification state
@ -21,10 +25,14 @@ reminders driven by the dated `reminder_schedule` table.
| --- | --- | | --- | --- |
| `npm install` | clean | | `npm install` | clean |
| `npm run typecheck` (root + workspaces) | clean, no errors | | `npm run typecheck` (root + workspaces) | clean, no errors |
| `npm test` (`vitest run`) | **344 tests pass across 66 files** | | `npm test` (`vitest run`) | **345 tests pass across 66 files** |
The 344 figure is the live `vitest run` count measured 2026-07-17, after the The 345 figure is the live `vitest run` count measured 2026-07-17, after the
quote-to-close funnel slice and its review fixes. Test files live under D18 go-live cluster **and its post-delivery review pass** (8 reviewer findings
— convert-and-send click bubbling, importer support-field mapping, Pipeline
convert parity, client-book filters, typed contact roles, due-date anchors on
dashboard/aging, and two atomicity gaps — all confirmed and fixed the same
day). Test files live under
`apps/hq/test/` (49 files, including `employees`, `employees-routes`, `apps/hq/test/` (49 files, including `employees`, `employees-routes`,
`client-owner`, `pipeline`, `quote-followup`, and `reminder-schedule`), `client-owner`, `pipeline`, `quote-followup`, and `reminder-schedule`),
`apps/hq-web/test/`, and `packages/{domain,auth,billing-engine,ui}/test/`. `apps/hq-web/test/`, and `packages/{domain,auth,billing-engine,ui}/test/`.
@ -58,12 +66,14 @@ quote-to-close funnel slice and its review fixes. Test files live under
## Database schema (`apps/hq/src/db.ts`) ## Database schema (`apps/hq/src/db.ts`)
The schema creates 26 tables on open; `migrate()` additively backfills three The schema creates 26 tables on open; `migrate()` additively backfills the
columns (`module.quote_content`, `email_log.bounced`, `client.owner_id`) on post-launch columns on older DBs (`module.quote_content`, `email_log.bounced`,
older DBs and widens two CHECK constraints via a guarded, transactional `document.due_date`, `staff_user.phone`/`title`, `client.owner_id` plus the
table rebuild (`rebuildTable`: `staff_user.role` gains `manager`, D18 support fields `anydesk`/`os`/`district`/`sector`/`db_password_enc`, and
`reminder.rule_kind` gains `quote_followup`) — a no-op on fresh DBs and on the same four support fields on `stg_client` so the APEX cutover carries them)
re-runs. and widens two CHECK constraints via a guarded, transactional table rebuild
(`rebuildTable`: `staff_user.role` gains `manager`, `reminder.rule_kind` gains
`quote_followup`) — a no-op on fresh DBs and on re-runs.
| Table | Purpose | | Table | Purpose |
| --- | --- | | --- | --- |
@ -237,24 +247,30 @@ Everything below is implemented and exercised by tests via the Express router
## Frontend screens (`apps/hq-web`) ## Frontend screens (`apps/hq-web`)
Shell (`Layout.tsx`): grouped sidebar (WORK / CATALOG / INSIGHT / ADMIN) with Shell (`Layout.tsx`): grouped sidebar (WORK / CATALOG / INSIGHT / ADMIN) with
lucide icons and a reminder-queue count badge, top bar with Ctrl+K command lucide icons and a reminder-queue count badge — collapsible to an icon rail via
palette, Gmail status pill, theme/accent switcher and user chip, plus the the footer / toggle (remembered in `localStorage`) — top bar with Ctrl+K
Gmail-disconnected banner. Owner-only items (Employees, Document Template) command palette, Gmail status pill, theme/accent switcher and a user menu
appear in ADMIN; the warm ops-console restyle is the 2026-07-17 redesign spec. Routes (`main.tsx`): (Profile / Logout), plus the Gmail-disconnected banner. Owner-only items
(Employees, APEX Import) appear in ADMIN; the warm ops-console restyle is the
2026-07-17 redesign spec. Routes (`main.tsx`):
| Route | Screen | What it does | | Route | Screen | What it does |
| --- | --- | --- | | --- | --- | --- |
| `/login` | `Login` | Split-panel sign-in — dark warm brand panel + form card (brand panel hides under 800px) | | `/login` | `Login` | Split-panel sign-in — dark warm brand panel + form card (brand panel hides under 800px) |
| `/` | `Dashboard` | Money headline cards, the reminder queue with send / preview / dismiss, and overdue / due-this-week / renewals / follow-ups / recent-payments tables | | `/` | `Dashboard` | Money headline cards, the reminder queue with send / preview / dismiss, and overdue / due-this-week / renewals / follow-ups / recent-payments tables |
| `/pipeline` | `Pipeline` | Cross-client chase-list: derived stage, amount, owner, age and colour band per row with an explicit next action; `All / Mine / Overdue / Lost` chips, owner narrowing (owner/manager), pagination | | `/pipeline` | `Pipeline` | Cross-client chase-list: derived stage, amount, owner, age and colour band per row with an explicit next action (incl. one-click confirmed **Convert & send**); `All / Mine / Overdue / Lost` chips, owner narrowing (owner/manager), pagination |
| `/clients` | `Clients` | Search, list, inline new-client create; row → client 360° | | `/reminders` | `Reminders` | Standalone reminder queue: status chips (Queued / Sent / Failed / Dismissed), Mine/All (managerial), send / preview / dismiss, pagination |
| `/clients` | `Clients` | Search + district/sector filters, status chips, inline new-client create; row → client 360° |
| `/clients/:id` | `ClientDetail` | Client 360°: avatar record header + status, **account-owner dropdown** (owner/manager), KPI row, a 12-month **relationship-pulse ribbon** (documents / payments / interactions / AMC on one axis; hover readout, click-through), and deep-linkable **tabs** (`?tab=`): Overview · Modules (assignment & lifecycle) · Documents · Payments & plans (record payment, advance, recurring — owner) · AMC (owner) · Interactions (log + follow-ups) · AWS usage | | `/clients/:id` | `ClientDetail` | Client 360°: avatar record header + status, **account-owner dropdown** (owner/manager), KPI row, a 12-month **relationship-pulse ribbon** (documents / payments / interactions / AMC on one axis; hover readout, click-through), and deep-linkable **tabs** (`?tab=`): Overview · Modules (assignment & lifecycle) · Documents · Payments & plans (record payment, advance, recurring — owner) · AMC (owner) · Interactions (log + follow-ups) · AWS usage |
| `/modules` | `Modules` | Module catalogue + dated price book; owner edits (create module, quote content with live sample preview, add prices), staff read-only | | `/modules` | `Modules` | Module catalogue + dated price book; owner edits (create module, quote content with live sample preview, add prices), staff read-only |
| `/reports` | `Reports` | Dues aging, module revenue, client profitability, and an AWS cost bar chart by month | | `/reports` | `Reports` | Dues aging, module revenue, client profitability, and an AWS cost bar chart by month |
| `/documents/new` | `NewDocument` | Quotation-in-minutes composer: client type-ahead, line editor, server-computed GST, and a live PDF-fidelity preview (desktop split view / mobile sheet) | | `/documents` | `Documents` | Register of every document, newest first: type chips + status filter, honest server pagination with true totals, one-click confirmed **Convert & send** on issued proformas |
| `/documents/new` | `NewDocument` | Quotation-in-minutes composer: client type-ahead, line editor, server-computed GST, due-date field on invoices (auto-filled from terms, editable), and a live PDF-fidelity preview (desktop split view / mobile sheet) |
| `/documents/:id` | `DocumentView` | Document page: PDF preview, state-driven action bar (issue / send / mark / convert / cancel / credit note / record payment), share & download group (copy link / WhatsApp / native share / revoke), event timeline and email log | | `/documents/:id` | `DocumentView` | Document page: PDF preview, state-driven action bar (issue / send / mark / convert / cancel / credit note / record payment), share & download group (copy link / WhatsApp / native share / revoke), event timeline and email log |
| `/settings/template` | `DocumentTemplate` | Owner-only letterhead editor (company + boilerplate + titles + logo) with a live sample preview | | `/settings/template` | `DocumentTemplate` | Owner-only letterhead editor (company + boilerplate + titles + logo) with a live sample preview |
| `/employees` | `Employees` | Owner-only staff management: add / edit / re-role / reset password / deactivate / reactivate, surfacing the last-owner and self-deactivation guards | | `/employees` | `Employees` | Owner-only staff management: add / edit / re-role / reset password / deactivate / reactivate, surfacing the last-owner and self-deactivation guards |
| `/profile` | `Profile` | Self-service: edit display name/phone (email + role read-only), change own password (current password required; other sessions purged) |
| `/import` | `ImportApex` | Owner-only APEX cutover: CSV pickers → staged rows with per-row problem chips → verification summary → commit locked until zero problems |
The composer, quote-content, and template previews all use one `LivePreview` The composer, quote-content, and template previews all use one `LivePreview`
component that renders the server's real `documentHtml` in a sandboxed component that renders the server's real `documentHtml` in a sandboxed

@ -256,4 +256,10 @@ in ten phases the same day. The calls that matter going forward:
owner/manager toggle Mine/Everyone (`?mine=1`). Sidebar collapses to an icon rail. owner/manager toggle Mine/Everyone (`?mine=1`). Sidebar collapses to an icon rail.
- **APEX import UI:** owner-only `/import` wraps the existing stage/verify/commit — - **APEX import UI:** owner-only `/import` wraps the existing stage/verify/commit —
commit stays locked while any staged row carries a problem. commit stays locked while any staged row carries a problem.
Suite at 344 tests / 66 files at delivery. Delivery was followed the same day by an independent review of the full slice diff:
8 findings (4 major — convert-and-send click bubbling on the register, importer not
mapping the WS-F support fields, Pipeline still on the old plain convert, missing
client-book filter UI; 4 minor — typed contact roles absent, dashboard/aging still
anchored on doc_date, non-atomic dbPassword PATCH, audit-outside-txn on three new
write paths) — all confirmed and fixed in one pass (`fix(review)` commit).
Suite at 345 tests / 66 files after the review pass.

@ -1,9 +1,15 @@
# HQ Console — Go-Live Cluster Spec (Due Dates, Import UI, Reminders/Documents Pages, Shell & Profile, Client Support Data) # 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 > **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 > (`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 > NOT in this slice — it is the dedicated task queued immediately after it, before the
> real-data import commit. > 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 ## 1. Overview & goals

Loading…
Cancel
Save