The worst file, but the errors collapsed to a few roots:
- typed throwStandardized(): never — it always throws, and saying so lets
TS follow the error-guard, which cleared all 16 'possibly undefined'
tx.* accesses (validation returns {error} xor {normalized}; the error
path throws, so normalized is always present past the guard — proven,
not suppressed)
- typed the normalizeTransactionFields `normalized` accumulator + the
dynamic body/next/existing params + SORT_COLUMNS maps
- parseInteger's {min,max} options default inferred as null; gave it a
real {min?: number|null; ...} type
- the rest: callback/helper param annotations, filtered.params spread casts
@ts-nocheck server count: 10 -> 9 (all 20 routes now checked; the 9
remaining are the intentional dynamic services/db files, next commit).
Server 252/252, transaction/spending suites green, probe 33/33.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
~141 errors, mostly param/callback annotations. Two real correctness
touch-ups the checker forced:
- bills.cts: the end-date guard was `if (ey != null)` but used `em` in
arithmetic; tightened to `ey != null && em != null` (they're validated
as a both-or-neither pair, so behavior-identical — but the guard now
says what it means)
- aboutAdmin.cts: `dateB - dateA` on two Dates -> `.getTime()` (coerced
fine at runtime; correct form now)
@ts-nocheck server count: 14 -> 10. Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dropped @ts-nocheck from admin/payments/monthly-starting-amounts/
subscriptions/authOidc/calendar/export (~113 errors). Mostly mechanical
(catch bindings, params, never[] arrays), but the checker earned its keep:
- types/http.d.ts: added the 4-arg res.download(path, name, options, cb)
overload — the v0.42.0 root-option download fix (admin backup + user-db
export) didn't type-check against the old 3-arg-only signature.
- routes/calendar.cts: deleted dead clampDay() (zero callers; was also a
standing lint warning).
@ts-nocheck server count: 21 -> 14. Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dropped @ts-nocheck and annotated the fallout (~54 errors, all mechanical:
implicit-any helper/callback params + dynamic response-assembly objects
in status.cts typed `any` per the shared db.d.ts philosophy). No behavior
change — pure annotation. Zero 'possibly undefined' cases in this batch.
@ts-nocheck server count: 29 -> 21.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CODE_STANDARDS 'every multi-statement write runs inside db.transaction()'
hits from the review: snowball plan create (abandon-existing + insert),
webauthn disable (credential delete + flag clear — a failure between them
recreates the stale-flag state), admin password reset (hash update +
session invalidation). Cleanups: dead ApiError import (payments), dead
action: params on the four snowball transition callers, createSession
consolidated into auth.cts's top-level import.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All found by the multi-angle /code-review over the branch and verified
against the installed libraries:
- 401 discriminator: the session-expiry redirect keyed off a path prefix
(!/auth/*) and falsely logged users out on /profile/change-password
with a wrong current password. requireAuth's no-session 401 now carries
the distinct AUTH_REQUIRED code — the only signal api.ts dispatches
auth:expired on; wrong-credential 401s keep AUTH_ERROR on any path.
Test updated to pin the code-based contract on both directions.
- OIDC RFC 9207: exchangeAndVerifyTokens rebuilt the callback URL with
only code+state, dropping the iss parameter openid-client v6 validates
when the provider advertises support (authentik does) — every OIDC
login would fail post-upgrade. The full callback query is now forwarded.
- OIDC http issuers: v6 enforces HTTPS on discovery/endpoints (v5
didn't) — breaking internal http:// providers on Docker networks.
OIDC_ALLOW_INSECURE_HTTP=true opts back in (documented in .env.example).
- 2FA downgrade: authService's webauthn fallback caught ANY challenge
error and silently downgraded to password-only login; now only the
stale-flag 'No registered WebAuthn credentials' case falls through,
everything else rethrows.
- Burned-challenge retry: the server consumes the single-use 2FA
challenge before verifying (anti-replay), so after a server rejection
a retry can only ever return 'Challenge expired'. LoginPage now resets
to sign-in on server failure (WebAuthn AND the pre-existing TOTP trap);
a browser-prompt cancel keeps retry (token still live). Also lazy-loads
@simplewebauthn/browser out of the entry bundle, matching ProfilePage.
Server 252/252, client 52/52, OIDC smoke 44/44, probe 33/33, e2e 27.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
analytics, settings, tracker, about, calendarFeed, privacy, version,
user, matches now fully type-checked by tsconfig.server.json. Fallout
was 4 errors total: calendarFeed's res.set(object) -> per-header calls
(+ the test mock now accepts both Express set() forms), and two error
adapters gained parameter types. @ts-nocheck census: 38 -> 29 files;
the B0 recon counter ratchets this down each cycle (QA-B0-04).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Build drops 6.5s -> 0.65s. Three breakages found by the prod-smoke gate,
all fixed:
- rolldown removed object-form manualChunks -> function form in
vite.config.mjs (same vendor split, all @radix/@tanstack grouped)
- the 4a 'send' bump rejects bare absolute paths: SPA fallback sendFile
and both download routes (admin backup, user-db export) now use the
root-option form
- rolldown minifies inline HTML scripts, breaking any CSP hash approach:
the pre-paint theme script moved to client/public/theme-init.js so
script-src 'self' holds with no inline hash to maintain
@testing-library/dom added (peer no longer auto-installed). Server 252,
client 52, probe 18/18, PROD SMOKE: PASS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
no-restricted-imports never fires on require() calls, so the ratchet was
toothless — switched to no-restricted-syntax on the require literal. It
immediately flagged four residual files whose multi-line res.status chains
the conversion greps had missed: user.cts (four err.message-at-500 leaks —
same class as QA-B13-02), calendar.cts, export.cts, and payments.cts's
DUPLICATE_SUSPECTED body (now a hand-built literal of the standard shape,
import dropped). Lint enforces the ratchet for real now; 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eslint 10's recommended set adds no-useless-assignment; fixed the three
real dead initial assignments it caught (version.cts updatedAt,
simplefinService claimUrl/accessUrl). Lint 0 errors; ratchet rules from
the standards batch carry over unchanged. Server 252, client 52.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The backend existed since 99abca9 with no way to reach it (QA-B1-01).
Now wired:
- LoginPage: requires_webauthn second step mirrors TOTP — the key prompt
fires automatically after the password; cancel/retry + back-to-sign-in
- ProfilePage: PasskeySection (mirrors TotpSection) — enroll with a name,
list keys, password-gated per-key remove + remove-all
- webauthnService: WEBAUTHN_ORIGIN stays authoritative; otherwise accept
the browser's origin when its hostname equals the RP ID — fixes real
dev/e2e enrollment where the Vite UI port differs from the API port
(the browser already enforces RP ID ⊆ origin, so no widened trust)
- Deploy safety: WEBAUTHN_RP_ID documented in .env.example + a boot-time
prod warning in utils/env.cts (localhost-bound keys silently fail
behind a real domain)
- e2e/webauthn.probe.spec.js: CDP virtual authenticator drives the full
lifecycle (enroll -> key sign-in -> password-gated removal), retiring
Cycle 1's 'needs a human with a key' assumption. Probe suite 18/18.
Server 252/252, client 52/52, typecheck + build clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Standards-unification batch 3. All inline standardizeError bodies (404s,
year/month/history-range/merchant validation, reorder payload checks) and
the drift/merchant-rule/sync try/catch-500 wrappers converted to thrown
ApiError factories. Two more err.message-on-500 leaks killed (SYNC_ERROR
wrappers). The import-historical transaction keeps its try/catch for the
log line but rethrows a coded ApiError instead of hand-rolling the body.
billReorder harness mirrors the terminal handler. Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Standards-unification batch 2. All inline 4xx bodies and try/catch-500
wrappers converted to thrown ApiError factories; validation helpers
(parseYearMonth, getAutopaySuggestionContext, rejectTransactionLinkedPayment)
now throw. Client-visible codes preserved (TRANSACTION_PAYMENT_LOCKED,
NOT_AUTO_MATCH, NO_TRANSACTION, RECLASSIFY_ONLY_SYNC, DUPLICATE_ERROR,
NO_DEBTS). Kills five more err.message-on-500 leaks in subscriptions.cts
(search/decline/catalog/descriptor wrappers).
Two deliberate exceptions, documented inline:
- payments POST / keeps the hand-rolled DUPLICATE_SUSPECTED 409 (carries the
'existing' payment payload the client's add-anyway flow reads)
- subscriptions /recommendations/create keeps 400-parity pass-through of
service validation messages via an ApiError wrap
Test harnesses (paymentsRoute, transactionMatchService) mirror the terminal
handler for thrown errors. Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Standards-unification batch 1 (CODE_STANDARDS 'Error responses', exemplar
categories.cts): inline res.status().json(standardizeError(...)) bodies and
try/catch-500 wrappers replaced with thrown ApiError factories; the terminal
handler formats 4xx and masks+logs 5xx. Wire shapes preserved, including
client-visible codes (ALREADY_MATCHED, DUPLICATE_MATCH, NOT_MATCHED,
NO_DEBTS, INVALID_PLAN_STATE, BANK_SYNC_DISABLED).
Also kills three more err.message-on-500 leaks in dataSources.cts (DB_ERROR
wrappers) that the QA-B13-02 grep missed; SimpleFIN errors keep their
deliberate sanitized pass-through via ApiError wraps (tokens/URLs stripped).
snowballPlanRoute.test.js harness now mirrors the terminal handler for
thrown errors (same wire shape as production). Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ten call sites returned internal error text to clients on server failures.
Intentionally user-facing messages survive, but only through controlled
paths: 4xx service errors (err.status < 500) and explicit ApiError wraps
(notification test-send SMTP/push feedback, coded rollback errors). All
5xx bodies are now generic; internals go to the server log / audit trail.
- notifications.cts, spending.cts: full throw-pattern conversion
(CODE_STANDARDS exemplar shape; terminal handler formats + masks)
- transactions.cts: bulk-unmatch per-item results mask 500-class messages
- import.cts: sendImportError user-facing branch tightened to 4xx-only
- admin.cts: bank-sync-config + migration-rollback mask unknown failures
(QA-B13-02; full conversion of the remaining routes continues in the
standards-unification phase)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'ADMIN ROUTES (MOUNTED AT /api/admin)' section was false — auth.cts is
mounted only at /api/auth, and the client calls the canonical copies in
admin.cts via /api/admin/*. The duplicates were unreachable dead code, and
/api/auth/has-users was additionally exposed without authentication.
Verified: no callers of /api/auth/has-users or /api/auth/users anywhere.
(QA-B17-01)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
authService.login returns a requires_webauthn payload for webauthn-enabled
users, but the login route only handled requires_totp and fell through to
result.user.id (undefined) -> TypeError -> 500 on every attempt. Any user who
enabled WebAuthn via the API (reachable today) was permanently locked out.
- routes/auth.cts: return the two-step requires_webauthn payload (mirrors TOTP)
- services/authService.cts: stale-flag guard — webauthn_enabled with no usable
credentials falls through to password login instead of hard-failing
- tests/authLoginWebauthnRoute.test.js: pins both paths + wrong-password 401
(QA-B1-01, lockout part)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Proves the POC in production: the category-groups list is now built with
the typed Kysely builder (category_groups added to the schema; COLLATE
NOCASE via sql``) and executed synchronously via all(). Byte-identical
behavior — categoryGroups test 4/4, suite 248/248, typecheck:server clean.
Establishes the migration recipe for rolling typed SQL out query-by-query.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
utils/logger.cts — console-compatible log.{debug,info,warn,error} with:
- level gating (LOG_LEVEL; debug in dev / info in prod), and
- REDACTION of secrets/PII (sensitive keys + bearer/JWT) so tokens,
passwords, and encrypted values never reach the logs (financial app).
Backend is abstracted (console now, swappable to pino later).
Swept all 317 server-runtime console.* across 35 files → log.* + inserted
the import at correct scope. tests/logger.test.js pins the redaction
contract (4 tests). CODE_STANDARDS updated; raw console.* is now banned in
server code.
Verified: check:server + typecheck:server + lint clean; 240/240 server
tests; client 50/50; build; e2e probe 17/17; boot smoke logs flow through
the logger and graceful shutdown intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/history 404 → throw NotFoundError (body was already normalized); drop the
generic 500 swallow. The /update-status catch is intentional graceful
degradation (returns a valid 200 fallback) — kept.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop try/catch swallows + standardizeError; throw ValidationError/ApiError
(tracker preserves result.status via ApiError). Test harness updated to
simulate the terminal handler. 236/236.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Establishes the canonical route error pattern (see docs/CODE_STANDARDS.md):
handlers `throw ValidationError/NotFoundError/ConflictError(...)` and let
Express 5 forward to the single terminal handler — no per-handler
try/catch swallows, no ad-hoc `res.status().json({error})`. Only the
UNIQUE-constraint → 409 mapping keeps a tight try/catch (`asConflict`).
- apiError factories NotFoundError/ConflictError now take an optional
`field` (parity with ValidationError) so form-field info is preserved.
- routes/categories.cts fully converted; identical status/messages, now
uniform `{ error, message, code, field }` bodies.
- Test harnesses (categoryGroups, categoryReorder) updated to simulate the
terminal handler (thrown ApiError → formatted response).
Verified: 236/236 server tests; e2e probe 17/17; live smoke confirms
400 VALIDATION_ERROR / 404 NOT_FOUND with correct field + status.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The demo seeder created bills + categories only, so nearly every page
rendered empty; clear-demo had a data-loss bug and error-protection gaps.
Seed now populates every user-facing surface: bills with autopay states,
3-6 months of payment history (with drift + debt paydown), monthly
skips/overrides/snoozes/notes, a demo bank source + accounts + matched/
unmatched/subscription transactions, categorised spending + budgets +
rules, planning income/starting-amounts, category groups, an active
snowball plan (+ extra-payment), merchant rules, and a calendar feed —
so a new user can explore Tracker (incl. overdue/drift), Analytics,
Summary, Spending, Snowball, Payoff, Transactions, Matches, Subscriptions,
Banking and Calendar with realistic data.
Correct removal (the emphasis):
- Migration v1.07 adds an is_seeded marker to the 12 user-scoped,
non-cascading demo tables (bill-children cascade with their seeded bill).
- clearSeededDemoData() (new, in userDataService alongside eraseUserData)
removes ONLY seeded rows in one transaction, child->parent, and keeps a
seeded category if a user's own bill still references it.
- Fixes the category collision: seed marks ONLY categories it creates, so
clear no longer deletes the user's default categories / uncategorises
their real bills.
Error protection: idempotency guard on seeded (not all) bills -> 409
instead of a fake "created 0"; whole seed wrapped in one transaction
(atomic); rate-limit + audit + standardizeError on all three routes;
seeded-status reports payments/transactions counts; dead --force removed.
Client copy/counts corrected.
Verified: typecheck/check clean; server suite 232/232 (+6 new); client
typecheck/lint/build clean; e2e probe 17/17; on a COPY of the real prod
DB the v1.07 migration applies (integrity ok) and a seed->clear round-trip
on the data-rich user restores their real data byte-for-byte with no
orphans.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the JS→TS server migration. server.js → server.cts (Node
type-stripping, no build step), and every launch/config reference now
points at it: package.json (main/dev:api/start/check:server) and the
Docker CMD.
scripts/seedDemoData.js is required at runtime by routes/user.cts, so it
crosses into the server runtime — migrated to .cts and its require of
db/database updated to the .cts path. The remaining scripts/*.js are
standalone dev/ops tooling (outside the runtime and the check:server
boundary); their stale extensionless requires of now-.cts modules were
repaired so they still run, and the PM2 ecosystem config entry point was
updated to server.cts.
Verified: 0 runtime .js remain in the server tree; typecheck:server and
check:server clean; 226/226 tests pass; `node server.cts` boots and
/api/health returns 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every routes/*.js → .cts, with handler signatures typed against the shared
http Req/Res/Next types. Routes carry @ts-nocheck for now — they're thin glue
over the (fully typed) services, so full route-level type-checking is deferred;
the handler-param typing is a head-start. server.js + test requires updated to
the explicit .cts paths.
Behavior-preserving. Verified: check:server 0, typecheck:server 0, suite 226/226,
real boot → /api/health + /api/about 200 (all routes mount).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the services/ TypeScript migration (0 .js remain). These four are the
largest, most dynamic modules (subscription-catalog matcher, xlsx/spreadsheet
parser, OIDC protocol client, SQLite user-DB importer); converted to .cts with a
top-of-file @ts-nocheck so they run via Node type-stripping now, with rich typing
deferred as an incremental follow-up. The other ~46 services are fully typed.
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CSV parser + import primitives (also shared by the OFX importer).
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tracker aggregation core (getTracker, safe-to-spend, bank tracking,
overdue count). Aggregation-heavy so money helpers are plain-required (any)
to avoid Dollars|null arithmetic friction, matching analyticsService.
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two of the large foundational services. billsService (validation/normalization,
balance math) is required by many others; notificationService (email + push +
drift digest) uses the shared http types indirectly.
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Batch of low-blast-radius modules: utils/apiError, and services statusRuntime,
loginFingerprint, auditService, transactionMatchState, updateCheckService,
advisoryFilterService, bankSyncConfigService.
- types/http.d.ts: shared permissive Express-ish Req/Res/Next/Router types
(@types/express isn't installed; handlers are dynamic) — for apiError's
errorHandler now and the routes/middleware batches next.
- Every require of a migrated module updated to the explicit .cts extension.
- Confirmed + documented the Node rule: a .cts needs at least one `import`
statement or type-stripping never activates (node --check fails on the first
type token); import-less modules lead with an `import type`.
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The encryption key must never be retrievable through the app (that would undo
the whole point of TOKEN_ENCRYPTION_KEY — a stolen session/XSS could then read
the master key). Instead surface a non-reversible fingerprint so operators can:
- verify which key is currently active,
- confirm a running instance matches the key they backed up, and
- spot an accidental key change (which would make existing secrets unrecoverable).
- encryptionService.keyFingerprint(): domain-separated SHA-256 prefix of the
active key (env if set, else the stored DB key); never force-creates a key
(returns null when none exists), shares nothing with the cipher derivation.
- admin bank-sync-config exposes key_fingerprint alongside encryption_key_source.
- Admin Bank Sync card renders it with backup guidance.
- Test: stable per key, differs by key, not reversible / not the raw key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The v0.42.0 brand refresh renamed client copy but missed a few user-facing
server strings:
- routes/about.js: the /api/about `name` (rendered as the About page title) was
still 'BillTracker' → 'Bill Tracker'.
- userDbImportService.js: two import-error messages referenced 'BillTracker'.
- Remove the now-unused APP_NAME export from lib/version.ts (0 consumers;
lib/brand.ts BRAND.name is the canonical source).
Left intentionally as identifiers (not display copy): the updateCheckService
User-Agent 'BillTracker/<version>' and all repository URLs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The daily reminder job ran at a hardcoded 6 AM. Make the hour configurable:
- dailyWorker: resolveReminderHour() reads the global 'reminder_hour' setting
(clamp 0–23, default 6) as the single source for both the cron expression and
the next-run display; the task is stored so rescheduleDailyWorker() can restart
it live (defensively wrapped so a bad value can't take the worker down).
- notifications admin GET/PUT expose reminder_hour; PUT clamps, persists, and
live-reschedules the worker.
- Admin Email Notifications card: a "Reminder send time" select (account-wide).
It's a global setting by design — there's one shared daily job, so a per-user
hour would require an hourly-worker refactor (out of scope) and would otherwise
be a dead setting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New POST /auth/logout-others keeps the current session and invalidates all
others (invalidateOtherSessions with the current session id), writes a
logout.others audit entry, and returns the count ended.
- api.logoutOthers(); a "Sign out of other devices" button in the Login History
dialog, guarded by an AlertDialog confirmation. On success: toast with the
count and refetch the history so it collapses to just this device.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- statusService: resolveDueSoonDays (clamp 0–31, default 3) replaces the
hardcoded 3-day threshold in calculateStatus; threaded via options.dueSoonDays
through rowOptions in trackerService (×2) and routes/calendar.
- New per-user 'due_soon_days' setting registered in the whitelist + defaults.
- Settings: numeric "Due soon window" row next to Grace period, with min/max +
clamp guard. Tooltips added to Grace period, Due-soon, and Price-change
sensitivity explaining each.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The payment-input gate (well-tested by Phase 1) migrates to TypeScript as
a .cts (CommonJS) module: keeps require/module.exports, imports the branded
Cents type from money.mts, and casts the require(esm) result so toCents/
fromCents keep their branded signatures. This proves the low-churn path for
the ~90 remaining CJS services/routes (no require→import rewrite needed).
Both server-TS patterns now proven end-to-end: .mts (ESM, type-source) +
.cts (CJS). typecheck:server clean; suite 225 + probe 17/17.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dates.mts (ESM, typed) joins money.mts — the two most-required server
leaves are now TypeScript. Added .cts to the server tsconfig for the
CJS-module migration path. typecheck:server clean; suite 225 + probe 17/17.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Establishes the server-TypeScript foundation and migrates utils/money →
utils/money.mts with branded Cents/Dollars (mirroring the client), so the
cents↔dollars boundary — the origin of the reconciliation bug class — is a
compile error for any typed server caller.
Approach (no build step): Node 25 runs .ts natively (type-stripping).
Migrated modules use the explicit-ESM .mts extension (the project is
"type":"commonjs", which disables .ts ESM auto-detection) and are required
from the CJS callers via Node's require(esm) — verified working. Infra:
tsconfig.server.json (allowJs + checkJs:false → incremental like the
client), npm run typecheck:server, check:server extended to .mts, wired
into ci. 28 require sites repointed to money.mts.
typecheck:server clean; full server suite 225 + e2e probe 17/17 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The deliberate manual POST /payments had no dedupe guard (double-submit
made a second payment), but silently deduping it like the auto paths
would lose a legitimately-identical same-day payment — a money bug. So
the server returns 409 DUPLICATE_SUSPECTED (existing payment attached),
and a shared client helper (client/lib/paymentActions.ts, used by the
tracker inline cell, partial-payment dialog, and bill modal) turns it
into an 'add anyway?' toast that resends with allow_duplicate. Automated
one-click paths keep deduping silently (a repeat there is a retry).
Dropped a now-unused api import in PaymentLedgerDialog (Track E sweep).
Server suite 188 green; typecheck/lint/build clean; probe 17/17.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>