Commit Graph

194 Commits (e5ed3517dce05f03667ed00bef7466ac704e8ed1)

Author SHA1 Message Date
Thomas Joise 7f6e2ce904 feat(d27): #5 owner MIS cockpit
repos-mis misCockpit(today): this-month vs last for billed (issued invoices) +
collections (payments), total outstanding (dues-aging), open pipeline value,
renewals due in 30d, open tickets, reminder queue — composed from the existing
repo aggregates so figures reconcile with Reports/Dashboard. Owner-only GET
/reports/mis. New /mis 'Cockpit' page: KPI tiles with MoM deltas + click-through
to the source pages. Money in tests. typecheck + web build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 days ago
Thomas Joise 419dfebbaa feat(d27): #1 Renewals page (command center UI)
New WORK page /renewals: 30/60/90-day window chips, table of due module +
AMC renewals (client, type badge, due date, days-until with red/amber tint,
amount, total), one-click 'Renewal quote' on module rows (-> proforma draft)
and 'Open AMC' on AMC rows. api: getRenewals + generateModuleRenewalQuote.
typecheck + web build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 days ago
Thomas Joise 02d6f13c95 feat(d27): #1 renewals command center + one-click module renewal quote (backend)
renewalsDue(from,to,today): unified list of module (client_module.next_renewal,
priced by the dated price book) + AMC (period_to, fixed amount) renewals with
amount + days-until, earliest-first. generateModuleRenewalQuote: one-click
proforma carrying the client's module kind/edition forward at the price-book
rate (AMC keeps its existing renewal-invoice). Routes GET /renewals,
POST /client-modules/:id/renewal-quote. Renewal reminders already fire from the
scheduler (renewal_due/amc_expiring) — this is the worklist + action. Tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 days ago
Thomas Joise 18e47eb448 feat(d26): functional-feature UIs — GST report, doc search, statement, bulk ops
- Reports: GST summary tab (FY month range, per-month CGST/SGST/IGST + total,
  emphasized TOTAL row).
- Documents: debounced free-text search feeding q.
- Client 360: printable account statement (Dr/Cr ledger + running/closing
  balance + advance, @media print isolation); buildStatement + 4 tests.
- Clients: owner-only bulk-edit selection -> set a field across many (POST
  /clients/bulk). Onboarding board: owner-only bulk 'mark done' on the pending
  drill-down (POST /projects/bulk-milestone). Both confirm + toast + refresh.
- api.ts: getGstSummary, bulkUpdateClients, bulkSetMilestone, documents q.
typecheck + web build clean, web tests 8/8, full suite 397 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 days ago
Thomas Joise 009179d8df feat(d26): functional gaps — GST summary, doc search, bulk ops (backend)
- gstSummary(range): per-month GST filing summary over issued INVOICE (adds) +
  CREDIT_NOTE (subtracts, s.34), CGST/SGST/IGST split from stored paise, with a
  grand total. Route GET /reports/gst-summary. Money locked in tests.
- listDocuments gains free-text q (doc number OR client name, case-insensitive
  both engines); GET /documents ?q=.
- bulkUpdateClients (owner) + bulkSetMilestone (owner) for cleanup at scale —
  POST /clients/bulk, POST /projects/bulk-milestone; each client keeps its own
  audit; milestone bulk writes one summary audit. Suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 days ago
Thomas Joise bbe125892d fix(d26): accent picker off top bar + mobile UX pass + responsive billing
- The 7-swatch accent selector is removed from the top bar (it now lives only
  in Settings > Appearance); the bar keeps a single compact light/dark ModeToggle.
  This declutters the header and fixes most of the mobile congestion.
- Mobile pass (<=640px): search takes the bar, Ctrl-K hint + Gmail pill hidden,
  avatar-only user chip; pages get real padding + bottom room; toolbars/chips
  wrap with air; inputs full-width & 42px tall; stats 2-up; tables padded.
- Billing composer: client search + line-item cards go full-width and stack on
  phones (composer-search / composer-line classes) so no container is undersized.
typecheck + web build clean, web tests 4/4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 9b920e50f9 feat(d25): bright doc type/status colours + sectioned client details + username UI
- Badge extended with green/blue/violet/gold/red tones (light+dark). Document
  TYPE badges: Quotation=gold, Proforma=violet, Invoice=teal, Credit note=red,
  Receipt=green (Documents, DocumentView, Client 360). STATUS brightened:
  paid/accepted=green, sent=blue, part-paid=amber, draft=grey, lost/cancelled=red.
- Client 360: new sectioned Details card (identity, full address, contacts
  labeled Office/Secretary/WhatsApp with mailto:/tel: links).
- Employees create form gains a required Username (login id) field + table
  column; Login field relabeled 'Username'.
typecheck + web build clean, web tests 4/4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 8092c78264 feat(d25): login by username; email demoted to a plain user field
- staff_user gains a unique username (both engines: migrate + pg 009); older
  rows and the seed owner backfill username=email so every existing login keeps
  working. login() matches username OR email (legacy fallback).
