You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/docs/superpowers/specs/2026-07-13-live-preview-des...

172 lines
11 KiB
Markdown

# Live Document Preview — Design (HQ console)
**Status:** approved (founder, 2026-07-13, via ultracode design panel + brainstorm).
**Origin:** spec [14-SPEC-HQ-CONSOLE.md](../../14-SPEC-HQ-CONSOLE.md) §3 already promised a
"live PDF preview on letterhead" in the composer; this is that, designed properly and
extended to three cheap sibling surfaces.
## 1. The one idea that makes it safe
The preview must **never drift from what prints**. That is guaranteed *architecturally,
not by testing*: the preview shows the output of the **same** `documentHtml()` template
and the **same** `computeBill()` tax engine that the PDF uses — there is no second
renderer and no client-side money math. The browser renders that HTML directly in an
iframe (puppeteer only rasterizes the identical HTML for the actual PDF).
Winning approach = engineering-minimalist core (smallest correct change, one shared
compute path) + grafted UX pieces (server-truth totals strip, permissive preview,
no-blank swap). Chosen by a 2-judge panel (value judge 8.5 ux / cost judge 9 minimal →
minimal core carrying ux's high-value grafts).
## 2. Server: one new pure function + one route
**Refactor (no behavior change):** extract the compute block of `createDraft` in
`apps/hq/src/repos-documents.ts` into a pure `prepareDraft(db, input): { lines, totals,
payload }` that does **not** persist. `createDraft` calls `prepareDraft` then inserts —
so save and preview cannot diverge by construction.
**New route** `POST /api/documents/preview` (`requireAuth`, api.ts style):
- Body: the **exact** `DraftInput` shape `POST /api/documents` takes.
- Builds a synthetic **unsaved** `Doc` (`docNo: null` → template prints "DRAFT";
`docDate: today`; `fy` via `fyOf`), runs `prepareDraft` + `documentHtml`.
- Returns `{ ok: true, html: string, totals: BillTotals, warnings: string[] }`.
- Persists nothing; writes no audit row (it is a read). Cost = a few SQLite reads +
string templating (single-digit ms); no puppeteer, no browser contention.
**Permissive vs strict:** the preview endpoint is **permissive** — a line with no
price-book row and no manual rate computes at ₹0 and is reported in `warnings[]` (so
staff can keep typing); **`POST /api/documents` (save) stays strict** and still rejects
the same bad document. Preview forgives; save refuses.
**Fidelity test:** one golden test asserts the preview route and `renderPdf` consume
**string-identical** HTML for a fixture draft + a `prepareDraft`↔`createDraft`
deep-equal parity test (totals/payload). Guards anyone ever forking the template.
## 3. Client: the composer becomes a split view
`apps/hq-web/src/pages/NewDocument.tsx`, layout **"form + A4 paper fills the rest"**
(founder pick):
- **Left**, fixed ~600px, own scroll: the existing form. Its bottom gets a **sticky
totals strip** — Taxable / (CGST+SGST *or* IGST) / Round-off / Payable — sourced from
the **response's `totals`**, deleting the client-side subtotal reducer at
`NewDocument.tsx:91` (the last client money math; its removal is the single biggest
trust win — the IGST split now appears live for out-of-state clients).
- **Right**, fills remaining width, sticky under the header: grey backdrop + a white
**A4 paper card** (210mm aspect, `transform: scale` to fit width) holding a
**sandboxed `srcdoc` iframe** of the real template HTML.
- **Narrow (<800px, owner's phone):** single-column form; a pinned bottom bar always
shows Payable + GST + a **Preview** button that opens the paper as a full-screen sheet
(fixed-dimension wrapper + `transform: scale` iOS Safari expands bare iframes).
The totals bar never disappears at any width.
**Update mechanics:**
- A shared client helper `buildDraftBody()` (extracted from the save path) builds the
request body, so preview and save POST **byte-identical** bodies.
- **Immediate** refire (0ms) on commit-shaped events (client pick, doctype radio,
module/kind/pack select, add/remove line, field blur); **400ms trailing debounce** on
keystrokes (qty/unit/description/content/terms).
- One in-flight request max: `AbortController` cancels superseded calls; a sequence
guard drops stale responses; skip the POST entirely when the serialized body is
unchanged.
- **Never blank the pane:** keep the previous paper visible, show a 2px shimmer + dim
totals to 60% while in flight, then double-buffer swap (write into a hidden stacked
iframe, restore `scrollTop`, 120ms cross-fade) no white flash, no scroll jump.
Changed totals cells pulse 250ms (causeeffect made visible).
**Empty/error states (client):** zero valid lines real letterhead, empty table, 0
(never a mock); no client picked muted "— pick a client —" block, totals shown with a
"split pending client" tag (18% total is invariant) that clears with a pulse on pick;
`warnings[]` price gaps inline "No price enter Unit ₹" + amber rate-cell flag;
server unreachable/400 keep last good paper, slim "Preview paused retrying" banner
with as-of timestamp, totals greyed (never present stale numbers as fresh);
`company.state_code` unset actionable link to Settings Company.
**Two honest screen/print deltas:** `@page` 14mm margins the card draws 14mm padding
with a faint toggleable "Show print area" guide; pagination is puppeteer's an
approximate dashed page-break line at ~269mm content height plus an on-demand "Exact PDF"
button (real puppeteer render) for the rare must-be-one-page quote. The ~5-line
`@media screen` paper styles live **inside templates.ts** (with a "page.pdf renders
print media" comment), not injected client-side, so the paper itself is template-owned;
only preview *chrome* (guides, pulses, tags) is client overlay.
## 4. Editable template data (founder addition, 2026-07-13)
**All the text the letterhead prints must be DB-editable from a page one fixed
(well-designed) layout for now, not a visual layout editor** (that is the store
product's "Print Templates" surface, doc 09, out of scope). GST split logic
(CGST/SGST vs IGST) is already derived from `company.state_code` vs the client state and
stays automatic; this is about the *displayed* data and boilerplate text.
**Model:** everything is a setting row (the existing `setting` table + `getSetting`/
`setSetting`), so a change is a DB write that the next render (and the live preview)
reflects immediately no release. Existing `company.*` (name, address, gstin,
state_code, phone, email, bank) already work; **add** these keys:
- `template.terms` (multi-line terms & conditions)
- `template.declaration` (GST declaration line, e.g. "We declare that this invoice shows
the actual price of the goods/services described and that all particulars are true.")
- `template.jurisdiction` (e.g. "Subject to <city> jurisdiction")
- `template.footer_note` (thank-you / support line)
- `template.signatory_label` (e.g. "For <Company> — Authorised Signatory")
- `template.logo` (image as a data: URI stored in the setting — no file store needed;
size-capped on upload)
- `template.title_<doctype>` (optional per-type title override; falls back to the
built-in TAX INVOICE / QUOTATION / PROFORMA INVOICE / CREDIT NOTE / RECEIPT)
`templates.ts` renders these where it currently hardcodes/omits them. All are optional —
absent keys render the sensible built-in default, so nothing breaks pre-configuration.
**Edit surface:** a **Document Template** page in hq-web (owner-gated, audited), grouping
the company block + the new `template.*` fields, with the **live letterhead preview beside
the form** (same preview mechanism as the composer, sample doc) so every edit is seen on
the page as it prints. This supersedes the smaller "company profile editor preview" idea
below — the Company Profile page's letterhead fields fold into this one Document Template
page. Routes: extend `GET/PUT /api/settings/company` to a `GET/PUT /api/settings/template`
covering both groups (owner-gated, each field audited via `setSetting`), plus a small
`POST /api/settings/template/logo` that validates + stores the data URI.
## 5. Two more cheap sibling surfaces (same round)
Both reuse the one preview route/mechanism — each is S effort:
1. **Module quote-content editor** (Modules page) — live render of how the "what's
included" bullets actually print (the 10px grey `ul.line-content`), using a one-line
sample doc. Today they're written blind.
2. **Reminder email preview** (manual queue / send) — the exact outgoing email (subject +
body from the pure `reminderEmail(kind, ctx)`), via `GET /api/reminders/:id/preview`.
Staff currently fire client-facing dunning mail unseen.
## 6. Deliberately NOT this round
Credit-note & receipt preview (mechanically derived, low value now); **store-product
surfaces** — documented as handoff to that workstream, not built here: Print Templates
visual editor (doc 09 explicitly specs it), Label Printing (already has a popup preview),
Price-list bulk revise, Schemes editor, WhatsApp/message-catalog text, and the **POS
printer-settings receipt preview** (high value — `packages/printing` already renders
42/32-col receipts as pure functions, so it's cheap when that team wants it).
## 7. Files
- `apps/hq/src/repos-documents.ts` — extract `prepareDraft`; `createDraft` calls it.
- `apps/hq/src/api.ts``POST /api/documents/preview`, `GET /api/reminders/:id/preview`.
- `apps/hq/src/templates.ts` — add `@media screen` paper styles + sample-doc helpers for
company/quote-content previews.
- `apps/hq-web/src/pages/NewDocument.tsx` — split layout, totals strip, LivePreview.
- `apps/hq-web/src/components/LivePreview.tsx` — new: debounced fetch + double-buffer
iframe (kept modest per YAGNI — a composer helper, generalized only if a 2nd/3rd
surface proves the shape).
- `apps/hq-web/src/pages/DocumentTemplate.tsx` — new owner-gated page: company block +
`template.*` fields + logo upload, live letterhead preview beside the form (supersedes
the CompanyProfile letterhead preview; keep CompanyProfile or fold it in).
- `apps/hq/src/api.ts``GET/PUT /api/settings/template`, `POST /api/settings/template/logo`.
- `apps/hq-web/src/pages/Modules.tsx`, dashboard reminder queue — wire the sibling previews.
- Tests: string-identity golden + prepareDraft/createDraft parity + preview-permissive /
save-strict divergence + route auth.
## 8. Success criteria
Staff draft a quotation and watch the letterhead build itself in under ~400ms per edit,
with GST (incl. IGST split for out-of-state) always shown by the server; the on-screen
paper is byte-identical to the PDF that later prints; no blank flashes; save remains
strict so bad drafts never persist; the same mechanism previews the company letterhead,
quote-content bullets, and reminder emails.