Commit Graph

70 Commits (24f52fad0a8e9663e4df1efc88df140283eeaeb2)

Author SHA1 Message Date
Thomas Joise 24f52fad0a feat: theme palettes + Geist font + dashboard cockpit styles
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>
3 days ago
Thomas Joise fa15027f4d feat(d28): clickable 'Open ↗' link on module URL fields
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>
3 days ago
Thomas Joise 58c97994bd feat(d28): SMS gateway live balance sync (BalAlert API) + daily auto + low alert
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>
3 days ago
Thomas Joise 650cde43a2 feat(d28): #3 ticket desk — SLA aging + overdue filter + bill-from-ticket
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>
3 days ago
Thomas Joise e83d70a4ff feat(d28): #6 SMS credit review + low-balance upsell
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>
3 days ago
Thomas Joise 5761544c74 feat(d27): #8 audit-log viewer (owner-only)
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>
3 days ago
Thomas Joise 7f6e2ce904 feat(d27): #5 owner MIS cockpit
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>
3 days ago
Thomas Joise 419dfebbaa feat(d27): #1 Renewals page (command center UI)
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>
3 days ago
Thomas Joise 18e47eb448 feat(d26): functional-feature UIs — GST report, doc search, statement, bulk ops
- Reports: GST summary tab (FY month range, per-month CGST/SGST/IGST + total,
  emphasized TOTAL row).
- Documents: debounced free-text search feeding q.
- Client 360: printable account statement (Dr/Cr ledger + running/closing
  balance + advance, @media print isolation); buildStatement + 4 tests.
- Clients: owner-only bulk-edit selection -> set a field across many (POST
  /clients/bulk). Onboarding board: owner-only bulk 'mark done' on the pending
  drill-down (POST /projects/bulk-milestone). Both confirm + toast + refresh.
- api.ts: getGstSummary, bulkUpdateClients, bulkSetMilestone, documents q.
typecheck + web build clean, web tests 8/8, full suite 397 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 days ago
Thomas Joise bbe125892d fix(d26): accent picker off top bar + mobile UX pass + responsive billing
- 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>
4 days ago
Thomas Joise 9b920e50f9 feat(d25): bright doc type/status colours + sectioned client details + username UI
- Badge extended with green/blue/violet/gold/red tones (light+dark). Document
  TYPE badges: Quotation=gold, Proforma=violet, Invoice=teal, Credit note=red,
  Receipt=green (Documents, DocumentView, Client 360). STATUS brightened:
  paid/accepted=green, sent=blue, part-paid=amber, draft=grey, lost/cancelled=red.
- Client 360: new sectioned Details card (identity, full address, contacts
  labeled Office/Secretary/WhatsApp with mailto:/tel: links).
- Employees create form gains a required Username (login id) field + table
  column; Login field relabeled 'Username'.
typecheck + web build clean, web tests 4/4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 10b849a990 feat(d25): module display order (sort_order) + username login
- 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>
4 days ago
Thomas Joise ce6b564e2b feat(pwa/d25): installable PWA + phone polish
- 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>
4 days ago
Thomas Joise 35a26de442 fix(review): frontend quality pass — a11y, UX safety, error handling (medium/low)
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>
4 days ago
Thomas Joise c5480cf8cd feat(d22): P2 — onboarding checklist per project + Projects (Onboarding) board
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>
4 days ago
Thomas Joise 892ae9e96e feat(d21): P3 — module Fields editor + self-rendering service form
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>
4 days ago
Thomas Joise ea958ace40 fix(d20): ticket module suggestions read the live catalog, not a constant
A module created tomorrow appears in the ticket dialog with zero code
changes; the five APEX codes remain only as a loading fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 1d598e8a96 feat(d20): P3 — Tickets page, Client 360 tickets/branches, module service cards
Tickets: WORK page with whole-desk status chips + counts, Mine filter,
search, honest pager, per-row assign + status quick actions, Drop behind
confirm, New-ticket dialog (client type-ahead, branch select, module/kind
datalists). Client 360: Tickets tab (client-scoped, same actions) +
Branches section (add/rename/deactivate) + per-module ServiceDataCard
(provider/username, portal password ••••/Reveal/Copy managerial-only,
editable labeled details, remark). Also: /Apex/ git-ignored — real export
holds plaintext credentials, must never land in git.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 60698fc08f fix(iwa): final-review fixes — dialog layering/scrim/scroll-lock, a11y labels+disabled+alerts, settings guards+errors, edit-mode locks, strict offsets, titles
27 adversarially-confirmed findings from the multi-agent whole-branch review (2 deferred).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 days ago
Thomas Joise 94018b9ae4 fix(review): close all 8 findings from the D18 slice review
MAJOR
- Documents register: Convert & send no longer bubbles into row navigation
  (stopPropagation guard on the actions cell)
- Pipeline rows: convert_invoice now uses the same confirmed convert-and-send
  as the Documents page (was the old plain draft convert)
- APEX importer carries anydesk/os/district/sector: stg_client columns
  (+ migrate guard), optional CSV headers staged, commit INSERT maps them
- Clients page: district/sector filter inputs wired to the server filters
  (server side widened to LIKE for narrow-as-you-type)

MINOR
- WhatsApp/secretary typed contact roles: datalist on the contact Role input,
  labeled rows (with Copy) on the Client 360 support card
- Dashboard overdue + dues-aging now anchor on COALESCE(due_date, doc_date) —
  an unpaid invoice not yet due no longer shows as overdue (test added)
