113 Commits (main)

Author SHA1 Message Date
Thomas Joise a267cd07e6 merge: readable linked payment/document chip + SMS alert loading/error state
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

# Conflicts:
#	apps/hq-web/src/pages/Dashboard.tsx
1 day ago
Thomas Joise 367fc0324e fix(dashboard): never report "all good" when the fetch failed
The SMS low-balance alert rendered only when the request succeeded with
lowCount > 0, so a FAILED check looked exactly like "no client is low" --
the dangerous direction. It now has three distinct states: a skeleton
while the check is in flight, an ErrorState with retry when it fails, and
a quiet positive caption only for a loaded, genuinely-empty result (which
also spells out `unknownCount`, since balances the gateway could not
return are not "fine" either).

Same three-state audit on the rest of the page: the "Onboarding stalled"
tile had the error case fixed already but still showed count 0 and
"Nothing stalled." while its fetch was in flight -- it now takes a
`loading` prop (new on Card) and shows a skeleton, and its count appears
only once the list has loaded. Every other bento card is fed by the one
dashboard payload, whose loading/error/empty states are already
distinguished at page level.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise b32f7c32c8 fix(client-detail): make the linked payment/document reference legible
The status-line reference under a done step was `white-space: nowrap` +
`overflow: hidden` inside a 104px node, so the linked payment's amount --
the only reason the chip exists -- was ellipsised away, and the `title`
that could have revealed it held static text instead.

- the reference now wraps (up to two short lines) rather than clipping;
  it grows the node downwards only, so the track keeps its 112px columns
  and its horizontal scroll on narrow screens is unchanged
- `title` + `aria-label` now carry the real detail: payment date, amount
  and reference / document type, number, date and payable
- the document chip (quotation_sent), which clipped the same way, is
  resolved against the ledger too and shows the document number instead
  of the static "view document ->"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 4de152a7f4 fix(ui): restore sticky table headers, dashboard loading flash, ticket-type over-fetch
- packages/ui/src/tokens.css: bound .wf-table-wrap to a viewport-derived max-height
  and matching overflow-y so it's a real scroll container again. overflow-x: auto
  alone left overflow-y at its computed "auto" (per the CSS overflow spec, a
  visible/non-visible axis pairing promotes visible to auto) with no height bound,
  so its scrollTop was permanently 0 and table.wf th's `position: sticky` never had
  anything to stick against — the header just scrolled away with the page on every
  table screen. Chose the bounded-wrapper fix (over deleting the dead sticky rule)
  since it keeps the header pinned as advertised; short tables render unchanged,
  long ones scroll inside the box exactly like the existing .dash-list pattern.
  Also moved the .wf-table-wrap rule out of apps/hq-web/src/app.css into tokens.css
  next to the other table.wf rules, since it's part of the @sims/ui component.
- apps/hq-web/src/pages/Dashboard.tsx: the "Onboarding stalled" card treated
  "still loading" the same as "genuinely empty", flashing "Nothing stalled."
  before the fetch resolved. Card now takes a loading prop and shows the usual
  skeleton placeholder while pending; the positive empty copy and error state
  are unchanged.
- apps/hq-web/src/pages/ClientDetail.tsx: getTicketTypes was fetched unconditionally
  on every Client 360 load for the tickets tab's Type column. Gated it behind the
  tickets tab being active (moved the tab computation earlier so it's available to
  gate the loader); NewTicketDialog's own getTicketTypes call for the new-ticket
  pre-fill is untouched.
- STATUS.md: prose under the verification table still said "The 416 figure..." while
  the table itself says 528 — updated the prose to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise a0225b2ba3 merge: frontend audit fixes + UI completion (status-line links, tickets guards, dashboard error state)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 0c3f6c2e3a fix(client-360): audit fixes for onboarding status line, tickets, dashboard
Pre-ship audit swarm findings on the Client Detail redesign:

- StatusLine's isPaymentStep now also matches the legacy advance_paid/
  balance_paid keys (the generic fallback template's vocabulary on modules
  with no seeded per-module tail — WHATSAPP, ATM, AMC, ...), so ticking the
  payment step there opens the picker instead of silently stamping a date.