- createStaff/createEmployee take an optional username (defaults to email);
  Employee/API expose it; POST /employees accepts username. Email stays a
  required contact field on the table (making it optional is a later table
  rebuild). Full backend suite 346 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 1727918173 feat(deploy): hardcode DB connection + owner email (no .env), update docs
Team decision: production DB connection and first-boot owner email are hardcoded in the app instead of a .env file, matching the box's other apps. Gated to NODE_ENV=production so dev/tests keep using SQLite (389 tests green). Committed value is the CHANGE_ME_PASSWORD placeholder; the real password is set only on the box, never pushed. server.ts: HARDCODED_DATABASE_URL. seed.ts: OWNER_EMAIL default. docker-compose.yml: drop env_file. docs: PATHS-AND-NAMES.md + .env.example updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 days ago
Thomas Joise 10b849a990 feat(d25): module display order (sort_order) + username login
- module.sort_order (both engines, migrate + pg 008); listModules orders by
  sort_order then code; createModule/updateModule + ModulePatch carry it, so
  the app-wide module order is owner-editable config (default 100 = trails).
- Login accepts a username: field is type=text (autocomplete username),
  relabeled 'Email or username' — a plain 'admin' login now works.
Module + web tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise ce6b564e2b feat(pwa/d25): installable PWA + phone polish
- Web manifest (standalone, id/scope/start_url=/, teal theme) + icon set
  generated from the HQ mark: pwa-192, pwa-512, maskable-512 (full-bleed
  safe-zone), apple-touch-icon 180, icon.svg. sharp added as a devDep for
  generation.
- Service worker (public/sw.js): installable + fast repeat loads without
  faking offline. NEVER touches /api or /share (always live); navigations
  network-first with an app-shell fallback; hashed assets cache-first. Registered
  from main.tsx in production builds only (skips Vite dev/HMR).
- index.html: manifest link, apple-touch-icon, apple/mobile-web-app meta,
  viewport-fit=cover; vite-env.d.ts adds vite/client types.
- Phone polish (app.css): safe-area insets, 16px inputs (kills iOS focus-zoom),
  40px tap targets, bottom-sheet dialogs, 2-up stat cards — on top of the existing
  <900px drawer + scrolling tables.
typecheck + web build clean, web tests 4/4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 70234161dc fix(d24): pg allocation row-lock + index ordering; docs (D24 record, 389 tests)
- recordPayment locks the target invoice row (SELECT ... FOR UPDATE, Postgres
  only — SQLite is serialized by the connection mutex) before reading
  outstanding, so concurrent payments cannot over-allocate on prod.
- INDEX_DDL now runs AFTER migrate() in openDb, so indexes referencing
  migrate-added columns (owner_id, lockout cols) build on older DBs.
- D24 remediation recorded in 06-DECISIONS; test counts 377 -> 389.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 9c59430c31 perf/fix(d24): DB indexes (both engines) + case-insensitive client search
Red-team backend-quality highs/mediums:
- Added 30 indexes on the foreign-key / status / date / renewal columns the
  lists, reports, joins and scans read — zero existed, so every query full-scanned
  (tolerable on dev SQLite, a real problem on production Postgres at 300 clients /
  thousands of tickets & documents). Same DDL on both engines: INDEX_DDL in the
  SQLite SCHEMA + pg migration 007 (CREATE INDEX IF NOT EXISTS, idempotent).
- Client search + district/sector filters now LOWER(...) LIKE LOWER(?): bare LIKE
  is case-insensitive on SQLite but case-SENSITIVE on Postgres, so search silently
  broke on the prod engine. Test locks case-insensitivity.
Verified: pg migrations 006+007 apply cleanly on local Postgres; suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 35a26de442 fix(review): frontend quality pass — a11y, UX safety, error handling (medium/low)
Red-team frontend clusters (a11y + UX + code quality), three coordinated passes:

Accessibility:
- DataTable clickable rows keyboard-operable (role=button, tabindex, Enter/Space).
- <main> landmark + skip link; icon-only buttons get aria-labels; inputs relying
  on placeholder alone get aria-labels; AWS-ranking link is a real <button>;
  Login fields wrapped in a <form> so Enter submits from either field; toasts
  role=alert for errors (urgency not by color alone).

UX safety:
- 'Notify all' and reminder 'Send' now behind ConfirmDialogs naming the count/
  client; document action bar disables while an action is in flight (no
  double-submit dupes); SendDialog surfaces why a send is blocked instead of
  failing silently; client search + ticket search debounced (~300ms); inline
  Client-360 edit failures surface via toast, not an off-screen banner; LivePreview
  gets a real Retry; destructive client-status transitions confirm first.

Code quality:
- ErrorBoundary wraps the app (a render throw no longer white-screens).
- useData clears stale data on dep change (no record flashing under a new id).
- Shared ErrorBoundary/SecretField/Pager components extracted; Pager adopted in
  Documents; write-call api types tightened off Record<string,unknown>.

