@ -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`) | **37 2 tests pass across 70 files** |
| `npm test` (`vitest run`) | **37 7 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 (0– 30 / 31– 60 / 61– 90 / 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