The historical HQ-1/2/3a build plans and the live-preview spec referenced sibling
Store apps (apps/store-server, apps/backoffice) as copy-sources and cited Store-only
docs. Rewrote those to describe the patterns intrinsically and to name the Store
product/repo where the scope boundary is genuine, so each plan stands alone in the
HQ repo. Technical content and task lists unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
**Goal:** Ship HQ-1 of [docs/14-SPEC-HQ-CONSOLE.md](../../14-SPEC-HQ-CONSOLE.md): `apps/hq` (API server + SQLite) and `apps/hq-web` (React UI) — client registry with APEX CSV import, module catalog + dated prices, client-module tracking, quotation/proforma/invoice/credit-note generation with letterhead PDF, Gmail send with token-death handling, and payment recording with allocation/TDS/advances.
**Goal:** Ship HQ-1 of [docs/14-SPEC-HQ-CONSOLE.md](../../14-SPEC-HQ-CONSOLE.md): `apps/hq` (API server + SQLite) and `apps/hq-web` (React UI) — client registry with APEX CSV import, module catalog + dated prices, client-module tracking, quotation/proforma/invoice/credit-note generation with letterhead PDF, Gmail send with token-death handling, and payment recording with allocation/TDS/advances.
**Architecture:** Mirror the store-server pattern exactly: express server + better-sqlite3 behind plain-function repositories (`repos-*.ts`), React 19 + Vite web app served from the server, pure logic reused from `@sims/domain` (money, UUIDv7, FY, `formatDocNo`) and `@sims/billing-engine` (`computeBill`). HQ keeps its **own** document/payment tables (retail `BillDoc`/`PaymentMode` types are goods-shaped; shared packages stay generic per D15). Doc series is HQ-local: table keyed `(doc_type, fy)`, numbers via the existing `formatDocNo(prefix, seq, 4)` → `QT/26-27-0001` (14 chars, passes `assertGstDocNo`).
**Architecture:** Follow the established server pattern: express server + better-sqlite3 behind plain-function repositories (`repos-*.ts`), React 19 + Vite web app served from the server, pure logic reused from `@sims/domain` (money, UUIDv7, FY, `formatDocNo`) and `@sims/billing-engine` (`computeBill`). HQ keeps its **own** document/payment tables (retail `BillDoc`/`PaymentMode` types are goods-shaped; shared packages stay generic per D15). Doc series is HQ-local: table keyed `(doc_type, fy)`, numbers via the existing `formatDocNo(prefix, seq, 4)` → `QT/26-27-0001` (14 chars, passes `assertGstDocNo`).
- All money is **integer paise** (`Paise` from `@sims/domain`); never floats.
- All money is **integer paise** (`Paise` from `@sims/domain`); never floats.
- All ids are **UUIDv7** via `uuidv7()` from `@sims/domain`.
- All ids are **UUIDv7** via `uuidv7()` from `@sims/domain`.
- **Every mutation writes an `audit_log` row** (same discipline as store-server).
- **Every mutation writes an `audit_log` row** (append-only audit discipline).
- SQL stays **portable** (D12 guardrail): standard SQL, quirks commented.
- SQL stays **portable** (D12 guardrail): standard SQL, quirks commented.
- HQ prices are **GST-exclusive** (`priceIncludesTax: false`) — B2B convention.
- HQ prices are **GST-exclusive** (`priceIncludesTax: false`) — B2B convention.
- Intra/inter-state: our state code comes from the `setting` table key `company.state_code` (seed `'32'`, Kerala — founder to confirm); client state ≠ ours ⇒ IGST.
- Intra/inter-state: our state code comes from the `setting` table key `company.state_code` (seed `'32'`, Kerala — founder to confirm); client state ≠ ours ⇒ IGST.
- HSN column carries **SAC codes** for our service lines; PDFs label it "SAC".
- HSN column carries **SAC codes** for our service lines; PDFs label it "SAC".
- Issued invoices are **never edited or deleted** — corrections are credit notes; cancel keeps the number consumed.
- Issued invoices are **never edited or deleted** — corrections are credit notes; cancel keeps the number consumed.
- Server port **5182** (5181 = store-server; 8080–85 Windows-reserved).
- Server port **5182** (8080–85 Windows-reserved).
- Secrets: `HQ_SECRET_KEY` (64 hex chars) encrypts the Gmail refresh token (AES-256-GCM). Never log tokens.
- Secrets: `HQ_SECRET_KEY` (64 hex chars) encrypts the Gmail refresh token (AES-256-GCM). Never log tokens.
- Tests: vitest, colocated under `apps/hq/test/` (Task 1 extends the vitest include).
- Tests: vitest, colocated under `apps/hq/test/` (Task 1 extends the vitest include).
- Commit after every task; messages end with `Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>`.
- Commit after every task; messages end with `Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>`.
@ -125,7 +125,7 @@ In `vitest.config.ts` change the `test` block to:
- Produces: `api.ts`: `apiFetch(path, opts?)` — adds `Authorization` from `localStorage['hq.token']`, redirects to `/login` on 401; `main.tsx` routes: `/login`, then inside `Layout`: `/` (Clients), `/clients/:id`, `/modules`, `/documents/new`, `/documents/:id`.
- Produces: `api.ts`: `apiFetch(path, opts?)` — adds `Authorization` from `localStorage['hq.token']`, redirects to `/login` on 401; `main.tsx` routes: `/login`, then inside `Layout`: `/` (Clients), `/clients/:id`, `/modules`, `/documents/new`, `/documents/:id`.
- `Layout.tsx`: `@sims/ui` shell + left nav (Clients · Modules · New Document) + **email-status banner**: on mount `GET /api/email/status`; when `dead` or not connected show a persistent amber bar "Gmail disconnected — sends are queued to manual. Run gmail-connect on the server." (the spec's dashboard banner).
- `Layout.tsx`: `@sims/ui` shell + left nav (Clients · Modules · New Document) + **email-status banner**: on mount `GET /api/email/status`; when `dead` or not connected show a persistent amber bar "Gmail disconnected — sends are queued to manual. Run gmail-connect on the server." (the spec's dashboard banner).
- [ ] **Step 1: Scaffold + implement the four files.**Follow `apps/backoffice/src/Login.tsx` and `Layout.tsx` as the style/pattern source; Login posts `/api/auth/login`, stores token + display name, navigates `/`.
- [ ] **Step 1: Scaffold + implement the four files.**Build Login + Layout in the shared `@sims/ui` style; Login posts `/api/auth/login`, stores token + display name, navigates `/`.
- [ ] **Step 2: Verify** — `cd apps/hq-web && npm run typecheck && npm run build` → clean build; `npm run dev` with the server running: login with the seeded owner works, banner shows "Gmail disconnected".
- [ ] **Step 2: Verify** — `cd apps/hq-web && npm run typecheck && npm run build` → clean build; `npm run dev` with the server running: login with the seeded owner works, banner shows "Gmail disconnected".
**Goal:** Ship HQ-2 of [docs/14-SPEC-HQ-CONSOLE.md](../../14-SPEC-HQ-CONSOLE.md) on top of the HQ-1 `apps/hq` + `apps/hq-web` codebase: recurring plans that generate invoices on schedule, a reminder engine (auto-send + manual queue, idempotent across crashes/catch-up, bounce-aware), AMC contracts with derived paid-state, an interaction/visit/call/training log with follow-ups, and a dashboard money-view that becomes the default page after login. All grounded in the real HQ-1 interfaces (`createDraft`/`issueDocument`, `sendDocumentEmail`, `getAccount`/`TokenDeadError`, `priceOn`, `writeAudit`, `formatINR`).
**Goal:** Ship HQ-2 of [docs/14-SPEC-HQ-CONSOLE.md](../../14-SPEC-HQ-CONSOLE.md) on top of the HQ-1 `apps/hq` + `apps/hq-web` codebase: recurring plans that generate invoices on schedule, a reminder engine (auto-send + manual queue, idempotent across crashes/catch-up, bounce-aware), AMC contracts with derived paid-state, an interaction/visit/call/training log with follow-ups, and a dashboard money-view that becomes the default page after login. All grounded in the real HQ-1 interfaces (`createDraft`/`issueDocument`, `sendDocumentEmail`, `getAccount`/`TokenDeadError`, `priceOn`, `writeAudit`, `formatINR`).
**Architecture:** Extend the store-server-shaped HQ pattern exactly as HQ-1 established it — express + better-sqlite3 behind plain-function repositories (`repos-*.ts`), pure logic reused from `@sims/domain` (`formatINR`, `fromRupees`, `uuidv7`, `fyOf`) and the HQ document engine. **New tables are added to the `SCHEMA` string in `db.ts` via `CREATE TABLE IF NOT EXISTS`** (no ALTER needed — the table simply appears on next `openDb`); **the one new column** (`email_log.bounced`) is added through the existing guarded `migrate(db)` PRAGMA/ALTER path. The scheduler is a **pure function**`runDailyScan(db, deps, today)` with an injected clock (never `Date.now()` inside logic) so tests are deterministic; it is wired into `server.ts` to run on boot and every 6h. Idempotency lives entirely in one UNIQUE constraint — `reminder(rule_kind, subject_id, due_period)` — reached via `INSERT OR IGNORE`, which is what makes catch-up after downtime safe with zero extra bookkeeping.
**Architecture:** Extend the HQ server pattern exactly as HQ-1 established it — express + better-sqlite3 behind plain-function repositories (`repos-*.ts`), pure logic reused from `@sims/domain` (`formatINR`, `fromRupees`, `uuidv7`, `fyOf`) and the HQ document engine. **New tables are added to the `SCHEMA` string in `db.ts` via `CREATE TABLE IF NOT EXISTS`** (no ALTER needed — the table simply appears on next `openDb`); **the one new column** (`email_log.bounced`) is added through the existing guarded `migrate(db)` PRAGMA/ALTER path. The scheduler is a **pure function**`runDailyScan(db, deps, today)` with an injected clock (never `Date.now()` inside logic) so tests are deterministic; it is wired into `server.ts` to run on boot and every 6h. Idempotency lives entirely in one UNIQUE constraint — `reminder(rule_kind, subject_id, due_period)` — reached via `INSERT OR IGNORE`, which is what makes catch-up after downtime safe with zero extra bookkeeping.
**Tech Stack:** unchanged from HQ-1 — TypeScript (ESM, strict), express ^4.21, better-sqlite3 ^11.7, puppeteer ^23 (PDF, injected into scheduler/reminder deps so tests never launch Chromium), vitest, React 19 + Vite 6 + react-router 7, `@sims/ui`. No new dependencies (bounce polling is `fetch`-based, same as `gmail.ts`).
**Tech Stack:** unchanged from HQ-1 — TypeScript (ESM, strict), express ^4.21, better-sqlite3 ^11.7, puppeteer ^23 (PDF, injected into scheduler/reminder deps so tests never launch Chromium), vitest, React 19 + Vite 6 + react-router 7, `@sims/ui`. No new dependencies (bounce polling is `fetch`-based, same as `gmail.ts`).
**Goal:** Ship the first slice of HQ-3 from [docs/14-SPEC-HQ-CONSOLE.md](../../14-SPEC-HQ-CONSOLE.md) §5–§6 on top of the completed HQ-1 + HQ-2 `apps/hq` + `apps/hq-web` codebase: per-client **AWS usage & cost** captured monthly (automated pull from Cost Explorer grouped by a cost-allocation tag, plus a manual owner fallback), the founder's **cross-client cost ranking** (rank + share of total), the three **reports** (dues aging, module-wise revenue, client profitability with margin), optional **payment receipts** as their own document series, and an owner-only **Company Profile** editor for the `company.*` settings the letterhead PDFs read. Everything grounds in the real HQ interfaces (`openDb`/`SCHEMA`/`migrate`, `writeAudit`, `uuidv7`, `outstandingPaise`, `splitProRata`/`modulePaidView`, `createDraft`/`issueDocument`/`insertDocRow`, `nextDocNo`, `getSetting`/`setSetting`, `documentHtml`/`renderPdf`, the `Fetcher` pattern from `gmail.ts`/`bounces.ts`, `dashboardView`, and the `@sims/ui` + `useData` page style).
**Goal:** Ship the first slice of HQ-3 from [docs/14-SPEC-HQ-CONSOLE.md](../../14-SPEC-HQ-CONSOLE.md) §5–§6 on top of the completed HQ-1 + HQ-2 `apps/hq` + `apps/hq-web` codebase: per-client **AWS usage & cost** captured monthly (automated pull from Cost Explorer grouped by a cost-allocation tag, plus a manual owner fallback), the founder's **cross-client cost ranking** (rank + share of total), the three **reports** (dues aging, module-wise revenue, client profitability with margin), optional **payment receipts** as their own document series, and an owner-only **Company Profile** editor for the `company.*` settings the letterhead PDFs read. Everything grounds in the real HQ interfaces (`openDb`/`SCHEMA`/`migrate`, `writeAudit`, `uuidv7`, `outstandingPaise`, `splitProRata`/`modulePaidView`, `createDraft`/`issueDocument`/`insertDocRow`, `nextDocNo`, `getSetting`/`setSetting`, `documentHtml`/`renderPdf`, the `Fetcher` pattern from `gmail.ts`/`bounces.ts`, `dashboardView`, and the `@sims/ui` + `useData` page style).
**Architecture:** Extend the store-server-shaped HQ pattern exactly as HQ-1/HQ-2 established it — express + better-sqlite3 behind plain-function repositories (`repos-*.ts`), pure logic reused from `@sims/domain` (`formatINR`, `fromRupees`, `uuidv7`, `fyOf`, `validateGstin`) and the HQ document engine. The **one new table** (`aws_usage`) is added to the `SCHEMA` string in `db.ts` via `CREATE TABLE IF NOT EXISTS` — it appears on next `openDb`, no migration entry needed. AWS ingestion is a pure module `aws-costs.ts` with an **injected `Fetcher`** (same shape as `gmail.ts`): `signAwsRequest` implements SigV4 with `node:crypto` (no AWS SDK), `pullMonthlyCosts(db, deps, month)` maps cost-allocation tag values to client codes and upserts rows, and `maybePullAwsCosts(db, deps, today)` gates it to once per calendar month. **Credentials are read only in the server/route wiring** (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`), never inside the pure functions; the scheduler tick calls the pull only when creds exist. Reports reuse the exact settlement primitives HQ-1/HQ-2 already ship: `outstandingPaise` for dues, and the largest-remainder `splitProRata` (promoted to an export) for module-wise revenue, so a paisa split here matches a paisa split in `modulePaidView`. Receipts flow through the existing `insertDocRow` → `issueDocument` path with their own `RCT/` series and carry **no GST lines** (a payment-acknowledgment layout in `templates.ts`).
**Architecture:** Extend the HQ server pattern exactly as HQ-1/HQ-2 established it — express + better-sqlite3 behind plain-function repositories (`repos-*.ts`), pure logic reused from `@sims/domain` (`formatINR`, `fromRupees`, `uuidv7`, `fyOf`, `validateGstin`) and the HQ document engine. The **one new table** (`aws_usage`) is added to the `SCHEMA` string in `db.ts` via `CREATE TABLE IF NOT EXISTS` — it appears on next `openDb`, no migration entry needed. AWS ingestion is a pure module `aws-costs.ts` with an **injected `Fetcher`** (same shape as `gmail.ts`): `signAwsRequest` implements SigV4 with `node:crypto` (no AWS SDK), `pullMonthlyCosts(db, deps, month)` maps cost-allocation tag values to client codes and upserts rows, and `maybePullAwsCosts(db, deps, today)` gates it to once per calendar month. **Credentials are read only in the server/route wiring** (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`), never inside the pure functions; the scheduler tick calls the pull only when creds exist. Reports reuse the exact settlement primitives HQ-1/HQ-2 already ship: `outstandingPaise` for dues, and the largest-remainder `splitProRata` (promoted to an export) for module-wise revenue, so a paisa split here matches a paisa split in `modulePaidView`. Receipts flow through the existing `insertDocRow` → `issueDocument` path with their own `RCT/` series and carry **no GST lines** (a payment-acknowledgment layout in `templates.ts`).
**Tech Stack:** unchanged from HQ-1/HQ-2 — TypeScript (ESM, strict), express ^4.21, better-sqlite3 ^11.7, puppeteer ^23 (PDF; injected into deps so tests never launch Chromium), vitest, React 19 + Vite 6 + react-router 7, `@sims/ui`. **No new dependencies** — SigV4 uses `node:crypto` (`createHash`, `createHmac`), the AWS call is `fetch`-based exactly like `gmail.ts`/`bounces.ts`.
**Tech Stack:** unchanged from HQ-1/HQ-2 — TypeScript (ESM, strict), express ^4.21, better-sqlite3 ^11.7, puppeteer ^23 (PDF; injected into deps so tests never launch Chromium), vitest, React 19 + Vite 6 + react-router 7, `@sims/ui`. **No new dependencies** — SigV4 uses `node:crypto` (`createHash`, `createHmac`), the AWS call is `fetch`-based exactly like `gmail.ts`/`bounces.ts`.
**Architecture:** The one idea that makes this safe — the preview shows the output of the **same**`documentHtml()` template and the **same**`computeBill()` tax engine the PDF uses. There is no second renderer and **no client-side money math**. We extract a pure `prepareDraft(db, input)` from `createDraft` (compute-only, no persist); `createDraft` then calls `prepareDraft` + persists, so save and preview cannot diverge by construction. A new `POST /api/documents/preview` builds a synthetic **unsaved**`Doc`, runs `prepareDraft` + `documentHtml`, and returns `{ ok, html, totals, warnings }` — persisting nothing, writing no audit. The browser renders that HTML directly in a sandboxed `srcdoc` iframe; puppeteer only rasterizes the identical HTML for the actual PDF. Sibling surfaces reuse the one renderer: the letterhead and quote-content previews go through `documentHtmlSample()` + a single thin `POST /api/previews/sample` route (unsaved edits posted in the request body); the reminder-email preview goes through the pure `reminderEmail(kind, ctx)` via `GET /api/reminders/:id/preview`, with the ctx-builder extracted so the previewed email equals the sent one.
**Architecture:** The one idea that makes this safe — the preview shows the output of the **same**`documentHtml()` template and the **same**`computeBill()` tax engine the PDF uses. There is no second renderer and **no client-side money math**. We extract a pure `prepareDraft(db, input)` from `createDraft` (compute-only, no persist); `createDraft` then calls `prepareDraft` + persists, so save and preview cannot diverge by construction. A new `POST /api/documents/preview` builds a synthetic **unsaved**`Doc`, runs `prepareDraft` + `documentHtml`, and returns `{ ok, html, totals, warnings }` — persisting nothing, writing no audit. The browser renders that HTML directly in a sandboxed `srcdoc` iframe; puppeteer only rasterizes the identical HTML for the actual PDF. Sibling surfaces reuse the one renderer: the letterhead and quote-content previews go through `documentHtmlSample()` + a single thin `POST /api/previews/sample` route (unsaved edits posted in the request body); the reminder-email preview goes through the pure `reminderEmail(kind, ctx)` via `GET /api/reminders/:id/preview`, with the ctx-builder extracted so the previewed email equals the sent one.
**Editable template data (spec §4, founder addition):** all letterhead **text data** becomes DB-editable through settings — one fixed, well-designed layout, **not** a visual layout editor (that is the store product's Print Templates surface, out of scope). GST split logic stays automatic (derived from `company.state_code` vs the client state). Beyond the existing `company.*` settings, `templates.ts` renders new optional `template.*` keys (`template.terms`, `template.declaration`, `template.jurisdiction`, `template.footer_note`, `template.signatory_label`, `template.logo` as a size-capped `data:` URI, and per-type `template.title_<DOCTYPE>`) **each with a built-in default so an absent key renders exactly today's output** — nothing breaks pre-configuration. These live in the same `setting` table (`getSetting`/`setSetting`), so an edit is a DB write the next PDF render and the live preview reflect immediately, no release. A new owner-gated **Document Template** page (`DocumentTemplate.tsx`) groups the `company.*` block + the `template.*` fields + a logo upload with the live letterhead preview beside the form — it **absorbs** the earlier company-profile-letterhead idea into one page. New routes `GET/PUT /api/settings/template` (both groups, owner-gated, each field audited via `setSetting`) and `POST /api/settings/template/logo` (validates content-type + size cap, stores the data URI) model on the existing `GET/PUT /api/settings/company`.
**Editable template data (spec §4, founder addition):** all letterhead **text data** becomes DB-editable through settings — one fixed, well-designed layout, **not** a visual layout editor (that is the Store product's Print Templates surface — a separate product/repo — out of scope). GST split logic stays automatic (derived from `company.state_code` vs the client state). Beyond the existing `company.*` settings, `templates.ts` renders new optional `template.*` keys (`template.terms`, `template.declaration`, `template.jurisdiction`, `template.footer_note`, `template.signatory_label`, `template.logo` as a size-capped `data:` URI, and per-type `template.title_<DOCTYPE>`) **each with a built-in default so an absent key renders exactly today's output** — nothing breaks pre-configuration. These live in the same `setting` table (`getSetting`/`setSetting`), so an edit is a DB write the next PDF render and the live preview reflect immediately, no release. A new owner-gated **Document Template** page (`DocumentTemplate.tsx`) groups the `company.*` block + the `template.*` fields + a logo upload with the live letterhead preview beside the form — it **absorbs** the earlier company-profile-letterhead idea into one page. New routes `GET/PUT /api/settings/template` (both groups, owner-gated, each field audited via `setSetting`) and `POST /api/settings/template/logo` (validates content-type + size cap, stores the data URI) model on the existing `GET/PUT /api/settings/company`.
**Tech Stack:** unchanged — TypeScript (ESM, strict), express ^4.21, better-sqlite3 ^11.7, puppeteer ^23 (never launched in tests), vitest ^3, React 19 + Vite 6 + react-router 7, `@sims/ui`. **No new dependencies.** The preview iframe uses native `srcdoc` + `AbortController`; no rasterization anywhere in the test suite.
**Tech Stack:** unchanged — TypeScript (ESM, strict), express ^4.21, better-sqlite3 ^11.7, puppeteer ^23 (never launched in tests), vitest ^3, React 19 + Vite 6 + react-router 7, `@sims/ui`. **No new dependencies.** The preview iframe uses native `srcdoc` + `AbortController`; no rasterization anywhere in the test suite.
@ -33,7 +33,7 @@ Everything from the HQ-1/HQ-2/HQ-3a plans still holds; the load-bearing ones for
**In:** `prepareDraft` extraction + parity/permissive; `POST /api/documents/preview`; the string-identity fidelity test; `templates.ts` rendering the new `template.*` fields with default-fallback; `@media screen` paper styles + `documentHtmlSample` + `POST /api/previews/sample`; `GET/PUT /api/settings/template` + `POST /api/settings/template/logo`; `reminderContext` extraction + `GET /api/reminders/:id/preview`; `LivePreview.tsx` (debounced/immediate fetch + double-buffer iframe); `NewDocument.tsx` split layout + `buildDraftBody` + server-truth totals strip (reducer deleted) + responsive narrow layout; the owner-gated **Document Template** page (company block + `template.*` + logo upload + live letterhead preview, absorbing the company-profile letterhead surface); module quote-content live print preview; reminder-email preview in the queue; final browser walk.
**In:** `prepareDraft` extraction + parity/permissive; `POST /api/documents/preview`; the string-identity fidelity test; `templates.ts` rendering the new `template.*` fields with default-fallback; `@media screen` paper styles + `documentHtmlSample` + `POST /api/previews/sample`; `GET/PUT /api/settings/template` + `POST /api/settings/template/logo`; `reminderContext` extraction + `GET /api/reminders/:id/preview`; `LivePreview.tsx` (debounced/immediate fetch + double-buffer iframe); `NewDocument.tsx` split layout + `buildDraftBody` + server-truth totals strip (reducer deleted) + responsive narrow layout; the owner-gated **Document Template** page (company block + `template.*` + logo upload + live letterhead preview, absorbing the company-profile letterhead surface); module quote-content live print preview; reminder-email preview in the queue; final browser walk.
**Out (do not plan):** a **visual layout** editor (drag/reposition/restyle — that is the store product's Print Templates surface, doc 09); credit-note & receipt preview; the other store-product surfaces (Label Printing, Price-list bulk revise, Schemes editor, WhatsApp/message-catalog, POS printer-settings receipt preview) — documented handoffs; `template.*` rendering on `receiptHtml` (receipts aren't previewed this round — a small follow-up); the "Exact PDF" on-demand puppeteer button and the approximate page-break line (deferred UX polish); generalizing `LivePreview` beyond the surfaces here (YAGNI). Do not re-litigate these.
**Out (do not plan):** a **visual layout** editor (drag/reposition/restyle — that is the Store product's Print Templates surface, doc 09 in the Store repo); credit-note & receipt preview; the other Store-product surfaces (Label Printing, Price-list bulk revise, Schemes editor, WhatsApp/message-catalog, POS printer-settings receipt preview) — documented handoffs; `template.*` rendering on `receiptHtml` (receipts aren't previewed this round — a small follow-up); the "Exact PDF" on-demand puppeteer button and the approximate page-break line (deferred UX polish); generalizing `LivePreview` beyond the surfaces here (YAGNI). Do not re-litigate these.
## File Structure
## File Structure
@ -2150,7 +2150,7 @@ git commit -m "feat(hq-web): reminder email preview in the manual queue" -m "Co-
- **Spec coverage (all of §1–§8):** one shared compute path (§1–2 → Tasks 1–3), permissive preview persisting nothing (§2 → Task 2), the editable `template.*` data + Document Template page absorbing the company-profile letterhead (§4 → Tasks 4, 6, 10), the `@media screen` template-owned paper (§3 → Task 5), the split composer with the deleted client reducer + server-truth totals + IGST-live + responsive narrow sheet (§3 → Task 9), and the two remaining sibling surfaces — quote-content and reminder email (§5 → Tasks 11–12). Out of scope stated where relevant: a **visual layout** editor (store product's Print Templates), `template.*` on `receiptHtml`, the "Exact PDF" button, and the dashed page-break line.
- **Spec coverage (all of §1–§8):** one shared compute path (§1–2 → Tasks 1–3), permissive preview persisting nothing (§2 → Task 2), the editable `template.*` data + Document Template page absorbing the company-profile letterhead (§4 → Tasks 4, 6, 10), the `@media screen` template-owned paper (§3 → Task 5), the split composer with the deleted client reducer + server-truth totals + IGST-live + responsive narrow sheet (§3 → Task 9), and the two remaining sibling surfaces — quote-content and reminder email (§5 → Tasks 11–12). Out of scope stated where relevant: a **visual layout** editor (Store product's Print Templates, separate repo), `template.*` on `receiptHtml`, the "Exact PDF" button, and the dashed page-break line.
- **No placeholders:** every server task ships a verbatim failing test and full implementation; every frontend task ships full component code + an explicit browser-verification step (iframe/matchMedia/layout/upload UI with no unit-testable surface).
- **No placeholders:** every server task ships a verbatim failing test and full implementation; every frontend task ships full component code + an explicit browser-verification step (iframe/matchMedia/layout/upload UI with no unit-testable surface).
- **Type consistency (checked against the real code):**`prepareDraft` returns `BillTotals`/`DocPayload` already imported in `repos-documents.ts`; the preview route's synthetic `Doc` sets exactly the fields `documentHtml` reads and ignored fields are harmless; `fyOf`/`type Client`/`type Doc` imports added where used; `documentHtml`'s settings-map param already types as `Record<string,string>`, so reading new `template.*` keys needs no signature change; `companySettings()` broadening feeds the same map to the pdf/send/preview/sample routes; the logo regex + 200 KB cap run inside the route (with `express.json` limit raised so a legitimate logo is not pre-rejected); client `DocTotals` is the exact subset the strip renders.
- **Type consistency (checked against the real code):**`prepareDraft` returns `BillTotals`/`DocPayload` already imported in `repos-documents.ts`; the preview route's synthetic `Doc` sets exactly the fields `documentHtml` reads and ignored fields are harmless; `fyOf`/`type Client`/`type Doc` imports added where used; `documentHtml`'s settings-map param already types as `Record<string,string>`, so reading new `template.*` keys needs no signature change; `companySettings()` broadening feeds the same map to the pdf/send/preview/sample routes; the logo regex + 200 KB cap run inside the route (with `express.json` limit raised so a legitimate logo is not pre-rejected); client `DocTotals` is the exact subset the strip renders.
- **Two intentional refinements, both preserving "one renderer, one compute path":** (1) `prepareDraft` returns `{ docType, clientId, docDate, totals, payload, warnings }` (a superset of the design's `{ lines, totals, payload }`) so save and preview share one `docDate`/`fy` and `warnings[]` has a home. (2) The design's "one preview route" is realized as the shared `LivePreview` mechanism + the single `documentHtml`/`documentHtmlSample` renderer, fed by `POST /documents/preview` (composer) and one thin `POST /previews/sample` (the two template-editing surfaces preview **unsaved** company/`template.*`/bullet edits that `/documents/preview` structurally cannot). Editable `template.*` is pure data through that same renderer — still no second renderer, and the PDF picks the fields up for free via the broadened `companySettings()`.
- **Two intentional refinements, both preserving "one renderer, one compute path":** (1) `prepareDraft` returns `{ docType, clientId, docDate, totals, payload, warnings }` (a superset of the design's `{ lines, totals, payload }`) so save and preview share one `docDate`/`fy` and `warnings[]` has a home. (2) The design's "one preview route" is realized as the shared `LivePreview` mechanism + the single `documentHtml`/`documentHtmlSample` renderer, fed by `POST /documents/preview` (composer) and one thin `POST /previews/sample` (the two template-editing surfaces preview **unsaved** company/`template.*`/bullet edits that `/documents/preview` structurally cannot). Editable `template.*` is pure data through that same renderer — still no second renderer, and the PDF picks the fields up for free via the broadened `companySettings()`.