- Every milestone-tick path (plain toggle, payment link, credentials,
  document link) now reloads the client-modules list so the module block
  header, summary table and status select never go stale after a tick.
- A done payment/document milestone step now surfaces its link (linked
  document -> /documents/<id>; linked payment -> date/amount from the
  loaded ledger) instead of showing no evidence the link landed.
- Tickets: guard TICKET_STATUS_LABEL lookups against non-status chip
  values (e.g. the synthetic "overdue" chip) that were throwing and
  blanking the whole console.
- Dashboard: distinguish a failed "Onboarding stalled" fetch from a
  genuine empty result instead of reporting "Nothing stalled." on error.
- Client 360 tickets tab: add the Type column, same rendering as the
  Tickets desk.
- Drop the redundant read-only status badge next to the editable
  ClientModuleStatusSelect in the module block header.
- Only mount NewTicketDialog while actually open, so it stops firing
  getModules/getTicketTypes on every Client 360 load.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 0bcbf8d410 fix(responsive): stop desktop tables and mobile rows from silently clipping content
Red-team pass on the redesigned Client 360 / Tickets / Dashboard / Modules screens found
real overlap/clipping bugs, all fixed:

- Client header still broke at 768px (tablet): the button cluster only stacked below the
  name under 640px, so at 768 the name got squeezed to ~38px and force-wrapped mid-word.
  Bumped the stacking breakpoint to 860px.
- Per-module Documents rows and the Payments Outstanding-panel rows were plain nowrap flex
  rows with no scroll; at 375px the amount/status badge silently clipped past the card
  edge with no way to reach them. Both now wrap.
