48 KiB
06 — Open Decisions (founder calls)
Each has a recommendation; none is blocking to start Phase 0, all must be locked to exit it.
D1 — Product name & positioning vs "SiMS Classic"
New name, or "SiMS" with a generation marker? Recommendation: keep the SiMS brand (it carries your existing base's trust), market the rebuild as SiMS Next or SiMS 2 — the migration pitch is easier when it's "your SiMS, reborn" rather than a stranger.
D2 — POS shell ✅ REVISED (2026-07-09): web app first
Founder call: the product is a web app. POS and back office are browser apps served by the store-server on LAN (later the same apps served from cloud). Zero installs at counters — the Oracle Forms deployment model, modernized. Consequences:
- Hardware routes through the store-server: web POS → HTTP → store-server → ESC/POS over TCP 9100 (printers + drawer). Scanners are keyboard-wedge (browser-native). Weighing scales: Web Serial API (Chrome/Edge) or a store-server serial bridge — spike item.
- Electron shell is kept as an optional kiosk wrapper (~100 lines, already built) for counters that need true offline billing or locked-down kiosk mode; same web bundle inside.
- Browser key conflicts (F11 fullscreen, F12 devtools): keymaps are DB config — the web preset leans on Numpad+ for cash-close and Chrome kiosk mode at counters.
- Offline story for browser POS (PWA + IndexedDB bill queue) is a Phase-2 hardening item; v1 counters depend on the store-server over LAN, exactly like Classic depends on the Oracle server today.
D3 — Sync: buy (PowerSync / ElectricSQL) vs build (custom outbox)
Recommendation: spike both in Phase 0; prefer buying the transport if it handles per-tenant partial sync cleanly. The domain rules (immutable documents, per-counter series, derived stock) are ours regardless.
D4 — GSP vendor for e-Invoice / e-Way
ClearTax vs Masters India vs others. Decide on: API reliability, per-document pricing at your volume, sandbox quality. Recommendation: shortlist two, integrate behind our own interface so switching is a config change.
D5 — Pricing tiers ✅ RESOLVED (direction)
Founder call (2026-07-08): prices aren't fixed yet and must be addable/changeable in the
DB so they reflect in the app without a release. Design adopted: plan, plan_feature,
and price_book tables (see 07-DB-AND-CONFIG.md §2) — new plans and
₹ amounts are rows, the license token is a signed snapshot of them. Actual ₹ amounts get
decided whenever you're ready; nothing blocks on it.
D6 — Accounting depth at launch
Full books (P&L/BS) in-app from Phase 2, or ledgers + Tally export first? Recommendation: ledgers + Tally export in Phase 2, full statements later. Most shops' CAs live in Tally; export buys goodwill for a fraction of the effort. Classic parity data should confirm how many customers actually used full accounting.
D7 — Store Hub (LAN sync box) as requirement or option
Multi-counter stores need counters to see shared stock/bills when internet is down. Recommendation: ship Store Hub as an optional role of the main billing PC (a checkbox, not a separate device) from Phase 2.
D8 — Android POS terminals (Sunmi-style) in scope? ✅ RESOLVED
Settled by the market-architecture brainstorm (08-MARKET-ARCHITECTURES.md, in the Store repo): every player winning high-throughput counters is desktop + local-first; Android wins micro-merchants and mobility. Decision: v1 = Windows desktop POS; Phase 3–4 = Android companion app (line-busting, stock-take, approvals) sharing the billing-engine package; full Android POS only if we later go down-market deliberately. D2 (Electron) stands.
D9 — Purchase Inbox channels at launch
Email-in + manual upload first; WhatsApp-forward intake later (needs WhatsApp Business API setup). Recommendation: email + upload in Phase 3, WhatsApp intake fast-follow.
D10 — Cloud region & provider
Any major provider with a Mumbai region works. Deciding inputs: managed-Postgres quality, egress pricing, your team's familiarity. Recommendation: pick boring; revisit never.
D13 — Support ticketing: build vs integrate (raised by 11-ADMIN-SUPPORT-CONSOLE.md)
The diagnostic bundle, error-code playbooks, and Store 360° are ours; the ticket queue itself is commodity. Recommendation: integrate a boring helpdesk in Phase 2; build only if the integration fights the error-code workflow.
D14 — Cloud staging ✅ RESOLVED: local-only first, cloud layered on later
Founder call (2026-07-09): v1 runs fully local (counters + in-store Oracle 12c per D12), then the cloud tier is added and switched on per store. Accepted consequences: owner app, Purchase Inbox, WhatsApp, central backup, silent auto-update, and HQ fleet features all wait for the cloud workstream. Guardrail (non-negotiable): client UUIDs + dormant outbox rows are written from the first local build, so cloud enablement is switch-on, not migration. See the staging note atop 04-ROADMAP.md (in the Store repo).
D11 — Existing team & hiring plan (partially answered)
Founder input (2026-07-08): a support team exists for Classic and continues; the product principle is a stable frontend with changes flowing from backend/DB, handled by the team — adopted as the config-driven design in 07-DB-AND-CONFIG.md. Still needed to calibrate the roadmap: builder headcount and skill mix for the new stack.
D12 — Local DB engine ✅ RESOLVED (for now): Oracle 12c stays at the store tier
Founder call (2026-07-09): the in-store DB remains Oracle 12c — it is already deployed shared infrastructure with multiple other systems running on it, so for the existing base the license and operations cost is sunk. Analysis and the adapted design are in 07-DB-AND-CONFIG.md §3.
Consequences accepted with this call:
- D3 resolves to "build": off-the-shelf sync engines (PowerSync/ElectricSQL) speak Postgres/SQLite only — sync is a custom outbox, with outbox tables living in Oracle and a store-side sync agent draining them to the cloud.
- Counters still carry a local SQLite buffer (offline bill queue + item-search cache) so a counter survives store-server or LAN death — the "counter never stops" goal is independent of the store engine.
- Backend connects via node-oracledb thin mode (no Oracle client install needed; supports 12.1+).
- New code stays engine-portable: repository/DAL layer, portable SQL by default, Oracle-only features only with a written justification — "for now" must stay cheap to revisit.
Revisit triggers (any one reopens this decision): onboarding NEW customers who don't already have Oracle in-store (license cost is no longer sunk); the other systems sharing the DB migrate off 12c or upgrade (19c/23ai); a security/compliance audit flags the unpatched engine; or sync friction in Phase 0–1 proves materially costlier than an engine port. Risk register entries in 07 §3 remain on record.
D15 — Internal HQ ops console: build now ✅ RESOLVED (2026-07-09; split to its own repo 2026-07-16)
Founder call (2026-07-09): an internal tool for running our own ~300-client business —
quotations/proforma/invoices in minutes, recurring billing with auto/manual email
reminders, per-module payment + AMC tracking, per-client AWS usage & cost, and the full
client interaction memory (installation, training, visits, calls, meetings, sales
outcomes) in a system instead of heads. Decision: start the
doc-11 HQ Console early, as apps/hq (built in the shared
monorepo at the time; split into its own repo C:/SiMS/hq on 2026-07-16) —
spec in 14-SPEC-HQ-CONSOLE.md. Cloud-hosted vendor-side
(D14 untouched — the store product stays local); SQLite behind portable repositories with
S3 backup; Gmail API sending from the company mailbox; automated AWS cost attribution;
full replacement of the internal Oracle APEX app — one-time verified migration, then
APEX goes read-only, no double entry. Reuses @sims/billing-engine (our invoices are GST
documents), @sims/domain (paise money, FY, doc series), @sims/ui, and the @sims/auth
patterns.
Alternatives rejected: fully separate codebase (rebuilds the engine/UI/patterns and builds doc-11 twice); off-the-shelf CRM (no module-level tracking against our catalog, no AWS attribution, no path to the fleet console).
Consequences accepted:
- This shipped as two deployables with different release rhythms. On 2026-07-16 they
were split into two independent repos (
C:/SiMS/hqandC:/SiMS/store) — separate histories, deploy scripts, and versioning; the shared packages were forked into each. - Doc-11's phase mapping splits: the ops slice ships early (HQ-1…3 in 14-SPEC-HQ-CONSOLE.md §6); the fleet half is unchanged and waits for the cloud tier per D14.
- One repo, two runtime-independent applications (founder re-confirmed 2026-07-10 after weighing a full split): separate servers, DBs, builds and deploys; only source packages are shared. Guardrails: a bundle-isolation check in the test suite (store bundle contains no HQ code and vice versa), per-app version tags, and the rule that HQ's DB is the cloud tier's nucleus — the tenant registry is born from HQ's client table (same UUIDs, promoted once), never built beside it. Postgres locked as the production engine (founder, 2026-07-10) — swap runs as its own task after HQ-2, before the real-data import, so live data never migrates engines; SQLite stays the dev/test engine behind the same repositories. Physical repo split stays a cheap, reversible option if practice ever demands it.
D16 — Quote-to-close funnel slice (2026-07-17)
Founder-approved design in
superpowers/specs/2026-07-17-quote-to-close-funnel-design.md; decisions locked
with it:
- Employee foundation rides
staff_user. The Employee entity is backed by the existingstaff_usertable — renaming it would churnsession.staff_id,interaction.staff_idand audit entity names for zero gain; "Employee" lives in the repo/API/UI vocabulary only. Roles widened toowner | manager | staffvia a guarded, transactional table rebuild (SQLite cannot ALTER a CHECK); the enum is validated in the repo. Session hardening rode along:verifySessionrequiresactive=1, and deactivation / password reset purge sessions in the same transaction. - Chase-list over kanban (option B). The pipeline is one cross-client ranked
list — most-overdue on top, colour bands, an explicit next action per row,
All | Mine | Overdue | Lostfilters — not a kanban board. - Derived pipeline stages, zero new pipeline storage. Stage (Enquiry / New
Project / Quoted-Waiting / Won / Lost) is never stored — computed from
client.status+ the client's latest live quotation; age anchors on the firstsentdocument event. The only new column anywhere in the slice isclient.owner_id(a bare lead has no creator to derive an owner from — A4). - Escalating owner+client reminders on the dated
reminder_scheduletable. New sendablequote_followuprule kind: the queued reminder row is the owning employee's nudge (owner deriveddoc_id → document.created_by), and its send is the client email with a public share link. Cadence and message text are dated config rows (rule 3) resolved byresolveSchedule— defaults 3/7/14 (quote) and 7/15/30 (invoice) ship as seeded rows plus code-constant fallbacks, so a cadence change is a new dated row, never a release. Catch-up fires only the highest crossed milestone; accept / lose / convert / cancel dismisses open nudges per-row (audited) in the same transaction; previews resolve shares read-only — minting happens only in the send path (60-day expiry, live links reused). - Convert recompute + duplicate-invoice guard (F3/F4). PROFORMA → INVOICE rebuilds
the carried lines through
computeBillon the invoice's owndoc_date(a tax invoice uses the rate that is law on its issue date), andconvertDocumentrejects any source with a live (non-cancelled) forward child — one sale, one invoice number. - Proforma supersede matrix. Corrections stay firewalled by document type: a draft
PI is simply re-drafted; an issued, unallocated PI may cancel (number stays consumed)
and be recreated as a
ref_doc_id-linked supersede; an issued INVOICE is never recreated — cancel-if-unpaid or credit note only. - Assumptions A1–A7 (defaulted, flagged in the spec): A1 module roster balances
support and revenue; A2 employees are a plain table (no org chart); A3 document
owner =
created_byfor v1 (nodocument.owner_iduntil a handoff feature is real); A4client.owner_idis the one genuinely new owner column; A5 notify-all text is owner-typed ad-hoc, not dated config; A6 roster visible to all staff, notify-all owner/manager only; A7 invoice-overdue anchors ondoc_date(no due-date column exists).
Recorded deviation from the funnel spec's blanket pagination rule (§5 / rule 8):
GET /employees returns the full set with total and no page/pageSize —
console users are a small bounded set (single-digit headcount), the count is displayed,
and nothing is silently truncated, which is the intent of rule 8. If the team ever
grows past a screenful, paginate it like every other list.
Delivery state (2026-07-17, final): all ten phases landed. Phases 1–6 (employee
foundation + management surface, owner plumbing, dated schedule, pipeline chase-list,
escalating quote follow-up) plus the F3/F4 convert hardening, then 7–10 in the same
day's second pass: one-click POST /documents/:id/convert-and-send (guards → one txn
convert+issue → post-commit email), proforma POST /documents/:id/supersede, invoice
dN milestone escalation (d7/d15/d30 from the dated schedule, F7 highest-only
catch-up), and the module → client roster (GET /modules/:id/clients + CSV export +
owner/manager Notify-all with per-recipient audit). Suite at 293 tests / 57 files.
D17 — HQ console visual redesign: warm ops-console (2026-07-17)
Founder-approved spec in superpowers/specs/2026-07-17-hq-console-redesign-design.md
(delivered same day; adversarial multi-agent review + fixes recorded there). Decisions
locked with it:
- Direction: "Direction A v2 discipline wearing the H-04 warm palette." Synthesised
from the two most recent design artifacts (the chosen Claude-Design "Direction A v2"
system and the H-04 Client 360 mockup): warm paper neutrals (
#FAFAF7/#E7E5DE/#1A1A17), flat border-defined surfaces, shadows only on overlays. - Teal is the default accent; primary buttons are ink, not accent. Accent colour is reserved for state (active nav, chips, links, focus). The 6 previous accents stay switchable; dark mode is warm-tinted with explicit per-accent dark values.
- Mono/sans split. JetBrains Mono for machine data (codes, doc numbers, GSTINs, ids); Inter with tabular numerals for money. Uppercase micro-labels never below 11px (WCAG).
- Ctrl-K command palette over more nav. Client search + pages + actions in one
keyboard-first overlay (generic
CommandPalettein@sims/ui; AND-term matcher). - Client 360 = record header + KPI row + relationship-pulse ribbon + deep-linkable tabs — the ribbon plots documents / payments / interactions / AMC on one 12-month axis from data the page already loads (no chart lib, no new endpoints).
- UI-only slice. No backend/schema changes;
@sims/uistays generic (no router or icon deps —lucide-reactlives inapps/hq-webonly). Known deferred: server-side "outstanding ₹" figure for the Client 360 KPI row (client-side paise math is banned).
D18 — Go-live cluster: due dates, ops pages, shell & profile, client support data ✅ DELIVERED (2026-07-17)
Founder-approved slice (spec superpowers/specs/2026-07-17-golive-cluster-design.md), built
in ten phases the same day. The calls that matter going forward:
- Invoice due dates = terms default + per-document override.
billing.payment_terms_days(seeded 15) is the operational default; the dated fact is thedue_datestamped on each invoice at issue (covers manual issue, convert-and-send and recurring generation in one place). Overdue reminders anchorCOALESCE(due_date, doc_date); the chase mail says "was due on X and is now N day(s) overdue"; legacy invoices keep the old anchor. - Client support data (APEX parity):
anydesk/os/district/sectoron the client; district+sector filter the book. The DB password is AES-256-GCM at rest (HQ_SECRET_KEY, loud refusal without it), NEVER in payloads (onlyhasDbPassword), write and reveal are owner/manager-gated with one audit row per reveal. - Roster single-write-path: the module→clients roster edits (kind/edition/renewal/
status, assign, unassign) reuse
updateClientModule/assignModule— the same audited functions Client 360 uses, so the two views cannot disagree.ClientModulePatchgained kind/edition with allowedKinds validation. - Quotation sectioning: QUOTATION prints one titled section per module (name +
what's-included + price; print page-break per module); INVOICE/PI/CN/RECEIPT keep the
billing grid. Rendered in the ONE
documentHtml, preserving preview↔PDF fidelity. - Self-service profile:
/meroutes are strictly self-scoped (role/email untouchable); changing your own password requires the current one and kills every OTHER session. - My Day: staff dashboards are always scoped to their own book server-side;
owner/manager toggle Mine/Everyone (
?mine=1). Sidebar collapses to an icon rail. - APEX import UI: owner-only
/importwraps the existing stage/verify/commit — commit stays locked while any staged row carries a problem. Delivery was followed the same day by an independent review of the full slice diff: 8 findings (4 major — convert-and-send click bubbling on the register, importer not mapping the WS-F support fields, Pipeline still on the old plain convert, missing client-book filter UI; 4 minor — typed contact roles absent, dashboard/aging still anchored on doc_date, non-atomic dbPassword PATCH, audit-outside-txn on three new write paths) — all confirmed and fixed in one pass (fix(review)commit). Suite at 345 tests / 66 files after the review pass.
D19 — Postgres switch executed (D15 delivered) ✅ DELIVERED (2026-07-17)
The engine slice queued after D18, done BEFORE the real APEX cutover so production data
is born in Postgres and nothing is ever migrated. Spec:
superpowers/specs/2026-07-17-postgres-switch-design.md. The calls that matter:
- One async DB interface, two engines. Every repo/route/scheduler/test awaits
db.all/get/run/exec/transaction. better-sqlite3 stays for dev + the fast suite (SqliteDb, statement-cached, manual BEGIN/SAVEPOINT); Postgres (PgDb, node-pg Pool) is selected byDATABASE_URL. Repos keep?placeholders and portable SQL; the pg adapter rewrites?→$n, pins transactions to one pooled client via AsyncLocalStorage (nested = savepoints), and parses int8/numeric to JS numbers (paise stay integer math). - Postgres schema choices: bigint for every paise column (int4 overflows at ₹2.15
crore), 0/1 flags stay INTEGER (repos say
active=1on both engines), JSON stays TEXT (node-pg auto-parses jsonb — would break JSON.parse at the repo edge), dates stay TEXT ISO. Numbered migrations + schema_migrations runner (migrations-pg.ts), applied atomically; the reminder rule_kind CHECK is born in its final widened form. - Proof: default suite 346/346 on SQLite (zero behavior change); 5-test pg
integration suite (opt-in
HQ_PG_TEST_URL, wipes its target) green on local PostgreSQL 17 — migrations-from-zero, series numbering + due-date stamp, paisa-exact payment→paid, ON CONFLICT exactly-once reminders, savepoint rollback, HTTP boot smoke. - Known nuance before go-live: LIKE is case-insensitive on SQLite, case-sensitive on
Postgres — client search will match case-sensitively in prod until repos switch to
LOWER(...) LIKE LOWER(?). - Local dev Postgres: PostgreSQL 17 Windows service, superuser
postgres, rolehq, databaseshq(runtime) +hq_test(integration suite). Prod = RDS at go-live, sameDATABASE_URLmechanism.
D20 — HQ hosting target: hq.simssoftware.com on the company AWS box (2026-07-17)
Founder call. The console goes live at https://hq.simssoftware.com:
- DNS stays at GoDaddy (where simssoftware.com lives) — one
Arecordhq→ the EC2 Elastic IP. No nameserver moves, no Route 53 migration; the rest of the domain (www, mail) is untouched. - The existing company EC2 hosts it as its own section (
/opt/sims-hq), beside whatever else the box serves — no new instance for v1. TLS via Caddy (or a server block in the box's existing nginx), pm2 from the repo root, only 80/443 public. - Sequencing: database first, then the server section. Provision RDS Postgres, set
DATABASE_URL(D19's switch), boot + migrate, run the APEX cutover into Postgres — and only then wire DNS/TLS and hand the URL to staff. Production data is born in Postgres; the SQLite dev DB is never migrated. - Runbook updated with the exact configs (GoDaddy record, Caddyfile/nginx block, RDS
sizing + security-group shape, pm2 invocation):
DEPLOY-HQ.md§0 and §7. The founder has hosted this way before — the runbook now carries the fiddly configuration detail so setup on the box is a checklist, not archaeology.
D20 — APEX parity: ticket desk, per-service data, branches, full importer ✅ DELIVERED (2026-07-17)
Driven by the real APEX export (C:\SiMS\hq\Apex, 25 tables). A 46-agent profiling+audit
swarm analysed the actual data before the importer was trusted. Spec:
superpowers/specs/2026-07-17-apex-parity-design.md. The calls that matter:
- Ticket desk is in scope (reversing the original non-goal): 4,567 APEX tickets are the
team's daily workbench. New
tickettable (team-visible; "mine" is a filter not a gate), status open/in_progress/waiting/closed/dropped, honest pagination, kind suggestions from history. Client 360 gains a Tickets tab. - client_module carries per-service operational data (provider/username/password_enc/ details JSON/remark) — the portal password encrypted + reveal audited, exactly like the client DB password. NOTE: D21 restructures this into a module-defined field schema.
- client_branch table (minimal; APEX ho_branch_list parity).
- Five modules (SMS, RTGS, WhatsApp, Mobile App, ATM) seeded from client YES/NO flags; ticket + roster module pickers read the live catalog, so a sixth module needs no code.
- Importer v2 (
import-apex-full.ts, 9 files) survived the swarm's real-data findings: bills are LINE ITEMS grouped by INVOICE_NO across three GST eras; 46 raw ticket statuses pattern-mapped; OLD_ID re-key chains collapsed; APEX test client (SID 329) + orphan refs skipped with counted notes; plaintext credentials encrypted at commit (refuses without HQ_SECRET_KEY). Trial import into local Postgres verified — see the spec banner for the reconciled counts (273 clients / 150 docs = ₹33.17L / 1,826 tickets / INVOICE seed 10252). Two real-data facts to carry forward: the export has NO state/GSTIN column (all docs split CGST/SGST at the company state) and one client's OS field holds a plaintext Linux root password (move it into an encrypted field + rotate at cutover).
D21 — Module-defined field schema (config on the module) ✅ DELIVERED (2026-07-17)
Spec superpowers/specs/2026-07-17-module-field-schema-design.md. Each module declares its
own operational fields (module.field_spec JSON: FieldDef key/label/type/options/required);
per-client values live on client_module as field_values (non-secret, type-validated
against the owning module's spec) + secrets_enc (one AES-GCM blob over a {key:plaintext}
map, N secrets, reveal audited). Owner defines a module's fields on the Modules page; the
Client 360 service card renders the form automatically; a new module or field needs NO code
and NO migration — the founder's flexibility mandate, delivered. Built additively over
D20 (login trio keeps its columns; retiring them into field_spec is a later cleanup after the
direction is confirmed). Verified on the real re-import: RTGS 13 fields / SMS 6 / MobileApp
1, 53/53 RTGS + 102/105 SMS links carrying structured values. Suite 363 green.
D23 — SMS volume-band billing (dated rate card) ✅ DELIVERED (2026-07-17)
SMS packs are the majority of billing, so they get first-class quantity-tiered pricing.
usage_rate_band is a dated per-module rate card (generic — any usage-billed module can
have one). resolveUsageRate(module, count, date) picks the band with the highest
min_qty <= count; a count below the lowest band is under the minimum order. buildLines
resolves a line's unit price as: explicit per-line override > tiered rate card > dated price
book — so an SMS line prices by count automatically, a save below the minimum is refused,
and the live preview warns instead of throwing. Founder card seeded (effective 2026-04-01):
≥50k → ₹0.40, ≥100k → ₹0.37, ≥200k → ₹0.34, ≥300k → ₹0.30 per SMS, minimum 50,000 — money
locked to the paisa in tests (50k→₹23,600, 100k→₹43,660, 200k→₹80,240, 300k→₹1,06,200 incl
18% GST). Owner routes GET/POST /modules/:id/rate-card. Rates are integer paise; a new card
is a new dated set of rows, so issued invoices keep the rate they were billed at.
Catalogue additions (2026-07-17) — CloudBackup + RecoverX configured as data
Two recently-developed modules set up entirely as configuration (no code), proving the D20/ D21 flexible model: CloudBackup & Disaster Management (8 fields incl. AWS creds as a secret; subscription ₹36k/yr + setup ₹25k + integration ₹10k) and RecoverX / Recovery Module (7 fields incl. CBS server IP, APEX URL, field-officer count, GPS consent, admin secret; app ₹35k + implementation ₹10k + integration ₹5k + AMC ₹15k/yr). Quote content seeded from the real quotations for the section-per-module PDF.
Suite at 372 tests / 70 files after D21 + D23.
D22 — Onboarding milestone tracker (reportable) ✅ DELIVERED (2026-07-17)
Founder call: track the common project status (advance paid / setup / installed / go-live /
balance paid / AMC) that applies to EVERY module sale, and make it reportable across the
book. project_milestone — each client_module (a "project") carries a checklist seeded from
the dated project.milestone_template setting (owner-editable; a project can add its own
steps). Ticking stamps the date, audited; ensureProjectMilestones is idempotent and
back-fills projects created before D22 (the 167 imported ones seeded). Cross-project board
(GET /projects/board — per-milestone done/total/pending) + drill-down
(GET /projects/pending/:key — who's pending that step). UI: a per-project checklist on the
Client 360 Modules tab + a new Onboarding work page (board + click-through to pending
projects). Also fixed a latent bug found here: uuidv7() is now monotonic within a process,
so ORDER BY id is genuine creation order (pagination + the sync cursor rely on it) — a
sub-millisecond tie no longer falls back to the random tail.
Suite at 377 tests / 71 files after D22.
D24 — Red-team audit remediation ✅ (2026-07-18)
A 14-surface multi-agent red-team + quality audit (report artifact + redteam-findings)
returned 96 verified findings — 0 critical, 16 high, 43 medium, 37 low. All 16 highs and
the load-bearing mediums were fixed in seven batches, each committed green with tests:
- Auth (A):
staff_user.failed_count/locked_until— 8-fail/15-min account lockout;login()constant-time (dummy scrypt on missing email — no enumeration oracle) returning a typed{ok,reason}; per-IP (20/min) + per-email (8/15min) limiters on/auth/login; per-user reveal limiter (20/min) on the three credential-reveal routes;trust proxysoreq.ipis the real client behind nginx.makeRateLimiter→rate-limit.ts(no cycle). - Authorization (B):
requireManagerialgate oncancel/credit-note(destructive) andPOST /documents/:id/share(the unauthenticated-public-link IDOR);PATCH /interactions/:idrestricted to author-or-managerial. Deliberate: general client read/edit stays open to staff — a shared support-team book is the intended model; the sensitive credential reveals are managerial + rate-limited. - Money (C): credit-note capped to the invoice's remaining creditable value (read+insert
one txn);
nextDocNoatomicUPDATE … RETURNING(row lock, no duplicate/burned numbers);markStatusrejects accepted/lost on non-QUOTATION docs;computeBillbounds qty magnitude (fractional kept for weighed Store goods) + HQbuildLinesrequires whole-unit integer qty;recordPaymentrejects TDS > cash received. - Concurrency (D):
SqliteDb.transactionserialized by an async mutex + AsyncLocalStorage re-entrancy (nested → savepoints) — the sharedtxnDepthcounter could interleave concurrent BEGIN/COMMIT and lose writes; scheduler isolates each recurring plan (one bad plan no longer aborts the whole scan + auto-send); notify-broadcast IIFE gets a trailing catch. - Rendering (E): strict CSP meta in both letterhead templates + puppeteer aborts every
non-
data:request — anesc()miss degrades to a rendering glitch, not SSRF/RCE. Preview ↔ PDF byte-fidelity preserved. - Backend perf (D24): 30 indexes on the hot FK/status/date columns, both engines
(
INDEX_DDL+ pg migration 007); client search + district/sector filtersLOWER(…) LIKE LOWER(?)— bare LIKE was case-SENSITIVE on Postgres and silently broke prod search. - Frontend (F): a11y (keyboard rows,
<main>+skip link, aria-labels, form Login, alert toasts), UX safety (confirm on mass-email + reminder send, in-flight disable to stop double-submit, debounced search, visible inline errors, ErrorBoundary), and shared component extraction. Explicitly accepted / deferred (documented, not fixed): async scrypt forverifyPin(large shared-package refactor; the event-loop-DoS vector is mitigated by the login rate limiter now); rendered-PDF caching on/share; api.ts file split; broad naming-consistency renames (staff/user/employee,at_wall, schema.sql drift) — too risky pre-go-live.
SQL-injection re-audit ✅ CLEAN (2026-07-18)
The SQL-injection surface (transiently blocked by a safety classifier during the D24 run)
was re-run as a focused 4-angle workflow (string-interpolation, dynamic identifiers, the
?→$n rewriter, the CSV importer) with adversarial verification. Result: 0 findings.
The auditors confirmed clean, angle by angle: every UPDATE SET clause is built from a fixed
source-literal column allowlist with ?-bound values; every WHERE fragment is a constant
with bound args; client/ticket search uses LOWER(col) LIKE LOWER(?) with the pattern bound;
all LIMIT/OFFSET are integer-clamped bound params and every ORDER BY is a static literal;
D21 field-spec keys are validated by KEY_RE and used only as JSON map keys inside a bound
field_values/secrets_enc blob (never as SQL identifiers); the CSV importer binds every
cell value and no CSV header becomes a column name; and db-pg.ts toDollarParams fails
closed (a mis-split breaks the query, never injects) since bound values never pass through it.
D25 — Installable PWA + phone compatibility ✅ (2026-07-18)
Founder ask: the console should be a PWA and phone-usable. Delivered without a heavy build
plugin: a hand-written manifest.webmanifest (standalone, teal theme, scope /) + an icon
set generated from the HQ mark (pwa-192/512, full-bleed maskable-512, apple-touch-icon,
icon.svg; sharp is a build-only devDep); a conservative service worker (public/sw.js)
that makes the app installable and fast on repeat loads but never intercepts /api or
/share (always live), does network-first navigations with an app-shell fallback, and
cache-first for hashed assets — registered from main.tsx in production builds only. Phone
polish on top of the existing <900px drawer + scrolling tables: safe-area insets
(viewport-fit=cover), 16px inputs (kills iOS focus-zoom), 40px tap targets, bottom-sheet
dialogs. Also hardened the deploy: nginx now sends X-Forwarded-For $remote_addr (replace,
not append) so the D24 per-IP rate limiters can't be bypassed by a spoofed header.
D25 addenda (2026-07-18)
- Module display order:
module.sort_order(both engines) — the app lists modules SMS > RTGS > WhatsApp > MobileApp > RecoverX > CloudBackup > ATM > AMC; owner-editable config (default 100 trails). Set on the live data. - Login by username:
staff_user.username(unique, both engines) is the login identifier;emailis demoted to a plain contact field on the user (kept required at the DB for now — making it optional is a later table rebuild). Existing accounts backfill username=email so logins are unbroken;login()matches username OR email. Employees can be created with an explicit username. - Nginx XFF hardened to
$remote_addr(replace) so the D24 per-IP limiters can't be spoofed. Note (open, user's deliberate edit):server.tscarries a placeholderHARDCODED_DATABASE_URLgated to production — must NOT be filled with the real password in source (it would push to Gitea); use a Docker secret / gitignored file instead. - In progress: brighter document type/status colours + sectioned Client-360 details + Employees username field (frontend).
D26 — Functional gaps: GST report, doc search, client statement, bulk ops, AWS UI (2026-07-18)
Founder call ("start all of it", functional first). Built the five gaps the daily work misses:
- GST filing summary (
gstSummary, GET /reports/gst-summary): per-month CGST/SGST/IGST + taxable + total over issued invoices (add) and credit notes (subtract, s.34), from the stored paise so it ties to the invoices exactly; money locked in tests. - Document search:
listDocumentsfree-textqon doc number OR client name (case-insensitive, both engines);GET /documents?q=. - Bulk operations (owner-only):
bulkUpdateClients(fill GSTIN/state/district across a set, each audited) +bulkSetMilestone(mark the already-live services on the onboarding board) — POST /clients/bulk, POST /projects/bulk-milestone. - Client statement + AWS cost UI: frontend over existing ledger / AWS endpoints. Backend committed green (350 tests); UIs follow. Ops (Gmail connect, server secrets, DB password out of source, backups) are the next slice after the features.
D27 — Review-screen program: Renewals, Cockpit, Audit viewer (2026-07-18)
Founder steer mid-program: "it's a small business — the main thing we need is screens we
can review to and fro." So we built read-and-navigate screens and dropped the heavy
automation (bank reconciliation, WhatsApp adapter, GST e-invoice/GSP — too much machinery
for ~300 clients). Delivered: Renewals command centre (renewalsDue, one-click renewal
proforma), owner Cockpit (misCockpit, this-month-vs-last composed from the same repo
aggregates the Reports/Dashboard use), Audit viewer (listAuditPage/auditFacets,
owner-only, filter + honest pagination, reading writes no audit).
D28 — SMS credit review + live gateway balance sync (2026-07-19)
The SMS module already declares an sms_balance service field (D26); this makes it
actionable. SMS balances screen lists every SMS client lowest-first, "Low" (< the dated
sms.low_balance_threshold) is the default upsell view, one click raises a top-up proforma,
and the owner Cockpit + Dashboard carry the low-count alert. Ticket SLA: an Age column,
ticket.sla_days (dated) drives an overdue mark + filter, and a "Bill" action opens New
Document with the client pre-selected (light ticket→invoice; /documents/new?client=).
D29 — Console visual refresh: cockpit dashboard + palettes + Geist (2026-07-19)
The Dashboard is reworked into a cockpit: a flat "Today" hero (date, greeting, the headline
overdue figure, Everyone↔Mine scope, quick actions), the four KPI stat cards, the reminder queue,
then a bento grid of the day's sections (overdue, due-this-week, renewals, follow-ups, recent
payments). Low-colour on purpose — ink + neutrals + one accent, red only for real failures
(.dash-* in apps/hq-web/src/app.css, wired to the existing /dashboard payload; honest data
only, no invented figures).
Theme gains a third axis — palette (the neutral ground). Alongside mode (light/dark) and the
seven accents, data-palette selects the greys: Warm (the default, unchanged), Slate
(cool), Graphite (dark-first), Zinc (near-mono). Backward-compatible and config-driven
(packages/ui theme.ts + tokens.css; warm needs no attribute), picked in Settings →
Appearance beside accent + mode. The UI font is swapped Inter → Geist Variable + Geist Mono
(bundled offline via @fontsource-variable/geist). Class names stay stable — restyling stays in
one place.
D30 — Home hub landing + sidebar reorg + section counts (2026-07-19)
A Home hub card launcher becomes the landing (/); the Dashboard moves to /dashboard. The
hub surfaces only the day-to-day sections (Dashboard, Clients, Documents, Reminders, Renewals,
Pipeline, Tickets, SMS credits, Modules) — each a card with a one-line purpose and a live count —
everything else stays in the sidebar. The sidebar is reorganized from one overloaded "Work"
group into six intent groups (Overview · Work · Clients & billing · Catalog · Insight · Admin)
with clearer names (MIS cockpit, SMS credits, APEX import); "New document" is an action, not a nav
row. Section count badges — neutral by default, amber for SMS-low, red for failed sends — sit
on Reminders / Pipeline / Tickets / Renewals / SMS / Clients / Documents-drafts / Modules, fetched
best-effort (a slow/failed one never blocks the shell). The Modules page gains a per-module
client count column.
D31 — Credentials stored in the clear (owner decision, 2026-07-19)
Founder call: "no encryption for anything — those credentials will be used regularly." The
held-out HQ_SECRET_KEY was a daily friction point (the SMS-gateway balance pull needs to
decrypt 78 logins every day), so credential encryption at rest is removed. crypto.ts
encrypt() now stores tagged plaintext (plain:…); decrypt() untags it with no key, and
still reads legacy AES-256-GCM payloads with the key purely to migrate them. The six
"refuses/cannot without HQ_SECRET_KEY" guards (module password, module secret, client DB
password + their reveals, the SMS refresh) are gone — set/reveal/use all work keyless.
Scope: this is the reversible credential vault only — module portal/gateway passwords,
module secret fields, client DB passwords, the Gmail refresh token. Login passwords stay
one-way scrypt-hashed (@sims/auth) — those never need to be read back, so nothing there
changes. Secrets are still kept out of list payloads and audit rows (only {set: true} /
the key name is audited, never the value), and reveal is still managerial + audited.
Migration: migrateLegacyCiphertext(db, keyHex) (POST /admin/migrate-plaintext, owner)
rewrites every existing AES row as plaintext using the key ONE final time — idempotent, one
audit summary. After a clean run the key is never needed again. Existing encrypted rows
cannot be read without the key (or a fresh plaintext source), so the key is required exactly
once. SECURITY: a DB dump/backup now exposes these logins in the clear — protect the DB
file and its backups (this raises the priority of the pending DB-secret + backup ops slice).
D32 — Remove the key concept entirely (2026-07-19)
The key was never in hand, so the D31 "unlock with the key once" path was impossible. Founder:
"remove the key concept — maybe in future we'll add it." crypto.encrypt/decrypt are now fully
keyless (the keyHex params remain, ignored, so a key can be wired back later); decrypt()
returns '' for any leftover legacy AES value instead of throwing (unreadable → treated as unset,
never fed back as ciphertext). The key-based /admin/migrate-plaintext is replaced by keyless
/admin/clear-legacy-credentials (clearLegacyCiphertext) which NULLs every unreadable
pre-D31 AES value (module passwords/secrets, client DB passwords) and drops the dead Gmail
account row, so each affected login reads as "not set" and is ready for fresh plaintext.
Consequence: the pre-D31 encrypted logins (incl. the 76 imported SMS passwords) are gone —
they must be re-populated in plaintext (APEX re-import, now keyless, or re-entry). HQ_SECRET_KEY
is no longer used anywhere.
Live balance fetch (founder: "configure the BalAlert API so we see the current count,
daily auto + low alert"): the provider panel (sms.balance_api_url, default
http://164.52.195.161/API/BalAlert.aspx) returns pipe-delimited code|STATUS|payload;
parseBalanceResponse reads the trailing number, flags ERROR lines, never guesses. Each SMS
subscription's already-imported username/password_enc are used — the automated path
decrypts directly (no reveal-audit spam) and audits sms_balance only when it changes;
per-poll telemetry (ok/error + time) lives in the unaudited sms_balance_check table
(operational log, not domain data). A managerial "Refresh from gateway" button polls all;
the scheduler runs one pass per calendar day (maybeRefreshSmsDaily, claims the day via a
setting). Requires HQ_SECRET_KEY set to the import-time key to decrypt — without it the
refresh no-ops with a clear message, it never stores anything unencrypted.
(Superseded by D31/D32: the key was removed; the pull now runs keyless.)
D33 — Database backups (ops, 2026-07-19)
scripts/backup.mjs (npm run backup) pg_dumps the database to a timestamped, compressed,
pg_restore-able file under ./backups (gitignored), keeping the newest HQ_BACKUP_KEEP
(default 14). It finds pg_dump on PATH or in the Windows C:\Program Files\PostgreSQL\*\bin
install. Restore: pg_restore --clean --if-exists --no-owner -d "$DATABASE_URL" <dump>.
Priority rose with D32 — dumps now contain plaintext credentials, so the backups/ dir
must stay access-controlled and off shared drives. Still pending in the ops slice: schedule
it (Task Scheduler / cron), off-box copy, and getting the DB password out of server.ts.
D34 — Employee master import + invited/first-login flag (2026-07-19)
Imported the 11-row employee master (empmaster.xlsx) into staff_user via
scripts/import-employees.ts: username = first-name slug, default password <username>@123
(founder's explicit choice), role staff, active per ACTIVE_STAT. New must_change_password
column drives an "Invited · must change pw" badge on the Employees page and sends the user to
Profile to change it on first sign-in; the flag clears on that first self-change. A background
security review flagged the predictable default password — kept per the founder's explicit
instruction, with the invited flag + change-nudge as the mitigation (full first-login
enforcement offered, not yet enabled).
D35 — Client type (PACS/Store) + directory-sourced LTD numbers (2026-07-19)
client.client_type = PACS (co-op society) vs Store (retail), backfilled by name
(ends in STORE → Store) and filterable on the Clients screen. Data-correction session
(founder-approved, review-sheet-first): 187 client names cleaned mechanically (co-op style →
CO-OPERATIVE, run-on splits, ARP00KARA→ARPOOKARA, spacing), applied after a backup and
audited. LTD/registration numbers looked up from the Kerala govt co-op directory (the
source of truth) via a multi-agent web-lookup workflow — 259 PACS across two runs (the
first-wave batches needed a smaller-batch re-run to clear a transient safety-classifier block);
141 now carry their LTD number in the name ( LTD NO. <n>, guarded against duplicates).
The ~26 real banks not found online + non-bank societies were left untouched (no guessing).
D36 — Login history (IP + device) + first-login nudge (2026-07-19)
Every /auth/login attempt is recorded in a login_event table (staff_id or null, username
tried, outcome success/failed/locked, IP, user-agent) — security telemetry, separate from the
domain audit, best-effort so it never breaks login. Owner-only Login history page (Admin):
who signed in / failed, when, from which IP + parsed device, outcome filter, paginated.
login() now returns mustChangePassword (D34) so an invited user lands on Profile to change
their default password.
D37 — Society category (Kerala co-op directory taxonomy) (2026-07-20)
client.category classifies each client the way the official Kerala co-op directory does:
Service Bank · Urban Bank · Vanitha · Housing · Employees · Agricultural · Marketing ·
Bank (other) · Society (other) · Store · Other. Derived from the name (most-specific-first;
a "VANITHA … SOCIETY" is Vanitha, not the generic Society), backfilled by a portable CASE and
applied to new/edited clients via categoryFromName. Clients screen gains a Category filter +
column. Live backfill: 135 Service Bank, 38 Employees, 15 Vanitha, 14 Store, 12 Agricultural,
11 Urban, …
D38 — Client Detail redesign: composed onboarding pipeline, status line & payments context (2026-07-20)
Reworks the Client 360 Modules + Payments tabs around how the business actually runs — a module is the end of a sales process, not a one-click assignment.
Composed onboarding templates (config over code). The step list is resolved as a shared lead
front + a per-module tail: project.milestone_template.front (Enquiry received · Visit / meeting
scheduled · Quotation sent · Quotation approved) applies to every module, and
project.milestone_template:<CODE> supplies that module's delivery steps (SMS: Document collection ·
DLT registration · Account creation · Installation · Testing · Training · Go-live · Payment received;
RTGS and Mobile App have their own). Edit the front once and every module follows. Both are
owner-editable in a new Modules → Onboarding steps editor. A one-time, idempotent migration
(scripts/apply-onboarding-pipeline.ts) moves existing projects onto the composed list, carrying
mapped ticks.
The status line drives the record. The checklist became a horizontal status line whose ticks
auto-advance client_module.status (quotation approved→ordered, installation→installed,
training→trained, go-live→live; never downgrades), so the status dropdown stops being hand-set. The
Payment received step links to a real payment (project_milestone.payment_id, step date mirrors
received_on). The Account creation step prompts for the module credentials — and credentials
are no longer required when adding a module (they don't exist yet at that point); they stay
editable anytime via the module's Access panel.
Module + payments surfaces. Details render as a read-only 50/50 sheet (Access | Details) with
one Edit per group — no always-on input boxes. Each module block lists its own documents
(line-item match) labelled Quote / Renewal / Bulk top-up / Invoice; SMS can raise a
bulk-credit purchase proforma (source='bulk_sms_topup') distinct from a subscription renewal
(source='module_renewal'). The Payments tab gains an Outstanding panel — issued invoices with a
balance plus sent/accepted proformas (un-issued drafts excluded, owner decision) — and a
Settled → document column per payment. The money engine is untouched (read-only ledger extension).
Also: a stalled-onboarding query for the dashboard/MIS, and a mobile fix so the record header
stacks instead of overlapping the client name.
Spec + plans under docs/superpowers/.