- PATCH /clients/:id is atomic: dbPassword + field patch in one transaction
- Audit same-txn: setClientDbPassword, stageCsv, and the import stage/commit
  route audits all wrapped in db.transaction

Suite: 345 tests / 66 files, typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise f30532b7d1 feat(import): APEX cutover web UI — stage/verify/commit over the existing importer (Phase 7, D18 WS-B)
- 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>
4 days ago
Thomas Joise b5d252b7dc feat(shell): sidebar collapse + My Day dashboard (Phase 5, D18 WS-E)
- 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>
5 days ago
Thomas Joise 30c2e3b60b feat(clients): support-access data — AnyDesk/OS, district+sector filters, encrypted DB password with audited reveal (Phase 6, D18 WS-F)
- 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>
5 days ago
Thomas Joise 28aa08c9c9 merge: Task 9 polish sweep from isolated worktree (favicon, titles, toasts)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 828f533a69 feat(templates): section-per-module quotation PDF (Phase 9, D18 WS-H)
- QUOTATION renders one titled section per line: module name, its what's-
  included block, SAC + qty × rate + amount; sections never split mid-page and
  each module after the first opens a fresh page in print
- summary line up top ('This proposal covers: …'); totals/terms/signature
  unchanged; INVOICE/PROFORMA/CN/RECEIPT keep the compact grid (regression-
  asserted); preview-fidelity golden test untouched and green
- 3 tests; suite green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise 747026fa3b feat(modules): roster full-edit + assign/unassign; one-click Convert & send on the documents register (Phase 8, D18 WS-G)
- 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>
5 days ago
Thomas Joise a882d108fe feat(hq-web): favicon, per-route tab titles, success-toast sweep
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 3f7f08a908 feat(hq-web): user chip opens a Profile/Logout menu (Phase 4 finish, D18 WS-E)
The chip was a bare link to /profile with a separate logout icon; the spec
(§5) calls for a small menu. Chip is now a button toggling a two-item menu
(Profile / Logout) that closes on outside click, Escape, and route change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise 08dda8bab3 fix(hq-web): edit dialogs clear blanked optional fields (follow-up, outcome, plan amount)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise d95bf6d467 feat(employees): self-service profile + change-own-password; phone/title fields (Phase 4, D18 WS-E)
- 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>
5 days ago
Thomas Joise 1a8a3faf5d feat(hq-web): AMC/plan/interaction/employee/module dialogs — edit everywhere, confirmed deactivations
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 59c41ee8f7 fix(hq-web): guard client dialog against double-submit and cancel-during-save
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 0aca8b42b4 feat(hq-web): client create/edit dialog with contacts repeater — clients finally editable
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise e612c5cf81 feat(hq-web): standalone Reminders page (go-live WS-C, phase 3)
- /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>
5 days ago
Thomas Joise 5d81550e34 feat(hq-web): send-email composer dialog + styled confirms — window.prompt/confirm eliminated
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 584fb291f8 feat(documents): paginated register with honest total + Documents page (Phase 2, WS-D)
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>
5 days ago
Thomas Joise bc140fc54c feat(documents): invoice due dates — terms default + override, honest overdue chase (Phase 1, D18)
- 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>
5 days ago
Thomas Joise 03402ba0e1 feat(ui): Toaster with pure store (TDD) + FormGrid; provider mounted at root
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 953cf1dff8 feat(modules): module→client roster, notify-all, CSV export (Phase 10, spec §10)
- 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>
5 days ago
Thomas Joise e9357bd8e6 fix(redesign): final-review fixes — palette races+a11y, avatar dark contrast, micro-label floor, drawer focus, pulse legend/labels/targets, warn contrast
26 adversarially-confirmed findings from the multi-agent whole-branch review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise fffcc2a05d feat(documents): one-click convert-and-send + proforma supersede (Phases 7-8, spec §8)
- POST /documents/:id/convert-and-send: guards before any write (proforma-only,
  Gmail alive, recipient resolved), ONE txn convert+issue, email strictly after
  commit — failed send never rolls back the issued invoice (warning instead)
- supersedeProforma: cancel (number stays consumed) + fresh linked PROFORMA
  draft carrying the payload verbatim; INVOICE hard-rejected to credit-note path
- POST /documents/:id/supersede route; DocumentView gains both buttons
- 10 tests; suite 280 green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise 400abc224f fix(funnel): review fixes — stop-chase on convert/cancel/lost, scoped ?status=, send guards, F3/F4 convert hardening
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>
5 days ago
Thomas Joise c22fc5ba1e feat(hq-web): split-panel login
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise c45b5692be feat(reminders): escalating quote follow-up (Phase 6, spec §7)
- 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>
5 days ago
Thomas Joise a244ddb2b0 feat(hq-web): reports chips + skeleton/error states across catalog pages
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 94019980bb feat(hq-web): document view record header + composer line cards
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 08bb25abfc feat(hq-web): relationship-pulse ribbon on Client 360 overview
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise eaecaedd3f feat(pipeline): cross-client chase-list — derived stages, schedule-driven bands, role-gated, paginated
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>
5 days ago
Thomas Joise c5a524bab5 feat(hq-web): Client 360 — record header, KPI row, tabbed sections, forms extracted
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 9aa376fa2b feat(hq-web): clients — status filter chips, mono codes, skeleton/error states
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago