docs: bring README/STATUS/CLAUDE current (D20-D33)

Corrects the now-wrong at-rest-encryption claims (D31/D32 removed HQ_SECRET_KEY —
credentials are plaintext, login passwords stay scrypt-hashed), updates the test count
(389 -> 416), the schema table count (26 -> 31) + new tables, and adds the screens and
capabilities built since 2026-07-17: tickets, onboarding, module-defined fields, the
live SMS gateway balance pull, Module data directory, Portals, global search, MIS
cockpit, audit viewer, Renewals, Settings->Operations, and the backup script (D33).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 3 days ago
parent 62eb937012
commit b09f545489

@ -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 (389 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 (416 tests) |
## Running it ## Running it
@ -61,12 +61,19 @@ cd apps/hq-web && npm run dev
- Owner login on first boot: `admin@tecnostac.com` + the one-time password printed in the - Owner login on first boot: `admin@tecnostac.com` + the one-time password printed in the
server log (change it immediately). The "Gmail disconnected" banner is expected until Gmail is connected. server log (change it immediately). The "Gmail disconnected" banner is expected until Gmail is connected.
- Env: `DATABASE_URL` (set → Postgres engine, absent → SQLite at `HQ_DATA_DIR`; D19), - Env: `DATABASE_URL` (set → Postgres engine, absent → SQLite at `HQ_DATA_DIR`; D19),
`SIMS_PIN_PEPPER` (set once, never change), `HQ_SECRET_KEY` `SIMS_PIN_PEPPER` (set once, never change), `GOOGLE_*` (Gmail), `AWS_*` (Cost Explorer),
(before Gmail), `GOOGLE_*` (Gmail), `AWS_*` (Cost Explorer), `HQ_PORT` (default 5182). `HQ_PORT` (default 5182).
- **Credentials are stored in the clear (D31/D32).** `HQ_SECRET_KEY` was removed — portal /
gateway passwords, module secrets, client DB passwords and the Gmail token are plaintext,
usable keyless (login passwords stay scrypt-hashed). A DB dump exposes them, so protect the
DB file and its backups. Back up with `npm run backup` (D33 → `pg_dump` to `./backups`).
- **SMS gateway** (D28): the SMS module's per-client balance is pulled live from the provider
panel (`sms.balance_api_url`, keyless) — manual "Refresh from gateway" + a daily scheduler
pass; low balances surface on the SMS credits screen, cockpit tile and dashboard alert.
- Postgres integration tests: `HQ_PG_TEST_URL=postgres://hq:hq_dev@localhost:5432/hq_test npm test` - Postgres integration tests: `HQ_PG_TEST_URL=postgres://hq:hq_dev@localhost:5432/hq_test npm test`
(wipes that database each run — never point it at real data). (wipes that database each run — never point it at real data).
- Health check: `GET /api/health` → `{ ok: true }`. - Health check: `GET /api/health` → `{ ok: true }`.
- Verified state: `npm install` + typecheck clean, 389 tests pass. Exercise real flows end - Verified state: `npm install` + typecheck clean, 416 tests pass. Exercise real flows end
to end before claiming done. to end before claiming done.
## Docs (kept in `docs/`) ## Docs (kept in `docs/`)

@ -26,15 +26,23 @@ document and conversation we've had with them.
- **Reports**, a **Dashboard**, an **append-only audit log** (every mutation), an **APEX - **Reports**, a **Dashboard**, an **append-only audit log** (every mutation), an **APEX
importer** for the initial 300-client load, and a **scheduler** (runs on boot + every 6h: importer** for the initial 300-client load, and a **scheduler** (runs on boot + every 6h:
reminders, bounce polling, AWS cost pull). reminders, bounce polling, AWS cost pull).
- **A redesigned ops UI (2026-07-17)** — warm light/dark theme with a teal accent, grouped - **A redesigned ops UI** — light/dark theme with accent + neutral palettes, grouped
sidebar with a live reminder badge, a **Ctrl-K command palette** (client search, pages, sidebar with a live reminder badge, a **Ctrl-K command palette** (clients, documents,
actions), and a **Client 360** with tabbed sections and a 12-month **relationship-pulse tickets, pages, actions), and a **Client 360** with tabbed sections and a 12-month
ribbon**; phone-usable drawer layout. Spec: **relationship-pulse ribbon**; phone-usable drawer layout.
`docs/superpowers/specs/2026-07-17-hq-console-redesign-design.md`. - **Support tickets** (aging/SLA/assignee workbench, bill-from-ticket), **onboarding**
milestones, and **per-module operational fields** each module declares (config, not code) —
surfaced per client on Client 360, across all clients in the **Module data** directory, and
as a **Portals** quick-list of every stored login. For the **SMS** module those fields
include a **live credit balance** pulled from the provider gateway (manual refresh + a daily
pass), with low balances flagged for a one-click top-up quote.
- **Renewals** command center, an owner **MIS cockpit**, an **audit-log viewer**, and
**database backups** (`npm run backup` → compressed, restorable `pg_dump`). Credentials are
stored in the clear (no `HQ_SECRET_KEY`), so the DB and its backups are the security boundary.
## Build state (start here) ## Build state (start here)
Working codebase, not a plan. `npm install` + typecheck are clean and **389 tests pass**. Working codebase, not a plan. `npm install` + typecheck are clean and **416 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)**.
@ -47,9 +55,10 @@ runbook (server, HTTPS, backups, Gmail/AWS/import wiring, the Postgres switch) i
dev/test engine; Postgres is the locked production engine (see DEPLOY-HQ.md). DB at dev/test engine; Postgres is the locked production engine (see DEPLOY-HQ.md). DB at
**`data/hq.db`**; server on **:5182** (also serves the built web UI and the public **`data/hq.db`**; server on **:5182** (also serves the built web UI and the public
`/share/:token` route). `/share/:token` route).
- **Frontend — `apps/hq-web`**: React + Vite. Pages: Dashboard, Pipeline, Clients, - **Frontend — `apps/hq-web`**: React + Vite. Pages: Home, Dashboard, Pipeline, Reminders,
ClientDetail, Modules, Reports, NewDocument, DocumentView, Employees (owner-only), Tickets, Onboarding, Clients, ClientDetail, Documents, NewDocument, DocumentView, Renewals,
DocumentTemplate (owner-only). SMS credits, Modules, Module data, Portals, Reports, MIS cockpit (owner), Employees (owner),
Profile, APEX Import (owner), Audit log (owner), Settings.
- **Shared packages** (trimmed forks): `@sims/domain` (ids, money, business-day, doc-series, - **Shared packages** (trimmed forks): `@sims/domain` (ids, money, business-day, doc-series,
gstin, documents), `@sims/auth` (pin/scrypt), `@sims/billing-engine` (compute, tax — our gstin, documents), `@sims/auth` (pin/scrypt), `@sims/billing-engine` (compute, tax — our
invoices are GST documents, computed to the paisa), `@sims/ui` (design system). invoices are GST documents, computed to the paisa), `@sims/ui` (design system).