typecheck clean (root + both workspaces), web build clean, web tests 4/4, full
suite 388 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 4cf513d528 fix(sec/d24): PDF render hardening — strict CSP + request interception
Red-team XSS/rendering finding (public-share renders our HTML in Chromium):
- Both letterhead templates get a strict CSP meta (default-src none; img-src
  data:; style-src unsafe-inline; base-uri/form-action none) — permits the
  inline CSS + data: logo the templates actually use, blocks all script
  execution and every outbound/file fetch. Identical in preview + PDF, so the
  byte-for-byte preview-fidelity golden test still passes.
- renderPdf aborts every non-data: network request at the browser layer.
Together an esc() miss becomes a harmless rendering glitch instead of an
SSRF/exfiltration/RCE-adjacent vector. Template + fidelity tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise aaad1e7a3a fix(sec/d24): concurrency + workflow isolation
Red-team correctness findings:
- SqliteDb.transaction now serializes top-level transactions with an async
  mutex and tracks re-entrancy via AsyncLocalStorage (nested calls -> savepoints,
  no deadlock). The old shared txnDepth counter let concurrent async callers
  interleave BEGIN/COMMIT on the one connection -> silent data loss; a 50-way
  concurrent read-modify-write test now lands exactly.
- generateRecurring wraps each plan in try/catch: one misconfigured plan is
  parked (audited 'generate_failed') and the scan continues, instead of aborting
  all remaining generation and the entire auto-send drain, every scan.
- POST /modules/:id/notify broadcast IIFE gets a trailing catch so a mid-loop
  throw answers the request instead of hanging it.
Full suite 388 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise f812555528 fix(sec/d24): money-integrity guards — CN cap, atomic doc-no, qty, TDS, markStatus
Red-team money findings:
- createCreditNote caps the CN to the invoice's remaining creditable value
  (invoice payable − already-credited by non-cancelled CNs), read+insert in
  one txn. Blocks a CN exceeding its invoice / unlimited CNs flipping it 'paid'.
- nextDocNo allocates via one atomic UPDATE ... RETURNING (row lock) instead of
  read-then-bump — no duplicate/burned numbers under concurrent issue (Postgres).
- markStatus rejects accepted/lost on non-QUOTATION docs — an issued invoice can
  no longer be hidden from overdue chasing via a quote-only transition.
- computeBill bounds qty magnitude (keeps fractional for weighed Store goods);
  HQ buildLines additionally requires whole-unit integer qty (SMS/module seats).
- recordPayment rejects TDS greater than the cash received — no fabricated-TDS
  'settlement' with no money.
money-guards test locks all four. Suite money paths green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 54cde7961b fix(sec/d24): gate destructive/sensitive ops to managerial
Red-team authorization findings:
- New requireManagerial middleware. Applied to POST /documents/:id/cancel,
  /credit-note (financially destructive — reverse money / consume a number)
  and /documents/:id/share (mints an UNAUTHENTICATED public PDF link — the
  IDOR exfiltration high). Any signed-in staff could previously do all three.
- PATCH /interactions/:id now 403s unless the viewer is the interaction's
  author or managerial — staff can no longer overwrite another employee's
  logged call/visit.
New authz-gates test locks both. Note (deliberate): general client read/edit
stays open to staff — a shared support-team book is the intended model; the
genuinely sensitive credential reveals are already managerial + rate-limited.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 10022965d1 fix(sec/d24): auth hardening — login lockout, rate limits, constant-time, trust proxy
Red-team highs/mediums (auth surface):
- staff_user gains failed_count/locked_until; login() locks an account for 15min
  after 8 consecutive failures, clears the counter on success (both engines).
- login() is now constant-time: a missing email still spends one scrypt verify
  against a fixed dummy hash — no timing/enumeration oracle. Returns a typed
  {ok:false,reason} instead of null.
- POST /auth/login gets per-IP (20/min) AND per-email (8/15min) fixed-window
  rate limiting layered over the lockout; 429 on trip.
- Credential reveal routes (db-password, module password, module secret) get a
  per-user reveal limiter (20/min) so one insider cannot bulk-decrypt everything;
  added missing getClientModule 404 guards.
- app.set(trust proxy,1) so req.ip is the real client behind nginx/Caddy and the
  per-IP limiters actually bucket per client.
