docs(d22): onboarding tracker D-record; test count 377

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 4 days ago
parent c5480cf8cd
commit 86af9ecb27

@ -1,11 +1,11 @@
# CLAUDE.md — HQ Ops Console # CLAUDE.md — HQ Ops Console
This repo (`C:/SiMS/hq`) is the **internal console for running our own software business** This repo (`C:/SiMS/hq`) is the **internal console for running our own software business** —
NOT software we ship to clients. It manages our ~300 existing Classic clients: client book, NOT software we ship to clients. It manages our ~300 existing Classic clients: client book,
sellable modules with a dated price book, documents (quotation / proforma / invoice / credit sellable modules with a dated price book, documents (quotation / proforma / invoice / credit
note) on editable letterhead templates with PDF rendering and shareable public links, note) on editable letterhead templates with PDF rendering and shareable public links,
call/interaction tracking, AMC contracts, AWS cost recovery, payments + allocations, call/interaction tracking, AMC contracts, AWS cost recovery, payments + allocations,
recurring plans, and reminders emailed via Gmail with bounce handling plus reports, a recurring plans, and reminders emailed via Gmail with bounce handling — plus reports, a
dashboard, an append-only audit log, an APEX importer, and a scheduler. dashboard, an append-only audit log, an APEX importer, and a scheduler.
> **The Store/POS retail product is a SEPARATE repo.** It is local-first software installed > **The Store/POS retail product is a SEPARATE repo.** It is local-first software installed
@ -20,12 +20,12 @@ dashboard, an append-only audit log, an APEX importer, and a scheduler.
| Frontend | `apps/hq-web/src` | React + Vite; pages Dashboard (My Day) / Pipeline / Reminders / Clients / ClientDetail / Documents / NewDocument / DocumentView / Modules / Reports / Employees (owner-only) / Profile / APEX Import (owner-only) / Settings | | Frontend | `apps/hq-web/src` | React + Vite; pages Dashboard (My Day) / Pipeline / Reminders / Clients / ClientDetail / Documents / NewDocument / DocumentView / Modules / Reports / Employees (owner-only) / Profile / APEX Import (owner-only) / Settings |
| Repos | `apps/hq/src/repos-*.ts` | one file per domain; plain functions over a `DB` handle | | Repos | `apps/hq/src/repos-*.ts` | one file per domain; plain functions over a `DB` handle |
| Schema | `apps/hq/src/db.ts` + `apps/hq/schema.sql` | `CREATE TABLE IF NOT EXISTS` + additive `migrate()` | | Schema | `apps/hq/src/db.ts` + `apps/hq/schema.sql` | `CREATE TABLE IF NOT EXISTS` + additive `migrate()` |
| Shared packages | `packages/*` | trimmed forks see below | | Shared packages | `packages/*` | trimmed forks — see below |
**Shared packages** (generic on purpose keep them so): `@sims/domain` (`ids`/UUIDv7, **Shared packages** (generic on purpose — keep them so): `@sims/domain` (`ids`/UUIDv7,
`money`/paise, `business-day`, `doc-series`, `gstin`, `documents`), `@sims/auth` (scrypt PIN `money`/paise, `business-day`, `doc-series`, `gstin`, `documents`), `@sims/auth` (scrypt PIN
+ lockout), `@sims/billing-engine` (`compute`, `tax`), `@sims/ui`. HQ owns its own + lockout), `@sims/billing-engine` (`compute`, `tax`), `@sims/ui`. HQ owns its own
document/payment types (quotation, proforma, credit note; NEFT/cheque/UPI modes) do not document/payment types (quotation, proforma, credit note; NEFT/cheque/UPI modes) — do not
push those into the shared packages. push those into the shared packages.
## Non-negotiable rules ## Non-negotiable rules
@ -36,21 +36,21 @@ distilled to what applies to HQ. A change that breaks one does not land.
| # | Rule | In this repo | | # | Rule | In this repo |
|---|---|---| |---|---|---|
| Money is integer paise | Every amount is `Paise` (integer). Fractional-rupee floats never enter the domain; convert at the edge with `fromRupees`, format with `formatINR`. Every rounding step is explicit. | all `*_paise` columns; `@sims/domain/money` | | Money is integer paise | Every amount is `Paise` (integer). Fractional-rupee floats never enter the domain; convert at the edge with `fromRupees`, format with `formatINR`. Every rounding step is explicit. | all `*_paise` columns; `@sims/domain/money` |
| One billing engine, re-verified | GST is computed **only** through `@sims/billing-engine` `computeBill` save and preview run the same path so they cannot drift. The server recomputes to the paisa; a mismatch is rejected. | `repos-documents.ts` | | One billing engine, re-verified | GST is computed **only** through `@sims/billing-engine` `computeBill` — save and preview run the same path so they cannot drift. The server recomputes to the paisa; a mismatch is rejected. | `repos-documents.ts` |
| Config over code, dated | Anything that varies — prices, tax rates/slabs, settings, doc-series prefixes, SAC codes, templates, message text — is a **DB row resolved by business date**, never a constant in code. A price or rate change is a new dated row, not a release. | `module_price_book`, `tax_class` (`effective_from`/`_to`), `doc_series`, `setting`, `template`, `reminder_schedule` (reminder cadence + follow-up text, resolved by `resolveSchedule`), `reminder-templates` | | Config over code, dated | Anything that varies — prices, tax rates/slabs, settings, doc-series prefixes, SAC codes, templates, message text — is a **DB row resolved by business date**, never a constant in code. A price or rate change is a new dated row, not a release. | `module_price_book`, `tax_class` (`effective_from`/`_to`), `doc_series`, `setting`, `template`, `reminder_schedule` (reminder cadence + follow-up text, resolved by `resolveSchedule`), `reminder-templates` |
| Documents immutable once issued | Issuing assigns the number; after that the document is never edited or deleted. Corrections are **credit notes** (own per-FY series, links the invoice via `ref_doc_id`, CGST Act s.34). Cancel keeps the number consumed; only issued, unpaid, unallocated docs may cancel. QT→PI→INV convert by carrying lines forward into a new draft. | `repos-documents.ts` `issueDocument`, `raiseCreditNote`, `cancelDocument`, `convertDocument` | | Documents immutable once issued | Issuing assigns the number; after that the document is never edited or deleted. Corrections are **credit notes** (own per-FY series, links the invoice via `ref_doc_id`, CGST Act s.34). Cancel keeps the number consumed; only issued, unpaid, unallocated docs may cancel. QTâ†PIâ†INV convert by carrying lines forward into a new draft. | `repos-documents.ts` — `issueDocument`, `raiseCreditNote`, `cancelDocument`, `convertDocument` |
| Portable-repo pattern | All data access is plain exported functions taking the `DB` handle first arg no ORM, no repo classes, no ambient singletons. Multi-write operations wrap in `db.transaction(...)`. Keep SQL portable (SQLite today, **Postgres is the locked production engine** D15); avoid engine-specific SQL. | every `repos-*.ts` | | Portable-repo pattern | All data access is plain exported functions taking the `DB` handle first arg — no ORM, no repo classes, no ambient singletons. Multi-write operations wrap in `db.transaction(...)`. Keep SQL portable (SQLite today, **Postgres is the locked production engine** — D15); avoid engine-specific SQL. | every `repos-*.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` | | 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 (372 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 (377 tests) |
## Running it ## Running it
``` ```
npm install # workspace root, once npm install # workspace root, once
# backend (serves API on :5182, and the built web UI + public /share links) # backend (serves API on :5182, and the built web UI + public /share links)
# RUN FROM THE REPO ROOT the DB path resolves to ./data/hq.db from CWD, so # RUN FROM THE REPO ROOT — the DB path resolves to ./data/hq.db from CWD, so
# `cd apps/hq && npm start` would boot a fresh empty DB in apps/hq/data instead. # `cd apps/hq && npm start` would boot a fresh empty DB in apps/hq/data instead.
npm run build --workspace @sims/hq npm run build --workspace @sims/hq
node apps/hq/dist/server.cjs # first boot on an empty data/ prints a one-time owner password node apps/hq/dist/server.cjs # first boot on an empty data/ prints a one-time owner password
@ -60,19 +60,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), `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).
- 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, 372 tests pass. Exercise real flows end - Verified state: `npm install` + typecheck clean, 377 tests pass. Exercise real flows end
to end before claiming done. to end before claiming done.
## Docs (kept in `docs/`) ## Docs (kept in `docs/`)
`14-SPEC-HQ-CONSOLE.md` (the HQ spec read first), `11-ADMIN-SUPPORT-CONSOLE.md` (the console `14-SPEC-HQ-CONSOLE.md` (the HQ spec — read first), `11-ADMIN-SUPPORT-CONSOLE.md` (the console
this grows into), `16-PROJECT-RULES.md` (hard rules), `07-DB-AND-CONFIG.md` (config-over-code + this grows into), `16-PROJECT-RULES.md` (hard rules), `07-DB-AND-CONFIG.md` (config-over-code +
DB strategy), `06-DECISIONS.md` (every decision has a D-number add one when you decide DB strategy), `06-DECISIONS.md` (every decision has a D-number — add one when you decide
something), `DEPLOY-HQ.md` (go-live runbook). Follow **The Tables Rule**: anything that can be a something), `DEPLOY-HQ.md` (go-live runbook). Follow **The Tables Rule**: anything that can be a
table IS a table, in docs and UI. table IS a table, in docs and UI.

@ -1,7 +1,7 @@
# SiMS HQ — Internal Ops Console # SiMS HQ — Internal Ops Console
The console for running our own software business. It manages our **~300 Classic clients** The console for running our own software business. It manages our **~300 Classic clients**
end to end and it replaces the internal Oracle APEX app. This repo is **internal only**; end to end — and it replaces the internal Oracle APEX app. This repo is **internal only**;
it is **not** shipped to clients. it is **not** shipped to clients.
One place for the client book, what each client has bought, the money they owe, and every One place for the client book, what each client has bought, the money they owe, and every
@ -9,24 +9,24 @@ document and conversation we've had with them.
## What it does ## What it does
- **Client book** the registry of ~300 clients: contacts, GSTIN, status, notes. - **Client book** — the registry of ~300 clients: contacts, GSTIN, status, notes.
- **Modules + price book** the sellable software modules with a **dated price book** (a - **Modules + price book** — the sellable software modules with a **dated price book** (a
price change is a new row, not a code release) and per-client subscriptions. price change is a new row, not a code release) and per-client subscriptions.
- **Documents** quotations (QT), proforma invoices (PI), invoices (INV) and credit notes - **Documents** — quotations (QT), proforma invoices (PI), invoices (INV) and credit notes
(CN), built on **editable letterhead templates**, rendered to **PDF** (headless Chromium), (CN), built on **editable letterhead templates**, rendered to **PDF** (headless Chromium),
with **shareable public links** (one token-guarded, rate-limited, revocable read route). with **shareable public links** (one token-guarded, rate-limited, revocable read route).
- **Interactions** call / visit / meeting tracking against each client. - **Interactions** — call / visit / meeting tracking against each client.
- **AMC contracts** and **AWS cost recovery** per-client cost attribution pulled monthly. - **AMC contracts** and **AWS cost recovery** — per-client cost attribution pulled monthly.
- **Payments + allocations**, **recurring plans**, and **reminders** sent via **Gmail** on - **Payments + allocations**, **recurring plans**, and **reminders** sent via **Gmail** on
the company mailbox, with **bounce handling** and a manual queue when the token dies. the company mailbox, with **bounce handling** and a manual queue when the token dies.
- **Quote-to-close funnel** employee accounts (owner / manager / staff), a cross-client - **Quote-to-close funnel** — employee accounts (owner / manager / staff), a cross-client
**pipeline chase-list** (stages derived, oldest-overdue on top), and **escalating quote **pipeline chase-list** (stages derived, oldest-overdue on top), and **escalating quote
follow-up** that nudges the owning employee and emails the client a share link on a dated follow-up** that nudges the owning employee and emails the client a share link on a dated
schedule (`reminder_schedule`), stopping automatically on accept / lose / convert. schedule (`reminder_schedule`), stopping automatically on accept / lose / convert.
- **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 (2026-07-17)** — warm light/dark theme with a teal accent, 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** (client search, pages,
actions), and a **Client 360** with tabbed sections and a 12-month **relationship-pulse actions), and a **Client 360** with tabbed sections and a 12-month **relationship-pulse
ribbon**; phone-usable drawer layout. Spec: ribbon**; phone-usable drawer layout. Spec:
@ -34,24 +34,24 @@ 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 **372 tests pass**. Working codebase, not a plan. `npm install` + typecheck are clean and **377 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)**.
## Architecture ## Architecture
- **Backend `apps/hq/src`**: an Express JSON API over **better-sqlite3**, behind portable - **Backend — `apps/hq/src`**: an Express JSON API over **better-sqlite3**, behind portable
repositories (`repos-*.ts`, one per domain: clients, modules, documents, payments, amc, repositories (`repos-*.ts`, one per domain: clients, modules, documents, payments, amc,
aws, interactions, recurring, reminders, shares, reports, dashboard, email). SQLite is the aws, interactions, recurring, reminders, shares, reports, dashboard, email). SQLite is the
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: Dashboard, Pipeline, Clients,
ClientDetail, Modules, Reports, NewDocument, DocumentView, Employees (owner-only), ClientDetail, Modules, Reports, NewDocument, DocumentView, Employees (owner-only),
DocumentTemplate (owner-only). DocumentTemplate (owner-only).
- **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).
## Planning docs ## Planning docs
@ -59,7 +59,7 @@ runbook (server, HTTPS, backups, Gmail/AWS/import wiring, the Postgres switch) i
| Doc | What it covers | | Doc | What it covers |
|-----|----------------| |-----|----------------|
| [docs/14-SPEC-HQ-CONSOLE.md](docs/14-SPEC-HQ-CONSOLE.md) | The console spec: shape, data model, features, delivery sequence (D15) | | [docs/14-SPEC-HQ-CONSOLE.md](docs/14-SPEC-HQ-CONSOLE.md) | The console spec: shape, data model, features, delivery sequence (D15) |
| [docs/superpowers/specs/2026-07-17-quote-to-close-funnel-design.md](docs/superpowers/specs/2026-07-17-quote-to-close-funnel-design.md) | The delivered D16 funnel slice employees, pipeline, escalating reminders, convert/supersede, module roster | | [docs/superpowers/specs/2026-07-17-quote-to-close-funnel-design.md](docs/superpowers/specs/2026-07-17-quote-to-close-funnel-design.md) | The delivered D16 funnel slice — employees, pipeline, escalating reminders, convert/supersede, module roster |
| [docs/superpowers/specs/2026-07-17-hq-console-redesign-design.md](docs/superpowers/specs/2026-07-17-hq-console-redesign-design.md) | The delivered warm ops-console UI redesign | | [docs/superpowers/specs/2026-07-17-hq-console-redesign-design.md](docs/superpowers/specs/2026-07-17-hq-console-redesign-design.md) | The delivered warm ops-console UI redesign |
| [docs/11-ADMIN-SUPPORT-CONSOLE.md](docs/11-ADMIN-SUPPORT-CONSOLE.md) | The vendor-side HQ / support console this app implements | | [docs/11-ADMIN-SUPPORT-CONSOLE.md](docs/11-ADMIN-SUPPORT-CONSOLE.md) | The vendor-side HQ / support console this app implements |
| [docs/07-DB-AND-CONFIG.md](docs/07-DB-AND-CONFIG.md) | Everything-in-DB design (messages, plans, settings) and the local DB engine analysis | | [docs/07-DB-AND-CONFIG.md](docs/07-DB-AND-CONFIG.md) | Everything-in-DB design (messages, plans, settings) and the local DB engine analysis |
@ -72,19 +72,19 @@ runbook (server, HTTPS, backups, Gmail/AWS/import wiring, the Postgres switch) i
```bash ```bash
npm install # from the repo root (workspaces: packages/*, apps/*) npm install # from the repo root (workspaces: packages/*, apps/*)
# Backend HQ server. Build the bundle, then run it FROM THE REPO ROOT: the DB # Backend — HQ server. Build the bundle, then run it FROM THE REPO ROOT: the DB
# path resolves to ./data/hq.db against the current working directory, so the # path resolves to ./data/hq.db against the current working directory, so the
# server must be launched from the repo root (not from apps/hq, which would look # server must be launched from the repo root (not from apps/hq, which would look
# in apps/hq/data and boot an empty database). # in apps/hq/data and boot an empty database).
npm run build --workspace @sims/hq npm run build --workspace @sims/hq
node apps/hq/dist/server.cjs # serves :5182 (API, built web UI, /share/:token) node apps/hq/dist/server.cjs # serves :5182 (API, built web UI, /share/:token)
# First boot on an empty data/ creates data/hq.db and prints a one-time owner # First boot on an empty data/ creates data/hq.db and prints a one-time owner
# password capture it. Log in as admin@tecnostac.com, then change it. # password — capture it. Log in as admin@tecnostac.com, then change it.
# Frontend hq-web dev server (Vite on :5183, proxies /api and /share to :5182) # Frontend — hq-web dev server (Vite on :5183, proxies /api and /share to :5182)
cd apps/hq-web && npm run dev cd apps/hq-web && npm run dev
``` ```
Nothing in `apps/hq/.env` is required to boot; each var unlocks a capability (Gmail sending, Nothing in `apps/hq/.env` is required to boot; each var unlocks a capability (Gmail sending,
AWS cost pull, security peppers) see `apps/hq/.env.example` and DEPLOY-HQ.md. Repo-wide AWS cost pull, security peppers) — see `apps/hq/.env.example` and DEPLOY-HQ.md. Repo-wide
checks from the root: `npm run typecheck` and `npm test`. checks from the root: `npm run typecheck` and `npm test`.

@ -1,14 +1,14 @@
# 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-17 · 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 /
proforma / invoices / credit notes with editable letterhead templates and PDF proforma / invoices / credit notes with editable letterhead templates and PDF
output, shareable public document links, call & interaction tracking, AMC output, shareable public document links, call & interaction tracking, AMC
contracts, AWS cost recovery, payments & allocations, recurring billing, and contracts, AWS cost recovery, payments & allocations, recurring billing, and
Gmail reminders with bounce handling plus reports, a money dashboard, an Gmail reminders with bounce handling — plus reports, a money dashboard, an
append-only audit trail, an APEX cutover importer, and a background scheduler. 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
@ -17,7 +17,7 @@ reminders driven by the dated `reminder_schedule` table. The same-day D18
go-live cluster added invoice due dates, the Documents and Reminders pages, 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 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.
## Verification state ## Verification state
@ -25,13 +25,13 @@ section-per-module quotation PDF — then closed all 8 findings from its review.
| --- | --- | | --- | --- |
| `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`) | **372 tests pass across 70 files** | | `npm test` (`vitest run`) | **377 tests pass across 71 files** |
The 346 figure is the live `vitest run` count measured 2026-07-17, after the The 346 figure is the live `vitest run` count measured 2026-07-17, after the
D18 go-live cluster **and its post-delivery review pass** (8 reviewer findings D18 go-live cluster **and its post-delivery review pass** (8 reviewer findings
convert-and-send click bubbling, importer support-field mapping, Pipeline — convert-and-send click bubbling, importer support-field mapping, Pipeline
convert parity, client-book filters, typed contact roles, due-date anchors on convert parity, client-book filters, typed contact roles, due-date anchors on
dashboard/aging, and two atomicity gaps all confirmed and fixed the same dashboard/aging, and two atomicity gaps — all confirmed and fixed the same
day). Test files live under 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`),
@ -39,12 +39,12 @@ day). Test files live under
## Architecture ## Architecture
- **Backend** `apps/hq/src`, Express over `better-sqlite3`. Every table sits - **Backend** — `apps/hq/src`, Express over `better-sqlite3`. Every table sits
behind plain-function "portable repositories" (the D12 pattern: functions take behind plain-function "portable repositories" (the D12 pattern: functions take
the DB handle, no ORM). SQLite-only SQL (`ON CONFLICT`, `INSERT OR IGNORE`) is the DB handle, no ORM). SQLite-only SQL (`ON CONFLICT`, `INSERT OR IGNORE`) is
flagged in comments for a later Postgres port. DB file: `data/hq.db` (WAL mode); flagged in comments for a later Postgres port. DB file: `data/hq.db` (WAL mode);
`HQ_DATA_DIR` overrides the location, `:memory:` is used by tests. `HQ_DATA_DIR` overrides the location, `:memory:` is used by tests.
- **Frontend** `apps/hq-web`, React 19 + Vite 6, `react-router-dom` v7 hash - **Frontend** — `apps/hq-web`, React 19 + Vite 6, `react-router-dom` v7 hash
router. Talks to the server over `/api` with a bearer token kept in router. Talks to the server over `/api` with a bearer token kept in
`localStorage`. In production the server serves the built SPA from `localStorage`. In production the server serves the built SPA from
`apps/hq-web/dist`; in dev Vite runs separately. `apps/hq-web/dist`; in dev Vite runs separately.
@ -54,10 +54,10 @@ day). Test files live under
`@sims/ui` (React component kit + theme). `@sims/ui` (React component kit + theme).
- **Build/run**: - **Build/run**:
- Root: `npm install`, `npm run typecheck`, `npm test`. - Root: `npm install`, `npm run typecheck`, `npm test`.
- Server: `apps/hq` `npm run build` (esbuild → `dist/server.cjs`), - Server: `apps/hq` — `npm run build` (esbuild → `dist/server.cjs`),
`npm start` (build + run). Listens on `HQ_PORT` (default **5182**); the `npm start` (build + run). Listens on `HQ_PORT` (default **5182**); the
scheduler starts with the server. scheduler starts with the server.
- 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`, `HQ_SECRET_KEY` (64 hex / 32 bytes, for token
@ -73,35 +73,35 @@ D18 support fields `anydesk`/`os`/`district`/`sector`/`db_password_enc`, and
the same four support fields on `stg_client` so the APEX cutover carries them) the same four support fields on `stg_client` so the APEX cutover carries them)
and widens two CHECK constraints via a guarded, transactional table rebuild and widens two CHECK constraints via a guarded, transactional table rebuild
(`rebuildTable`: `staff_user.role` gains `manager`, `reminder.rule_kind` gains (`rebuildTable`: `staff_user.role` gains `manager`, `reminder.rule_kind` gains
`quote_followup`) a no-op on fresh DBs and on re-runs. `quote_followup`) — a no-op on fresh DBs and on re-runs.
| Table | Purpose | | Table | Purpose |
| --- | --- | | --- | --- |
| `staff_user` | Console logins / employees — owner · manager · staff role, scrypt salt+hash, active flag | | `staff_user` | Console logins / employees — owner · manager · staff role, scrypt salt+hash, active flag |
| `session` | Bearer session tokens with expiry | | `session` | Bearer session tokens with expiry |
| `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 |
| `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 |
| `document_event` | Per-document timeline (created / issued / sent / converted / cancelled / credit_note ) | | `document_event` | Per-document timeline (created / issued / sent / converted / cancelled / credit_note …) |
| `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 — encrypted refresh token, 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 |
| `stg_client` | APEX import staging for clients (per-row problem list) | | `stg_client` | APEX import staging for clients (per-row problem list) |
| `stg_invoice` | APEX import staging for invoices (per-row problem list) | | `stg_invoice` | APEX import staging for invoices (per-row problem list) |
| `recurring_plan` | Recurring billing cadence, amount-or-price-book, next run, auto/manual policy | | `recurring_plan` | Recurring billing — cadence, amount-or-price-book, next run, auto/manual policy |
| `amc_contract` | AMC contracts coverage, period, amount, reminder days, linked invoice | | `amc_contract` | AMC contracts — coverage, period, amount, reminder days, linked invoice |
| `interaction_type` | Interaction type lookup (call, site visit, training, ) | | `interaction_type` | Interaction type lookup (call, site visit, training, …) |
| `interaction` | Client interaction log outcome, notes, follow-up date | | `interaction` | Client interaction log — outcome, notes, follow-up date |
| `reminder` | Reminder rows rule kind (now incl. `quote_followup`), subject, due period, status; `UNIQUE(rule_kind, subject_id, due_period)` idempotency key | | `reminder` | Reminder rows — rule kind (now incl. `quote_followup`), subject, due period, status; `UNIQUE(rule_kind, subject_id, due_period)` idempotency key |
| `reminder_schedule` | Dated reminder cadence + follow-up message text per rule kind `effective_from`/`_to` window, CSV day offsets (a cadence change is a new dated row) | | `reminder_schedule` | Dated reminder cadence + follow-up message text per rule kind — `effective_from`/`_to` window, CSV day offsets (a cadence change is a new dated row) |
| `aws_usage` | Per-client per-month AWS storage/transfer/cost; `UNIQUE(client_id, month)` upsert key | | `aws_usage` | Per-client per-month AWS storage/transfer/cost; `UNIQUE(client_id, month)` upsert key |
## Backend capabilities ## Backend capabilities
@ -109,14 +109,14 @@ and widens two CHECK constraints via a guarded, transactional table rebuild
Everything below is implemented and exercised by tests via the Express router Everything below is implemented and exercised by tests via the Express router
(`apps/hq/src/api.ts`) or the scheduler. (`apps/hq/src/api.ts`) or the scheduler.
- **Auth & roles** email + password login (scrypt via `@sims/auth`), 14-day - **Auth & roles** — email + password login (scrypt via `@sims/auth`), 14-day
bearer sessions, `requireAuth` / `requireOwner` middleware, and **owner / bearer sessions, `requireAuth` / `requireOwner` middleware, and **owner /
manager / staff roles** with a shared server-side `ownerScope` gate: staff are manager / staff roles** with a shared server-side `ownerScope` gate: staff are
forced to their own rows (any widening param is ignored), owner/manager see forced to their own rows (any widening param is ignored), owner/manager see
everything and may narrow via `?owner=`. `verifySession` requires the account everything and may narrow via `?owner=`. `verifySession` requires the account
to still be active, so deactivation kills unexpired tokens immediately. First to still be active, so deactivation kills unexpired tokens immediately. First
boot seeds an owner and prints a one-time password to stdout. boot seeds an owner and prints a one-time password to stdout.
- **Employee management** staff CRUD over the existing `staff_user` table - **Employee management** — staff CRUD over the existing `staff_user` table
(D16) via `repos-employees.ts` and the `/employees` routes: `GET /employees` (D16) via `repos-employees.ts` and the `/employees` routes: `GET /employees`
for any signed-in user (name/owner pickers; returned whole with a `total`), for any signed-in user (name/owner pickers; returned whole with a `total`),
owner-only create / rename / re-role / password-reset / deactivate / owner-only create / rename / re-role / password-reset / deactivate /
@ -125,90 +125,90 @@ Everything below is implemented and exercised by tests via the Express router
patches are rejected cleanly; deactivation **and** password reset purge the patches are rejected cleanly; deactivation **and** password reset purge the
user's sessions in the same transaction. Password hashes never leave the user's sessions in the same transaction. Password hashes never leave the
repo module and are never audited. repo module and are never audited.
- **Clients** — create / list / search (name·code·GSTIN) / get / patch, GSTIN - **Clients** — create / list / search (name·code·GSTIN) / get / patch, GSTIN
checksum validation, auto-generated client codes, contacts as JSON, lead checksum validation, auto-generated client codes, contacts as JSON, lead →
active → dormant → lost status, and an **account owner** active → dormant → lost status, and an **account owner**
(`PATCH /clients/:id/owner`, owner/manager only, audited as `set_owner`) so (`PATCH /clients/:id/owner`, owner/manager only, audited as `set_owner`) so
bare leads are routable to an employee. bare leads are routable to an employee.
- **Modules & price book** owner-managed catalogue, dated price book - **Modules & price book** — owner-managed catalogue, dated price book
(`priceOn` = latest effective row date), allowed billing kinds (`priceOn` = latest effective row ≤ date), allowed billing kinds
(one_time / monthly / yearly / usage), multi-subscription rule, per-module (one_time / monthly / yearly / usage), multi-subscription rule, per-module
"what's included" quote-content lines. "what's included" quote-content lines.
- **Client modules** assign modules to clients, full delivery lifecycle - **Client modules** — assign modules to clients, full delivery lifecycle
(quoted → ordered → installing → installed → trained → live → expired / (quoted → ordered → installing → installed → trained → live → expired /
cancelled), install/complete/train dates, next-renewal date, single-active cancelled), install/complete/train dates, next-renewal date, single-active
guard for non-multi-sub modules. guard for non-multi-sub modules.
- **Documents** draft compose (`prepareDraft` is a pure compute half shared by - **Documents** — draft compose (`prepareDraft` is a pure compute half shared by
save **and** the live preview, so they can't drift), issue (assigns the series save **and** the live preview, so they can't drift), issue (assigns the series
number), legal status marks (sent / accepted / lost), the QT → PI → INV number), legal status marks (sent / accepted / lost), the QT → PI → INV
conversion chain via `ref_doc_id`, cancel (number stays consumed; blocked once conversion chain via `ref_doc_id`, cancel (number stays consumed; blocked once
payments are allocated), full/partial credit notes recomputed at the original payments are allocated), full/partial credit notes recomputed at the original
invoice date, and a permissive preview endpoint. Issued documents are never invoice date, and a permissive preview endpoint. Issued documents are never
edited corrections are credit notes. Conversion is hardened (funnel spec edited — corrections are credit notes. Conversion is hardened (funnel spec
F3/F4): a document with a live (non-cancelled) forward child refuses a second F3/F4): a document with a live (non-cancelled) forward child refuses a second
convert — one sale, one invoice — and PROFORMA → INVOICE rebuilds the carried convert — one sale, one invoice — and PROFORMA → INVOICE rebuilds the carried
lines through `computeBill` on the **invoice's own date**, so a dated tax lines through `computeBill` on the **invoice's own date**, so a dated tax
change between proforma and invoice lands at the rate that is law on issue change between proforma and invoice lands at the rate that is law on issue
day. Accepting, losing, converting, or cancelling a quotation dismisses its day. Accepting, losing, converting, or cancelling a quotation dismisses its
open follow-up nudges in the same transaction (one audited row each). open follow-up nudges in the same transaction (one audited row each).
- **GST / billing** `@sims/billing-engine` `computeBill` with intra-state - **GST / billing** — `@sims/billing-engine` `computeBill` with intra-state
CGST/SGST vs inter-state IGST split by place of supply, round-to-rupee, SAC CGST/SGST vs inter-state IGST split by place of supply, round-to-rupee, SAC
labelling. Place-of-supply is fail-loud: a missing `company.state_code` throws labelling. Place-of-supply is fail-loud: a missing `company.state_code` throws
rather than silently defaulting. rather than silently defaulting.
- **PDF rendering** — puppeteer HTML → A4 PDF (lazy singleton browser), - **PDF rendering** — puppeteer HTML → A4 PDF (lazy singleton browser),
self-contained inline-CSS letterhead (`templates.ts`: `documentHtml`, self-contained inline-CSS letterhead (`templates.ts`: `documentHtml`,
`receiptHtml`, `documentHtmlSample`). Letterhead identity/text, doc titles, `receiptHtml`, `documentHtmlSample`). Letterhead identity/text, doc titles,
logo (data-URI) and accent colour all read live from settings; the accent is logo (data-URI) and accent colour all read live from settings; the accent is
validated before interpolation. validated before interpolation.
- **Public share links** owner mints a 256-bit opaque token (default +30d - **Public share links** — owner mints a 256-bit opaque token (default +30d
expiry, or never), lists and revokes. One unauthenticated route expiry, or never), lists and revokes. One unauthenticated route
`GET /share/:token` mounted outside `/api`, per-IP rate-limited (30/60s), `GET /share/:token` mounted outside `/api`, per-IP rate-limited (30/60s),
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 an AES-256-GCM-encrypted refresh token. 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
attempt is logged. attempt is logged.
- **Bounce handling** polls the mailbox for mailer-daemon / postmaster DSNs - **Bounce handling** — polls the mailbox for mailer-daemon / postmaster DSNs
since the last poll, matches failed recipients against `email_log`, flips them since the last poll, matches failed recipients against `email_log`, flips them
`bounced`, and raises an `email_bounced` reminder idempotently. `bounced`, and raises an `email_bounced` reminder — idempotently.
- **Payments & allocations** record payments (bank / upi / cheque / cash / - **Payments & allocations** — record payments (bank / upi / cheque / cash /
other) with TDS, allocate oldest-invoice-first or explicitly (never exceeding other) with TDS, allocate oldest-invoice-first or explicitly (never exceeding
outstanding or settling power), TDS pooled with cash as settling power, outstanding or settling power), TDS pooled with cash as settling power,
advance-on-account computed on read, settlement status auto-flip advance-on-account computed on read, settlement status auto-flip
(part_paid / paid), credit-note-aware outstanding, RECEIPT generation, and a (part_paid / paid), credit-note-aware outstanding, RECEIPT generation, and a
pro-rata per-module billed-vs-settled view. pro-rata per-module billed-vs-settled view.
- **Recurring billing** monthly/yearly plans priced by amount or price book, - **Recurring billing** — monthly/yearly plans priced by amount or price book,
auto/manual policy. Generation in the daily scan is transactional (claim + auto/manual policy. Generation in the daily scan is transactional (claim +
issue invoice + advance `next_run`, all-or-nothing) and at-most-once per period; issue invoice + advance `next_run`, all-or-nothing) and at-most-once per period;
auto plans send strictly after commit. auto plans send strictly after commit.
- **AMC contracts** create / patch / deactivate, paid state derived from the - **AMC contracts** — create / patch / deactivate, paid state derived from the
linked invoice's settlement (or a `legacy_paid` manual flag for imports), and linked invoice's settlement (or a `legacy_paid` manual flag for imports), and
one-click renewal-invoice generation against the seeded AMC module. one-click renewal-invoice generation against the seeded AMC module.
- **Interactions** seven seeded interaction types, logging with outcome and an - **Interactions** — seven seeded interaction types, logging with outcome and an
optional follow-up date that feeds the daily scan. optional follow-up date that feeds the daily scan.
- **Pipeline chase-list** `GET /pipeline` (`repos-pipeline.ts`): one - **Pipeline chase-list** — `GET /pipeline` (`repos-pipeline.ts`): one
cross-client ranked list with **derived** stages (Enquiry / New Project / cross-client ranked list with **derived** stages (Enquiry / New Project /
Quoted-Waiting / Won / Lost never stored; computed from `client.status` + Quoted-Waiting / Won / Lost — never stored; computed from `client.status` +
the client's latest live quotation, so the slice added zero pipeline storage the client's latest live quotation, so the slice added zero pipeline storage
beyond `client.owner_id`). Age anchors on the first `sent` document event; beyond `client.owner_id`). Age anchors on the first `sent` document event;
the Waiting → Chase → Nudge → Final-nudge ladder and the green/amber/red the Waiting → Chase → Nudge → Final-nudge ladder and the green/amber/red
bands read the same dated `reminder_schedule` offsets the reminder engine bands read the same dated `reminder_schedule` offsets the reminder engine
uses. Role-gated through `ownerScope` (staff see only their rows quote uses. Role-gated through `ownerScope` (staff see only their rows — quote
owner = `created_by`, lead owner = `client.owner_id`), oldest-overdue sorts owner = `created_by`, lead owner = `client.owner_id`), oldest-overdue sorts
first, `all | mine | overdue | lost` filters (Lost hidden unless asked for), first, `all | mine | overdue | lost` filters (Lost hidden unless asked for),
paginated with an honest `total`. paginated with an honest `total`.
- **Reminders & scheduler** a deterministic daily scan (clock injected) detects - **Reminders & scheduler** — a deterministic daily scan (clock injected) detects
`invoice_overdue` (monthly bucket), `renewal_due`, `amc_expiring`, `follow_up`, `invoice_overdue` (monthly bucket), `renewal_due`, `amc_expiring`, `follow_up`,
and **`quote_followup`** an escalating chase on sent quotations anchored on and **`quote_followup`** — an escalating chase on sent quotations anchored on
the first `sent` event, at day milestones (default 3/7/14) resolved from the the first `sent` event, at day milestones (default 3/7/14) resolved from the
**dated `reminder_schedule` table** (`resolveSchedule`; message text is dated **dated `reminder_schedule` table** (`resolveSchedule`; message text is dated
too, with code-constant fallbacks so a missing row never silences the engine). too, with code-constant fallbacks so a missing row never silences the engine).
The queued row is the owning employee's nudge (owner derived The queued row is the owning employee's nudge (owner derived
`doc_id document.created_by`); sending it emails the client a public share `doc_id → document.created_by`); sending it emails the client a public share
link previews resolve an existing live link read-only and write nothing, link — previews resolve an existing live link read-only and write nothing,
while the real send path is the only place a share is minted (60-day expiry, while the real send path is the only place a share is minted (60-day expiry,
live links reused; an unset `share.base_url` refuses loudly). Catch-up after live links reused; an unset `share.base_url` refuses loudly). Catch-up after
downtime enqueues only the highest crossed milestone, and every reminder goes downtime enqueues only the highest crossed milestone, and every reminder goes
@ -220,25 +220,25 @@ Everything below is implemented and exercised by tests via the Express router
templated emails in `reminder-templates.ts` work as before. The scheduler templated emails in `reminder-templates.ts` work as before. The scheduler
ticks on boot and every 6h (`unref`'d), also running the bounce poll and the ticks on boot and every 6h (`unref`'d), also running the bounce poll and the
monthly AWS cost pull. monthly AWS cost pull.
- **AWS cost recovery** per-client per-month usage & cost, entered manually by - **AWS cost recovery** — per-client per-month usage & cost, entered manually by
the owner or pulled from Cost Explorer. The pull is SigV4-signed with the owner or pulled from Cost Explorer. The pull is SigV4-signed with
`node:crypto` (no SDK), grouped by the `client` cost-allocation tag, assumes `node:crypto` (no SDK), grouped by the `client` cost-allocation tag, assumes
INR, upserts one row per (client, month), and is gated to once per calendar INR, upserts one row per (client, month), and is gated to once per calendar
month. Cross-client cost ranking and per-client cost aggregation are exposed. month. Cross-client cost ranking and per-client cost aggregation are exposed.
- **Reports** — dues aging (030 / 3160 / 6190 / 90+ buckets), module revenue - **Reports** — dues aging (0–30 / 31–60 / 61–90 / 90+ buckets), module revenue
(billed vs settled), client profitability (billed / settled / AWS cost / (billed vs settled), client profitability (billed / settled / AWS cost /
margin), and the AWS cost ranking. margin), and the AWS cost ranking.
- **Dashboard view** "today's money": overdue invoices, recurring due this - **Dashboard view** — "today's money": overdue invoices, recurring due this
week, renewals this month, follow-ups today, recent payments, the live reminder week, renewals this month, follow-ups today, recent payments, the live reminder
queue (owner-scoped like `GET /reminders`, first page + honest total), and queue (owner-scoped like `GET /reminders`, first page + honest total), and
headline totals. headline totals.
- **Settings / letterhead** owner-editable company profile (GSTIN & state-code - **Settings / letterhead** — owner-editable company profile (GSTIN & state-code
validated) and template text (terms, declaration, jurisdiction, footer, validated) and template text (terms, declaration, jurisdiction, footer,
signatory label, per-type titles), logo upload (base64 PNG/JPEG/GIF/WebP/SVG, signatory label, per-type titles), logo upload (base64 PNG/JPEG/GIF/WebP/SVG,
200 KB), and a live sample preview through the one real renderer. ≤200 KB), and a live sample preview through the one real renderer.
- **Audit trail** every mutation writes a before/after row; ids are - **Audit trail** — every mutation writes a before/after row; ids are
monotonically-bumped UUIDv7 so same-millisecond writes stay strictly ordered. monotonically-bumped UUIDv7 so same-millisecond writes stay strictly ordered.
- **APEX importer** stage `clients.csv` + `invoices.csv` (each row carries its - **APEX importer** — stage `clients.csv` + `invoices.csv` (each row carries its
own problem list), a verification report, and a commit that refuses to run own problem list), a verification report, and a commit that refuses to run
while any staged row has problems; commit marks rows `source='apex'` and seeds while any staged row has problems; commit marks rows `source='apex'` and seeds
the INVOICE series past the legacy numbers. CLI: the INVOICE series past the legacy numbers. CLI:
@ -247,8 +247,8 @@ 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 collapsible to an icon rail via lucide icons and a reminder-queue count badge — collapsible to an icon rail via
the footer / toggle (remembered in `localStorage`) — top bar with Ctrl+K the footer ‹/› toggle (remembered in `localStorage`) — top bar with Ctrl+K
command palette, Gmail status pill, theme/accent switcher and a user menu command palette, Gmail status pill, theme/accent switcher and a user menu
(Profile / Logout), plus the Gmail-disconnected banner. Owner-only items (Profile / Logout), plus the Gmail-disconnected banner. Owner-only items
(Employees, APEX Import) appear in ADMIN; the warm ops-console restyle is the (Employees, APEX Import) appear in ADMIN; the warm ops-console restyle is the
@ -256,43 +256,43 @@ command palette, Gmail status pill, theme/accent switcher and a user menu
| 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 (incl. one-click confirmed **Convert & send**); `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 |
| `/reminders` | `Reminders` | Standalone reminder queue: status chips (Queued / Sent / Failed / Dismissed), Mine/All (managerial), send / preview / dismiss, pagination | | `/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` | `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` | `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 |
| `/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) |
| `/import` | `ImportApex` | Owner-only APEX cutover: CSV pickers → staged rows with per-row problem chips → verification summary → commit locked until zero problems | | `/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
double-buffered iframe the on-screen paper is the same HTML puppeteer double-buffered iframe — the on-screen paper is the same HTML puppeteer
rasterizes for the PDF. rasterizes for the PDF.
## Known gaps / not yet wired ## Known gaps / not yet wired
- **Gmail is not connected in production yet** until `gmail-connect` is run, - **Gmail is not connected in production yet** — until `gmail-connect` is run,
document sends and auto-reminders queue to the manual dashboard queue and the document sends and auto-reminders queue to the manual dashboard queue and the
disconnected banner shows. disconnected banner shows.
- **AWS cost pull is env-gated** the scheduled and manual pulls run only when - **AWS cost pull is env-gated** — the scheduled and manual pulls run only when
`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` are configured; otherwise usage `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` are configured; otherwise usage
is owner-entered by hand. is owner-entered by hand.
- **A few tax/identity defaults are provisional** the seeded company state code - **A few tax/identity defaults are provisional** — the seeded company state code
(`32`, Kerala) and several SAC codes carry "founder / CA to confirm" notes. (`32`, Kerala) and several SAC codes carry "founder / CA to confirm" notes.
_Closed by the D18 go-live cluster (2026-07-17): APEX import now has an owner-only _Closed by the D18 go-live cluster (2026-07-17): APEX import now has an owner-only
web UI (`/import`: stage → per-row problems → verify → confirm-gated commit); web UI (`/import`: stage → per-row problems → verify → confirm-gated commit);
standalone Reminders (`/reminders`) and Documents (`/documents`, honestly paginated) standalone Reminders (`/reminders`) and Documents (`/documents`, honestly paginated)
pages exist; invoices carry **due dates** (terms default + override, stamped at 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, an encrypted DB password with audited reveal); the