@ -1,7 +1,7 @@
# SiMS HQ — Build Status # SiMS HQ — Build Status
_Internal ops console for running our software business. Not shipped to clients._ _Internal ops console for running our software business. Not shipped to clients._
_Last reviewed: 2026-07-17 · Version 0.1.0_ _Last reviewed: 2026-07-19 · Version 0.1.0_
HQ is the back-office console we use to run ~300 Classic client relationships: HQ is the back-office console we use to run ~300 Classic client relationships:
the client book, our software module catalogue and dated price book, quotations / the client book, our software module catalogue and dated price book, quotations /
@ -19,23 +19,36 @@ 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 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. section-per-module quotation PDF — then closed all 8 findings from its review.
**Since 2026-07-17 (D20–D33).** Support **tickets** (aging/SLA/assignee workbench,
bill-from-ticket), **onboarding** milestones, client **branches**, and **module-defined
operational fields** each module declares in its `field_spec` (config over code) — shown per
client on Client 360, across all clients in the **Module data** directory, and as a **Portals**
quick-list of every stored login. For **SMS** those fields include a **live credit balance**
pulled from the provider gateway (`sms.balance_api_url`, keyless): a manual "Refresh from
gateway" and a once-a-day scheduler pass, with low balances flagged on the SMS-credits screen,
the owner cockpit tile and the dashboard for a one-click top-up quote. Also added: a **Renewals**
command center, an owner **MIS cockpit**, an **audit-log viewer**, a **global search** (Ctrl-K
now spans clients + documents + tickets), a **Settings → Operations** panel (SMS gateway URL,
low-balance threshold, ticket SLA), Postgres as a first-class engine (`DATABASE_URL`; D19), and
a **database backup** script (`npm run backup`; D33). **D31/D32: credentials are stored in the
clear** — `HQ_SECRET_KEY` was removed, so portal/gateway passwords, module secrets, client DB
passwords and the Gmail token are plaintext and keyless (login passwords stay scrypt-hashed);
a DB dump exposes them, so the DB file and its backups are the security boundary.
## Verification state ## Verification state
| Check | Result | | Check | Result |
| --- | --- | | --- | --- |
| `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`) | **389 tests pass across 74 files** | | `npm test` (`vitest run`) | **416 tests pass across 85 files** (5 Postgres-gated skipped) |
The 346 figure is the live `vitest run` count measured 2026-07-17, after the The 416 figure is the live `vitest run` count measured 2026-07-19; the suite has
D18 go-live cluster **and its post-delivery review pass** (8 reviewer findings grown from the D18 go-live cluster through the D20–D33 work (tickets, module fields,
— convert-and-send click bubbling, importer support-field mapping, Pipeline renewals, cockpit, audit viewer, SMS gateway, module directory/portals/search, and
convert parity, client-book filters, typed contact roles, due-date anchors on the D31/D32 plaintext-credential change with its own migration + review). Test files
dashboard/aging, and two atomicity gaps — all confirmed and fixed the same live under `apps/hq/test/`, `apps/hq-web/test/`, and
day). Test files live under `packages/{domain,auth,billing-engine,ui}/test/`.
`apps/hq/test/` (49 files, including `employees`, `employees-routes`,
`client-owner`, `pipeline`, `quote-followup`, and `reminder-schedule`),
`apps/hq-web/test/`, and `packages/{domain,auth,billing-engine,ui}/test/`.
## Architecture ## Architecture
@ -60,13 +73,16 @@ day). Test files live under
- Web: `apps/hq-web` — `npm run dev` / `npm run build`. - Web: `apps/hq-web` — `npm run dev` / `npm run build`.
- CLI: `npm run import` (APEX importer), `scripts/gmail-connect.ts` (one-time - CLI: `npm run import` (APEX importer), `scripts/gmail-connect.ts` (one-time
Gmail OAuth). Gmail OAuth).
- Env: `HQ_PORT`, `HQ_DATA_DIR`, `HQ_SECRET_KEY` (64 hex / 32 bytes, for token - Env: `HQ_PORT`, `HQ_DATA_DIR`, `DATABASE_URL` (Postgres engine; D19),
encryption), `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`, `SIMS_PIN_PEPPER`, `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`,
`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`. `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`. **`HQ_SECRET_KEY` removed (D32)** —
credentials are stored in the clear, no key needed.
- Backups: `npm run backup` (D33) — `pg_dump` to `./backups`, restorable with
`pg_restore`.
## Database schema (`apps/hq/src/db.ts`) ## Database schema (`apps/hq/src/db.ts`)
The schema creates 26 tables on open; `migrate()` additively backfills the The schema creates 31 tables on open; `migrate()` additively backfills the
post-launch columns on older DBs (`module.quote_content`, `email_log.bounced`, post-launch columns on older DBs (`module.quote_content`, `email_log.bounced`,
`document.due_date`, `staff_user.phone`/`title`, `client.owner_id` plus the `document.due_date`, `staff_user.phone`/`title`, `client.owner_id` plus the
D18 support fields `anydesk`/`os`/`district`/`sector`/`db_password_enc`, and D18 support fields `anydesk`/`os`/`district`/`sector`/`db_password_enc`, and
@ -82,7 +98,12 @@ and widens two CHECK constraints via a guarded, transactional table rebuild
| `client` | Client registry — code, name, GSTIN, state, contacts (JSON), status, account owner (`owner_id` → `staff_user`), source | | `client` | Client registry — code, name, GSTIN, state, contacts (JSON), status, account owner (`owner_id` → `staff_user`), source |
| `module` | Software module catalogue — SAC, allowed billing kinds, multi-sub flag, quote-content lines | | `module` | Software module catalogue — SAC, allowed billing kinds, multi-sub flag, quote-content lines |
| `module_price_book` | Dated prices per module/edition/kind (latest effective row wins) | | `module_price_book` | Dated prices per module/edition/kind (latest effective row wins) |
| `client_module` | A module assigned to a client — lifecycle status, install/complete/train dates, next renewal | | `client_module` | A module assigned to a client — lifecycle status, install/complete/train dates, next renewal, provider/username/password (plaintext, D32), module-defined `field_values` (D21) |
| `usage_rate_band` | Dated per-unit usage pricing with a minimum order (e.g. SMS ≥ 50,000 units) |
| `client_branch` | Client branches/offices (APEX parity) |
| `project_milestone` | Onboarding checklist items per client_module |
| `ticket` | Support workbench tickets — status, assignee, opened/closed dates, source (`apex`/`hq`) |
| `sms_balance_check` | SMS gateway poll telemetry (status/balance/message/checked_at) — operational, not audited (D28) |
| `tax_class` | GST rate classes (dated); `GST18` seeded at 18% | | `tax_class` | GST rate classes (dated); `GST18` seeded at 18% |
| `doc_series` | Per (doc_type, FY) running number + prefix | | `doc_series` | Per (doc_type, FY) running number + prefix |
| `document` | QT / PROFORMA / INVOICE / RECEIPT / CREDIT_NOTE — totals, status, ref chain, JSON payload | | `document` | QT / PROFORMA / INVOICE / RECEIPT / CREDIT_NOTE — totals, status, ref chain, JSON payload |
@ -90,7 +111,7 @@ and widens two CHECK constraints via a guarded, transactional table rebuild
| `document_share` | Opaque public share tokens — expiry, revoked flag | | `document_share` | Opaque public share tokens — expiry, revoked flag |
| `payment` | Money received — mode, reference, amount, TDS | | `payment` | Money received — mode, reference, amount, TDS |
| `payment_allocation` | How a payment is applied across invoices | | `payment_allocation` | How a payment is applied across invoices |
| `email_account` | The single Gmail sending identity — encrypted refresh token, active/dead status | | `email_account` | The single Gmail sending identity — refresh token (plaintext since D32), active/dead status |
| `email_log` | Append-only send trace — sent/failed, gmail message id, error, bounced flag | | `email_log` | Append-only send trace — sent/failed, gmail message id, error, bounced flag |
| `setting` | Key/value store — `company.*` identity, `template.*` letterhead text, reminder + AWS config | | `setting` | Key/value store — `company.*` identity, `template.*` letterhead text, reminder + AWS config |
| `audit_log` | Append-only before/after JSON for every mutation | | `audit_log` | Append-only before/after JSON for every mutation |
@ -166,7 +187,7 @@ Everything below is implemented and exercised by tests via the Express router
renders the one document inline as PDF; unknown/expired/revoked tokens return a renders the one document inline as PDF; unknown/expired/revoked tokens return a
data-free "link unavailable" page. Tokens are never logged or audited. data-free "link unavailable" page. Tokens are never logged or audited.
- **Email (Gmail)** — `gmail-connect` loopback OAuth (send + readonly scopes) - **Email (Gmail)** — `gmail-connect` loopback OAuth (send + readonly scopes)
stores an AES-256-GCM-encrypted refresh token. Sends go over the Gmail REST API stores the refresh token (plaintext since D32). Sends go over the Gmail REST API
(no SDK): access-token exchange, raw MIME build with PDF attachment, send. (no SDK): access-token exchange, raw MIME build with PDF attachment, send.
`invalid_grant` flips the account to `dead` and raises the dashboard banner. `invalid_grant` flips the account to `dead` and raises the dashboard banner.
Document emails and templated reminder emails share the send path; every Document emails and templated reminder emails share the send path; every
@ -267,6 +288,14 @@ command palette, Gmail status pill, theme/accent switcher and a user menu
| `/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` | `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/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 |
| `/tickets` | `Tickets` | Support workbench: status chips + counts, per-row assignee, **Age** column with SLA overdue flag + Overdue chip, search, one-click **Bill** (New Document pre-filled) |
| `/projects` | `Projects` | Onboarding: per-client-module milestone checklist board |
| `/renewals` | `Renewals` | Module subscriptions + AMC coming due, earliest first, one-click renewal/top-up proforma |
| `/sms-balances` | `SmsBalances` | SMS credit balances (live from the gateway), lowest first; Low/All/Not-recorded chips, per-row top-up quote, owner **Refresh from gateway** |
| `/module-data` | `ModuleDirectory` | Pick a module → every client on it with that module's fields as columns, searchable, portal URLs clickable |
| `/portals` | `Portals` | Every stored login across all modules — Open ↗ links, 🔑 = password on file (reveal on Client 360) |
| `/mis` | `Mis` | Owner cockpit: this-month billing / collections / outstanding / pipeline / renewals / tickets / SMS-low, each tile click-through |
| `/audit` | `Audit` | Owner audit-log viewer: entity/action filters (from server facets), paginated before/after key-diff |
| `/settings` | `Settings` | Settings hub (Pinterest-style left rail, `?s=` deep-links): Company profile · Document template (live preview) · Reminders (dated cadence rows, append-only) · Sharing default expiry · Integrations status · Appearance. Owner-only except Appearance; `/settings/template` redirects here | | `/settings` | `Settings` | Settings hub (Pinterest-style left rail, `?s=` deep-links): Company profile · Document template (live preview) · Reminders (dated cadence rows, append-only) · Sharing default expiry · Integrations status · Appearance. Owner-only except Appearance; `/settings/template` redirects here |
| `/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) | | `/profile` | `Profile` | Self-service: edit display name/phone (email + role read-only), change own password (current password required; other sessions purged) |
@ -295,7 +324,7 @@ pages exist; invoices carry **due dates** (terms default + override, stamped at
issue — the overdue chase says "was due on X, N days overdue"); self-service issue — the overdue chase says "was due on X, N days overdue"); self-service
profile + own-password change; sidebar collapses; the dashboard has a My Day mode profile + own-password change; sidebar collapses; the dashboard has a My Day mode
(staff always scoped to their book); clients carry support-access data (AnyDesk, (staff always scoped to their book); clients carry support-access data (AnyDesk,
OS, district/sector filters, an encrypted DB password with audited reveal); the OS, district/sector filters, a DB password — plaintext since D32 — with audited reveal); the
module roster is fully editable both ways (one audited write path with Client 360); module roster is fully editable both ways (one audited write path with Client 360);
"Convert & send" is one click from the register and the document view; and a "Convert & send" is one click from the register and the document view; and a
multi-module quotation prints one titled section per module._ multi-module quotation prints one titled section per module._

Loading…
Cancel
Save