clientLedger now attaches allocations (docNo + amount) to each payment and
returns an outstanding[] list — issued invoices with outstandingPaise > 0
plus open (draft/sent/accepted) proformas, oldest-first by docNo. Read-only
extension over the existing settlement math (outstandingPaise, allocations);
no money/allocation logic changed. Mirrors the widened Payment/Ledger shapes
onto apps/hq-web/src/api.ts for the upcoming Payments tab.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds client.category (SQLite migrate + PG migration 014), derived from the name to match
the official Kerala co-op classification: Service Bank / Urban Bank / Vanitha / Housing /
Employees / Agricultural / Marketing / Society (other) / Store / Other. Backfilled by a
portable CASE (most-specific-first); new/edited clients derive it via categoryFromName.
Clients screen gains a Category filter dropdown + column. Live backfill: 135 Service
Bank, 38 Employees, 15 Vanitha, 14 Store, 12 Agricultural, 11 Urban, … typecheck + full
suite (416) green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Captures every /auth/login attempt in a login_event table (SQLite schema + PG migration
013): staff_id (or null for an unknown user), username tried, outcome (success/failed/
locked), IP, and user-agent — best-effort, never breaks login. New owner 'Login history'
page (Admin): who signed in / failed, when, from which IP + parsed device, outcome filter,
paginated. login() now returns mustChangePassword; an invited user on their default
password is sent to Profile to change it on sign-in. typecheck + auth/employee tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds client.client_type (SQLite migrate + PG migration 012), backfilled from the name:
a name ending in STORE/STORES = 'Store', everything else = 'PACS' (14 Stores of 273).
New clients default the same way; editable per client. Clients screen gains an
All types / PACS / Store filter and a Type column. typecheck + client tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- staff_user gains must_change_password (SQLite migrate + PG migration 011): the
invited-employee indicator, cleared on the user's first self password-change.
- createEmployee takes phone/title/mustChangePassword; Employee carries mustChangePassword.
- scripts/import-employees.ts (tsx, Postgres- or SQLite-aware) seeds the 11 from
empmaster.xlsx: username = first-name slug, default password <username>@123, role staff,
active per ACTIVE_STAT, must-change flagged. Idempotent. Ran on live DB — 11 added.
- Employees list: 'Invited · must change pw' badge + a notice explaining the default
password pattern so the owner can share logins. Verified: adithya/adithya@123 → 200,
inactive raveena blocked. typecheck + employee tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- SMS credits screen (managerial): Username + Password columns shown plainly (D32
plaintext), a Show/Hide toggle, and 'Download logins (CSV)' (client, code, username,
password, provider, balance, install date, phone). Password decrypted server-side only
for owner/manager viewers (smsBalances includeSecrets); staff still see balances only.
- Module data directory: rows now carry the lifecycle install details — Status, Installed
date, Next renewal columns alongside the module's own fields, so the whole book shows
when each client went live plus all their service data. typecheck + tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
- POST /import/stage (CSV text), GET /import/status (report + first-50 problem
rows with honest totals), POST /import/commit — all owner-only, audited;
commit still refuses while ANY staged row carries problems (existing guard)
- /import page (ADMIN nav, owner-only): file pickers → problem tables →
verification stats (incl. invoice-value cross-check) → confirm-gated Commit;
re-stage freely, series seeding message surfaced
- 2 route tests (403s for staff; bad→refuse→clean→commit round-trip incl.
series seeding past legacy numbers); suite 344 green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- sidebar collapses to an icon rail (footer toggle, localStorage-persisted;
nav tooltips via title; <900px hamburger untouched)
- dashboardView gains mine-scoping: staff are ALWAYS scoped to their book
(clients they own, docs/payments they created, their follow-ups, their queue);
owner/manager get a Mine/Everyone toggle (?mine=1)
- 1 test; suite green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- additive client columns; toClient never maps db_password_enc (payloads carry
only hasDbPassword); listClients gains district/sector filters
- setClientDbPassword: AES-256-GCM via HQ_SECRET_KEY, refuses loudly without a
key; revealClientDbPassword decrypts + writes one audit row per reveal
- PATCH /clients/🆔 dbPassword is owner/manager-gated inside the same route;
POST /clients/:id/reveal-db-password (owner/manager)
- client dialog gains district/sector/AnyDesk/OS; Client 360 gains the Support
card (copy AnyDesk, •••• + Reveal/Set for managerial)
- 5 tests; suite green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- listClientsByModule rows carry cmId; ClientModulePatch gains kind/edition
(kind validated against module.allowedKinds — same audited write path as
Client 360, so the two views can never disagree)
- Modules roster: inline edit (kind/edition/renewal/status), ✕ unassign with
confirm (deactivates the link, totals shrink), Assign client… type-ahead
- Documents register: issued proformas get a Convert & send quick action
(same convert-and-send endpoint + confirm as the document view)
- 2 tests; suite 333 green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- GET/PATCH /me (self-scoped whitelist — role/email untouchable) + POST /me/password
- changeOwnPassword: current password required; every OTHER session dies in the
same txn, the changing session survives; audited without hashes
- staff_user.phone/title additive columns, surfaced on Employees (columns + edit
dialog) and the new Profile page; user chip in the shell opens /profile
- 7 tests; suite 331/331 across 64 files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- /reminders page: Queued/Sent/Failed/Dismissed chips mapping to ?status=,
managerial-only Mine/All toggle via ?owner= (staff are server-forced to
their own rows), kind/client/doc/due/status table with Send/Preview/
Dismiss on open rows, honest server-driven pager (rule 8).
- Extract the Dashboard queue actions into shared
components/ReminderQueue.tsx (QueueActions, RULE_LABEL, reminderTone)
with a re-entry guard on Send/Dismiss; Dashboard queue card links
"View all" to /reminders.
- api.ts: getReminders now takes { status, owner, page, pageSize } and
returns the full QueueItem rows + page metadata; login stores the
staff id (hq.id) so Mine can narrow by owner.
- Nav item under WORK + route; no backend changes. A contract test pins
the sent/dismissed chips, ?owner= narrowing and cross-chip staff scope
at HTTP level (apps/hq/test/reminders-page.test.ts).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
listDocuments now returns { documents, total, page, pageSize } (default 50,
cap 200, newest first) with the client name joined server-side — no N+1.
GET /documents passes page/pageSize through; type/status/clientId filters
compose against the same WHERE for both count and page (rule 8: no silent
caps). clientLedger walks the paginated repo in 200-row batches so the
ledger still surfaces every document. New /documents screen: type chips,
status filter, mono doc numbers, amount column, pager honouring the server
total; rows open /documents/:id. Nav entry under WORK (exact-match so New
Document keeps its own highlight).
Note: the GET /documents route hunk itself landed via 8861b46, swept into
a concurrent commit from the same working tree; this commit carries the
repo/ledger/web halves and the tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- document.due_date additive column; billing.payment_terms_days seeded (15)
- issueDocument stamps doc_date+terms on INVOICE when absent — covers manual
issue, convert-and-send and recurring generation in one place
- drafts accept dueDate (INVOICE only, validated); composer date field
auto-filled from GET /settings/billing; invoice PDF prints Due Date
- overdue scan + reminder anchor on COALESCE(due_date, doc_date); email says
'was due on X and is now N day(s) overdue' when a due date exists
- legacy fixtures pinned to due_date=NULL (sanctioned: they test the doc_date
anchor); 9 new tests; suite 309/309 across 60 files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- listClientsByModule: reverse lookup over active links, ordered by client name,
paginated with a true total; per-row dated price via priceOn + next renewal;
footer revenue total spans ALL links (active=0 excluded by design, stated)
- GET /modules/:id/clients (paginated) + /clients.csv (streams every row, no cap)
- POST /modules/:id/notify (owner/manager): resolves the full roster first, sends
via the Gmail path, one email_log + one audit row per attempted recipient,
names every unreachable client in the response (warn, never truncate)
- Modules page roster panel: table + revenue footer + Notify-all composer + CSV
- 8 tests; suite green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review findings on the quote-to-close slice, each verified against the code:
- convertDocument: ANY conversion now flips the source to 'invoiced' (previously
only ->INVOICE), so a QT->PI quote leaves the daily scan's 'sent' set and the
pipeline's Quoted/Waiting — the chase never resumes after convert (spec 7 stop
conditions). Scan additionally excludes quotes with a live forward child
(covers rows converted before this fix) and quotes of lost clients.
- convertDocument rule-4 guard (spec 8 F4): a live non-cancelled forward child
blocks re-convert — one sale, one legal document; cancelling the child
re-opens the path. Rule-2 fix (spec 8 F3): ->INVOICE recomputes the carried
lines through computeBill on the invoice's own doc_date (mirrors
createCreditNote); QT->PI stays copy-only (both non-legal).
- cancelDocument dismisses a cancelled quotation's open follow-up nudges in the
same transaction, per-row audited — dead paper is not chased.
- GET /reminders?status= no longer bypasses the owner scope and pagination: the
status param narrows the same scoped listQueue view. Web badge (Layout) now
uses the honest scoped total instead of an unscoped flat list length.
- listQueue/queueCounts staff scope keeps doc-less reminders (renewal_due,
amc_expiring, follow_up, email_bounced) visible — they have no derived owner
and are shared work; previously they vanished for every staff viewer.
- sendReminder resolves the recipient BEFORE minting a public share (a send
that can never succeed leaves no live artifact) and refuses quote_followup
sends loudly when share.base_url is unset instead of emailing a dead relative
link. The scheduler auto-drain parks a hard-failing send as 'failed' with its
reason instead of crashing the scan.
- reminderContext takes an injectable today (threaded from deps.now in the send
path) so dated schedule text resolves deterministically.
- Pipeline: quote-less lost clients (client.status=lost) now appear under
filter=lost (spec 9); Dashboard queue table marks first-page truncation
honestly (rule 8).
Typecheck clean; 270 tests green (255 before, +15 covering each fix).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- reminder.rule_kind gains 'quote_followup': SCHEMA widened + rebuild-once
migration via the shared rebuildTable helper (UNIQUE idempotency key and
status/policy_applied CHECKs preserved verbatim; guarded, idempotent)
- runDailyScan: new scan block over sent quotations anchored on the first
'sent' event; dated day offsets from resolveSchedule; catch-up guard fires
only the single highest crossed milestone; policy from setting
quote.followup.policy (default manual); auto sends drain strictly after
the scan writes commit, at-most-once via the unique key
- reminderContext resolves (never mints) a live share and dated subject/body;
graceful wording when the quote has no docNo; minting happens ONLY in the
send path with a ~60-day expiry, reusing a live share
- STOP cleanup: markStatus(accepted|lost) and convertDocument dismiss open
quote_followup rows per-row (each audited) in the same transaction
- listQueue: paginated with honest total, owner-scoped via
doc_id -> document.created_by, labelled rows; dashboard + /reminders wired
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 5 (D-PIPE option B, spec §6b/§9): repos-pipeline.ts listPipeline unions the
latest non-cancelled QUOTATION per client with bare leads (Enquiry); stage is never
stored; age anchors on the first 'sent' document_event; next action + colour band
read resolveSchedule('quote_followup') day_offsets (no hardcoded 3/7/14); staff are
server-forced to their own rows via ownerScope; Lost hidden unless filtered; sorted
actionable-oldest-first with LIMIT/OFFSET + honest total. GET /pipeline route plus
the Pipeline page (filter chips, managerial owner dropdown, band row tones, lifecycle
action buttons, pager) wired into nav right after Dashboard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 3 of the quote-to-close funnel spec: client.owner_id added additively
(SCHEMA + PRAGMA-guarded migrate) so a bare lead is routable to an employee;
setClientOwner repo fn (validates active employee, audits set_owner in-txn);
PATCH /clients/:id/owner gated inline to owner/manager; shared ownerScope role
gate (staff forced to self, widening ignored) for later phases; ClientDetail
gains an owner/manager-only Account owner dropdown fed by GET /employees.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 2 of the quote-to-close funnel slice. Six /employees routes in
apps/hq/src/api.ts (GET requireAuth for the picker; POST/PATCH/deactivate/
reactivate/password owner-only, acting as the signed-in user); Employees
page (table + add/edit/reset-password forms, email immutable on edit,
count shown, no truncation); owner-only nav item beside Document Template;
typed client fns in hq-web api.ts. 6 new route tests (192 total).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a Share & download group to DocumentView: Download PDF (hits
?download=1 via a bearer-authed blob + save anchor), Create/Copy a public
/share/<token> view link with the live-share list and one-click Revoke,
Share on WhatsApp (wa.me/<client digits> prefilled with greeting + doc no
+ total + the share link), and native share (navigator.share of the link,
falling back to Download where unsupported).
Client api.ts gains the Share type, shares on getDocumentFull, createShare/
revokeShare, and downloadDocumentPdf. Vite dev proxies /share to the hq
server so a minted link opens against the dev origin exactly as in prod
(the hq server serves both the SPA and the public /share route on one
origin). Server share/download endpoints and the golden renderer are
unchanged.
Browser-verified: minted a link, opened /share/<token> in a fresh
unauthenticated context (200 application/pdf, viewer renders the doc),
revoked and confirmed a 404 "Link unavailable" page with no data leak, and
confirmed the WhatsApp deep link is prefilled.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>