- makeRateLimiter extracted to rate-limit.ts (shared, no server<->api cycle).
Tests: lockout, counter-reset, unknown-email, and existing auth all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise c9649c795b chore: commit pending working-tree improvements (env doc + headless-Chromium container flags)
Two small improvements that were uncommitted in the working tree:
- .env.example: document HQ_OWNER_EMAIL; refresh the Postgres section (D19 delivered)
- pdf.ts: launch Chromium with --no-sandbox --disable-dev-shm-usage so it runs in a
  container; inert on a dev machine, and we only ever render our own self-contained HTML

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise c5480cf8cd feat(d22): P2 — onboarding checklist per project + Projects (Onboarding) board
Per client-module in the Modules tab: a MilestoneChecklist (tick with
date, + Add step), reloading in place. New Onboarding work page: Stats
header + per-milestone board (done/total, pending badge, warn/ok tint)
with click-through to the pending projects for each milestone (client
links to Client 360). Nav + /projects route. typecheck + web build clean,
web tests 4/4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 3c2e51ae6c feat(d22): P1 — onboarding milestone tracker (backend) + monotonic uuidv7
project_milestone: each client_module (a project) carries a checklist
seeded from the dated project.milestone_template setting (advance paid /
setup / installed / go-live / balance / AMC — owner-editable). Ticking
stamps the date; audited. ensureProjectMilestones is idempotent and
back-fills imported projects. Cross-project board (per-milestone pending
counts) + drill-down (who's pending go-live). Routes: GET/POST
/client-modules/:id/milestones, GET /projects/board, /projects/pending/:key.
assignModule seeds the checklist for new projects.

Also fixes a latent flaky-ordering bug: uuidv7() now bumps monotonically
within a process, so ORDER BY id is genuine creation order (the guarantee
pagination + the sync cursor already assume) — sub-millisecond ties no
longer fall back to the random tail. Suite 377 green, stable across runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise e91e1741b8 feat(d23): SMS volume-band billing (dated rate card, money locked)
usage_rate_band: a dated per-module rate card of volume bands (generic;
SMS packs the first user). resolveUsageRate picks the band by count
(highest min_qty <= count); below the lowest band = under the minimum
order. buildLines resolves price as: explicit per-line override >
tiered rate card > dated price book — so an SMS line prices by count
automatically, a save under 50,000 is refused, preview warns. Rates are
integer paise (0.40=40p), a new card is a new dated set of rows so issued
invoices keep their rate. Owner routes GET/POST /modules/:id/rate-card.
Tests lock the founder card to the paisa: 50k→₹23,600, 100k→₹43,660,
200k→₹80,240, 300k→₹1,06,200 incl 18% GST. Suite 372 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 51023f9ec4 feat(d21): P4 — importer seeds module field_spec + writes field_values
ensureModule seeds each module field_spec (SMS/RTGS/MOBILEAPP declared
fields; ATM/WHATSAPP empty — their one datum stays in the D20 provider
column), back-filling only when a module spec is still empty so an
owner-customized spec is never clobbered. SMS/RTGS/MobileApp service data
now writes into client_module.field_values keyed to the spec; provider/
username/portal-password keep the D20 columns; unkeyed values still land in
details so nothing is lost. Tests assert seeded specs + keyed values; suite
363 green. Real dry run unchanged: 273 clients, 150 docs, 167 links.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 892ae9e96e feat(d21): P3 — module Fields editor + self-rendering service form
Modules page: owner FieldSpecEditor where a module defines its own fields
(label, snake_case key, type, select options, required, reorder) → saved via
patchModule({ fieldSpec }); staff read-only. Client 360 service card: below
the D20 login block, a dynamic form built from the owning module fieldSpec —
one typed input per field (text/number/date/boolean/select), required honored,
non-secret edits collected into one PUT; secret fields as ••••/Reveal/Copy +
Set/Update mirroring the portal-password pattern (managerial, audited).
Empty fieldSpec renders nothing extra. Additive — D20 UI intact. typecheck +
web build clean, web tests 4/4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 427ac31c57 feat(d21): P1+P2 — module-defined field schema (backend)
module.field_spec (FieldDef[]: key/label/type/options/required) declares a
module's own operational fields, validated on create/update. client_module
gains field_values (JSON map of non-secret values, type-checked against the
owning module's spec) + secrets_enc (one AES-GCM blob over a {key:plaintext}
map; the clear key-list prefix lets the UI show which secrets are set without
decrypting). Routes: PUT /client-modules/:id/fields (staff), POST
/client-modules/:id/secrets + /reveal-secret (managerial, audited, plaintext
never logged). Additive over D20 — provider/username/password stay. Both
engines (SCHEMA + migrate guards + pg 003). 11 new tests; module suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 1328a9e898 feat(d20): importer v2 rework from the 46-agent real-data audit
Bills are LINE ITEMS not documents: group by INVOICE_NO across three GST
eras (GST pseudo-line / 2026 columns-only / pre-GST zero-tax). 46 raw
ticket statuses -> 5 via ordered pattern map; OLD_ID re-key chains
collapse so superseded rows do not double-import. YES/NO flags respect
real vocabulary (GENERAL ENQUIRY != yes); ATM cell value becomes the
module provider. SID 329 test client + orphan refs skip (counted notes,
distinct from commit-blocking problems). District normalize, NO DATA
scrub, payment normalize, richer-row merge for duplicate service rows,
shared-number-space series seed. Dry run over the real export: 273
clients, 150 documents = Rs 33,16,800.24, 72 payments, 1,826 tickets,
667 interactions, INVOICE seed 10252 (matches APEX next_val). Suite 358
green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise ea958ace40 fix(d20): ticket module suggestions read the live catalog, not a constant
A module created tomorrow appears in the ticket dialog with zero code
changes; the five APEX codes remain only as a loading fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise b301272464 feat(d20): P4 — full-APEX importer v2 (9 files, stage/commit, CLI)
parseWorkbookCsvs + stageApexFull (pure validation pass, zero writes) +
commitApexFull (one transaction): clients w/ encrypted DB passwords +
secretary contacts, YES-flags -> live client_module rows for the 5
modules, branches, bill_filed -> QT/PI/INV history with paisa-exact
CGST/SGST split + payment/allocation rows + series seed, 4.5k tickets
with status/date mapping, SMS/RTGS service data -> client_module
provider/username/password_enc/details, project lists -> interaction
notes with tolerant APEX date parsing. Plaintext passwords never stored
or audited; missing HQ_SECRET_KEY is a blocking problem. CLI:
npm run import:full -- --dir <folder> [--commit]. 7 new tests; suite
359 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 1d598e8a96 feat(d20): P3 — Tickets page, Client 360 tickets/branches, module service cards
Tickets: WORK page with whole-desk status chips + counts, Mine filter,
search, honest pager, per-row assign + status quick actions, Drop behind
confirm, New-ticket dialog (client type-ahead, branch select, module/kind
datalists). Client 360: Tickets tab (client-scoped, same actions) +
Branches section (add/rename/deactivate) + per-module ServiceDataCard
(provider/username, portal password ••••/Reveal/Copy managerial-only,
editable labeled details, remark). Also: /Apex/ git-ignored — real export
holds plaintext credentials, must never land in git.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise c58f0fdfc5 feat(d20): P2 — ticket/branch/service-data routes, gated + audited
GET/POST/PATCH /tickets (chips filters, mine=1, search, honest pagination,
counts + kind suggestions in one payload); client branches GET/POST/PATCH;
PATCH /client-modules/:id gains the managerial-gated portal password (atomic
with the rest of the patch) + POST /client-modules/:id/reveal-password
(audited per reveal). HTTP gating test: staff 403 on password set/reveal,
full ticket + branch round-trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 9fd4062cfe feat(d20): P1 — ticket desk, client branches, per-service data (schema + repos)
New tables ticket + client_branch (both engines: SCHEMA + pg 002-apex-parity);
client_module gains provider/username/password_enc/details/remark with
migrate guards. repos-tickets: team-visible desk, honest pagination, status
lifecycle stamping closed_on, counts + kind suggestions from history.
repos-branches: minimal audited CRUD. repos-modules: service fields on
ClientModule (hasPassword only — ciphertext never leaves), validated details,
setModulePassword/revealModulePassword mirroring the client DB password
discipline. 5 new tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 95df5cb5b9 feat(db): D19 P4 — DATABASE_URL engine selection + Postgres integration suite
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>
4 days ago
Thomas Joise 5f9f961d20 feat(db): D19 P1-P3 — async DB interface, dual engines, Postgres adapter
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>
4 days ago
Thomas Joise 08be48817e docs(spec): D19 Postgres switch design + pg driver dependency
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>
4 days ago
Thomas Joise 60698fc08f fix(iwa): final-review fixes — dialog layering/scrim/scroll-lock, a11y labels+disabled+alerts, settings guards+errors, edit-mode locks, strict offsets, titles
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>
5 days ago
Thomas Joise 94018b9ae4 fix(review): close all 8 findings from the D18 slice review
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>
5 days ago
Thomas Joise f30532b7d1 feat(import): APEX cutover web UI — stage/verify/commit over the existing importer (Phase 7, D18 WS-B)
- 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>
5 days ago
Thomas Joise b5d252b7dc feat(shell): sidebar collapse + My Day dashboard (Phase 5, D18 WS-E)
- 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>
5 days ago
Thomas Joise 30c2e3b60b feat(clients): support-access data — AnyDesk/OS, district+sector filters, encrypted DB password with audited reveal (Phase 6, D18 WS-F)
- 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>
5 days ago
Thomas Joise 28aa08c9c9 merge: Task 9 polish sweep from isolated worktree (favicon, titles, toasts)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 828f533a69 feat(templates): section-per-module quotation PDF (Phase 9, D18 WS-H)
- 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>
5 days ago
Thomas Joise 747026fa3b feat(modules): roster full-edit + assign/unassign; one-click Convert & send on the documents register (Phase 8, D18 WS-G)
- 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>
5 days ago
Thomas Joise a882d108fe feat(hq-web): favicon, per-route tab titles, success-toast sweep
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 3f7f08a908 feat(hq-web): user chip opens a Profile/Logout menu (Phase 4 finish, D18 WS-E)
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>
5 days ago
Thomas Joise 08dda8bab3 fix(hq-web): edit dialogs clear blanked optional fields (follow-up, outcome, plan amount)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise d95bf6d467 feat(employees): self-service profile + change-own-password; phone/title fields (Phase 4, D18 WS-E)
- 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>
5 days ago
Thomas Joise 1a8a3faf5d feat(hq-web): AMC/plan/interaction/employee/module dialogs — edit everywhere, confirmed deactivations
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 59c41ee8f7 fix(hq-web): guard client dialog against double-submit and cancel-during-save
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 0aca8b42b4 feat(hq-web): client create/edit dialog with contacts repeater — clients finally editable
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise e612c5cf81 feat(hq-web): standalone Reminders page (go-live WS-C, phase 3)
- /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>
5 days ago
Thomas Joise 5d81550e34 feat(hq-web): send-email composer dialog + styled confirms — window.prompt/confirm eliminated
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 584fb291f8 feat(documents): paginated register with honest total + Documents page (Phase 2, WS-D)
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>
5 days ago
Thomas Joise 8861b46afd feat(settings): configurable default share-link expiry (owner, audited)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise d28bca5cf0 fix(settings): validate whole reminders PUT body before writing any setting
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 36fcf795a8 test(documents): assert due-date stamp on the recurring-generation issue path
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>
5 days ago
Thomas Joise bc140fc54c feat(documents): invoice due dates — terms default + override, honest overdue chase (Phase 1, D18)
- 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>
5 days ago
Thomas Joise aa05060181 feat(settings): reminder day-settings + append-only dated schedule endpoints (owner, audited)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 03402ba0e1 feat(ui): Toaster with pure store (TDD) + FormGrid; provider mounted at root
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 953cf1dff8 feat(modules): module→client roster, notify-all, CSV export (Phase 10, spec §10)
- 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>
5 days ago
Thomas Joise 4a1747958e feat(reminders): invoice dN milestone escalation from the dated schedule (Phase 9, spec §8)
- 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>
5 days ago
Thomas Joise e9357bd8e6 fix(redesign): final-review fixes — palette races+a11y, avatar dark contrast, micro-label floor, drawer focus, pulse legend/labels/targets, warn contrast
26 adversarially-confirmed findings from the multi-agent whole-branch review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise fffcc2a05d feat(documents): one-click convert-and-send + proforma supersede (Phases 7-8, spec §8)
- 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>
5 days ago
Thomas Joise 400abc224f fix(funnel): review fixes — stop-chase on convert/cancel/lost, scoped ?status=, send guards, F3/F4 convert hardening
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>
5 days ago
Thomas Joise c22fc5ba1e feat(hq-web): split-panel login
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise c45b5692be feat(reminders): escalating quote follow-up (Phase 6, spec §7)
- reminder.rule_kind gains 'quote_followup': SCHEMA widened + rebuild-once
  migration via the shared rebuildTable helper (UNIQUE idempotency key and
  status/policy_applied CHECKs preserved verbatim; guarded, idempotent)
- runDailyScan: new scan block over sent quotations anchored on the first
  'sent' event; dated day offsets from resolveSchedule; catch-up guard fires
  only the single highest crossed milestone; policy from setting
  quote.followup.policy (default manual); auto sends drain strictly after
  the scan writes commit, at-most-once via the unique key
- reminderContext resolves (never mints) a live share and dated subject/body;
  graceful wording when the quote has no docNo; minting happens ONLY in the
  send path with a ~60-day expiry, reusing a live share
- STOP cleanup: markStatus(accepted|lost) and convertDocument dismiss open
  quote_followup rows per-row (each audited) in the same transaction
- listQueue: paginated with honest total, owner-scoped via
  doc_id -> document.created_by, labelled rows; dashboard + /reminders wired

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise a244ddb2b0 feat(hq-web): reports chips + skeleton/error states across catalog pages
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 94019980bb feat(hq-web): document view record header + composer line cards
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 08bb25abfc feat(hq-web): relationship-pulse ribbon on Client 360 overview
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise eaecaedd3f feat(pipeline): cross-client chase-list — derived stages, schedule-driven bands, role-gated, paginated
Phase 5 (D-PIPE option B, spec §6b/§9): repos-pipeline.ts listPipeline unions the
latest non-cancelled QUOTATION per client with bare leads (Enquiry); stage is never
stored; age anchors on the first 'sent' document_event; next action + colour band
read resolveSchedule('quote_followup') day_offsets (no hardcoded 3/7/14); staff are
server-forced to their own rows via ownerScope; Lost hidden unless filtered; sorted
actionable-oldest-first with LIMIT/OFFSET + honest total. GET /pipeline route plus
the Pipeline page (filter chips, managerial owner dropdown, band row tones, lifecycle
action buttons, pager) wired into nav right after Dashboard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise c5a524bab5 feat(hq-web): Client 360 — record header, KPI row, tabbed sections, forms extracted
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 9aa376fa2b feat(hq-web): clients — status filter chips, mono codes, skeleton/error states
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 00ce1d657e feat(hq-web): dashboard — skeletons, toned stats, tinted queue, 2-col grid
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 278187631b feat(hq-web): grouped sidebar shell, top bar, Ctrl+K palette, responsive-lite
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 0be928fbf8 fix(hq-web): Employees page no longer shows a permanent Loading state on fetch failure
employees stays undefined when getEmployees rejects, so the page rendered
the error Notice AND an eternal "Loading…" EmptyState below it. Branch on
list.error first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise 56b00b60c6 fix(reminders): deterministic tie-break in resolveSchedule on equal effective_from
ORDER BY effective_from DESC alone picks an engine/plan-dependent winner
when two reminder_schedule rows for the same rule_kind share an
effective_from (a same-day cadence correction under rule 3's
new-row-never-edit makes this likely). Append id DESC — UUIDv7 ids are
time-ordered, so the latest insert wins deterministically on SQLite and
Postgres alike.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise 5865aafe06 fix(employees): purge sessions on password reset; reject empty patches and duplicate emails cleanly
Review findings on the Phase 1-2 employee slice:

- setEmployeePassword now deletes the employee's sessions in the same
  transaction — resetting a suspected-compromised password must not leave
  the attacker's existing 14-day bearer token valid (deactivateEmployee
  already did this; the reset path is the natural lockout action).
