startServer picks Postgres when DATABASE_URL is set, SQLite file otherwise.
New pg-engine.test.ts (opt-in via HQ_PG_TEST_URL, wipes its target schema):
migrations from zero, invoice issue with series numbering + due-date stamp,
paisa-exact payment to paid, exactly-once reminder upsert via ON CONFLICT,
savepoint-nested rollback (incl. the mixed dbPassword+patch write), and a
full HTTP boot smoke (health/login/clients) on the postgres engine.
Verified locally against PostgreSQL 17: 5/5. Default suite (sqlite)
346/346, typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P1: every repo, route, scheduler path and test now speaks one async DB
interface (all/get/run/exec/transaction). better-sqlite3 stays underneath
for dev/tests via SqliteDb (statement cache, manual BEGIN/SAVEPOINT
nesting); openDb stays sync so fixtures remain one-liners. Zero behavior
change: 346/346 tests, typecheck clean.
P2: migrations-pg.ts — numbered Postgres migration set (001-init: bigint
paise, integer flags, TEXT ISO dates, TEXT JSON; reminder CHECK born in
final widened form) + schema_migrations runner.
P3: db-pg.ts — pg Pool behind the same interface: ?->$n rewrite outside
string literals, transactions pinned to one pooled client via
AsyncLocalStorage with savepoint nesting, int8/numeric parsed to JS
numbers so paise math is identical on both engines.
Also: reminder upsert INSERT OR IGNORE -> ON CONFLICT DO NOTHING
(portable, same changes-count semantics).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Locked decisions: pg driver behind an async Db interface with dual
engines (sqlite for fast tests, pg for prod), bigint paise, TEXT ISO
dates, jsonb payloads, numbered migrations, ?->$n in the adapter,
DATABASE_URL selects the engine. Build order: async refactor first
(zero behavior change), then schema, adapter, dual-engine suite, smoke.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
27 adversarially-confirmed findings from the multi-agent whole-branch review (2 deferred).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
STATUS: count + review summary, D18 sentence in the intro, honest migrate()
column list, routes table gains /reminders /documents /profile /import and the
collapse/user-menu shell description. README + CLAUDE: 345. D18 record in
06-DECISIONS gains the review outcome. Spec banner notes the post-review
deltas (CSV support columns, LIKE filters).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MAJOR
- Documents register: Convert & send no longer bubbles into row navigation
(stopPropagation guard on the actions cell)
- Pipeline rows: convert_invoice now uses the same confirmed convert-and-send
as the Documents page (was the old plain draft convert)
- APEX importer carries anydesk/os/district/sector: stg_client columns
(+ migrate guard), optional CSV headers staged, commit INSERT maps them
- Clients page: district/sector filter inputs wired to the server filters
(server side widened to LIKE for narrow-as-you-type)
MINOR
- WhatsApp/secretary typed contact roles: datalist on the contact Role input,
labeled rows (with Copy) on the Client 360 support card
- Dashboard overdue + dues-aging now anchor on COALESCE(due_date, doc_date) —
an unpaid invoice not yet due no longer shows as overdue (test added)
- PATCH /clients/:id is atomic: dbPassword + field patch in one transaction
- Audit same-txn: setClientDbPassword, stageCsv, and the import stage/commit
route audits all wrapped in db.transaction
Suite: 345 tests / 66 files, typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- POST /import/stage (CSV text), GET /import/status (report + first-50 problem
rows with honest totals), POST /import/commit — all owner-only, audited;
commit still refuses while ANY staged row carries problems (existing guard)
- /import page (ADMIN nav, owner-only): file pickers → problem tables →
verification stats (incl. invoice-value cross-check) → confirm-gated Commit;
re-stage freely, series seeding message surfaced
- 2 route tests (403s for staff; bad→refuse→clean→commit round-trip incl.
series seeding past legacy numbers); suite 344 green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- sidebar collapses to an icon rail (footer toggle, localStorage-persisted;
nav tooltips via title; <900px hamburger untouched)
- dashboardView gains mine-scoping: staff are ALWAYS scoped to their book
(clients they own, docs/payments they created, their follow-ups, their queue);
owner/manager get a Mine/Everyone toggle (?mine=1)
- 1 test; suite green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- additive client columns; toClient never maps db_password_enc (payloads carry
only hasDbPassword); listClients gains district/sector filters
- setClientDbPassword: AES-256-GCM via HQ_SECRET_KEY, refuses loudly without a
key; revealClientDbPassword decrypts + writes one audit row per reveal
- PATCH /clients/🆔 dbPassword is owner/manager-gated inside the same route;
POST /clients/:id/reveal-db-password (owner/manager)
- client dialog gains district/sector/AnyDesk/OS; Client 360 gains the Support
card (copy AnyDesk, •••• + Reveal/Set for managerial)
- 5 tests; suite green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- QUOTATION renders one titled section per line: module name, its what's-
included block, SAC + qty × rate + amount; sections never split mid-page and
each module after the first opens a fresh page in print
- summary line up top ('This proposal covers: …'); totals/terms/signature
unchanged; INVOICE/PROFORMA/CN/RECEIPT keep the compact grid (regression-
asserted); preview-fidelity golden test untouched and green
- 3 tests; suite green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- listClientsByModule rows carry cmId; ClientModulePatch gains kind/edition
(kind validated against module.allowedKinds — same audited write path as
Client 360, so the two views can never disagree)
- Modules roster: inline edit (kind/edition/renewal/status), ✕ unassign with
confirm (deactivates the link, totals shrink), Assign client… type-ahead
- Documents register: issued proformas get a Convert & send quick action
(same convert-and-send endpoint + confirm as the document view)
- 2 tests; suite 333 green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The chip was a bare link to /profile with a separate logout icon; the spec
(§5) calls for a small menu. Chip is now a button toggling a two-item menu
(Profile / Logout) that closes on outside click, Escape, and route change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- GET/PATCH /me (self-scoped whitelist — role/email untouchable) + POST /me/password
- changeOwnPassword: current password required; every OTHER session dies in the
same txn, the changing session survives; audited without hashes
- staff_user.phone/title additive columns, surfaced on Employees (columns + edit
dialog) and the new Profile page; user chip in the shell opens /profile
- 7 tests; suite 331/331 across 64 files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- /reminders page: Queued/Sent/Failed/Dismissed chips mapping to ?status=,
managerial-only Mine/All toggle via ?owner= (staff are server-forced to
their own rows), kind/client/doc/due/status table with Send/Preview/
Dismiss on open rows, honest server-driven pager (rule 8).
- Extract the Dashboard queue actions into shared
components/ReminderQueue.tsx (QueueActions, RULE_LABEL, reminderTone)
with a re-entry guard on Send/Dismiss; Dashboard queue card links
"View all" to /reminders.
- api.ts: getReminders now takes { status, owner, page, pageSize } and
returns the full QueueItem rows + page metadata; login stores the
staff id (hq.id) so Mine can narrow by owner.
- Nav item under WORK + route; no backend changes. A contract test pins
the sent/dismissed chips, ?owner= narrowing and cross-chip staff scope
at HTTP level (apps/hq/test/reminders-page.test.ts).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
listDocuments now returns { documents, total, page, pageSize } (default 50,
cap 200, newest first) with the client name joined server-side — no N+1.
GET /documents passes page/pageSize through; type/status/clientId filters
compose against the same WHERE for both count and page (rule 8: no silent
caps). clientLedger walks the paginated repo in 200-row batches so the
ledger still surfaces every document. New /documents screen: type chips,
status filter, mono doc numbers, amount column, pager honouring the server
total; rows open /documents/:id. Nav entry under WORK (exact-match so New
Document keeps its own highlight).
Note: the GET /documents route hunk itself landed via 8861b46, swept into
a concurrent commit from the same working tree; this commit carries the
repo/ledger/web halves and the tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the WS-A coverage gap: due-dates.test.ts proved the manual-issue and
convert paths but relied on code inspection for recurring generation. A manual
recurring plan now runs through runDailyScan and the generated invoice's
due_date is asserted as doc_date + seeded 15-day terms. Suite 310/310.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- document.due_date additive column; billing.payment_terms_days seeded (15)
- issueDocument stamps doc_date+terms on INVOICE when absent — covers manual
issue, convert-and-send and recurring generation in one place
- drafts accept dueDate (INVOICE only, validated); composer date field
auto-filled from GET /settings/billing; invoice PDF prints Due Date
- overdue scan + reminder anchor on COALESCE(due_date, doc_date); email says
'was due on X and is now N day(s) overdue' when a due date exists
- legacy fixtures pinned to due_date=NULL (sanctioned: they test the doc_date
anchor); 9 new tests; suite 309/309 across 60 files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ConfirmDialog: a rejected onConfirm no longer closes the dialog as if it
succeeded; the rejection is handled (no unhandledrejection), busy clears,
and the error message is shown inline (role=alert)
- ConfirmDialog: Cancel / scrim / Escape / X are gated while busy, so the
dialog cannot be dismissed (and onClose cannot double-fire) mid-flight
- Dialog: Escape + Tab trap moved to a document-level keydown listener so
they still work when focus escapes the card (e.g. lands on <body>)
- Dialog: focus restore now runs via effect cleanup, covering the
{show && <Dialog/>} unmount-while-open pattern
- Dialog: initial focus skips disabled controls
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- listClientsByModule: reverse lookup over active links, ordered by client name,
paginated with a true total; per-row dated price via priceOn + next renewal;
footer revenue total spans ALL links (active=0 excluded by design, stated)
- GET /modules/:id/clients (paginated) + /clients.csv (streams every row, no cap)
- POST /modules/:id/notify (owner/manager): resolves the full roster first, sends
via the Gmail path, one email_log + one audit row per attempted recipient,
names every unreachable client in the response (warn, never truncate)
- Modules page roster panel: table + revenue footer + Notify-all composer + CSV
- 8 tests; suite green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- invoice_overdue scan: monthly bucket → d7/d15/d30 milestones resolved via
resolveSchedule (dated config, rule 3); each fires once via the idempotency key
- F7 catch-up guard: scan gaps and the monthly→dN cutover enqueue only the
highest crossed milestone, never the whole ladder
- ctx.daysOverdue populated in reminderContext (A7: anchored on doc_date) so the
'N day(s) past due' template text renders; injectable today for determinism
- daysBetweenIso helper; 5 tests; suite green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- POST /documents/:id/convert-and-send: guards before any write (proforma-only,
Gmail alive, recipient resolved), ONE txn convert+issue, email strictly after
commit — failed send never rolls back the issued invoice (warning instead)
- supersedeProforma: cancel (number stays consumed) + fresh linked PROFORMA
draft carrying the payload verbatim; INVOICE hard-rejected to credit-note path
- POST /documents/:id/supersede route; DocumentView gains both buttons
- 10 tests; suite 280 green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review findings on the quote-to-close slice, each verified against the code:
- convertDocument: ANY conversion now flips the source to 'invoiced' (previously
only ->INVOICE), so a QT->PI quote leaves the daily scan's 'sent' set and the
pipeline's Quoted/Waiting — the chase never resumes after convert (spec 7 stop
conditions). Scan additionally excludes quotes with a live forward child
(covers rows converted before this fix) and quotes of lost clients.
- convertDocument rule-4 guard (spec 8 F4): a live non-cancelled forward child
blocks re-convert — one sale, one legal document; cancelling the child
re-opens the path. Rule-2 fix (spec 8 F3): ->INVOICE recomputes the carried
lines through computeBill on the invoice's own doc_date (mirrors
createCreditNote); QT->PI stays copy-only (both non-legal).
- cancelDocument dismisses a cancelled quotation's open follow-up nudges in the
same transaction, per-row audited — dead paper is not chased.
- GET /reminders?status= no longer bypasses the owner scope and pagination: the
status param narrows the same scoped listQueue view. Web badge (Layout) now
uses the honest scoped total instead of an unscoped flat list length.
- listQueue/queueCounts staff scope keeps doc-less reminders (renewal_due,
amc_expiring, follow_up, email_bounced) visible — they have no derived owner
and are shared work; previously they vanished for every staff viewer.
- sendReminder resolves the recipient BEFORE minting a public share (a send
that can never succeed leaves no live artifact) and refuses quote_followup
sends loudly when share.base_url is unset instead of emailing a dead relative
link. The scheduler auto-drain parks a hard-failing send as 'failed' with its
reason instead of crashing the scan.
- reminderContext takes an injectable today (threaded from deps.now in the send
path) so dated schedule text resolves deterministically.
- Pipeline: quote-less lost clients (client.status=lost) now appear under
filter=lost (spec 9); Dashboard queue table marks first-page truncation
honestly (rule 8).
Typecheck clean; 270 tests green (255 before, +15 covering each fix).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
E2E walked in the real app: login split panel, shell (groups/badge/health),
Ctrl+K palette with live client search, dashboard, clients chips, Client 360
(tabs + pulse ribbon click-through), document view + PDF, reports chips,
dark mode, 420px drawer. Verification session minted directly in the dev DB
and deleted after; servers stopped; stray apps/hq/data DB removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>