@ -366,3 +366,19 @@ secret; app ₹35k + implementation ₹10k + integration ₹5k + AMC ₹15k/yr).
seeded from the real quotations for the section-per-module PDF. seeded from the real quotations for the section-per-module PDF.
Suite at 372 tests / 70 files after D21 + D23. Suite at 372 tests / 70 files after D21 + D23.
## D22 — Onboarding milestone tracker (reportable) ✅ DELIVERED (2026-07-17)
Founder call: track the common project status (advance paid / setup / installed / go-live /
balance paid / AMC) that applies to EVERY module sale, and make it reportable across the
book. `project_milestone` — each client_module (a "project") carries a checklist seeded from
the dated `project.milestone_template` setting (owner-editable; a project can add its own
steps). Ticking stamps the date, audited; `ensureProjectMilestones` is idempotent and
back-fills projects created before D22 (the 167 imported ones seeded). Cross-project board
(`GET /projects/board` — per-milestone done/total/pending) + drill-down
(`GET /projects/pending/:key` — who's pending that step). UI: a per-project checklist on the
Client 360 Modules tab + a new **Onboarding** work page (board + click-through to pending
projects). Also fixed a latent bug found here: `uuidv7()` is now monotonic within a process,
so `ORDER BY id` is genuine creation order (pagination + the sync cursor rely on it) — a
sub-millisecond tie no longer falls back to the random tail.
Suite at 377 tests / 71 files after D22.

Loading…
Cancel
Save