- updateEmployee throws 'Nothing to update' when the patch resolves to
  zero recognized fields, instead of returning 200 and writing a no-op
  before==after audit row; PATCH /employees/:id with {} or wrongly-typed
  fields now 400s.
- createEmployee pre-checks UNIQUE(email) inside the transaction and
  throws 'Email already in use' instead of surfacing the raw
  engine-specific 'UNIQUE constraint failed: staff_user.email' text
  (would differ on Postgres, the locked production engine — D15).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise dc3d651b08 feat(reminders): dated reminder_schedule table + resolveSchedule resolver (Phase 4)
reminder_schedule (id, rule_kind, effective_from/_to, day_offsets CSV,
subject, body) added to SCHEMA (CREATE TABLE IF NOT EXISTS, exec'd on every
open = the additive migration path). resolveSchedule(db, ruleKind, today)
picks the row active on the business date (from<=today, to NULL or >today,
latest from wins) with code-constant fallbacks (quote_followup 3/7/14 +
spec s7 subject/body; invoice_overdue 7/15/30). seedIfEmpty seeds one
open-ended dated row per kind, audited in the same transaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise efdb34e710 feat(clients): account-owner plumbing — client.owner_id, audited owner route, role gate
Phase 3 of the quote-to-close funnel spec: client.owner_id added additively
(SCHEMA + PRAGMA-guarded migrate) so a bare lead is routable to an employee;
setClientOwner repo fn (validates active employee, audits set_owner in-txn);
PATCH /clients/:id/owner gated inline to owner/manager; shared ownerScope role
gate (staff forced to self, widening ignored) for later phases; ClientDetail
gains an owner/manager-only Account owner dropdown fed by GET /employees.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise d4388c4f63 feat(employees): management surface — /employees API routes + owner-only Employees page
Phase 2 of the quote-to-close funnel slice. Six /employees routes in
apps/hq/src/api.ts (GET requireAuth for the picker; POST/PATCH/deactivate/
reactivate/password owner-only, acting as the signed-in user); Employees
page (table + add/edit/reset-password forms, email immutable on edit,
count shown, no truncation); owner-only nav item beside Document Template;
typed client fns in hq-web api.ts. 6 new route tests (192 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise 1af7b87aab feat(hq): employee foundation — manager role, employee repo, session hardening (phase 1)
- rebuildTable helper: guarded, txn-wrapped CHECK-constraint rebuild (SQLite-only path)
- staff_user.role CHECK widened to owner|manager|staff (SCHEMA + migrate for old DBs)
- repos-employees.ts: list/get/create/update/setPassword/deactivate/reactivate,
  audited in-txn, password columns never selected, last-active-owner + self guards
- verifySession requires active=1; deactivation purges sessions in the same txn
- 9 tests (apps/hq/test/employees.test.ts); suite now 186 green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise 7e1c93cee0 Split: make this the standalone HQ project (remove Store)
This repo keeps its 60-commit HQ history and becomes independent of the Store
product. Removed the Store apps (pos, store-server, backoffice), the Store-only
packages (config, scanning, search-core, printing — printing was already a dead
dependency in hq-web), and the Store planning docs.

Shared packages are forked and trimmed to exactly what HQ uses: domain keeps
ids/money/business-day/doc-series/gstin/documents; auth keeps pin; billing-engine
keeps compute/tax. Store-only tests for the removed modules were dropped, and
auth.test.ts was trimmed to its PIN blocks. Package renamed sims-hq; root
tsconfig/vitest paths narrowed to the four kept packages.

Verified independently: npm install + typecheck clean, 177 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 3d3487c85f wip(store-product): returns, GST returns, offline queue, security hardening + docs
Snapshot of the store-product workstream's in-progress work (apps/pos,
apps/backoffice, apps/store-server, packages): returns + credit notes, GST
returns exports, offline bill queue, rate-limiting, session policy, print
guard, batch tracking, and the SEC-C6/SEC-A5 auth fixes; plus docs/18 red-team
review. Committed to preserve work for the repo push. HQ console already
committed separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 4559958fa4 docs(hq): go-live runbook, env template and S3 backup script
Turnkey deployment for the AWS box: documented env vars, pm2/systemd run,
HTTPS reverse-proxy, nightly SQLite->S3 backup with rotation, and the Postgres
switch note. No secrets; unblocks go-live the moment the server/creds arrive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 5fe86f40ba chore(hq): demo-seed script + gitignore demo data/ruvector
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise cfa1cf81f6 feat(hq-web): DocumentView share & download action group
Add a Share & download group to DocumentView: Download PDF (hits
?download=1 via a bearer-authed blob + save anchor), Create/Copy a public
/share/<token> view link with the live-share list and one-click Revoke,
Share on WhatsApp (wa.me/<client digits> prefilled with greeting + doc no
+ total + the share link), and native share (navigator.share of the link,
falling back to Download where unsupported).

