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>
- 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>
- 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>
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 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>
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>
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>
New tables ticket + client_branch (both engines: SCHEMA + pg 002-apex-parity);
client_module gains provider/username/password_enc/details/remark with
migrate guards. repos-tickets: team-visible desk, honest pagination, status
lifecycle stamping closed_on, counts + kind suggestions from history.
repos-branches: minimal audited CRUD. repos-modules: service fields on
ClientModule (hasPassword only — ciphertext never leaves), validated details,
setModulePassword/revealModulePassword mirroring the client DB password
discipline. 5 new tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P1: every repo, route, scheduler path and test now speaks one async DB
interface (all/get/run/exec/transaction). better-sqlite3 stays underneath
for dev/tests via SqliteDb (statement cache, manual BEGIN/SAVEPOINT
nesting); openDb stays sync so fixtures remain one-liners. Zero behavior
change: 346/346 tests, typecheck clean.
P2: migrations-pg.ts — numbered Postgres migration set (001-init: bigint
paise, integer flags, TEXT ISO dates, TEXT JSON; reminder CHECK born in
final widened form) + schema_migrations runner.
P3: db-pg.ts — pg Pool behind the same interface: ?->$n rewrite outside
string literals, transactions pinned to one pooled client via
AsyncLocalStorage with savepoint nesting, int8/numeric parsed to JS
numbers so paise math is identical on both engines.
Also: reminder upsert INSERT OR IGNORE -> ON CONFLICT DO NOTHING
(portable, same changes-count semantics).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>