Two 'easily get to it' access screens:
- Portals (Catalog): every subscription with a stored login/URL across all modules,
Open ↗ links, 🔑 = password on file (reveal stays on Client 360). listPortals().
- Global search: the Ctrl-K palette now spans clients + issued documents + tickets in
one /search call (repos-search.globalSearch), each hit routing to its record; per-type
top-N with an honest 'capped' flag (no silent truncation).
Full suite 410 green; all three endpoints smoke-tested on live data (portals 78,
search capped, RTGS directory 13 fields).
Note: includes pages/Home.tsx (the concurrent dashboard-redesign session's Home hub)
so main.tsx's existing /home route resolves and the tree stays buildable; that session
owns its further iteration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New 'Module data' screen (Catalog): pick a module, see all active clients on it with
that module's own field-spec values as columns — the whole book at once (all RTGS IPs,
all CloudBackup buckets, all RecoverX portals), searchable, portal URLs clickable, each
row links to the client. moduleDirectory(db,moduleId) returns fieldSpec + rows; secret
VALUES never leave the server (shown as a lock), reveal stays on Client 360. Reuses the
field-specs so a new module field appears here with zero code change. typecheck green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'All this should be db': the three knobs the new features used to read from code
defaults are now editable DB settings in one easy place — Settings → Operations
(owner). GET/PUT /settings/operations reads/writes sms.balance_api_url (SSRF-guarded
before store), sms.low_balance_threshold, ticket.sla_days. No release needed to
retune any of them; the app already resolves each from the DB with a safe fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a `data-palette` theme axis (neutral ground) alongside mode + accent:
Warm (default), Slate, Graphite, Zinc — selectable in Settings > Appearance,
persisted per browser. Backward-compatible: `warm` needs no attribute so the
existing look is unchanged; accent (7 hues) and light/dark stay orthogonal.
Swap the UI font Inter -> Geist Variable + Geist Mono Variable (bundled offline).
Add the `.dash-*` cockpit styles finishing the redesigned Dashboard markup
already in tree (flat Today hero, KPI row, reminder queue, bento sections;
low-colour, red reserved for real failures).
packages/ui theme.ts/tokens.css/ThemeSwitcher.tsx are shared — restyles every
page. Typecheck clean; 410 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Any module service field holding an http(s) value now shows a direct 'Open ↗' link
beside it on Client 360 — the RecoverX/CloudBackup/MobileApp portal + AWS-console
shortcuts the founder asked for, working for any URL-valued field (no new type).
Also (DB config, via PATCH /modules — audited, not code): extended the RecoverX,
CloudBackup and MobileApp field-specs with the operational fields requested
(workspace, portal URL, admin login, install date, payment status; S3 bucket / IAM
user / folder / server for cloud; corp id / server / config for the mobile app).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Security review of 58c9799: (1) block loopback/link-local/private targets for the
admin-set sms.balance_api_url before sending client credentials (assertSafeGatewayUrl
— rejects 127/10/172.16-31/192.168/169.254/localhost/::1/.internal), so the setting
can't be pointed at cloud metadata or internal services; (2) redirect:'manual' so a
3xx can't carry credentials to another host; (3) error messages never interpolate the
credential-bearing URL. http:// transport kept — it is the vendor panel's only
interface and the operator supplied it. Tests cover the block list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pull each SMS client's actual credit balance from the provider panel instead of
manual entry. sms.balance_api_url (dated setting, default 164.52.195.161/API/
BalAlert.aspx) returns pipe-delimited code|STATUS|payload; parseBalanceResponse
reads the trailing number and flags ERROR lines, never guessing. Uses each
subscription's already-imported username/password_enc — the automated path decrypts
directly (no reveal-audit spam) and audits sms_balance ONLY when it changes;
per-poll telemetry lands in the unaudited sms_balance_check table (SQLite schema +
PG migration 010). Managerial 'Refresh from gateway' button (POST /reports/sms-
balances/refresh) + per-client refresh; scheduler does one pass per day
(maybeRefreshSmsDaily). Low-balance alert on the Dashboard + cockpit tile. Needs
HQ_SECRET_KEY (import-time key) to decrypt — no-ops with a clear message otherwise,
never stores plaintext. Parser unit-tested; full suite 408 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Age column on the workbench (days since opened); a still-open ticket aged past
ticket.sla_days (dated setting, default 7) shows red with a warning. New 'Overdue'
filter chip with an honest server-side count (still-open AND opened on/before the
SLA cutoff). Each row gains a 'Bill' action that opens New Document with the client
pre-selected (/documents/new?client=<id>) — the light ticket->invoice path, staff
add the lines. Kept deliberately un-bulky: no kanban, reuses the existing table.
Test locks the overdue filter + count. typecheck + full suite (403) green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New 'SMS balances' screen: every SMS client with its credit balance (the module's
own sms_balance service field), lowest first so the ones about to run dry sit on
top. Low = below sms.low_balance_threshold (dated setting, config over code);
'Low' is the default view = the upsell list. One click raises a top-up proforma
(reuses the module renewal-quote path); client name links to the Modules tab where
the balance is kept. Owner cockpit gains an 'SMS balance low' tile as the at-a-
glance alert. Read-only report, no audit. Test locks parse/flag/sort. typecheck +
web build clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
listAuditPage (filters: entity/action/user/entity-id/date; honest total; newest
first) + auditFacets. GET /audit owner-only. New /audit 'Audit log' page: entity/
action filters from server facets, paginated table with a per-row before->after
key diff, actor names mapped via employees. Reading the log writes no audit row.
Test locks filter+pagination. typecheck + web build clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
repos-mis misCockpit(today): this-month vs last for billed (issued invoices) +
collections (payments), total outstanding (dues-aging), open pipeline value,
renewals due in 30d, open tickets, reminder queue — composed from the existing
repo aggregates so figures reconcile with Reports/Dashboard. Owner-only GET
/reports/mis. New /mis 'Cockpit' page: KPI tiles with MoM deltas + click-through
to the source pages. Money in tests. typecheck + web build clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Durable specs for tickets/reconciliation/MIS/sms-balance/tds/audit-viewer/
notifications/einvoice (+ renewals, delivered) with schema/repo/route/UI/test
detail and a collision-safe build order. WhatsApp design was transiently
classifier-blocked; to re-run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New WORK page /renewals: 30/60/90-day window chips, table of due module +
AMC renewals (client, type badge, due date, days-until with red/amber tint,
amount, total), one-click 'Renewal quote' on module rows (-> proforma draft)
and 'Open AMC' on AMC rows. api: getRenewals + generateModuleRenewalQuote.
typecheck + web build clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
renewalsDue(from,to,today): unified list of module (client_module.next_renewal,
priced by the dated price book) + AMC (period_to, fixed amount) renewals with
amount + days-until, earliest-first. generateModuleRenewalQuote: one-click
proforma carrying the client's module kind/edition forward at the price-book
rate (AMC keeps its existing renewal-invoice). Routes GET /renewals,
POST /client-modules/:id/renewal-quote. Renewal reminders already fire from the
scheduler (renewal_due/amc_expiring) — this is the worklist + action. Tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- gstSummary(range): per-month GST filing summary over issued INVOICE (adds) +
CREDIT_NOTE (subtracts, s.34), CGST/SGST/IGST split from stored paise, with a
grand total. Route GET /reports/gst-summary. Money locked in tests.
- listDocuments gains free-text q (doc number OR client name, case-insensitive
both engines); GET /documents ?q=.
- bulkUpdateClients (owner) + bulkSetMilestone (owner) for cleanup at scale —
POST /clients/bulk, POST /projects/bulk-milestone; each client keeps its own
audit; milestone bulk writes one summary audit. Suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The 7-swatch accent selector is removed from the top bar (it now lives only
in Settings > Appearance); the bar keeps a single compact light/dark ModeToggle.
This declutters the header and fixes most of the mobile congestion.
- Mobile pass (<=640px): search takes the bar, Ctrl-K hint + Gmail pill hidden,
avatar-only user chip; pages get real padding + bottom room; toolbars/chips
wrap with air; inputs full-width & 42px tall; stats 2-up; tables padded.
- Billing composer: client search + line-item cards go full-width and stack on
phones (composer-search / composer-line classes) so no container is undersized.
typecheck + web build clean, web tests 4/4.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- staff_user gains a unique username (both engines: migrate + pg 009); older
rows and the seed owner backfill username=email so every existing login keeps
working. login() matches username OR email (legacy fallback).
- createStaff/createEmployee take an optional username (defaults to email);
Employee/API expose it; POST /employees accepts username. Email stays a
required contact field on the table (making it optional is a later table
rebuild). Full backend suite 346 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Team decision: production DB connection and first-boot owner email are hardcoded in the app instead of a .env file, matching the box's other apps. Gated to NODE_ENV=production so dev/tests keep using SQLite (389 tests green). Committed value is the CHANGE_ME_PASSWORD placeholder; the real password is set only on the box, never pushed. server.ts: HARDCODED_DATABASE_URL. seed.ts: OWNER_EMAIL default. docker-compose.yml: drop env_file. docs: PATHS-AND-NAMES.md + .env.example updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- module.sort_order (both engines, migrate + pg 008); listModules orders by
sort_order then code; createModule/updateModule + ModulePatch carry it, so
the app-wide module order is owner-editable config (default 100 = trails).
- Login accepts a username: field is type=text (autocomplete username),
relabeled 'Email or username' — a plain 'admin' login now works.
Module + web tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Web manifest (standalone, id/scope/start_url=/, teal theme) + icon set
generated from the HQ mark: pwa-192, pwa-512, maskable-512 (full-bleed
safe-zone), apple-touch-icon 180, icon.svg. sharp added as a devDep for
generation.
- Service worker (public/sw.js): installable + fast repeat loads without
faking offline. NEVER touches /api or /share (always live); navigations
network-first with an app-shell fallback; hashed assets cache-first. Registered
from main.tsx in production builds only (skips Vite dev/HMR).
- index.html: manifest link, apple-touch-icon, apple/mobile-web-app meta,
viewport-fit=cover; vite-env.d.ts adds vite/client types.
- Phone polish (app.css): safe-area insets, 16px inputs (kills iOS focus-zoom),
40px tap targets, bottom-sheet dialogs, 2-up stat cards — on top of the existing
<900px drawer + scrolling tables.
typecheck + web build clean, web tests 4/4.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the app's trust-proxy=1, an appended client-supplied XFF header would let a
caller spoof req.ip and bypass the per-IP login/reveal rate limiters. Replacing
with $remote_addr (the unforgeable socket peer) keys the limiters on the true
client.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- recordPayment locks the target invoice row (SELECT ... FOR UPDATE, Postgres
only — SQLite is serialized by the connection mutex) before reading
outstanding, so concurrent payments cannot over-allocate on prod.
- INDEX_DDL now runs AFTER migrate() in openDb, so indexes referencing
migrate-added columns (owner_id, lockout cols) build on older DBs.
- D24 remediation recorded in 06-DECISIONS; test counts 377 -> 389.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Red-team backend-quality highs/mediums:
- Added 30 indexes on the foreign-key / status / date / renewal columns the
lists, reports, joins and scans read — zero existed, so every query full-scanned
(tolerable on dev SQLite, a real problem on production Postgres at 300 clients /
thousands of tickets & documents). Same DDL on both engines: INDEX_DDL in the
SQLite SCHEMA + pg migration 007 (CREATE INDEX IF NOT EXISTS, idempotent).
- Client search + district/sector filters now LOWER(...) LIKE LOWER(?): bare LIKE
is case-insensitive on SQLite but case-SENSITIVE on Postgres, so search silently
broke on the prod engine. Test locks case-insensitivity.
Verified: pg migrations 006+007 apply cleanly on local Postgres; suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Red-team frontend clusters (a11y + UX + code quality), three coordinated passes:
Accessibility:
- DataTable clickable rows keyboard-operable (role=button, tabindex, Enter/Space).
- <main> landmark + skip link; icon-only buttons get aria-labels; inputs relying
on placeholder alone get aria-labels; AWS-ranking link is a real <button>;
Login fields wrapped in a <form> so Enter submits from either field; toasts
role=alert for errors (urgency not by color alone).
UX safety:
- 'Notify all' and reminder 'Send' now behind ConfirmDialogs naming the count/
client; document action bar disables while an action is in flight (no
double-submit dupes); SendDialog surfaces why a send is blocked instead of
failing silently; client search + ticket search debounced (~300ms); inline
Client-360 edit failures surface via toast, not an off-screen banner; LivePreview
gets a real Retry; destructive client-status transitions confirm first.
Code quality:
- ErrorBoundary wraps the app (a render throw no longer white-screens).
- useData clears stale data on dep change (no record flashing under a new id).
- Shared ErrorBoundary/SecretField/Pager components extracted; Pager adopted in
Documents; write-call api types tightened off Record<string,unknown>.
typecheck clean (root + both workspaces), web build clean, web tests 4/4, full
suite 388 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Red-team XSS/rendering finding (public-share renders our HTML in Chromium):
- Both letterhead templates get a strict CSP meta (default-src none; img-src
data:; style-src unsafe-inline; base-uri/form-action none) — permits the
inline CSS + data: logo the templates actually use, blocks all script
execution and every outbound/file fetch. Identical in preview + PDF, so the
byte-for-byte preview-fidelity golden test still passes.
- renderPdf aborts every non-data: network request at the browser layer.
Together an esc() miss becomes a harmless rendering glitch instead of an
SSRF/exfiltration/RCE-adjacent vector. Template + fidelity tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Red-team correctness findings:
- SqliteDb.transaction now serializes top-level transactions with an async
mutex and tracks re-entrancy via AsyncLocalStorage (nested calls -> savepoints,
no deadlock). The old shared txnDepth counter let concurrent async callers
interleave BEGIN/COMMIT on the one connection -> silent data loss; a 50-way
concurrent read-modify-write test now lands exactly.
- generateRecurring wraps each plan in try/catch: one misconfigured plan is
parked (audited 'generate_failed') and the scan continues, instead of aborting
all remaining generation and the entire auto-send drain, every scan.
- POST /modules/:id/notify broadcast IIFE gets a trailing catch so a mid-loop
throw answers the request instead of hanging it.
Full suite 388 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One name end to end: deploy/nginx-hq.conf -> deploy/sims.hq.conf matching /etc/nginx/conf.d/sims.hq.conf. Add deploy/PATHS-AND-NAMES.md as the single production reference (paths, names, ports, everyday commands).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Red-team money findings:
- createCreditNote caps the CN to the invoice's remaining creditable value
(invoice payable − already-credited by non-cancelled CNs), read+insert in
one txn. Blocks a CN exceeding its invoice / unlimited CNs flipping it 'paid'.
- nextDocNo allocates via one atomic UPDATE ... RETURNING (row lock) instead of
read-then-bump — no duplicate/burned numbers under concurrent issue (Postgres).
- markStatus rejects accepted/lost on non-QUOTATION docs — an issued invoice can
no longer be hidden from overdue chasing via a quote-only transition.
- computeBill bounds qty magnitude (keeps fractional for weighed Store goods);
HQ buildLines additionally requires whole-unit integer qty (SMS/module seats).
- recordPayment rejects TDS greater than the cash received — no fabricated-TDS
'settlement' with no money.
money-guards test locks all four. Suite money paths green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Red-team authorization findings:
- New requireManagerial middleware. Applied to POST /documents/:id/cancel,
/credit-note (financially destructive — reverse money / consume a number)
and /documents/:id/share (mints an UNAUTHENTICATED public PDF link — the
IDOR exfiltration high). Any signed-in staff could previously do all three.
- PATCH /interactions/:id now 403s unless the viewer is the interaction's
author or managerial — staff can no longer overwrite another employee's
logged call/visit.
New authz-gates test locks both. Note (deliberate): general client read/edit
stays open to staff — a shared support-team book is the intended model; the
genuinely sensitive credential reveals are already managerial + rate-limited.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Red-team highs/mediums (auth surface):
- staff_user gains failed_count/locked_until; login() locks an account for 15min
after 8 consecutive failures, clears the counter on success (both engines).
- login() is now constant-time: a missing email still spends one scrypt verify
against a fixed dummy hash — no timing/enumeration oracle. Returns a typed
{ok:false,reason} instead of null.
- POST /auth/login gets per-IP (20/min) AND per-email (8/15min) fixed-window
rate limiting layered over the lockout; 429 on trip.
- Credential reveal routes (db-password, module password, module secret) get a
per-user reveal limiter (20/min) so one insider cannot bulk-decrypt everything;
added missing getClientModule 404 guards.
- app.set(trust proxy,1) so req.ip is the real client behind nginx/Caddy and the
per-IP limiters actually bucket per client.
- makeRateLimiter extracted to rate-limit.ts (shared, no server<->api cycle).
Tests: lockout, counter-reset, unknown-email, and existing auth all green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Production hosting artifacts (D20 hosting slice), previously untracked:
- Dockerfile: node:20-bookworm image, Chromium libs for puppeteer, builds
web + server, runs server.cjs on 5182
- docker-compose.yml: single hq service, external accurate-postgres via
host.docker.internal, localhost-bound (host nginx terminates TLS), env_file
- deploy/nginx-hq.conf: the reverse-proxy config
No secrets — .env and data stay gitignored and out of the image (.dockerignore).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Excludes node_modules/dist (rebuilt in-image; host copies are Windows-native),
.git, .env and data — matching the existing Dockerfile + docker-compose.yml.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two small improvements that were uncommitted in the working tree:
- .env.example: document HQ_OWNER_EMAIL; refresh the Postgres section (D19 delivered)
- pdf.ts: launch Chromium with --no-sandbox --disable-dev-shm-usage so it runs in a
container; inert on a dev machine, and we only ever render our own self-contained HTML
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per client-module in the Modules tab: a MilestoneChecklist (tick with
date, + Add step), reloading in place. New Onboarding work page: Stats
header + per-milestone board (done/total, pending badge, warn/ok tint)
with click-through to the pending projects for each milestone (client
links to Client 360). Nav + /projects route. typecheck + web build clean,
web tests 4/4.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
project_milestone: each client_module (a project) carries a checklist
seeded from the dated project.milestone_template setting (advance paid /
setup / installed / go-live / balance / AMC — owner-editable). Ticking
stamps the date; audited. ensureProjectMilestones is idempotent and
back-fills imported projects. Cross-project board (per-milestone pending
counts) + drill-down (who's pending go-live). Routes: GET/POST
/client-modules/:id/milestones, GET /projects/board, /projects/pending/:key.
assignModule seeds the checklist for new projects.
Also fixes a latent flaky-ordering bug: uuidv7() now bumps monotonically
within a process, so ORDER BY id is genuine creation order (the guarantee
pagination + the sync cursor already assume) — sub-millisecond ties no
longer fall back to the random tail. Suite 377 green, stable across runs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
usage_rate_band: a dated per-module rate card of volume bands (generic;
SMS packs the first user). resolveUsageRate picks the band by count
(highest min_qty <= count); below the lowest band = under the minimum
order. buildLines resolves price as: explicit per-line override >
tiered rate card > dated price book — so an SMS line prices by count
automatically, a save under 50,000 is refused, preview warns. Rates are
integer paise (0.40=40p), a new card is a new dated set of rows so issued
invoices keep their rate. Owner routes GET/POST /modules/:id/rate-card.
Tests lock the founder card to the paisa: 50k→₹23,600, 100k→₹43,660,
200k→₹80,240, 300k→₹1,06,200 incl 18% GST. Suite 372 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ensureModule seeds each module field_spec (SMS/RTGS/MOBILEAPP declared
fields; ATM/WHATSAPP empty — their one datum stays in the D20 provider
column), back-filling only when a module spec is still empty so an
owner-customized spec is never clobbered. SMS/RTGS/MobileApp service data
now writes into client_module.field_values keyed to the spec; provider/
username/portal-password keep the D20 columns; unkeyed values still land in
details so nothing is lost. Tests assert seeded specs + keyed values; suite
363 green. Real dry run unchanged: 273 clients, 150 docs, 167 links.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Modules page: owner FieldSpecEditor where a module defines its own fields
(label, snake_case key, type, select options, required, reorder) → saved via
patchModule({ fieldSpec }); staff read-only. Client 360 service card: below
the D20 login block, a dynamic form built from the owning module fieldSpec —
one typed input per field (text/number/date/boolean/select), required honored,
non-secret edits collected into one PUT; secret fields as ••••/Reveal/Copy +
Set/Update mirroring the portal-password pattern (managerial, audited).
Empty fieldSpec renders nothing extra. Additive — D20 UI intact. typecheck +
web build clean, web tests 4/4.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
module.field_spec (FieldDef[]: key/label/type/options/required) declares a
module's own operational fields, validated on create/update. client_module
gains field_values (JSON map of non-secret values, type-checked against the
owning module's spec) + secrets_enc (one AES-GCM blob over a {key:plaintext}
map; the clear key-list prefix lets the UI show which secrets are set without
decrypting). Routes: PUT /client-modules/:id/fields (staff), POST
/client-modules/:id/secrets + /reveal-secret (managerial, audited, plaintext
never logged). Additive over D20 — provider/username/password stay. Both
engines (SCHEMA + migrate guards + pg 003). 11 new tests; module suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>