Same pre-ship audit fix as apply-onboarding-pipeline.ts and migrate-onboarding-
templates.ts: replace the raw `DATABASE_URL ?? ''` read with the shared
requireDbUrlForEnv() resolver, log the resolved engine/target before mutating,
hard-fail instead of silently opening a throwaway SQLite file in production, and
close the DB handle in a finally block.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
FIX D: stalledProjects treated a no-tick project's missing done_on as the
sentinel '0000-00-00', always older than any cutoff, so every
freshly-assigned project immediately showed up on the "Onboarding stalled"
tile. It's now measured against client_module.created_at (stamped by
assignModule); when that's also unknown (predates the column, or an import
path that doesn't stamp it) the project is excluded rather than assumed
ancient.
FIX E: STEP_STATUS only mapped the new template's keys, but TAIL_FALLBACK
(any module with no seeded/owner-configured tail -- WHATSAPP, ATM, AMC, ...)
still uses the legacy 'installed' key, so ticking "Installation done" on
those modules left status at 'quoted' forever. Added installed->installed
to STEP_STATUS (checked: no legacy payment key needs a mapping, since
payment never drove status even in the new vocabulary).
FIX F: bulkSetMilestone did a bare UPDATE, so a bulk go_live never advanced
client_module.status and a bulk untick never cleared a stale
payment_id/document_id link. Each id now routes through setMilestone itself
(same status-advance + link-clearing path a single tick uses), inside the
existing transaction -- nested transaction() calls become savepoints on
both engines, so a project without the milestone key is caught and skipped
without losing "bulk ops only touch projects that have the milestone".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
advance_paid/balance_paid (and the interim advance_payment/balance_payment)
all collapse onto the single new payment_received key, and the carry used
Map.set -- whichever row was visited second silently overwrote the first's
done_on/payment_id, counted as neither carried nor dropped, with the audit
`before` left `undefined` so the loss was unreconstructible.
pickSurvivingTick() now picks deterministically (prefers the tick with a
real linked payment_id; otherwise the later done_on), the losing tick is
counted in a new `collapsed` field, and the audit `before` payload is now
the full pre-migration row set instead of `undefined` -- any discarded tick
is reconstructible from the append-only log.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GET /projects/stalled had no try/catch, and a caller-supplied ?days= flowed
straight into `new Date(...).toISOString()`. Confirmed the exact crash:
Number('1e300') passes Number.isFinite (so the old guard let it through),
but the resulting cutoff math overflows the valid Date range and
.toISOString() throws RangeError. Uncaught in an async Express handler this
is an unhandled rejection, which kills the whole process on Node 22 -- any
authenticated role, including plain staff, could take the console down with
one GET.
`days` is now validated (integer, 0-3650) with an explicit 4xx on anything
hostile, falling back to the configured default only when the param is
absent (matches the frontend, which never sends ?days= at all). Audited
every other route this feature added to the same section: /projects/board
and /projects/pending/:key had the identical no-try/catch shape and are now
wrapped too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
apply-onboarding-pipeline.ts and migrate-onboarding-templates.ts resolved the
engine as `DATABASE_URL ?? ''`, diverging from server.ts's production
fallback. On the prod box (DATABASE_URL unset) each script opened a brand-new
empty SQLite file, "succeeded" against it, and Postgres was never touched.
resolveDbUrl()/requireDbUrlForEnv()/pgTargetDescription() now live in
db-pg.ts as the one place this is decided; server.ts and both scripts call
it. The scripts additionally hard-fail (never fall back to SQLite) when
NODE_ENV=production resolves nothing, log their target engine before doing
any work, and close the DB handle in a finally.
Also lands client_module.created_at (nullable, additive) — schema groundwork
FIX D uses to measure a no-tick project's age from its real creation date
instead of treating a missing done_on as infinitely old.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
26 changed files with 802 additions and 173 deletions
// Pre-ship audit FIX C: advance_paid AND balance_paid (and the interim advance_payment /
// balance_payment) all collapse onto the single new `payment_received` key. The old code
// used Map.set, so whichever row was visited last silently overwrote the other — its
// done_on AND payment_id lost, counted as neither `carried` nor `dropped`, and the audit
// `before` was `undefined` so the loss was unreconstructible.
it('when two old keys collapse onto the same new key, deterministically keeps the linked tick, counts the loser, and records the full pre-migration state in the audit before payload',async()=>{
constcmId=awaitmakeSmsClientModule()
constclient=awaitdb.get<{client_id: string}>(`SELECT client_id FROM client_module WHERE id=?`,cmId)
constpaymentId='pay-collapse-1'
awaitdb.run(
`INSERT INTO payment (id, client_id, received_on, mode, amount_paise, created_by, created_at)