Client api.ts gains the Share type, shares on getDocumentFull, createShare/
revokeShare, and downloadDocumentPdf. Vite dev proxies /share to the hq
server so a minted link opens against the dev origin exactly as in prod
(the hq server serves both the SPA and the public /share route on one
origin). Server share/download endpoints and the golden renderer are
unchanged.

Browser-verified: minted a link, opened /share/<token> in a fresh
unauthenticated context (200 application/pdf, viewer renders the doc),
revoked and confirmed a 404 "Link unavailable" page with no data leak, and
confirmed the WhatsApp deep link is prefilled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 25b96e2514 feat(hq): attachment download filename on GET /documents/:id/pdf?download=1
Serve the document PDF as an attachment with a doc-no-derived filename
("QT/26-27-0001" -> "QT-26-27-0001.pdf", null docNo -> "draft.pdf") when
?download=1 is present; keep the default inline so the composer iframe
preview still works. renderPdf is now injectable into apiRouter so the
header logic is tested without launching Chrome (house DI pattern).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 63e377d649 feat(hq): public GET /share/:token document view route
Mount the one unauthenticated read route OUTSIDE the /api auth router.
A live token resolves via validateShare to exactly one document, rendered
inline (application/pdf) through the existing documentHtml -> renderPdf path;
unknown/expired/revoked tokens return a plain 404 "link expired or invalid"
HTML page carrying no document data. Adds a dependency-free per-IP fixed-window
rate limiter to blunt token brute-forcing. renderPdf is injected so the route
is TDD-tested without launching Chrome. Public reads write no audit; the token
is never logged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise a1759864bf feat(hq): shareable document link backend (mint/validate/revoke/list)
Add document_share table (CREATE TABLE IF NOT EXISTS; opaque token, optional
expiry, revoke flag) and repos-shares.ts:

- mintShare: 256-bit randomBytes hex token (64 hex, unique), default +30d expiry,
  audited — the token itself is never written to the audit trail or any log.
- validateShare: resolves a live token to its one document; null on
  unknown / revoked / expired (ISO-8601 UTC lexical compare).
- revokeShare: flips revoked=1, audited.
- listShares: a document's shares, scoped and newest-first.

Wire POST /api/documents/:id/share and
POST /api/documents/:id/share/:shareId/revoke (requireAuth, document-scoped
share guard) and surface shares on GET /api/documents/:id. TDD: 14 tests cover
token shape/uniqueness, validate rejections, revoke, and audit-without-token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise d29025189c feat(hq): beautify document template — "Slate Ledger" visual system + template.accent
Rewrite documentHtml to a restrained, print-first GST layout: hairline table
rules, tabular money aligned to the paisa, a single accent driving every
structural color (letterhead rule, title, table-header rule + color-mix tint,
docmeta edge, amount-in-words edge, filled grand-total band).

Add optional template.accent setting (validated hex-or-fallback #334155
slate-indigo) read from the settings map and interpolated only after
validation, so a raw setting string can never be injected into the CSS.
print-color-adjust:exact so the accent tint and grand-total fill survive
puppeteer. DRAFT stays: accent chip in docmeta + optional watermark for
un-issued docs. lineRow/totalsRows untouched; @media screen paper + @page
14mm preserved; renderer stays one pure function so preview===pdf identity holds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 0d57c267ca feat(hq-web): reminder email preview in the manual queue
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 0b340fd85f feat(hq-web): module quote-content live print preview
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise b65e03e1c4 feat(hq-web): Document Template page — editable letterhead data + live preview
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 08e067c7cc feat(hq-web): composer split view — server-truth totals strip, live paper, responsive
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise c2eb7acba0 feat(hq-web): LivePreview — debounced double-buffer iframe + preview fetchers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 52e8a03c81 feat(hq): GET /reminders/:id/preview — exact email via shared reminderContext
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 48731fdb2b feat(hq): GET/PUT /settings/template + logo upload (owner-gated, audited)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise c2f94df06d feat(hq): screen paper styles + documentHtmlSample + POST /previews/sample
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise a948a92350 feat(hq): render editable template.* letterhead fields with default fallback
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 1ac44ae7de test(hq): golden fidelity — preview HTML byte-identical to the PDF's HTML
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago