|
|
# 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](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](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](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](11-ADMIN-SUPPORT-CONSOLE.md) **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](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/hq` and `C:/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](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
|
|
|
existing `staff_user` table** — renaming it would churn `session.staff_id`,
|
|
|
`interaction.staff_id` and audit entity names for zero gain; "Employee" lives in the
|
|
|
repo/API/UI vocabulary only. Roles widened to `owner | manager | staff` via a guarded,
|
|
|
transactional table rebuild (SQLite cannot ALTER a CHECK); the enum is validated in
|
|
|
the repo. Session hardening rode along: `verifySession` requires `active=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 | Lost` filters — 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 first
|
|
|
`sent` document event. The only new column anywhere in the slice is
|
|
|
`client.owner_id` (a bare lead has no creator to derive an owner from — A4).
|
|
|
- **Escalating owner+client reminders on the dated `reminder_schedule` table.** New
|
|
|
sendable `quote_followup` rule kind: the queued reminder row **is** the owning
|
|
|
employee's nudge (owner derived `doc_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 by `resolveSchedule` — 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 `computeBill` on the **invoice's own `doc_date`** (a tax
|
|
|
invoice uses the rate that is law on its issue date), and `convertDocument` rejects
|
|
|
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_by` for v1 (no `document.owner_id` until a handoff feature is
|
|
|
real); A4 `client.owner_id` is 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 on `doc_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 `CommandPalette` in `@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/ui` stays generic (no router or
|
|
|
icon deps — `lucide-react` lives in `apps/hq-web` only). 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 the `due_date` stamped on each
|
|
|
invoice at issue (covers manual issue, convert-and-send and recurring generation in one
|
|
|
place). Overdue reminders anchor `COALESCE(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`/`sector` on 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 (only `hasDbPassword`), 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. `ClientModulePatch` gained
|
|
|
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:** `/me` routes 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 `/import` wraps 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 by `DATABASE_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=1` on 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`, role `hq`,
|
|
|
databases `hq` (runtime) + `hq_test` (integration suite). Prod = RDS at go-live, same
|
|
|
`DATABASE_URL` mechanism.
|
|
|
|
|
|
## 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 `A` record `hq` → 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 `ticket` table (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 proxy` so
|
|
|
`req.ip` is the real client behind nginx. `makeRateLimiter` → `rate-limit.ts` (no cycle).
|
|
|
- **Authorization (B):** `requireManagerial` gate on `cancel` / `credit-note` (destructive)
|
|
|
and `POST /documents/:id/share` (the unauthenticated-public-link IDOR); `PATCH
|
|
|
/interactions/:id` restricted 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); `nextDocNo` atomic `UPDATE … RETURNING` (row lock, no duplicate/burned numbers);
|
|
|
`markStatus` rejects accepted/lost on non-QUOTATION docs; `computeBill` bounds qty
|
|
|
magnitude (fractional kept for weighed Store goods) + HQ `buildLines` requires whole-unit
|
|
|
integer qty; `recordPayment` rejects TDS > cash received.
|
|
|
- **Concurrency (D):** `SqliteDb.transaction` serialized by an async mutex + AsyncLocalStorage
|
|
|
re-entrancy (nested → savepoints) — the shared `txnDepth` counter 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 — an `esc()` 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 filters `LOWER(…) 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 for `verifyPin`
|
|
|
(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; `email` is 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.ts` carries a placeholder
|
|
|
`HARDCODED_DATABASE_URL` gated 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**: `listDocuments` free-text `q` on 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)
|
|
|
`apps/hq/scripts/backup.ts` → `apps/hq/src/backup.ts` (`npm run backup`) `pg_dump`s 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`.
|
|
|
It resolves its target through the SAME shared resolver as the server and the maintenance
|
|
|
scripts (`resolveDbUrl`, D19) and logs the resolved host/db before dumping — the original
|
|
|
`scripts/backup.mjs` read `DATABASE_URL` straight off the environment and so could never
|
|
|
reach production, where that variable is not exported.
|
|
|
|
|
|
## 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/`.
|