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>
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>
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>
- 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>
- /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>
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>
Browser-walk fixes (Task 12 step 3): reload the reminder queue after a failed send so the row flips to 'failed' live, and reload AMC contracts after recording a payment so the Paid badge derives fresh.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>