- Biggest one: every DataTable silently lost its rightmost columns at desktop width
  (>=901px) — table.wf's display:block/table toggle assumed overflow-x:auto still worked
  once display flipped to table, but Chromium computes overflow-x as visible on a
  display:table box, so wide tables (Tickets' 11 columns) overflowed the viewport with no
  scrollbar. Fixed at the root: DataTable now wraps its <table> in a .wf-table-wrap scroll
  container instead of relying on the table element to scroll itself.

Verified against screenshots at 375/768/1440, light+dark, before and after; typecheck and
the full test suite (498 passed) stay clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 7494f53e2f fix(client-detail): payment step gate matches shipped key payment_received
The status-line payment picker only fired for advance_payment/balance_payment,
which no shipped template contains (the composed tails and the migration both
standardise on payment_received) — so payment linking was unreachable and
project_milestone.payment_id was never written by the UI. Keeps the legacy keys
for projects not yet migrated. Also refreshes the stale STATUS test count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 357735e04d feat(client-detail): quotation step links a document; tickets get a type
Frontend half of two Client-Detail follow-ups, plus the StatusLine doc refresh.

Quotation step -> document link:
- Milestone gains documentId; setMilestone threads an optional trailing
  documentId (contract: doneOn, paymentId, documentId).
- Ticking `quotation_sent` on the status line now opens DocumentPickerDialog
  instead of stamping a bare date. It mirrors PaymentPickerDialog exactly —
  same state shape in ClientDetail, same Dialog props — and lists the client's
  QUOTATION/PROFORMA documents straight from the already-loaded ledger (no
  extra fetch), with "Create new document…" handing off to the composer.

Ticket classification:
- Ticket gains `type`; getTicketTypes() reads the `ticket.types` setting
  (config over code) with a code fallback only for the load window.
- New-ticket dialog gets a Type select; Tickets page gets Type filter chips
  wired to the ?type= list param and a Type column.
- Every ticket's module cell deep-links to /clients/<id>?tab=modules, and each
  Client 360 module block gets "Raise ticket for this module", which opens the
  new-ticket dialog pre-filled with that client + module code.

Also refreshes the now-stale StatusLine JSDoc so it describes all three
hand-off dialogs — payment, credentials (account_creation) and document.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 88a19f154e chore(client-detail): drop the dead .cdr-out .ofoot CSS rule
The outstanding panel never rendered a footer row — the rule has had no
matching markup since the panel landed. Removing it keeps app.css honest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 83633c86ee feat(dashboard): add Onboarding stalled tile
Surfaces stalledProjects() on the Dashboard bento grid, listing active
projects parked past the stall threshold; each row jumps to the
client's Modules tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise 4e747427d2 feat(onboarding): owner editor for shared front + per-module tail templates
Adds owner-only, audited routes for the two config settings milestoneTemplate
composes (project.milestone_template.front and project.milestone_template:<CODE>),
plus a Modules page section to edit them: staged label rows with add/remove/
reorder/Save, keys auto-derived from labels when blank, uniqueness enforced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 day ago
Thomas Joise db8ec9c546 feat(client-detail): prompt for module credentials on account_creation tick
Ticking the "Account creation & registration" onboarding step now opens a
small credentials dialog (mirrors the existing payment-step picker) instead
of a bare toggle. Saving reuses ServiceDataCard's exact patchClientModule
call and managerial password gate, then marks the milestone done; "Skip"
marks it done without touching credentials so the step never hard-blocks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 025b334b09 feat(client-detail): drop mandatory assign creds, add per-module status control, trim summary table
- AssignModuleForm: remove SMS-forced username/password gate at assign time
  (creds now captured later via module Access -> Edit); reword to "Add a
  module" with a one-line hint on what assigning starts.
- Modules summary table: drop Installed/Completed/Trained columns (dates
  live in the per-module status line); keep Module/Kind/Status/Billed/Settled.
- Each module's collapsible block now has an inline ClientModuleStatusSelect
  next to its status badge, so status is editable without opening the table.
- Payments tab: remove the Outstanding panel's duplicate "Advance on
  account" footer line -- the pre-existing StatCard is the single source.
- Remove RowDate, now unused after the summary-table trim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 78ed947422 feat(payments): outstanding panel + settled-to-document column
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise e8f00503a4 feat(client-detail): per-module documents list + bulk SMS purchase action
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise e5ed3517dc feat(client-detail): onboarding as horizontal status line with payment-step linking
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 9d3087c1fe feat(client-detail): module details as 50/50 report with single edit per group
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise 67f69fc999 style(client-detail): CSS for 50/50 sheet, status line, outstanding panel
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 days ago
Thomas Joise fdf31b4dbd feat(payments): ledger exposes outstanding docs + per-payment allocations
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>
2 days ago
Thomas Joise 9cb5127faf feat(d37): society category (Kerala co-op directory taxonomy) on clients
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>
2 days ago
Thomas Joise 809976fd8b feat(d36): login history (IP + device) + first-login change nudge
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>
2 days ago
Thomas Joise a81034d4ed feat(d35): client type (PACS / Store), classified by name
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>
2 days ago
Thomas Joise e8e9db33ea feat(d34): import employee master + invited/pending-login flag
- 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>
2 days ago
Thomas Joise 6e3a79adb8 chore(nav): move Clients up to Overview (after Dashboard)
Clients is the most-used screen, so it moves from the 'Clients & billing' group to the
top Overview group, right after Dashboard. Documents/Renewals/SMS credits stay in
Clients & billing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise da67b8b393 feat(sms): live-only balance list, no full-row red, mandatory creds on assign
- SMS balances now lists ONLY live SMS subscriptions (status='live'); quoted/installing
  ones are excluded (105 -> 79 real). The daily/manual gateway pull is likewise live-only.
- Removed the full-row red tint on low rows — the red balance badge alone signals it.
- Assigning the SMS module now REQUIRES the gateway username + password (both the Client
  360 'Assign module' form and the Modules-page '+ Add client' panel): Assign is blocked
  until they're filled, and they're set on the new subscription in the same flow. So a
  live SMS client always has the credentials the balance pull needs.
Test updated (assign live); typecheck + suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise 1bf0e4ee45 fix(ui): password field in the service-data Edit form (Client 360 modules)
The portal/gateway password could only be set via a separate 'Set…' button in the read
view — easy to miss, and absent from the Edit form (where you'd look for it). Add a
Password field to the Edit form (owner/manager) alongside Username: blank keeps the
current one, typing sets it. Rides the same audited PATCH the button used. So on a
client's SMS module you can now add/change the gateway password right where you edit
the login. typecheck + build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise 7742b583c3 fix(ui): group each client module into its own collapsible coloured section
On Client 360 → Modules, a client with many modules was one long flat stack of service
cards + checklists — hard to scroll and find, say, RTGS. Now each module is its own
collapsible section with a coloured accent header (module name + status + kind); all of
that module's data lives inside it. Click a header to collapse/expand. Headings stand
out so you can jump straight to the module you want.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise 6939115f6c fix(ui): pager alignment, one-line billing kinds, Fields explanation
- Table pagers (Modules roster, Client 360 tickets) used an unstyled .wf-pager so the
  summary text and Prev/Next butted together. Add .wf-pager flex styling: summary on the
  left, Prev/page/Next pushed to the right end.
- Module Details: 'Billing kinds' label + checkboxes now on one line (was stacked).
- Module Fields: add a plain-language explainer — what the fields are (with an SMS
  example) and WHERE they show up (each client's Modules tab + Catalog → Module data).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise 7f740a7e2e feat(ui): split module screen into Clients | Setup tabs + prominent Add-client
A module's detail screen was one long stack mixing day-to-day (who's on it) with owner
config (details/fields/quote/prices). Now two tabs:
- Clients (default): the roster + install details, with a prominent primary
  '+ Add client to this module' button (was a quiet 'Assign client…'). Tab shows the
  client count.
- Setup (owner only): Details, Fields, Quote content, Price book.
Opening a module always lands on Clients. typecheck + build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise 6178e0a6a3 feat(ui): friendly Add-a-field form for module fields
Replaces the raw per-row grid (label/key/type/options/help/required inputs) with:
- Existing fields as clean cards (name, plain-language type, required badge, options, hint)
  with Edit / Remove / reorder.
- '+ Add a field' opens a dialog in plain language: Field name, 'What kind of
  information?' (Text / Number / Date / Yes-No / Choice from a list / Secret, each with a
  one-line hint), Options (only for a list), Hint, Required?.
- The internal lower_snake_case key is AUTO-derived from the field name (unique) and never
  shown — existing fields keep their key (it ties to stored data). Each add/edit/remove/
  reorder saves immediately (no separate Save step). typecheck + build clean; backend
  fieldSpec validation unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise 8485e69ff0 fix(ui): module page as master→detail screen (clear which module you're in)
Selecting a module used to dump a stack of panels below the list with no indication of
which module — confusing (founder feedback on 'Fields — Mobile App'). Now clicking a
module opens its OWN screen: a breadcrumb + big header naming the module (name · code ·
SAC · kinds), a Back button, then clearly-labelled sections — Details (editable),
Clients on this module, Fields (plain-language: 'the details you record for each client
on this module'), Quote content, Price book. The list hides while you're in a module.
Row action is now 'Open →'. typecheck + build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise 56686dd4d6 feat: show + download SMS logins; install details in Module data
- 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>
2 days ago
Thomas Joise f0bba301e9 feat(docs): require a reason when cancelling a document
Cancelling an issued document now needs a reason: cancelDocument takes it, refuses an
empty one, and records it on the 'cancelled' event (visible in the doc timeline) and in
the audit after-image. The Cancel dialog gains a required reason box (the confirm stays
open with an error until one is given). Supersede & recreate cancels with 'Superseded &
recreated'. Documents are never hard-deleted (immutability) — cancel is the path, now
always explained. Full suite 416 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise d68fc257a2 fix(docs): summary line for legacy imports + tidy Record-payment panel
- Tax invoice line table: APEX-imported documents carry only totals (0 lines), so the
  table printed blank with just the grand total. Add a display-only fallback summary
  line derived from the doc's own totals — description from the doc-number's module
  prefix, qty 1, rate = taxable, GST% back-computed. Issued docs unchanged (immutability).
  New HQ documents already carry full line detail; unaffected.
- DocumentView: the Record-payment form dropped in bare between the share row and the
  PDF. Wrap it in a titled card with a Close button so it reads as a clean section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise b065332f89 fix(ui): explicit Edit button per module row (inline form), separate from client roster
Clicking a module row shows its clients (roster) — which fought with editing. Add an
Edit button per row (owner) that opens the inline 'Edit module' form directly (name,
SAC, billing kinds, multi-sub, active), stopPropagation so it doesn't also open the
roster; Save/Cancel close it. Row-click still drills into clients + field-spec + quote
+ prices. typecheck + build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise 432587c7a1 fix(ui): Client 360 shows documents + payments newest-first
The ledger returns rows oldest-first, so 'Recent documents' sliced the OLDEST 5 and
the Documents/Payments tables listed oldest→newest. Sort newest-first in the view
(by date then doc-no for docs; received-on then id for payments) so recent transactions
lead. Frontend-only; no shared query/test touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise 074e810415 fix(ui): edit-module details form + keyboard nav in New Document client picker
- Modules: selecting a module now shows an 'Edit module' form (owner) — name, SAC,
  billing kinds, multi-sub, active (via existing PATCH /modules; code stays the
  identity). Header hints that clicking a module opens its editors. Fills the gap where
  a module's core details couldn't be edited from the UI.
- New Document client type-ahead: arrow Up/Down move the highlight, Enter selects, Esc
  closes, hover tracks — proper combobox keyboard nav (was mouse-only). typecheck + build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise 23089a9b1f feat(d30): clients-per-module count on Modules page + comment fix
- Modules page now shows a "Clients" column — how many clients sit on
  each module — via one lightweight best-effort roster count per module
  (cell shows '…' until it loads, never blocks the table). (Modules.tsx)
- Fix a stale comment: the reminder-queue badge now lives on the Reminders
  nav item, not Dashboard. (Layout.tsx)

All sidebar count badges kept as-is (Reminders / Pipeline / Tickets /
Renewals / SMS / Clients / Documents-drafts / Modules). The dead
@fontsource inter+jetbrains deps were already dropped from package.json.

Typecheck clean; web build green. Verified live: Modules "Clients" column
renders (seed AMC module -> 0), badges intact, zero console errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3 days ago
Thomas Joise 8ed56a97d5 feat(d30): Home as landing page + more sidebar counts
- Make the Home hub the landing: `/` now renders Home and the Dashboard
  moves to `/dashboard` (nav item, routes, and the hub's Dashboard card
  all updated). Unknown routes still fall back to `/` (Home).
- Add sidebar count badges for Clients (total book size), Documents
  (drafts still to finish) and Modules (count) — neutral, best-effort,
  alongside the existing Reminders/Pipeline/Tickets/Renewals/SMS counts.

Typecheck clean; web build green. Verified live: / → Home, /dashboard →
Dashboard, and the Modules count badge renders from real data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3 days ago
Thomas Joise b8e2be628f feat(d30): trim Home hub to main sections + sidebar count badges
- Home hub (/home) now shows only the day-to-day sections — Dashboard,
  Clients, Documents, Reminders, Renewals, Pipeline, Tickets, SMS credits,
  Modules — as a flat card grid; everything else stays in the sidebar.
- Sidebar section count badges (Layout.tsx): Reminders, Pipeline, Tickets,
  Renewals and SMS now show a live count, fetched best-effort. Badges are
  neutral by default, amber for SMS low balance, red for failed sends; the
  reminder-queue badge moves from Dashboard to Reminders. Hidden when the
  rail is collapsed.
- Neutral count-badge base + hub spacing (app.css).

Typecheck clean; web build + 410 tests green. The server.ts prod DB
password is intentionally left uncommitted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3 days ago
Thomas Joise 69c59815f0 feat(d28): Portals quick-list + global search (clients/docs/tickets)
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>
3 days ago
Thomas Joise 3f974e9d3d feat(d28): Module directory — every client on a module x its fields
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>
3 days ago
Thomas Joise 343cc6795d feat(d28): Settings → Operations (SMS gateway URL, low threshold, ticket SLA)
'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>
3 days ago
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