CHANGELOG
What's shipped.
User-facing updates — new features, performance wins, fixes, and security hardening. Synced live from the engineering changelog on every deploy.
v4.2.0
July 4, 2026Mobile-web excellence, billing and entitlement integrity, and structured loggingWhat shipped
- Polish
The mobile web app got a native-feel pass (Tasks #1238, #1241, #1246, #1247).
Viewport and Radix bundle wins, numeric keyboards via inputMode on number inputs, mobile Lighthouse budgets, web-vitals RUM segmented by device class, a tablet viewport in visual regression, touch-safe hover reveals, a 320px overflow guard on data-dense surfaces, offline coverage for the full money loop, PWA installed-app polish, network-aware route prefetch, Web Share and haptics, OS dynamic type honored at the root, swipe-to-mark-paid, pull-to-refresh, a virtualized bills list, and the notification bell in persistent mobile chrome.
- Polish
Native FCM v1 push delivery landed credential-gated (Task #1238).
Delivery stays off until real Firebase credentials exist, per the no-fake-sync rule.
- Polish
Backend logs are structured JSON with redaction (#1250).
Log lines are machine-parseable and sensitive fields are scrubbed at the formatter.
- Polish
CI spends less without losing coverage (#1253-#1257).
Smoke reuses the e2e build artifact, nightly Playwright shards trimmed from 3 to 2, the per-push E2E tier is path-gated to frontend-affecting changes, and superseded main runs are cancelled.
Fixes
- Security
Billing and entitlement integrity was remediated (Task #1236).
The F-series audit closed billing/entitlement findings (F004/F015/F031/F032), financial-correctness findings (F003/F027/F028/F010/F029/F030/F014), reliability findings (F008/F019-F021/F007/F011/F012/F026/F006), and security plus doc-drift findings (F013/F023-F025/F034) directly on main.
- Polish
The R-series remediation closed durability and privacy gaps (Task #1256).
A failed OCR job no longer deletes its source document, mobile TOTP verify uses an atomic attempt claim (closing a TOCTOU), csv_import_sessions is registered for account erasure, progressive-net allocation can never assign a negative net, the last-known-good Pro cache is evicted on entitlement revoke, the billing-ledger outbox dead-letters poison rows, and a dead-lettered OCR job marks its extraction row failed.
- Polish
Discarded offline-outbox entries are surfaced to the user instead of vanishing silently (Task #1238).
Discarded offline-outbox entries are surfaced to the user instead of vanishing silently (Task #1238).
- Polish
Stale billing-ledger tests were repaired (#1258, #1259)
to simulate the truly unconfigured state.
- Fix
The clipped descender in the landing hero headline was fixed.
The clipped descender in the landing hero headline was fixed.
Documentation
- Polish
KnownWaivers was refreshed for current app maturity (#1251)
with the additivity guard extended to Alembic migrations and DESIGN-001 promoted from Waived to Implemented.
- Polish
Deploy, secrets, and agent docs now match the Railway + npm reality (#1252).
Deploy, secrets, and agent docs now match the Railway + npm reality (#1252).
v4.1.0
July 3, 2026Design-system handoff, upload content hardening, and the OCR experiment shelved honestlyWhat shipped
- Polish
Viably design-system handoff primitives were adopted (#1206).
The handoff UI kit's primitives now back the app surface, and demo-session plus edge-auth regressions found while verifying the handoff were fixed in the same change.
- Polish
Uploads validate by real content type (#1215, #1216).
File-type checks now inspect actual content instead of the declared MIME header, and the detected content type is returned rather than mutating a read-only request property.
- Polish
Document OCR was built end to end, then shelved (#1211-#1218, #1220, #1225).
OCR ran via a Gemini-backed in-container worker with JSON-mode parsing, poppler support for scanned and image PDFs, and preprocessing fixes — but the pipeline did not meet the quality bar, so the upload feature is hidden from the UI and OCR mentions were scrubbed from app copy and the Pro upgrade callout instead of shipping a half-working promise.
- Security
A deep-audit remediation train closed all ten work packages (Task #1219, #1221)
and re-baselined code-review findings were resolved against current main (#1224).
Fixes
- Polish
SafeWeekHero status and mode pills are aligned again (#1230).
SafeWeekHero status and mode pills are aligned again (#1230).
- Fix
The shelved OCR pipeline was left coherent, not broken (#1212, #1213, #1217).
Completed results persist (purge_at serialization), the response parser is hardened, and image preprocessing saves as JPEG so enhancement is not skipped.
v4.0.1
July 2, 2026Security, billing, and privacy audit remediation in three wavesFixes
- Security
Auth hardening (wave C).
MFA verify-login attempts are capped atomically via claim_attempt, Google OAuth can no longer take over an existing local account, /register enumeration is throttled, and DDoS blocklist cold-start behavior was corrected to fail open so the app cannot self-DoS.
- Polish
Money-path honesty (wave D).
Non-positive CSV bill amounts are rejected, debt payoff beyond the forecast window is disclosed instead of implied, and financial context is computed single-flight so concurrent requests cannot double-compute.
- Polish
Retention and lifecycle (wave A).
Heal-stale routes through the purge cascade, purge_at blobs and rows are reaped, insight_snapshots are pruned, lifecycle sweeps drain fully, daily snapshots dedupe, poison OCR jobs are quarantined, and workers survive transient Redis outages.
- Polish
Billing resilience (wave B).
Entitlement rows carry source_event_id for webhook ordering, last-known-good Pro is served when the ledger is briefly unavailable, and only a full charge refund revokes Pro — partial and goodwill refunds keep the subscription active.
- Polish
Demo isolation (wave E).
Demo sessions are fully isolated from live accounts via distinct demo-scoped cookies, retryable offline mutations stay in the mobile outbox, and stress-lab plus mobile budget/paycheck surfaces render backend display labels instead of re-deriving money client-side.
- Polish
Step-up deadlocks were removed from billing (#1209, #1210).
Zero-factor accounts can bootstrap their first step-up factor, factorless accounts are routed to 2FA setup instead of a dead-end modal, and subscription checkout plus the Stripe Customer Portal no longer demand step-up re-auth; Stripe 15.3.0 object access was fixed alongside.
- Polish
Major-version tooling migrations landed (Tasks #1180-#1194).
ESLint 10, DayPicker 10, Sonner 2, UUID 14, js-yaml 5, Babel 8, Mypy 2, ReportLab 5, and Redis client 8 — and the frontend package manager moved from yarn to npm (Task #1190).
v4.0.0
July 1, 2026PostgreSQL cutover: Mongo retired, product and ledger stores split, platform modernizedWhat shipped
- Polish
All product state moved from MongoDB to PostgreSQL (Tasks #1018, #1021, #1025, #1027-#1101).
A SQLAlchemy 2.0 + Alembic foundation and a repository seam with per-domain backends landed first, shifts piloted the pattern, and then every domain followed: users (100% behind the seam), the entire auth family (sessions, passkeys, MFA, TOTP, step-up, recovery), pay rules and component history, notifications, documents and OCR state, billing outbox and financial-impact ledgers, cost-of-living datasets, workplaces, releases, and telemetry.
- Polish
The cutover ran on rails (Tasks #1103-#1113).
A cutover registry with per-domain gates, live two-store reconcile checks (reconcile_table and reconcile_all), a backfill-driver CLI completing the cutover triad, an Alembic-to-tables drift guard, shadow-compare with timezone normalization, an end-to-end cutover-lifecycle test that flips modes against both live stores, and a runbook kept the flip verifiable at every step.
- Polish
The backend fails closed by design (Task #1025).
Postgres reads and writes fail closed, startup fails on a stale schema, request paths and lifespan are guarded against direct Mongo access, and repositories own their own index setup.
- Polish
Product and ledger Postgres are separate stores.
Product Postgres owns user and app state; Ledger Postgres owns billing and entitlement ledger rows, keeping money-ledger writes isolated from app-state churn.
- Polish
Mongo is fully retired.
Every Mongo repository branch, fallback, validator, and runtime artifact was deleted; Mongo left the env template, compose file, and startup path. Remaining references are migration evidence and historical archive only.
- Polish
The platform was modernized underneath (Task #1167).
Node 24, Postgres 18, Redis 8, a Python 3.12 backend trial, dependency patch trains across frontend, backend, marketing, and mobile, and a refreshed ClamAV image digest.
Ops
- Polish
CI cost is governed.
Heavy jobs are gated to nightly and manual runs, a fast e2e smoke tier runs on every push to main, the docker smoke build is cached, and the e2e frontend builds once with shards downloading the artifact.
- Polish
The repo moved to a lean obligation-driven workflow.
The Hyper-Waterfall process tree was retired in favor of obligation-mapped changes with direct verification.
v3.28.0
June 26, 2026Twelve bug-hunt rounds: durability, truthfulness, tax re-verification, and live-HTTP CIWhat shipped
- Polish
Public trust pages were redesigned (Task #963).
Design, typography, and spacing were rebuilt and internal jargon was stripped from user-facing copy.
- New
Settings, shifts, forecast, paycheck, and dashboard got a polish and defensive-correctness pass (#959, #960, #962)
including hardened bulk-schedule backend paths.
- Polish
The 2026 tax engine was re-verified (#979, #983).
Flat-rate deduction handling was hardened, and head-of-household boundary plus West Virginia, Georgia, and Massachusetts 2026 rate corrections landed.
- Polish
Billing ledger integrity improved (#977).
GDPR ledger purge, entitlement resilience, and webhook idempotency.
- Polish
A live-HTTP integration tier joined CI (#1017, R11-R12).
A curated suite runs against a booted backend, expanded with COL-freshness and BLS-loader coverage plus a full-stack frontend-and-backend job.
- Polish
Cost-of-living sources were refreshed with an official granularity upgrade (Task #1007)
alongside data-integrity indexes and observability (Task #1006).
Fixes
- Security
Auth and request safety (#978, #984).
TOTP replay prevention, idempotency key and error hardening, TOTP secrets decrypted before verification, and merchant_aliases purged on account erasure.
- Polish
Missing-data truthfulness (#980, #981, #998).
Plaid degraded balances render honestly, CSV imports no longer fabricate times, delete guards and NaN badges landed, key collisions and color-only affordances were fixed, viability location labels are honest, and paystub reconciliation is guarded.
- Polish
Financial correctness (#986, #990).
The same number is no longer computed twice by different surfaces, and ACTC earned-income base, pressure-map date, and no-payment debt cases were corrected.
- Speed
Infrastructure durability (#985, #991, #995, #999).
Redis-tracker backoff, idempotency TTL anchoring, OCR reclaim, WebSocket self-heal, cache-invalidation GC, worker requeue ordering, exactly-once Stripe webhook side-effects, worker dead-letters, web-push delivery hardening, and GDPR export fixes.
- Fix
Mobile stability (#982, #989, #994, #996).
GoRouter is built once and refreshed via Listenable, an MFA cast crash and double-navigation were fixed, transaction ids are deterministic, sync cursors are user-scoped, secondary actions retry after a 401 refresh, and cache state clears on logout.
- Fix
Offline correctness (#993).
Offline bill duplicates and offline shift refresh on outbox flush were fixed.
- Security
Notifications (#992).
Reconcile suppression, OCR notify isolation, and web-push delivery-failure and registration hardening.
- Polish
Deferred backlogs were reconciled (R7-R10, #1001-#1005).
Pay-rule validation, atomic receipt writes, OCR and step-up retention, time-off cascade, Plaid ordering, mobile IANA timezones, and a verify-first gate lock.
v3.27.0
June 23, 2026Pay Rules Studio Common Setup, PTO accrual, overtime double-pay fixes, and CI revivalWhat shipped
- Polish
A simple Common Setup form now fronts the Pay Rules Studio (#881, #883, #885, #887, #889, #891, #893, #895, #897).
A guided form (including an Evening Differential) covers the common case, hands off to the advanced builder for the rest, and the builder itself gained in-form Back/Next/Save, a sticky unsaved-changes save bar, a truly-fixed bottom step nav, a reorganized Verify This Rule section, and page-level polish.
- New
PTO/ETO accrual landed on pay rules (#899, #905, #907, #909).
Accrual is captured on the rule, balances are computed, per-period estimates and presets ship, PTO surfaces on the paycheck, and pay rules link to Time Off policies automatically or manually.
- Polish
Overtime multipliers stopped double-paying (PAY-001) (#875, #877, #879).
is_overtime and is_holiday flags are honored on studio rules, manual overtime-scoped multipliers respect the flag, and weekly overtime is no longer paid twice through overtime multipliers.
- Polish
Billing lifecycle handling matured (Task #946).
Webhook handlers for payment_succeeded, refund, and dispute with audited entitlement revocation, backoff polling with sync and timeout states, portal retry, tenant-scoped caches for reports insights, and reports-export truncation and progress UX with an error boundary.
- New
Pay-rule templates became backend-sourced (Task #900)
with picker polish and corrected consecutive-premium defaults, dashboard number coercion consolidated onto one shared helper (#901), and dashboard rhythm, empty, loading, and impact-rail polish landed.
- Fix
CI came back to life (Tasks #918-#957).
With Actions billing restored, generated-artifact drift was fixed, Playwright E2E was sharded across three runners, yarn and browser caches cut cold-start time, action pins were refreshed for Node 24, a markdown doc-link drift checker landed, and flaky command-palette and welcome-modal E2E races were fixed.
Fixes
- Polish
Worker containers stopped reporting false-unhealthy via a Redis heartbeat liveness probe (#912).
Worker containers stopped reporting false-unhealthy via a Redis heartbeat liveness probe (#912).
- Polish
Security floors were bumped (#916, #917)
for msgpack and undici alongside a Dependabot sweep of 34 frontend and 25 backend patch updates.
- Fix
Test isolation flakes were fixed (#903, #911, #913).
USDA freshness cross-event-loop isolation, DEPLOYMENT_MODE isolation in the strict-financial-cache test, and a loop-aware client hardening.
Documentation
- New
A Stripe-on-Railway billing runbook was added (Task #873).
A Stripe-on-Railway billing runbook was added (Task #873).
v3.26.0
June 19, 2026First-party Google sign-in and destructive-action safetyWhat shipped
- Polish
Google sign-in is first-party (Task #862).
The signup entry was restored, a first-party Google auth backend replaced the third-party dependency, the frontend flow migrated, the MFA callback was hardened, the email-verification gate is preserved after signup with a dedicated code form, and the production changelog release feed was restored.
- Polish
Destructive actions are provider-aware (Tasks #863, #865).
Google-auth users no longer face an impossible password step-up before destructive operations, financial reset requires a typed confirmation, and the step-up contract is provider-aware end to end.
- Polish
Demo sessions got cheaper and safer (Task #865).
Settings demo tools are gated, active demo sessions are reused, capped sessions are recycled, and the demo launch page moved to the warm theme.
Fixes
- Polish
Demo-unavailable failures are honest (RATE-001, DEMO-001) (#870).
Hitting the demo capacity cap shows a truthful unavailable state instead of a fake error.
- Polish
Warm landing contrast was strengthened (Task #867)
with pricing kept on the public warm surface and primary ink on landing copy.
- Polish
The backend unit suite is hermetic against a verbatim .env.example copy (Task #871).
The backend unit suite is hermetic against a verbatim .env.example copy (Task #871).
v3.25.0
June 18, 2026Warm and Dusk themes, plain-language voice system, and a June security-hardening programWhat shipped
- New
The app gained a warm editorial default theme and a theme picker (#805, #807).
The landing page was promoted to the warm editorial redesign and the app default moved to cream and terracotta with a picker for choosing themes.
- Polish
A Dusk cool-ink and teal theme joined as the editorial counterpart (#810, #818, #847, #854, #859).
Accent tokens, editorial type, chart and elevation polish, and two calm passes — softened rim-light, shadow halo, and borders — brought Dusk to exact Warm parity.
- Polish
A central voice system made copy plain-language (#821, #823, #825, #827, #829, #831).
Phase 1 de-jargoned copy and clarified paycheck, stability, and KPI surfaces; Phase 2 added a central copy module and voice guide, then migrated dashboard, bills, goals, paycheck, budget, debts, shifts, chart, and reports empty-states onto it.
- Polish
Money surfaces became answer-first (#833, #835, #837, #843, #845).
Bills, Goals, and Budget summary headers and the viability risk-band headline now lead with the answer, duplicate safe-to-spend displays were removed from the hero, the payday impact rail always shows recent changes while marking minor ones, and Trust, Security, and Privacy pages surfaced in the Settings nav.
- Security
A production security-hardening program landed (#819)
followed by resolution of all open Dependabot alerts across frontend npm and backend pip (#849).
Fixes
- Polish
Four audits were run and remediated in the same week (#851, #856, #857, #858).
A comprehensive review (plus five previously deferred items), an app functionality audit with low-severity remediations, a UI/UX audit with full remediation, and a fintech championship audit covering H1-H8 highs plus mediums and lows.
- Polish
Theme regressions were closed fast (#812, #814, #816).
A washed-out warm login surface, the active app theme leaking into public surfaces, and a lingering dusk class that made every theme render as Dusk.
- Polish
Silent default consecutive-shift premiums were suppressed (PAY-001 stage 1, #853).
Silent default consecutive-shift premiums were suppressed (PAY-001 stage 1, #853).
- Polish
Typography and focus polish landed (#839, #841).
Hard-coded 12 and 13px text moved onto the --vb-text scale and focus rings became focus-visible on non-Radix controls.
Documentation
- Polish
CLAUDE.md gained a codebase orientation (#855)
so agents and humans onboard from one map.
v3.24.0
June 13, 2026Pay-engine unification, API-contract tail, and overlay tokenizationWhat shipped
- Polish
Legacy hourly pay unified onto the component engine (#780, #784, #797, #798, #800, #801, #802).
ADR-018 decided how pay-rule multipliers stack with overtime, the legacy hourly path now computes through the component engine behind a verified equivalence golden, fractional weekly-overtime precision was fixed, an explicit overtime-stacking flag landed on pay rules with honest stacking disclosure, and the vestigial stacks_with_ot field was removed.
- Polish
Viability and decision API contracts were hardened (#779, #789, #790, #791, #792, #793, #794).
Decisions plus shift, viability share, CPI, scenario, export, and core viability routes moved to typed response models with dependency-injected auth.
- New
Overlay surfaces moved onto --vb-overlay tokens (#778, #785, #786, #787, #788).
Pay-stack, app-route, dashboard, and remaining overlays were tokenized and pill buttons were removed.
- Polish
Navigation and information architecture got calmer (#763, #796, #799).
Onboarding drafts persist, mobile breadcrumbs are clickable, nav renames and a Planning grouping landed, the quick bar keeps five distinct destinations, Planning hub routes consolidated, and Income Baseline folded into Forecast with /income-floor redirecting.
- Polish
A money-path E2E evidence gate landed (#783).
Money-surface changes now require executable end-to-end evidence.
Fixes
- Security
Frederick County, Maryland local income tax now uses marginal brackets (#795).
A re-audit of the local-income-tax work found the county's brackets applied flat-per-tier; estimates now apply them marginally.
- Security
Viability cashflow audit follow-ups were remediated (#762)
and useful local test-stability fixes were recovered (#761).
- Polish
Release gate blockers were cleared and patched backend dependency pins are guarded.
Release gate blockers were cleared and patched backend dependency pins are guarded.
v3.23.0
June 12, 2026Post-June-6 financial veracity, calm UX, demo extraction, and landing refreshWhat shipped - financial veracity and cost-of-living accuracy
- Security
Financial veracity audit findings were remediated (#702).
The June 11 financial audit found 24 calculation, formula, constant, and financial-data issues; the same release remediated or formally waived every finding. APR clamping now surfaces disclosures and preserves payday-loan negative-amortization behavior, fixed per-paycheck deductions no longer overstate part-time net pay, mixed legacy/component pay rules preserve scalar differentials, percentage components use stable bases, exempt rules stop accruing weekly overtime, and frontend rounding now mirrors backend half-away-from-zero money behavior.
- Polish
State tax and payroll constants were corrected for 2026 (#702, #736, #737, #738).
Hawaii, Vermont, California, South Carolina, Wisconsin, Oregon, Maine, Minnesota, Massachusetts, Georgia, flat-tax states, MFJ schedules, HOH brackets, EITC earned-income approximations, and OBBBA overtime/tips disclosure all received either updated 2026 treatment or explicit waiver disclosure. Follow-up gates now verify tax vintage labels, COL tax approximation labels, and the financial audit ledger.
- Polish
Federal overtime and employee-side payroll taxes were modeled (#752).
The federal qualified overtime-premium deduction now covers tax years 2025-2028 with caps, phaseout, MFS ineligibility, sunset behavior, and visible assumptions, while CA, NY, NJ, WA, and HI employee-side payroll programs now feed paycheck estimates through capped 2026 tables. The same hardening slice closed transport benchmark netting, APR clamp disclosure, and debt-bill aliasing follow-ups from the deep-dive audit.
- Polish
Resident local income taxes now feed paychecks and viability (#759).
NYC resident tax and Maryland county/Baltimore City local income tax estimates now flow through the annual estimator, per-period paycheck adapter, paycheck tax profiles, PTO recomputes, viability targets, sensitivity analysis, alternate-ZIP comparisons, and income display rows. OH municipal taxes and PA EIT remain explicitly disclosed as unmodeled instead of being half-modeled.
- Polish
Cost-of-living transport and housing moved to truer source data (#709, #726).
The transport fuel multiplier no longer double-dampens state gasoline-price differences, and HUD ZIP-level SAFMR data now replaces county FMR when a ZIP is SAFMR-covered. The committed SAFMR dataset covers 38,601 unique ZIPs, NYC 10001 now resolves through the ZIP SAFMR blend, and COL methodology moved through 3.7.0 and 3.8.0 with golden-test updates.
- Polish
Viability provenance and residual claims were reconciled (#701, #710, #723).
Stale ACS/MIT/COL provenance-gap claims were re-verified against current code and turned into no-overclaim invariants, five previously unresolved viability findings were proven fixed and locked, and the obligations audit now executes under the heavy gate instead of self-attesting.
- Speed
Financial-context scale and warmup paths were hardened (#740, #748).
ACS ZCTA rents warm at startup, and the financial-context cache uses Redis when `REDIS_URL` is configured while preserving the local single-worker fallback. Multi-worker startup now requires Redis so safe-to-spend context cannot drift across workers.
What shipped - calmer dashboard, login, landing, and app surfaces
- Security
The calm UX gate became enforceable (#673, #686, #730).
`audit:calm` now guards jargon, page-job headers, registry drift, KPI count, and single-CTA budgets. User-visible jargon was removed from recovery-lane, allocation, and income-floor pages; every money surface has a page-job contract; and Playwright webServer backend wiring was corrected for the calm KPI/CTA E2E budget.
- New
Dashboard hierarchy and notice arbitration shipped (#711).
The dashboard now has one primary hero, a priority-ordered notice dock capped at two items, first-run notices collapsed into one slot, time-horizon labels on money figures, an inline stability explainer, and a true empty state for fresh accounts instead of fabricated-looking placeholder values.
- Security
Plain-language UI/UX follow-up landed (#703, #733).
The full UI/UX and cognitive-load audit produced an anchor report, then Phases 2-4 tightened plain language, flow friction, and polish without changing underlying financial math.
- Fix
Login and public landing polish caught up (#667, #672, #751, #754).
The login dashboard bounce was fixed, clipped landing-hero descenders were repaired, and the landing page gained the week-rail motif, GSAP scroll choreography, a focused hero motion layer, and phone-preview sheen so the public surface now reflects the current product instead of an older static story.
- Polish
Premium typography was stabilized after the serif experiments (#645, #649, #659, #662, #666, #670).
The app moved to Figtree plus self-hosted IoskeleyMono, limited display serif use to safe editorial contexts, reverted sidebar serif navigation, and standardized money numerals on aligned mono figures.
What shipped - Pay Rules Studio, public demo, and architecture
- Polish
Pay Rules Studio decomposition continued without behavior drift (#735, #743-#747).
Contract templates, the component history rail, the default tax profile card, pay bucket sections, pay rule basics, and preview shift controls were extracted from the large pay-stack page while preserving template data, test IDs, and pay-rule behavior.
- Polish
Public demo seed code moved out of the giant router (#695, #749, #750, #753).
Demo PDF writing, pay-rule component builders, static fixture data, and seed orchestration were extracted into `backend/services/demo/` modules. Fixture values, seeded persona anchors, route auth/reset guardrails, and public-session guardrails stayed unchanged while `backend/routers/demo.py` became smaller and easier to review.
- Polish
CSV shift imports now share one parsing contract (#755).
Legacy `/api/import/shifts/*` preview and confirm endpoints route through canonical CSV import primitives for byte caps, row caps, formula uninject, shift identity, and fingerprints while preserving the legacy response shape. Upload caps are now aligned at 10,000 rows and 5 MB, and oversized uploads return HTTP 413.
- Fix
Import and export API contracts were hardened (#756, #757, #758).
Legacy shift import preview/confirm now declare fixed response models and enforce auth through FastAPI dependencies, bills import moved auth into the same dependency pattern, and shift/bill/debt export auth now runs before handler bodies while preserving Pro gates and streaming CSV behavior.
- Polish
Settings, household, and shared helpers were cleaned up (#647, #651, #652, #661, #689, #742).
Settings split into domain homes, the deleted Household feature left behind no orphaned typedefs, relocation obligations were mapped and browser-verified, duplicated frontend helpers were centralized, and visual status-pill helpers were shared.
- Security
Deep-review hardening landed across contracts, resilience, docs, and CSRF trust (#696, #698).
The deep-review remediation train addressed financial honesty, API contracts, resilience, documentation, and CSRF/SameSite trust-chain follow-ups, then re-aligned stale backend validation tests so the verification suite matched current contracts.
Fixes
- Security
Pay-stack and settings regressions were fixed (#654, #655).
Audit-log rows missing an id now get stable keys, and pay-stack preview requests strip server-managed fields before sending payloads to the backend.
- New
Local and CI ergonomics were hardened (#646, #693, #707).
The frontend production launch config moved to port 3001 to avoid dev/prod EADDRINUSE collisions, the backend pre-push gate was unblocked after source-count drift, and pure branch-delete pushes no longer run the heavy pre-push gate.
- Polish
Dead CSS and preview surfaces were removed (#682, #684).
No-op `gpu-scroll` / `gpu-grid` classes and production-disabled design preview pages were deleted so the shipped app and audits track only live surfaces.
Documentation and validation
- Security
Whole-app documentation caught up (#675, #687, #688).
The comprehensive app spec and status specification now reflect remediated audit items instead of stale June 6 assumptions.
- New
Validation expanded with source contracts and golden locks.
The release tranche added or updated financial, COL, dashboard, calm-gate, Pay Rules Studio, demo extraction, Redis cache, import/export contract, local-tax, and obligations tests. Representative verified gates included 455 backend financial tests, 232 frontend unit tests, 2,528 backend unit tests for SAFMR, 2,542 backend unit tests for demo fixture extraction, 2,562 backend unit tests in the import/export hardening train, 2,610 backend unit tests after local-tax propagation, and Webpack production builds where the Windows Turbopack symlink limitation still applies.
v3.22.0
June 6, 2026Viability financial-correctness remediation, real data provenance, and a backend API-contract, governance, and UI-elevation programWhat shipped — financial correctness
- Polish
Viability tax math now uses real 2026 figures (#619, #616).
Real 2026 brackets and standard deductions landed for 18 progressive-tax states, Ohio's 0% first bracket is honored, the FICA wage base respects Section-125/HSA exemptions, and 2026 401(k)/HSA contribution caps are enforced. The dashboard net now includes refundable EITC and ACTC instead of dropping them, and `compute_taxes` no longer drops flat state tax for non-bracket states.
- Polish
The viability score became continuous and internally consistent (#620).
The score curve is now continuous (no discontinuous jumps at band edges), the risk band can no longer contradict the score label, and alt-ZIP comparison honors the active benchmark mode and the user's bills instead of silently dropping them.
- Polish
Outflow handling and advice got more honest (#621, #632).
Bills absorbed by the benchmark are surfaced rather than hidden, heuristic cost-of-living advice is flagged as such, shift-gap guidance is computed on a typical-net basis (not best-case net), and the TY2026 refundable Child Tax Credit cap ($1,700, IRS Rev. Proc. 2025-32) is verified and documented.
- Fix
Financial-truth P0s and the viability cache key were fixed (#402, #412).
Composition-level financial-truthfulness P0s were closed, and the viability cache key now incorporates the overtime field and material pay-rule inputs so changing them busts the cache.
- Polish
Composition-seam coverage and executable obligation verification landed (#618).
A composed end-to-end viability golden plus COL/waterfall/sales-tax invariants now guard the full pipeline, the input cache key busts on ZIP/income/filing/household/bills/debts, and the obligation audit actually executes and records its verification commands instead of self-attesting.
- Polish
Cost-of-living inputs are now real, sourced data (#626, #631).
Synthetic `acs_rent` was replaced with real ACS B25064 rent (25,751 ZCTA rows + 51 state medians), MIT Living Wage coverage claims were corrected to honest state-level, EIA natural-gas data was refreshed, and a provenance contract test now asserts that every displayed label matches its actual derivation.
- Polish
The viability frontend stopped fabricating numbers (#623, #431, #636).
Compare and DTI surfaces render through `MoneyLabel` (no more fabricated `$0` / `0%` when data is missing), viability panels render an honest missing-state, an `analyze()` race guard prevents stale results from overwriting fresh ones, the category bar gained a screen-reader summary, and a display-contract tail surfaced absorbed bills and closed the remaining VF-F2/VF-F5 gaps.
What shipped — backend contracts and structure
- Polish
Backend routes moved to typed, injected API contracts (ADR-016, T5).
Sixteen routers — dashboard, forecast, shifts, paycheck calendar, budget, time off, pay rules, goals, workplaces, household, shift templates, scenario, and the remaining T5 set (#607) — were migrated to `Depends(current_user)` authentication and `response_model` envelopes, each with new contract tests, plus active-session API responses are now typed (#580). Behavior is preserved (`extra="allow"` envelopes guarantee fields without changing payloads).
- Polish
The largest backend files were decomposed (ADR-004).
`passkey.py`, `col_admin.py`, and `admin.py` were split into router packages behind thin shims, and `viability_engine.py` was decomposed in phases (dataset-meta, display, debt, and obligations seams extracted into `services/_viability_engine/`) with zero golden-test edits.
- Polish
Route pagination and document-save contracts were salvaged (#615).
Useful in-flight work was recovered and landed as route pagination coverage and document-save ownership/contract checks.
What shipped — governance and best practices (F-series)
- Polish
Local enforcement gates were restored and hardened (#401, #409, #435, #439).
A local pre-push enforcement gate (F1) runs the fast lint/typecheck/unit subset (CI is billing-blocked), with the backend gate resolving the venv interpreter and a Windows Yarn fallback (#584).
- Polish
A strict-TypeScript ratchet was started and grown (#421, #556).
Strict typing began (F4) and expanded to pricing and the query fetchers/persistence layer.
- Polish
Money-validation and contract coverage expanded (#403, #424, #426, #544, #560, #561, #586).
Frontend contract validation, backend auth contract models, and a money-validation contract were added and extended across multiple endpoints (F2).
- Polish
Supply-chain and infrastructure governance landed (#407, #418, #420, #428).
Supply-chain provenance gates, a Docker-digest Dependabot train (F10), and an IaC waiver documenting platform-owned provisioning (F8) were added.
- Polish
Repo-wide formatting and a coverage floor were enforced (#413, #551, #587).
Prettier tooling plus a repo-wide format and `format:check` gate (F11) landed, alongside component + financial-store unit tests with a coverage floor (F7).
- Polish
Data-at-rest encryption was documented in the deploy checklist (#542/#550, F6).
Data-at-rest encryption was documented in the deploy checklist (#542/#550, F6).
- Polish
Dependency drift was guarded (#488).
A canonical requirements base plus a runtime-drift guard prevent silent dependency divergence (P2-6).
What shipped — UI elevation follow-up (A/B/C tracks) and surface
- Polish
Design-system enforcement and performance budgets landed (#400, #408).
A P8 design-lint enforces the `--vb-*` scales and a P7 lighthouse gate enforces CLS/LCP/TBT budgets.
- Polish
The A-track normalized iconography, radii, and motion primitives (#422, #430, #440, #443, #445, #433, #437).
A shared `<Icon>` wrapper + iconography scale, exact radii migrated to `--vb-radius-*` tokens, `Segmented` (pricing) and `AnimatedNumber` (score heroes) adoption, and a client-navigation focus-to-`#app-main` a11y fix.
- Polish
The B-track extended contrast gates and data-viz (#447, #449, #456).
A contrast gate now covers authed money surfaces (B2), shared chart primitives and DOM chart colors align to semantic tokens (B3).
- Polish
The C-track de-tinted the light theme to pure achromatic (#450/#451, #452).
Residual `text-violet-500` was dropped from the populated trend-chart header.
- Polish
The frontend stack was consolidated.
Icons standardized on Phosphor (dead `form.jsx` and `lucide-react` removed) and buttons consolidated onto the shared `Button` component.
- Polish
Typography was unified (#629).
574+ hand-rolled labels collapsed onto `.vb-label`, 386 non-label sites moved to `text-xs`, a fluid type scale and a weight ladder were adopted, and a type-floor design-audit rule was wired into the pre-push gate (sub-floor and rem-ratchet counts now zero).
What shipped — mobile, performance, and developer experience
- New
Mobile nurse UX was reintegrated (#604, #606).
The mobile-first nurse experience was reintegrated, with a post-login dashboard handoff hint.
- Speed
Login and navigation latency dropped (#588, #591, #605, #609).
Dev defaults to Turbopack with Sentry alias parity, login/sidebar route prefetching and cache priming plus a route-transition benchmark landed, and login latency / CSP hydration were fixed.
- Polish
Generated artifacts stay fresh (#589, #608).
Generated OpenAPI/API typedefs and release artifacts were refreshed.
- Polish
Scenario analysis was brought back online (#638).
The `/scenarios` LLM path resolves a provider via env fallback (Emergent → Gemini → Groq) and soft-grounds output (ungrounded dollar figures are redacted instead of returning a 503), with the AI-003 obligation preserved.
- Polish
Repo hygiene (#453, #475, #481, #489, #492).
A root Flutter duplicate, generated marketing logs, root `backend_test.py`, and a dead smoke-workflow were untracked/retired, and `_calculate_shift_gross_legacy` dead code was removed (#436/#438).
Fixes
- Security
Security hardening batch.
Email verify-code uses an atomic attempt gate with constant-time comparison (#498); the reset-password token is stripped from the URL after capture (#539); tokens in the public-audit PDF link are escaped (#540); the Apple receipt bundle check hard-fails and accepts in-app purchases (#513); mobile web refresh tokens now rotate (#571); a stale CSP `connect-src` waiver was closed (#578); the audit log declares its auth dependency (#583); preserved session security hardening was promoted (#585); security boundaries were hardened (#404); and the account-deletion obligation was verified (#574).
- Fix
Concurrency and performance fixes.
WebSocket Redis pub/sub commands are serialized against the reader loop (#509), distributed-cache transient failures use temporary backoff instead of permanent disable (#510), S3 objects are cached in `materialize_to_local_path` (#497), idempotency completed-response TTL is anchored to completion time (#522), and un-stamped in-flight jobs are no longer requeued as stale (#520).
- Fix
Financial correctness fixes.
The money-rounding contract is documented HALF_UP (the inverted "banker's" comment was corrected) and `compute_taxes` net is floored at zero to match `estimate_net_from_gross` (#469); `reverse_gross_from_net` threads self-employment income (#523); the transport COL category key is unified (#521); anomaly baseline/current values are made consistent with the displayed delta (#537); COL freshness is anchored to true data vintage (#538); the essentials reserve is prorated by actual days with disclosure (#487); state-tax fallbacks and shift-window assumptions are disclosed (#477); the monthly payday anchor is preserved (#575); consecutive-weekend shift flags are backfilled (#581); capped pay-rule shift repoints are repaired (#582); and deduction overshoot is exposed in the pay trace (#579). The `money()` formatter regression in the pay-stack review was restored (#496).
- Fix
CRITICAL mobile regression closed (#535).
The mobile offline outbox was passing a FastAPI `Depends` sentinel as the user, raising a `TypeError` and bricking keys; the auth dependency-injection path is fixed.
- Polish
Tail-round robustness (#566-#573).
A payment-ledger immutability guard plus Stripe webhook dedup (#566); persona attribution bound to signup context, no cross-user pollution (#567); OCR quota reservation released on extraction failure (#568); backend robustness for ISO week, CSV cap, OCR original-size, and funnel lease (#569); edge hardening with `TrustedHostMiddleware`, `perf_metrics` cardinality bounds, and an nginx Cloudflare-IP fix (#570); and FE polish for `USCostMap` NaN, `ShareLinkModal` local expiry, and `AnimatedNumber` mid-tween (#572).
- Fix
Smaller correctness fixes.
Funnel signups are attributed to a single persona (no cross-user double-count, #536); the goal Contribute action is guarded against double-submit (#511) with the goals contract subsequently restored (#534); the onboarding default date off-by-one (local date parts vs UTC) is fixed (#512); the Plaid connection `created_at` is preserved on re-exchange (#519); a feedback screenshot with a bad extension returns 400 not 500 (#524); silent except-pass blocks are logged and a background task holds a strong reference (#476); and reverse-gross preserves "unavailable" instead of fabricating `$0`.
- Security
Whole-app audit reconciliation (#593, #594, #610, #611, #612, #576, #577, #592).
A multi-phase audit-remediation drive (error/cache, auth/offline, mobile-a11y, financial-display, light-charts, payroll-math, security-hardening, backend-tails) plus a round-two pass closed correctness, a11y/mobile, and test-coverage findings; verified financial audit residuals were closed and the audit backlog docs / suppression reasons reconciled.
v3.21.0
June 3, 2026Monochrome design-system elevation, June security hardening, and query-layer TypeScript migrationWhat shipped
- New
The app pivoted to a monochrome "Vercel" design language (ADR-015).
Color classes across UI components were repointed to new design tokens, display headings were tightened app-wide, and typographic consistency (Vercel/Apple wording) was applied across authenticated and public surfaces.
- Polish
A staged UI elevation program (P1-P8) landed.
P1 foundation scales + an OKLCH-even neutral ramp that de-tints the dark theme; P2 component-primitive parity (states, tokens, semantic variants, Vercel-tight radii, Sonner toasts, unified inputs, Spinner/Kbd/indeterminate Progress); P3 layout primitives, prose measure, and a fluid display-type system across authed pages and public heroes; P4 motion primitives (`AnimatedNumber`, `Segmented`) plus cohesive interaction motion, spacing rhythm, and tactile `.vb-tap` press on shared icon buttons; P5 chart palette aligned to the monochrome neutrals; P8 a visual-regression harness that locks the design system in. See `docs/design/ui-elevation-roadmap.md` and `docs/design/visual-regression.md`.
- Polish
Login background and landing hero were upgraded.
A dark-first gradflow login shader (pinned `gradflow` 0.1.0, lazy + reduced-motion fallback) and an aurora effect on the landing hero shipped, with an `A11Y-MOTION-LOGIN` waiver documenting the animated-for-all-users decision.
- New
The onboarding tour was refreshed.
Tour steps and interaction handling were updated to match the new design language.
- Polish
App-level device protection shipped.
An app-lock feature with biometric authentication and Step-Up Authentication gained a Target ID so re-auth challenges bind to the specific sensitive action.
- Polish
The TanStack Query data layer migrated to TypeScript (L17).
`lib/query/persistence`, `lib/query/client`, and `lib/query/fetchers` are now `.ts`, with global style and cache-life type definitions added.
- Polish
Worker queue reliability was hardened.
A reliable processing-list queue, Postgres migration checksums, header-based export-truncation flagging, and streamed file-object uploads replace the prior best-effort paths.
- Security
The stack-maximization program was formally closed.
Its implementation closure report was published (Task #375); status now lives in `docs/audit/stack-maximization-closure-report.md`.
Fixes
- Security
June security-hardening batch.
Weak JWT secret is now fatal in production; scheduler lease TTL and paycheck-calendar sort guards added; route-limit bypass key handling reworked; recovery-code race closed; quiet-hours, demo seed/reset, and email-expiry paths fail closed; CSV import strips BOM and round-trips/dedupes correctly; path and image handling hardened.
- Security
Edge and infrastructure hardening preserved.
Mongo socket timeout, Postgres pool, nginx/Vercel edge config, compose secret handling, idempotency race + atomic claim coverage, account-erasure registry, and mobile security hardening were carried forward with tests.
- Polish
Rate limiter degrades gracefully.
It now falls back to in-memory storage when Redis is unreachable, with improved Redis tracker error handling.
- Fix
Financial correctness fixes.
Goal required-monthly-contribution uses calendar months; payroll golden fixtures and zero-value-present handling added across pay structures; pay-stack functionality and query invalidation improved; the app-shell mode strip renders backend money labels instead of reformatting client-side.
- Security
Audit ledger reconciled.
`PRIV-DELETE-001` downgraded to Implemented; deferred H-3 / queue / CSP items waived; demo reaper, cache fail-open, upload cap, and export-truncation flag landed on final files.
v3.20.0
May 31, 2026Mobile-first answer spine, route-audit closeout, and financial-trust hardeningWhat shipped
- New
Answer-first route structure expanded across the app.
Dashboard, shifts, paycheck, forecast, time off, bills, debts, budget, allocation, goals, household, income floor, recovery lane, snapshots, decisions, monthly review, and reports now lead with a direct answer and move proof/math into disclosure surfaces.
- Polish
Mobile money clarity became a route contract.
Mobile proof summaries, command surfaces, tap-target polish, skeletons, load-error notices, and route-experience tests were added so primary money routes stay usable at phone widths.
- New
Shared backend money display labels became the standard.
Dashboard, Paycheck, Forecast, Shifts, Bills, Debts, Goals, Budget, Viability, Reports, Pay Stack, Household, Recovery Lane, Documents, and related proof panels now prefer backend-owned labels/receipts over frontend-only money wording.
- Fix
Paycheck and safe-to-spend math were tightened.
Fixes covered null-hours crashes, Decimal-style paycheck reconciliation, trace gross/net reconciliation, allocation penny handling, essentials reserve behavior, monthly pay-period tiling, payday-boundary bill policy, and predictive-shortfall cushion sanity bounds.
- Polish
Pay Stack and Pay Rules Studio were hardened.
The visual pay stack now preserves backend receipt labels, avoids cramped overlap, uses shared money formatting, and keeps the four-bucket pay model aligned with pay-rule previews.
- New
Operating mode behavior was expanded.
Mode changes now invalidate/refresh more financial surfaces and appear on dashboard/paycheck/forecast surfaces instead of remaining a mostly decorative setting.
- Security
Route-audit fixes landed.
Auth refresh, public viability share, bills filtering, import dedupe, COL public download/search/admin guards, mobile sync, mobile outbox, documents, notifications, releases, observability, admin repair, demo caps, and frontend route efficiency issues were addressed.
- Security
A route-audit closure matrix was added.
`docs/audit/route-audit-2026-05-30-closure.md` reconciles the source report count mismatch, classifies every source bullet as fixed, duplicate, accepted with reason, or validation-limited, and lists the gates run.
- Polish
Flutter is now the recorded mobile direction.
ADR-013 and Task #251 retired the old native iOS path from active scope and made `viably_flutter` the sole mobile client scaffold.
- Polish
Self-service account deletion shipped.
The privacy route, UI, and E2E coverage now support account deletion without relying on manual admin work.
- Polish
The public domain/email migration moved to `viably.tech`.
Canonical SEO URLs, contact email references, and mobile API base configuration were updated from the older domain.
- Fix
Local developer recovery improved.
Session-start hooks and dev-stack recovery tooling were added to reduce broken-localhost restarts and stale-build confusion.
- New
Hyper-Waterfall process docs were added.
The repo now has a documented staged workflow for large audits and implementation closeouts.
Fixes
- Polish
Public share privacy tightened.
Public viability-share output now strips sensitive income/residual fields and avoids dev-salt viewer hashing in production.
- Polish
Legacy refresh-token revocation hardened.
Sid-less refresh tokens no longer bypass session revocation.
- Fix
Mobile sync cursor loss fixed.
Incremental sync now advances from delivered changes instead of server time, preventing skipped records.
- Polish
Import duplicates reduced.
Shift CSV import deduplicates within the batch and against existing shift keys.
- Polish
COL data routes are safer and cheaper.
Public downloads require a state filter, search supports cursor-style pagination, and refresh-all checks the canonical collection.
- New
Documents and OCR routes are more bounded.
Reprocess reserves quota, extraction lists can page, file preview streams object-storage data, and save/update routes verify ownership.
- Polish
Notifications, releases, and observability avoid avoidable hot-path work.
Independent reads run concurrently, release notification tasks are retained, and public observability recompute is single-flight cached.
- Polish
Money formatting audits pass again.
Contract surfaces now call shared `formatCurrency` helpers instead of reintroducing local `$${amount}` formatting.
- Polish
Viability compare now respects the ZIP compare cap.
Free accounts are blocked before the expensive side-by-side analysis path runs.
v3.19.1
May 23, 2026Release feed recovery and changelog parser coverageWhat shipped
- New
Release parser accepts the newer changelog section taxonomy.
`backend/services/changelog_parser.py` now treats headings such as Security, Runtime, Cost-of-living, Refresh pipelines, Documentation, Registries shipped, Real bugs fixed, High-impact gates, Refactor, Code-review triage, and P0-P3 as release-detail sections.
- Polish
Non-release sections remain excluded.
Scope, Verified, Tests, and implementation notes can still exist in `memory/CHANGELOG.md` without being promoted into public release bullets.
- Fix
Regression coverage added for heading drift.
`backend/tests/unit/test_changelog_parser_release_sections.py` proves that representative newer headings are parsed while operational sections stay out.
- Polish
Real changelog catch-up versions are guarded.
The same test asserts that the restored May catch-up versions from v3.15.0 through v3.11.0 remain parseable with public release items.
- Polish
Local release rows can be resynced deterministically.
Restarting the backend now runs the changelog-to-release sync with the restored parser and yields a semver-ordered feed that keeps the latest release first.
v3.19.0
May 23, 2026Pay Rules Studio, four-bucket pay taxonomy, and paycheck-period accuracyWhat shipped
- Polish
Pay Rules Studio becomes the primary pay setup surface.
Pay-rule editing moved out of duplicated Settings/Income panels and into the unified Pay Rules Studio workflow.
- Polish
Four user-facing pay buckets replace the earlier three-bucket language.
Worked-shift pay is now explained as baseline pay, incentives, differentials, and premium pay/overtime so users do not have to reason in backend-oriented component names.
- Polish
Stack behavior copy is simplified.
Raw "stack group" language is hidden behind clearer user wording: how the item combines, whether it adds, the highest item wins, it replaces a base rate, it multiplies pay, or it is preview-only.
- Polish
Advanced "competes with" behavior remains available when needed.
Highest-wins and replacement-style pay items can still define the competing group without making every user learn the internal model first.
- Polish
Pay component history is tracked.
Pay-rule component edits now have a history trail so changes to a differential, bonus, or override can be audited rather than silently overwriting the past.
- Polish
Draft pay-rule preview is hardened.
Draft preview payloads validate nested pay-rule shapes before calculation so malformed preview data cannot crash the pay engine or bypass expected type boundaries.
- New
FLSA classification and pay structure fields added.
Pay rules now capture employee classification and base-pay structure concepts, giving the studio a clearer legal/accounting frame before users customize variable pay.
- New
Numerical guardrails were added to pay calculations.
Pay Stack inputs now constrain extreme values and rounding behavior before they flow into gross/net estimates.
- New
Bucket totals and counted-item summaries ship in pay receipts.
`describe_shift_pay` style output can show per-bucket counts and totals so the visual receipt matches what the engine counted.
- Security
Diagnostics and rounding options were expanded.
The pay stack audit view now exposes more calculation notes and rounding choices for users with complex contracts.
- Polish
Preview shift and visual receipt coverage expanded.
E2E tests cover the Pay Stack visual pay-contract builder, preview shift behavior, taxonomy map labels, bucket focus controls, and calculation-mode visibility.
- New
Pay Rules Studio layout was tightened for responsive use.
The page received multiple layout/refactor passes to reduce overlap, improve narrow-width readability, and make the bucket summary less cramped.
- Polish
Next paycheck period calculation now uses the active workplace/pay-rule period.
Financial context computes the next paycheck window from the relevant workplace/pay schedule rather than falling back to an overly narrow or default period.
- New
Dashboard paycheck integration uses the corrected period.
The next-paycheck dashboard section now has the backend data path needed to show all qualifying worked/planned shifts for the period instead of one undercounted shift.
Fixes
- Polish
Historical shifts are preserved when pay rules change.
Pay-rule updates preserve prior estimates where appropriate while recalculating affected current/future estimates.
- Polish
Settings pay-rule duplication was removed.
Legacy Settings references now route users toward Pay Rules Studio.
- Polish
Income pay-rule duplication was reduced.
E2E coverage and references were updated so the app stops presenting multiple competing pay-rule editors.
- Polish
E2E auto-verify is restricted to explicit test environments.
Test-only email verification behavior cannot activate accidentally in normal development or production-like runs.
- Polish
Financial impact events are redacted from worker refresh results.
Worker-facing refresh payloads no longer echo sensitive impact details.
- Polish
Repository hygiene scripts and setup docs were refreshed.
Local setup and hygiene checks now reflect the current repo shape.
v3.18.0
May 22, 2026Payday Safety spine, native iOS foundation, and evidence-first actionsWhat shipped
- New
Payday Safety contract models and tests added.
The backend now has explicit payday-safety DTOs/tests around the canonical safe-week contract so surfaces can share language without duplicating math.
- New
Dashboard actions became evidence-backed.
Primary recommendations now lean on source facts and deterministic financial context rather than generic "smart recommendation" copy.
- Polish
Fast Start telemetry shipped.
First-answer flows can track when a user reaches their initial Payday Safety answer, which helps measure whether onboarding is doing its real job.
- Polish
Payday Safety methodology was documented.
The docs now explain the contract and method so judges, operators, and future maintainers can trace how the app reaches the headline answer.
- Polish
Native iOS core implementation began.
`mobile/native_ios` gained the foundation for an iOS app that mirrors backend truth instead of forking financial formulas.
- New
Swift version configuration added.
Native iOS build configuration now declares the Swift version expected by the project.
- New
Mobile routing and navigation structures were added.
The native client gained route/navigation scaffolding for the main Viably surfaces.
- Polish
ShiftForm and PayRulesSection were refactored onto newer UI primitives.
The web pay setup path became more accessible and more consistent with the newer component system.
- New
CONTRIBUTING and SECURITY guidance added.
Repository-facing docs now make local contribution and security expectations clearer.
- New
Payday Safety verification script tests added.
Unit coverage checks the focused verification path for the Payday Safety tranche.
- Security
CSP compliance tests added.
QA coverage now verifies important CSP behavior rather than relying only on manual browser inspection.
Fixes
- Polish
Native remembered-login behavior hardened.
iOS stale keychain restore paths now block reinstall session restoration and default remembered login to off.
- New
Payday Safety telemetry only fires for the first answer.
The telemetry endpoint is gated so repeated dashboard visits do not spam first-answer events.
- Polish
Pay-rule recalculation behavior improved.
Pay-rule logic preserves historical shifts and recalculates affected estimates when current assumptions change.
v3.17.0
May 21, 2026Assumption-change notification refresh and mobile security hardeningWhat shipped
- New
Financial assumption notification refresh job added.
A backend refresh path regenerates affected financial notifications after core money assumptions change.
- Polish
Related notification UI updates shipped.
The frontend can show refreshed/fallback notification states instead of silently showing stale assumptions.
- Polish
Financial notification fallback states improved.
Notification surfaces now handle missing or pending refresh data more honestly.
- Polish
Mobile API documentation updated.
Mobile-facing API docs were brought forward with the newer notification and fallback behavior.
- Polish
Client auth cleanup was hardened on logout failure.
Logout clears client state even if server revocation fails, reducing shared-device and stale-session risk.
- Polish
Mobile outbox storage is scoped by user.
Offline/mobile queued state is isolated per user and cleared on logout.
- Polish
Signup auto-verification bypass removed.
Header-based signup auto-verification can no longer bypass normal email-verification expectations.
- Polish
Admin and trial credentials rotated.
Local/test credentials and CI references were updated after credential hygiene work.
- Polish
Plaintext legacy password docs were redacted.
`test_credentials.md` no longer carries the old plaintext password value.
- Polish
Docker Compose Postgres/Redis exposure tightened.
Local infrastructure defaults were updated to avoid unnecessarily exposed services and weak credentials.
- Polish
Public demo object storage cleanup hardened.
Demo seeding now limits object-storage exhaustion risk.
- Fix
Native duplicate declaration fixed.
`ViablyAppModel` no longer declares `currentUserID` twice.
v3.16.0
May 19, 2026P0-P3 stabilization, paystub proof, PyMongo Async migration, and release-gate hardeningWhat shipped
- New
Safe-week financial context was centralized.
The financial calculation path was refactored around a shared safe-week contract so dashboard, forecast, notifications, and related surfaces can agree on the same answer.
- Polish
Paystub reconciliation shipped.
Users can compare predicted pay against actual paystub data, giving the forecast engine a proof-and-calibration loop instead of only estimates.
- New
Paystub guided actions and actuals application workflow added.
Reconciliation can now drive user-visible next steps and apply corrected actuals back into financial records.
- Polish
OCR failure handling improved.
Document/paystub flows expose richer failure metadata so users and operators can tell what failed instead of seeing a dead-end upload.
- Polish
Worker queue diagnostics improved.
Background work gained better status visibility for follow-up processing.
- Polish
Demo mode was enriched.
Demo seeding now better supports a judge-grade story with realistic money state, paycheck composition, and safe-week context.
- New
Paycheck composition and Safe Week Hero were improved.
Dashboard money components became more explainable and visually useful.
- New
Dashboard responsiveness improved.
Dashboard components were tuned for better layout behavior across viewports.
- Polish
Financial insights gained grounding evidence.
Recommendation surfaces now carry more specific source facts instead of generic advice copy.
- Polish
Financial stability calculations and notifications improved.
Stability math and notification triggers gained better coverage for the "short before payday" story.
- Polish
CSV import/export money handling improved.
Import paths now carry gross/net estimates, source details, auto-match defaults, and better money handling.
- Polish
Shift import templates show pay-rule context.
CSV import displays pay-rule information so imported shifts can be reviewed against the correct contract.
- New
Frontend TypeScript configuration added.
The app gained a TypeScript configuration path while preserving the existing JavaScript codebase.
- New
Frontend UI contract tests added.
Unit coverage now locks selected UI contracts, native iOS scaffold behavior, and P1 obligation expectations.
Refactor
- Polish
Mongo client migrated from Motor to PyMongo Async.
Backend Mongo access moved from `AsyncIOMotorClient` toward `AsyncMongoClient`, and related tests/imports were updated.
- Polish
Server imports and test user registration were cleaned up.
The server path and test registration helpers were refactored for the newer backend shape.
- Polish
Integration tests now use environment-based admin credentials.
Tests no longer rely on hardcoded admin credentials.
- Polish
README was refreshed for product and architecture clarity.
The repo front door now better describes Viably's product promise, architecture, and verification paths.
Security and correctness
- Polish
Public release projection no longer leaks creator ids.
Public release responses exclude internal creator identifiers.
- Polish
Release-gate dotenv loading avoids shell evaluation.
Release and pre-push dotenv handling no longer treats env files as executable shell.
- Polish
Backend env defaults were hardened for metrics tokens.
A source-controlled metrics token cannot be mistaken for a production operator secret.
- Security
CSRF origin checking was hardened against forwarded-header spoofing.
CSRF origin checking was hardened against forwarded-header spoofing.
- Polish
Internal revalidation auth now runs before body parsing.
Internal revalidation auth now runs before body parsing.
- Polish
OAuth cookie/session exchange no longer shares cookies across exchanges.
OAuth cookie/session exchange no longer shares cookies across exchanges.
- Polish
MFA recovery links were hardened against origin spoofing, and token execution was repaired after URL redaction.
MFA recovery links were hardened against origin spoofing, and token execution was repaired after URL redaction.
- Polish
Long bcrypt passwords are handled correctly in auth flows.
Long bcrypt passwords are handled correctly in auth flows.
- Polish
Server-side email verification is enforced for authenticated APIs.
Server-side email verification is enforced for authenticated APIs.
- Polish
Passwordless-account login failure paths were hardened.
Passwordless-account login failure paths were hardened.
- Polish
JWT secret misconfiguration no longer leaks configuration detail.
JWT secret misconfiguration no longer leaks configuration detail.
- Polish
Unverified JWT fallback now correctly returns to session auth.
Unverified JWT fallback now correctly returns to session auth.
- Fix
Rate limiter fallback fails closed, authenticated rate-limit keys are set before route auth, and alert endpoint keying was fixed.
Rate limiter fallback fails closed, authenticated rate-limit keys are set before route auth, and alert endpoint keying was fixed.
- Fix
DDoS bypass scope preserves WAF/blocklist checks and route soft-block parsing was fixed.
DDoS bypass scope preserves WAF/blocklist checks and route soft-block parsing was fixed.
- Polish
CORS wildcard extras are rejected.
CORS wildcard extras are rejected.
- Polish
Logout now clears client state on API failure and avoids refresh-loop behavior.
Logout now clears client state on API failure and avoids refresh-loop behavior.
- Fix
Authenticated GET cache invalidation and API cache-control no-store bypasses were fixed.
Authenticated GET cache invalidation and API cache-control no-store bypasses were fixed.
- Polish
Financial stores reset on auth boundaries and stale load promises cannot overwrite current user state.
Financial stores reset on auth boundaries and stale load promises cannot overwrite current user state.
- Polish
Service/outbox owner sync preserves same-owner queued entries while protecting cross-user state.
Service/outbox owner sync preserves same-owner queued entries while protecting cross-user state.
- Polish
Document upload/OCR and feedback fields are bounded against DoS.
Document upload/OCR and feedback fields are bounded against DoS.
- Polish
Public viability share snapshots and PDF exports sanitize untrusted fields.
Public viability share snapshots and PDF exports sanitize untrusted fields.
- Polish
Public status/changelog/release feeds were hardened against information disclosure.
Public status/changelog/release feeds were hardened against information disclosure.
- Polish
COL admin probe errors redact provider API keys.
COL admin probe errors redact provider API keys.
- Polish
Admin CSV uploads are size-gated before parsing and manual upload file type handling was repaired.
Admin CSV uploads are size-gated before parsing and manual upload file type handling was repaired.
- Polish
Health/readiness semantics and route rate limiting were hardened.
Health/readiness semantics and route rate limiting were hardened.
- Polish
Public demo and snapshot endpoints were restricted or redacted where needed.
Public demo and snapshot endpoints were restricted or redacted where needed.
Data-quality fixes
- Polish
COL refresh writes stay behind the publish pipeline.
COL refresh writes stay behind the publish pipeline.
- Polish
BEA SARPP long-shape parser and manual template ingestion were repaired.
BEA SARPP long-shape parser and manual template ingestion were repaired.
- Polish
USDA refreshers now share PDF vintage requirements and update vintages only through the full parser.
USDA refreshers now share PDF vintage requirements and update vintages only through the full parser.
- Fix
EIA, MIT, SAFMR, ZCTA, commute, local-date, ZIP timezone, and household-profile edge cases were fixed.
EIA, MIT, SAFMR, ZCTA, commute, local-date, ZIP timezone, and household-profile edge cases were fixed.
- Speed
COL cache aliasing and redundant CPI index creation were corrected.
COL cache aliasing and redundant CPI index creation were corrected.
- Polish
Monthly payday defaults and forecast pay-period underflow were repaired.
Monthly payday defaults and forecast pay-period underflow were repaired.
- Polish
Mode cash runway no longer clamps non-positive values into unrealistic runway.
Mode cash runway no longer clamps non-positive values into unrealistic runway.
- Polish
Debt months, income report month bounds, shared goal contributions, and extreme finite money values were normalized.
Debt months, income report month bounds, shared goal contributions, and extreme finite money values were normalized.
- Polish
Missed shifts are excluded from hours analytics.
Missed shifts are excluded from hours analytics.
- Polish
Consecutive shift premiums and gross-only paid-shift trace rows were reconciled.
Consecutive shift premiums and gross-only paid-shift trace rows were reconciled.
- Polish
PTO unpaid policies no longer fall back to paid type.
PTO unpaid policies no longer fall back to paid type.
Ops and tests
- Security
Security checklist, pytest, audit, pre-push, smoke, Docker, and GitHub Action failure masking were repaired.
Security checklist, pytest, audit, pre-push, smoke, Docker, and GitHub Action failure masking were repaired.
- Polish
Docker builds can skip optional OpenAPI typegen through `OPENAPI_TYPEGEN_OPTIONAL=1`.
Docker builds can skip optional OpenAPI typegen through `OPENAPI_TYPEGEN_OPTIONAL=1`.
- Polish
Dependency updates landed for backend idna and related groups.
Dependency updates landed for backend idna and related groups.
- Polish
Admin/test secrets were removed from tests and fixtures.
Admin/test secrets were removed from tests and fixtures.
- New
P1 obligation, native iOS scaffold, and UI contract tests were added.
P1 obligation, native iOS scaffold, and UI contract tests were added.
v3.15.1
May 18, 2026Security patch rollup after mobile-readiness releaseSecurity and reliability fixes
- Polish
Plaid disconnect revokes items and clears stored bank credentials.
Plaid disconnect revokes items and clears stored bank credentials.
- Polish
Startup workplace repair avoids full collection materialization.
Startup workplace repair avoids full collection materialization.
- Speed
Bootstrap auth cache checks include the user boundary.
Bootstrap auth cache checks include the user boundary.
- Security
CSRF origin checks resist forwarded-header spoofing.
CSRF origin checks resist forwarded-header spoofing.
- Polish
Shared OAuth HTTP client cookie handling is isolated.
Shared OAuth HTTP client cookie handling is isolated.
- Polish
Lockfile and smoke workflows were hardened against branch/token exposure.
Lockfile and smoke workflows were hardened against branch/token exposure.
- Polish
Internal revalidate authentication runs before request body parsing.
Internal revalidate authentication runs before request body parsing.
- Polish
Changelog SSRF was removed by dropping host-derived fetch URLs.
Changelog SSRF was removed by dropping host-derived fetch URLs.
- Polish
Sentry client events redact sensitive URL parameters.
Sentry client events redact sensitive URL parameters.
- Polish
Secret-restore workflow was hardened against git leakage.
Secret-restore workflow was hardened against git leakage.
- Polish
Broad `.env` ignore coverage and live-test secret removal were restored.
Broad `.env` ignore coverage and live-test secret removal were restored.
- Polish
Public status feed and changelog heading filters were tightened.
Public status feed and changelog heading filters were tightened.
- Polish
ErrorBoundary telemetry URL handling was hardened.
ErrorBoundary telemetry URL handling was hardened.
- Polish
Financial stores reset on auth boundaries to prevent cross-user leakage.
Financial stores reset on auth boundaries to prevent cross-user leakage.
- Polish
Missing Plaid credentials no longer block local disconnect cleanup.
Missing Plaid credentials no longer block local disconnect cleanup.
- Polish
Merchant aliases use tenant-scoped uniqueness.
Merchant aliases use tenant-scoped uniqueness.
- Polish
MFA recovery links resist Origin spoofing.
MFA recovery links resist Origin spoofing.
- Polish
CORS extras reject wildcard origins.
CORS extras reject wildcard origins.
- Polish
DDoS bypass and soft-block promotion behavior were tightened.
DDoS bypass and soft-block promotion behavior were tightened.
- Polish
Credential docs no longer expose sensitive test values.
Credential docs no longer expose sensitive test values.
- Polish
Public viability shares and PDF exports sanitize untrusted fields.
Public viability shares and PDF exports sanitize untrusted fields.
- Security
OAuth callback state validation protects against login CSRF.
OAuth callback state validation protects against login CSRF.
- Polish
Login `next` redirects reject backslash/open-redirect tricks.
Login `next` redirects reject backslash/open-redirect tricks.
- Polish
Shared goal contributions reject non-finite values.
Shared goal contributions reject non-finite values.
- Polish
Tax calibration fixtures and docs were sanitized.
Tax calibration fixtures and docs were sanitized.
- Fix
Frontend auth GET cache invalidation race was fixed.
Frontend auth GET cache invalidation race was fixed.
- Polish
Public snapshot headlines no longer leak private data.
Public snapshot headlines no longer leak private data.
- Polish
Telemetry rate limiting and funnel attribution were hardened.
Telemetry rate limiting and funnel attribution were hardened.
- Polish
COL refresh writes remain behind the publish pipeline.
COL refresh writes remain behind the publish pipeline.
- Fix
Security checklist and CI failure masking were fixed so broken gates actually fail.
Security checklist and CI failure masking were fixed so broken gates actually fail.
v3.15.0
May 18, 2026Mobile launch readiness, push infrastructure, instant-app performance, and architecture runwayWhat shipped
- Polish
Browser push notifications shipped behind a VAPID gate.
`services/web_push.py` and `routers/push_notifications.py` add `/api/notifications/push/*` subscribe, unsubscribe, and config endpoints. When `WEB_PUSH_VAPID_PUBLIC_KEY`, `WEB_PUSH_VAPID_PRIVATE_KEY`, or `WEB_PUSH_VAPID_SUBJECT` are unset, the API returns `enabled: false` with the missing fields so Settings can show an honest disabled state instead of a broken subscribe button.
- New
Notification scheduler skeleton added.
`services/notification_scheduler.py` lays the groundwork for shift reminders, payday-arrival alerts, tight-week pings, and paycheck-landed notifications without firing production pushes yet.
- Security
Offline outbox strips sensitive headers before queueing.
`lib/idbOutbox.js` and `public/outbox-sync-sw.js` remove `Authorization`, cookies, CSRF tokens, and step-up confirmation tokens before mutations enter IndexedDB. Replay strips again so older queued browser entries are protected too.
- Security
Offline outbox hardening tests added.
`tests/unit/test_offline_outbox_hardening.py` locks the header allowlist and replay-scrubbing contract.
- Speed
Idempotency cache scoped per user.
The `idempotent(action_name)` decorator now resolves the authenticated user before keying so tenants sharing an IP cannot collide. Checkout, bills, shifts, workplaces, pay rules, and budget categories now honor `Idempotency-Key` for safe flaky-network replays.
- Speed
Cold tab-switches substantially improved.
Dashboard to Viability transitions dropped from roughly 500-800 ms to roughly 100-150 ms across route prewarm, cache, and skeleton fixes.
- New
Idle prefetch now fires on every authenticated mount.
The prefetch pass moved from dashboard-only with a 1.5 second delay to once-per-session on any authenticated route after a 500 ms idle delay.
- Polish
Priority-queue prefetcher shipped.
`lib/prefetchPageData.js` now prioritizes hover, focus, touchstart, and pointerdown intent over background warming, caps API concurrency, and normalizes query-string routes to warm their base path.
- Fix
Navigation pending hints no longer shift layout.
Sidebar links reserve a fixed hint slot and only show pending state when navigation is actually blocked.
- Speed
Router and API cache windows lengthened.
Next.js dynamic stale time moved to 90 seconds, static stale time to 300 seconds, and the in-memory API cache window to 45 seconds so back navigation feels instant without hiding financial mutation invalidations.
- Polish
Bootstrap pre-warms viability data.
`/api/bootstrap` now includes `/api/col/states-summary` and `/api/viability/latest`; `lib/auth-context.js::primeCache` seeds the client cache so the viability snapshot and staleness banner render from first paint.
- Speed
Viability layout reordered for faster first paint.
The cost map moved below the form and results, and `viability/loading.js` now mirrors the new primary-interaction-first shape.
- New
Route-specific loading skeletons added.
Dashboard, Viability, Shifts, Bills, Paycheck, and Income Floor now have layout-matched `loading.js` files instead of the generic app fallback.
- New
Brotli response middleware added.
`services/brotli_middleware.py` can compress FastAPI responses before they reach the client.
- New
Pooled async HTTP client added.
`services/http_client.py` replaces ad-hoc `httpx.AsyncClient` creation across services, reducing repeated TLS and connection setup cost.
- Polish
Cross-platform frontend maintenance scripts shipped.
`frontend/scripts/build-analyze.js` and `frontend/scripts/clean-next.js` replace POSIX-only package scripts so Windows, Linux, and CI run the same commands.
- Fix
Redis-aware performance infrastructure added.
Distributed cache, rate limiter, DDoS guard, and regression alerts use Redis when `REDIS_URL` is configured while preserving local fallback behavior.
- New
React Compiler enabled.
`next.config.js` enables the React Compiler after compiler-safety linting passed, allowing heavy client pages like Dashboard and Viability to benefit from automatic memoization.
- Polish
Sidebar consolidated into task hubs.
The 18-item flat navigation collapsed into Income, Bills and Debt, Planning, and Scenarios / Viability hubs, with Dashboard, Reports, Documents, and Settings as perimeter routes.
- Speed
React Query migration started.
Multiple frontend components now use TanStack Query with keys aligned to bootstrap prewarming so primed cache entries are reused.
- New
Landing page redesigned.
The public landing now leads with "Know what is safe before payday," trace-status stat cards, Methodology / Personas / Pricing / Trust navigation, and a faster "Try it" CTA.
- Polish
Accessible state cost map replaced `react-usa-map`.
A local keyboard- and screen-reader-friendly cartogram now renders state tier coloring without depending on the stale third-party SVG package.
- Fix
Index-as-key anti-patterns fixed.
`OnboardingTour.jsx`, `TestimonialCarousel.jsx`, and the `pressure-map` weekday row now use stable keys to preserve component identity under updates.
- Polish
Render-time ref mutation removed from `useViabilityData`.
The `formRef.current = form` update moved into an effect, clearing a React 19 / Compiler antipattern while keeping async callbacks current.
- Fix
Next.js 16 middleware registration fixed.
The app returned to the `frontend/middleware.js` convention so Turbopack registers middleware consistently, and CSP script loading was adjusted to keep nonce protection for inline scripts while allowing Next's self-hosted chunks.
- Polish
Flutter mobile reference app scaffolded.
`mobile/viably_flutter/` contains Android and iOS toolchains, an architecture README, and shared fixtures for contract testing.
- New
Mobile BFF router added.
`routers/mobile.py` exposes `/api/mobile/v1/*` auth, device, push, billing, bootstrap, sync, and changelog endpoints shaped for native clients.
- New
Hybrid mobile architecture contract tests added.
`tests/unit/test_hybrid_mobile_architecture.py` verifies mobile payload shape, pagination, timestamp handling, and no internal `_id` leakage.
- Polish
Local LLM adapter replaced `emergentintegrations`.
`services/llm_chat.py` preserves the `LlmChat` / `UserMessage` surface directly on LiteLLM, removing the dependency conflict between `emergentintegrations`, `openai`, and `litellm`.
- Polish
OpenAI and LiteLLM dependency line unblocked.
`emergentintegrations==0.1.0` was removed, `litellm` moved to `1.85.0`, and `openai` moved to `2.24.0`, eliminating the fresh-install resolver conflict.
- Polish
JWT rotation binds refresh tokens to session ids.
Refresh tokens carry an `sid` claim tied to the device cookie so replay from another device revokes the family instead of minting another token.
- New
MFA recovery execution hardened.
Recovery now requires token and new password atomically, validates password strength before token mutation, and has updated integration coverage for empty and unknown token paths.
- Polish
Document storage abstraction shipped.
Document uploads now go through an object-storage adapter with S3-compatible backend support and disk fallback via `DOCUMENT_STORAGE_BACKEND`.
- New
Postgres ledger scaffolding added.
`services/postgres.py` and a Postgres-compatible ledger schema are DSN-gated so deployments without `POSTGRES_URL` continue on Mongo fallback.
- New
Redis worker queue added.
`services/worker_queue.py` provides a BLPOP loop for future background work such as push scheduling, admin index work, and Stripe-event reconciliation.
- Security
Agent conventions documented.
`AGENTS.md` now records pinned versions, test gates, and hardening invariants for incoming automated patches.
- Security
Audit triage verifier added.
`scripts/triage_audit.py` validates audit claims before agents act on them, reducing false-positive bulk-edit risk.
- Security
Obligation verifier added.
`tools/audit/verify-obligations.mjs` joined the obligation-tracking pipeline for `docs/audit/ShiftSavvyyObligations.csv`.
- Polish
Security-touching PR template tightened.
`.github/PULL_REQUEST_TEMPLATE.md` now calls out extra checks for security, push-notification, and idempotency changes.
- Polish
CI workflow refined.
`.github/workflows/ci.yml` was adjusted for action-runtime warnings and frozen-lockfile installs.
- Polish
Stale local artifacts moved out of source.
Old `.codex-logs/` and `local-uvicorn.*.log` artifacts were removed from source-controlled paths.
Follow-ups
- Speed
Cache Components full migration remains deferred.
The broad route-handler migration was attempted and cleanly reverted because `app/layout.js` still reads the CSP nonce through `await headers()`. `memory/CACHE_COMPONENTS_MIGRATION_AUDIT.md` records the Phase 0 options.
- Polish
Google Search Console sitemap submission remains manual.
The app-side sitemap exists, but final submission still happens in Search Console.
- Polish
Postgres ledger activation remains operator-gated.
Set `POSTGRES_URL` in the deploy panel to move from Mongo fallback to the Postgres ledger path.
- Polish
Web push activation remains operator-gated.
Set VAPID public/private/subject env vars before production push can be enabled.
- Polish
Scheduler trigger rules remain the next retention step.
The push stack is ready; shift reminder, payday-arrival, tight-week, and paycheck-landed rules still need to be wired to user financial context.
v3.14.0
May 17, 2026May catch-up: security, performance, methodology, and dataset provenance sprintSecurity and deploy hardening
- Polish
Tenant-scoped pay rules.
Shift create/update now rejects pay-rule ids that do not belong to the authenticated user, and pay-rule ids moved to full UUID values instead of short predictable ids.
- Polish
Stripe checkout ownership tightened.
Checkout and portal return URLs now come from a server-side allowed-origin path instead of trusting a client-provided origin, and checkout status polling is bound to the authenticated user.
- Security
CSRF posture tightened.
Refresh and logout are no longer exempt from CSRF origin checks; middleware coverage pins the stricter behavior.
- Security
Document path traversal hardening.
Document status path checks now use `commonpath` so prefix tricks cannot escape the intended document directory.
- Polish
Feedback upload validation hardened.
Image uploads now validate file magic bytes instead of trusting filename or content-type metadata.
- Polish
Metrics and operator health privacy.
Production metrics remain bearer-gated by `METRICS_TOKEN`, and health/debug endpoints avoid leaking deployment posture that helps reconnaissance.
- Polish
Frontend failure states made actionable.
API timeouts, login failures, and app-shell unavailable states now surface retryable errors instead of leaving users stuck on indefinite loading.
Runtime and performance
- Speed
Next.js 16 and React 19 production path.
The frontend is now aligned to the Next.js 16 / React 19 line with Cache Components, Partial Prerendering, streamed nonce-safe CSP boundaries, and a committed `yarn.lock` for reproducible installs.
- Security
Nonce CSP preserved under modern rendering.
Request-bound CSP nonce work was isolated so Partial Prerendering can run without weakening strict `script-src` behavior.
- Polish
Image optimizer locked down.
The public optimizer now uses explicit local/remote allowlists, WebP output, a single approved quality, disabled SVG optimization, no local IP access, and no redirects. Private document previews intentionally stay outside the optimizer path.
- Polish
Backend health now fails fast.
`/api/health` uses `HEALTH_DB_TIMEOUT_SECONDS` so a down or slow MongoDB does not hang the lightweight liveness check; `/api/readiness` remains the stricter deploy gate.
- Polish
OpenAPI generation cleaned up.
Backend `GET` and `HEAD` health handlers were split to remove duplicate operation IDs, and internal Next preview proxy routes are hidden from client generation.
- Polish
Accessible cost-map replacement.
The `react-usa-map` dependency was replaced with a local keyboard/screen-reader friendly cost-map cartogram.
- New
Local startup repeatability.
Added the PowerShell startup path so local frontend/backend/Mongo setup is less dependent on manual environment steps.
Cost-of-living and viability accuracy
- Polish
Methodology advanced to v3.6.0.
The cost-of-living engine now treats live source provenance as part of the methodology contract instead of burying vintages in source modules.
- Polish
Fourteen source families are tracked.
The seed pipeline now registers every active COL source family in `source_vintages`, `/api/col/meta/version`, `/api/col/sources`, public methodology output, and synthesized record metadata.
- Polish
Dataset freshness registry made public-safe.
Freshness status now reports aggregate counts without leaking internals; the current local run shows 14 total sources, 13 fresh, 1 due, 0 stale, and 0 unknown.
- Polish
Official-source refresh pass completed.
COL sources were updated against current official or primary sources: BLS CPI April 2026, BEA 2024 RPP state/metro, HUD FY2026 FMR, Census ACS 2020-2024, EIA electricity and natural gas, USDA March 2026 Food Plans, MIT 2026 Living Wage, KFF/CMS 2026 benchmark premiums, AAA May 17 gas prices, and Tax Foundation 2026 local-tax tables.
- Polish
AAA gas prices corrected.
The transportation fuel input now uses the May 17, 2026 AAA state table at three-decimal precision plus the same-day national average of `$4.513`; the ingest script preserves AAA precision and documents the national-average update step.
- New
Local taxes added to viability.
Sales-tax and owner-property-tax estimates now use Tax Foundation 2026 rates, Census ACS 2020-2024 home values, explicit data-source metadata, and frontend tax-breakdown display support.
- Polish
Owner and renter housing provenance clarified.
ACS rent, ACS tenant-paid utility allowance, ACS SMOC, HUD FMR, and BEA RPP now publish current vintages and clearer anti-double-counting notes.
- Polish
Viability calculator now carries source vintages.
Location and calculation output includes the COL dataset version, source vintage map, and local-tax source ids so users and auditors can see what data powered a result.
- New
Methodology page aligned with the app.
Public methodology text now derives source rows from live metadata, describes the current formula/fallback chain, and aligns with the app-improvement-goals anchor.
Refresh pipelines and provenance controls
- Polish
BLS refreshers now prefer the public API.
CPI temporal and regional refresh scripts use the BLS public API by default with spreadsheet fallback and official release-date metadata.
- Polish
Census ACS refreshers were modernized.
Rent, utility allowance, SMOC, and home-value paths now understand 2020-2024 ACS 5-year table-based data and emit release-date provenance.
- Polish
EIA natural gas has a no-key fallback.
The refresh path can use the EIA API when configured or the official monthly XLS file when not.
- Polish
USDA/KFF/AAA/CSV ingests now preserve published-at metadata.
Manual and semi-automated ingests update `published_at`, source id, vintage, and coverage consistently.
- Polish
Refresh orchestrator target ids updated.
Admin upload templates, refresh history, and source resolution now point at the current source ids and no longer advertise stale target names.
- Fix
Freshness tests added.
A new provenance regression test locks that all current source families are registered, synthesis uses live vintages, ACS rent is current, and no source is stale or unknown.
Documentation and operator readiness
- Security
README dataset provenance section added.
The README now lists active data sources, vintages, refresh commands, and the public endpoints used to audit dataset freshness.
- Polish
Deploy checklist updated.
Deployment now calls out COL methodology v3.6.0, data-provenance checks, current refresher commands, optional BEA/EIA keys, and post-reseed verification.
- Polish
App improvement goals reconciled.
`docs/app-improvement-goals-anchor.md` now records the dataset freshness/provenance pass as completed and aligned with the shift-worker finance goal.
- Polish
May release docs preserved.
The May 8 security/performance update and May 10 full-app update documents capture the broader runtime, security, and smoke-test work that preceded this changelog catch-up.
v3.13.0
May 01, 2026Hackathon-judge audit: 15 P0/P1/P2 fixes + observability dashboardP0 — security + money math
- Polish
Committed-in-repo secrets rotated.
`JWT_SECRET`, `ADMIN_PASSWORD`, and `RATE_LIMIT_BYPASS_KEY` were sitting in `backend/.env` as production-shaped values (64-char hex, 24-char random, specific bypass token). Every fork of the repo inherited the same JWT signing key — a token minted against one deploy validated against every other. Rotated all three to visually-obvious placeholders (`dev-jwt-secret-rotate-before-production-deploy`, `change-me-before-prod`, `viably_test_bypass_2026`). Extended `services/hardening.py` with a `_COMMITTED_DEV_SECRETS` frozenset; any production boot still carrying one of these values now hard-fails startup with a specific "matches a value committed to the repo" error. Dev/preview boots unchanged.
- Polish
Public Prometheus scrape + operator health leak closed.
`/api/metrics`, `/metrics`, `/api/_health/cors`, and `/api/_health/auth-domains` were anonymous — any attacker could fingerprint process uptime, live `viably_audit_log_writes_total{action="auth.login"}` rates, and the full CORS allow-list without authentication. Bearer-gated with a new `METRICS_TOKEN` env var (constant-time compare, accepts both `Authorization: Bearer` and `X-Metrics-Token` headers so Prometheus's `bearer_token_file` config works without change). Dropped `app_env` from both health-endpoint responses — pure recon signal telling attackers whether a deploy had relaxed dev-mode rate limits.
- Polish
Floating-point money drift in tax forecast.
`services/tax.py` was rounding all 10 money fields on `TaxBreakdown` with `round(x, 2)` applied to Python floats — IEEE-754 representation error accumulates when a forecast sums 26 biweekly paychecks × 9 components. For a user cross-referencing against an Excel model, the $0.02 drift over a year destroys trust. Migrated every money field to `services.finance._round_money` (Decimal HALF_UP). Added `tests/unit/test_tax_decimal_rounding_accumulation.py` — 3 tests that sum 26 periods and assert drift ≤ $0.01 per field, plus a float-tail check that flags values like `0.30000000000000004`.
P1 — framework + dependency upgrades
- Polish
Next.js 14.2.35 → 16.2.4
(skipped past 15). Killed 2 HIGH CVEs (HTTP request deserialization DoS, Server Components DoS) plus 4 other HIGH findings in transitive deps. Migrations: `headers()` → `await headers()` in `app/layout.js`, `{ params }` → `{ params: Promise<...> }` in `app/for/[persona]/page.js` + `app/s/[id]/page.js`, `middleware.js` → `proxy.js` file rename + `export function middleware` → `export function proxy` (Next 16 deprecation). `yarn audit --level=high` now reports **0 HIGH** (was 6).
- New
`.dockerignore` added.
Docker build context was transferring ~1 GB per build (`.git` 213 MB + `frontend/node_modules` 729 MB + `.next/` + every committed `.env`). New `.dockerignore` drops context to <20 MB and keeps secrets out of image layers.
- New
Bundle-size optimization (120 KB parsed / 37 KB gzip per authed page).
Bundle analyzer (new `yarn build:analyze` script) revealed `framer-motion` was loading on every authenticated route via an eager `NotificationCenter` import — even though the component only renders when the user clicks the notification bell. Converted to `next/dynamic({ ssr: false })` in `app/(app)/layout.js`. Post-fix analyzer confirms framer-motion isolated to its own 133 KB chunk, loading only on drawer open. Phosphor correctly per-route-split via existing `optimizePackageImports`.
- New
Sentry scaffolding (no-op when DSN empty).
Added `sentry.{client,server,edge}.config.js`. `ErrorBoundary.componentDidCatch` now forwards to `Sentry.captureException` alongside the existing in-house telemetry beacon. Enable in production by setting `NEXT_PUBLIC_SENTRY_DSN` — no code change needed.
- Polish
`bcrypt.gensalt(rounds=12)` explicit.
Was relying on the library default; a future `pip upgrade bcrypt` changing the default (as has happened twice in its history) could silently weaken hash cost. Explicit 12 matches OWASP 2025 recommendations and mirrors `_RECOVERY_BCRYPT_ROUNDS` in `services/totp.py`.
- Fix
Landing-page heading hierarchy fix.
`MethodologyPreview.jsx` had an `<h3>` that jumped past the page's `<h1>` with no intervening `<h2>`. Lighthouse accessibility flag; screen-reader users navigating by heading hop from page title directly into widget subtitle. Promoted to `<h2>`.
- Polish
Python deps refreshed.
`motor 3.3.1 → 3.7.1`, `pymongo 4.5.0 → 4.17.0`, `python-multipart 0.0.24 → 0.0.27` (closed file-upload DoS in older multipart — affects our receipt-OCR path), `postcss → 8.5.13` (top-level; CSS-stringify XSS patch).
P2 — hygiene
- Speed
Service-worker cache purge on logout.
`public/outbox-sync-sw.js` was caching `/api/bootstrap`, `/api/dashboard`, `/api/bills` into browser Cache Storage. Cookies cleared on logout but cache didn't — a tablet handed off from the day nurse to the night nurse could serve the previous user's safe-to-spend and bills on the next offline load. Added a `purge-cache` message handler to the SW; `auth-context.js:logout()` now `postMessage({type:'purge-cache'})`s the SW to `caches.delete()` every `viably-runtime-*` cache entry before the redirect.
- Polish
TypeScript for the four shared registries.
`lib/personas.ts`, `lib/pricing.ts`, `lib/pro-features.ts`, `lib/route-protection.ts`. Minimal `tsconfig.json` with `allowJs:true, checkJs:false` so Next.js auto-detects TS without touching the 250+ existing `.js` files. Registry consumers now get IntelliSense on every import. All four contract tests updated to probe `.ts` file paths.
- Polish
FAQPage + BreadcrumbList JSON-LD.
`/pricing` emits `FAQPage` structured data derived from the same `pricingFaqEntries(cfg)` helper that renders the visible UI — rich-result accordion in Google SERPs can never drift from the page copy. `/for/[persona]` emits `BreadcrumbList` (Home → For → Persona) for sitelinks under the search result.
- Polish
Rate-limit landing telemetry.
`POST /api/telemetry/landing-event` tightened from the 120/min `default` bucket to the 60/min `write` bucket. Closes an anonymous Mongo-write vector — one visitor_id + one IP at 120/min could fill the `landing_events` collection with 172k rows/day.
- Security
Operator-env hygiene check.
`services/hardening.check_operator_env` warns loudly in production logs if `METRICS_TOKEN` or `NEXT_PUBLIC_SENTRY_DSN` are unset — non-fatal (a hackathon demo can ship without Sentry) but visible in startup logs so operators see what they forgot. Preflight CI script classifies these as warnings (exit 0) while keeping all other issues deploy-blocking.
Observability dashboard
- New
Grafana dashboard JSON
at `/app/docs/grafana-dashboard.json`. Wires the existing `viably_audit_log_writes_total`, `viably_slow_requests_total`, `viably_rate_limit_hits_total`, `viably_auth_attempt_total`, and HTTP status counters into 10 pre-built panels (login volume + success rate, brute-force-bucket saturation, slow-query trail, CSP violation rate, p50/p95/p99 request latency, 4xx/5xx rates by route). Self-hostable with one `import dashboard JSON` click in Grafana against any Prometheus scrape target pointed at the backend's `/metrics` endpoint.
Docs
- Polish
`/app/docs/production-env.md` — ship-blockers vs operator-hygiene vs optional env vars
Exactly what to set on first deploy.
- New
`/app/docs/grafana-dashboard.json` — import once, ops visibility forever.
`/app/docs/grafana-dashboard.json` — import once, ops visibility forever.
v3.12.0
Apr 30, 2026DRY consolidation sweep: four shared registries + contract testsRegistries shipped
- Polish
`lib/route-protection.js`
protected URL prefixes + public-app paths. Previously hand-duplicated between `middleware.js` and `lib/auth-context.js`. `PUBLIC_APP_PATHS` gained `/changelog`, `/methodology`, `/trust` which fixed a logout-then-back-button redirect loop.
- Polish
`lib/personas.js`
the 4 persona-landing records (slug, nav_label, SEO copy, card content, icon name). Consumed by `app/sitemap.js`, `app/for/[persona]/page.js`, `components/landing/PersonaCards.jsx`, and the landing footer. **Fixed a live 404**: the landing surfaced a `/for/teachers` card and footer link to a page that didn't exist; meanwhile `/for/prn` was a fully-built SEO page with zero inbound links (orphaned). Consolidation exposed both gaps; the contract test's drift-guard prevents recurrence.
- Polish
`lib/pricing.js`
fallback monthly/annual/trial constants + 9 derived formatters (SEO title/desc, JSON-LD offer, FAQ answers, persona tagline). **Fixed a live price-drift bug**: the pricing FAQ rendered "After 14 days you're charged **$5**/month" (from a stale `?? 500` cents fallback) while every other surface said $8. Eight files migrated from hardcoded `$8/mo` / `$80/yr` / `14-day` literals. Env-override path (`NEXT_PUBLIC_PRO_MONTHLY_PRICE_CENTS`, etc.) flips every surface at once from the deploy panel.
- Polish
`lib/pro-features.js`
`PEEK_ROWS` (PricingPeek comparison table), `FREE_FEATURES` / `PRO_FEATURES` (pricing-page narrative lists), `PRO_LIMIT_LABELS` + `PRO_LIMIT_GATE_IDS` (Pro-cap gate registry). Two downstream surfaces (`/pricing` + the landing's `PricingPeek`) had already drifted on tier copy — "Priority support" vs "Priority email support", "Receipt OCR · auto-categorize" vs "Receipt OCR — snap a photo". Contract test includes a **backend-alignment guard** that parses `backend/routers/settings.py::_PRO_LIMIT_TYPES` and asserts frontend ID set == backend allowlist byte-for-byte; catches the class of bug where a one-sided gate addition silently drops to generic banner copy or 400s on the first cap hit.
Side fixes
- New
Logout redirect loop: `middleware.js` was redirecting authed users to `/login` via an inverted check
Flipped the route-protection predicate to use the new shared list.
- New
Cost-of-living preview tile on landing page: price was drifting between the mini-preview ($9/$72 placeholder) and the live `/api/billing/config` ($8/$80 real)
Wired to the real endpoint with a graceful fallback.
v3.11.0
Apr 29, 2026Production reliability sprint: refresh-storm fix, mobile shifts overhaul, observability rollout, changelog parser fixReal bugs fixed
- Polish
Production refresh-storm (P0)
. The `/api/auth/refresh` interceptor in `lib/api.js` had no cooldown — when a session genuinely expired, every concurrent 401 (totp/status, bootstrap, security-score, …) raced to refresh the moment the previous attempt resolved. Production logs showed sustained waves of refreshes hitting the auth_strict rate limiter into 429. Added a 30-second `refreshCooldownUntil` window: once `/auth/refresh` fails with 401/429/transport error, every subsequent 401 propagates immediately without retrying. Added `REFRESH_SKIP_PATTERNS` so background pollers (`/totp/status`, `/billing/status`, etc.) never trigger refresh on 401 in the first place. Verified: 20-call burst against `/totp/status` now produces exactly 1 refresh attempt (was 20).
- Fix
Settings → Security tab crashed with React #130
. `RecentActivityPanel.jsx` imported `Activity` from `@phosphor-icons/react`, but that name was renamed to `Pulse` in v2.x — the bundled `undefined` rendered as JSX raised React error #130, which trips the global ErrorBoundary. CI never caught it because no spec mounted the Security tab in a production build. Fixed import + added `tests/e2e/10-settings-security-tab.spec.js` regression that asserts every panel `data-testid` mounts.
- New
Mobile shifts page squashed + missing money values
. Two latent bugs interacting: (a) `PageChrome` had `h-14` + `shrink-0` actions cluster which on /shifts (5-button toolbar) demanded 280 px and forced `<main>` wider than the viewport, parking row money + actions off-screen; (b) `VirtualShiftList` hard-coded `estimateSize: () => 84` for the desktop single-row layout, but mobile rows stack `flex-col` at ~169 px tall — so every absolute-positioned row sat ~85 px on top of the next, hiding money + actions of the row above. Fixed both: `<main>` now `min-w-0 overflow-x-hidden`, PageChrome stacks vertically below `sm`, and `VirtualShiftList` wires TanStack Virtual's `measureElement` to read each row's real height post-mount. iOS Safari verification (iPhone 14 device profile): 9 rows, 0 overlaps, every row has `$` + action buttons.
- Polish
Release notes regressed to v3.5.0
. `/api/releases/latest` was using `db.releases.find_one()` with no sort — Mongo returned whatever the index decided was first (the lifespan seeder's pinned v3.5.0 row, or an admin-typo'd test entry like `0.0.eec616`). `/api/releases/whats-new` had a related bug — sorting by `published_at` instead of semver, so any release a sysadmin re-touched recently jumped above genuinely-newer semver versions. Both endpoints now sort via the same `_semver_sort_key` as `/public` and filter out non-parseable versions.
- Polish
Changelog parser was dropping 24 versioned sessions silently.
Three independent parser bugs combined to produce a "release notes pinned at v3.10.0" symptom even though CHANGELOG.md had v3.10.1 → v3.10.24 plus this session 159: (a) `_SESSION_HEADER` regex didn't allow the `Rxx` revision marker (`Session 158 R25`); (b) the parser only consumed the FIRST `**Version:** vX.Y.Z` line per session header but session 158 R25 stacks 22 of them; (c) `_BULLET` regex only matched `-` not numbered `1. 2. 3.` items. Fixed all three: relaxed regex, implicit-session-split on subsequent Version lines, numbered-item support.
High-impact gates added
- New
Auto-update channel for stale tabs
. `public/outbox-sync-sw.js` now `postMessage`s `viably:sw-updated` to every controlled client when a new SW version activates. `lib/serviceWorker.js` listens and fires a non-blocking sonner toast ("New version available — Reload") on the second-and-later activation per tab. Long-lived iOS Safari tabs now pick up new JS bundles without manual intervention.
- Polish
Proactive session-expired redirect
. `api.js` dispatches `viably:session-expired` when `/auth/refresh` fails with 401 or transport error; AuthProvider listens, calls `/api/auth/logout` to clear the access cookie (otherwise the edge middleware bounces /login → /dashboard in a loop because it only checks cookie *presence*), then redirects to /login. Works regardless of which tab/component first noticed the dead session.
- Security
CSRF-safe Cooldown reset on login/logout
. `api.invalidate()` (called on login + logout) now also resets `refreshCooldownUntil` and any in-flight refresh promise so a new session can refresh on its first 401.
Observability
- Polish
`ops/grafana/dashboards/viably.json` extended from 10 → 13 panels: per-route ava
`ops/grafana/dashboards/viably.json` extended from 10 → 13 panels: per-route availability (target ≥ 99.5 %), per-route p95 latency, 30-day error-budget burn.
- New
`ops/prometheus/alerts.yml` (new) — paging rules for 5xx > 2 %, p99 > 5 s, CORS
`ops/prometheus/alerts.yml` (new) — paging rules for 5xx > 2 %, p99 > 5 s, CORS allowlist drop; warnings for 429 storm, 401 spike, bootstrap cache cold, per-route SLO burn.
- Polish
`ops/grafana/README.md` updated with import + provisioning + Datadog OpenMetrics
`ops/grafana/README.md` updated with import + provisioning + Datadog OpenMetrics equivalent + recommended thresholds.
Refactor
- Polish
`routers/documents.py` (835 LOC) → `routers/_documents/{_shared,upload,save,statement,status}.py` (each < 235 LOC)
`routers/documents.py` is now a 4-line re-export shim. Audit tests (`test_r26_11_audit_cleanup.py`) updated to track relocated `save_*_from_extraction` handlers.
Code-review triage
- Polish
ADR-008
staged decomposition of monolithic page components (dashboard 594 LOC, viability 960 LOC, admin/col 760 LOC).
- Polish
ADR-009
middleware complexity reduction (`_make_csrf_origin_check`, `_observability`, `_deferred_startup`) with characterization-tests-first approach.
v3.10.24
Apr 26, 2026Critical bug fixes from deep-dive code reviewFixes
- Fix
Iowa earner at $50k/year, single, before fix: state tax computed as ~$18,625
Iowa earner at $50k/year, single, before fix: state tax computed as ~$18,625
- Fix
After fix: state tax = **$1,600.76** (verified)
After fix: state tax = **$1,600.76** (verified)
- Fix
Pre-fix users would have seen take-home ~$1,400/month instead of ~$3,200/month —
Pre-fix users would have seen take-home ~$1,400/month instead of ~$3,200/month — a 56% understatement
- Polish
Viability calculator would have flagged Iowa cities as "unaffordable" for normal
Viability calculator would have flagged Iowa cities as "unaffordable" for normal salaries
v3.10.23
Apr 26, 2026Critical bug fixes from deep-dive code reviewWhat shipped
- Polish
`<ErrorState onRetry={fn} />` — full-card retry surface.
`<ErrorState onRetry={fn} />` — full-card retry surface.
- Polish
`<PartialDataNotice message="..." onRetry={fn} />` — inline amber banner for "se
`<PartialDataNotice message="..." onRetry={fn} />` — inline amber banner for "section X failed but the rest rendered".
- Polish
All empty-state CTAs use `<button type="button">` (keyboard-reachable, focusable
All empty-state CTAs use `<button type="button">` (keyboard-reachable, focusable).
- Polish
Heading element is `<h3>` consistently.
Heading element is `<h3>` consistently.
- Polish
`role="alert"` + `aria-live="assertive"` on error variant only.
`role="alert"` + `aria-live="assertive"` on error variant only.
- Polish
Icons are `aria-hidden="true"` (decorative).
Icons are `aria-hidden="true"` (decorative).
- Polish
No reliance on color alone — every variant has a distinct icon.
No reliance on color alone — every variant has a distinct icon.
- Polish
Focus-visible ring (`focus-visible:ring-2 focus-visible:ring-primary/40`) on eve
Focus-visible ring (`focus-visible:ring-2 focus-visible:ring-primary/40`) on every button.
- Polish
`/bills` empty: unified component used + CTA reachable + no fake numbers
`/bills` empty: unified component used + CTA reachable + no fake numbers
- Polish
`/budget` empty: unified component used + CTA reachable
`/budget` empty: unified component used + CTA reachable
- Polish
`/debts` empty: unified component used
`/debts` empty: unified component used
- Polish
`/shifts` empty: unified component used + CTA reachable
`/shifts` empty: unified component used + CTA reachable
- Polish
Semantic structure: every `data-empty-variant` element has an `<h3>` + button
Semantic structure: every `data-empty-variant` element has an `<h3>` + button
v3.10.21
Apr 26, 2026Critical bug fixes from deep-dive code reviewWhat shipped
- Polish
Web Vitals
LCP, INP, CLS captured via `PerformanceObserver`. No external dependency. Color-coded by Google's good/needs-improvement/poor thresholds. Cards show "unavailable" empty state if browser doesn't support the metric.
- Speed
API requests + client cache
Total request count, cache size, hit rate (with raw hit/miss counts), eviction count.
- Polish
Top endpoints
8 most-called paths in this tab.
- Polish
Slow requests
Last 10 requests ≥ 500 ms. Redacted: only (path, method, status, duration_ms, ts) — never query strings, headers, or bodies.
- Polish
Long tasks
Count, total ms, max ms.
- Speed
Server-side bootstrap cache
Reads `/api/metrics` Prometheus exposition; surfaces `viably_bootstrap_cache_{hits,misses}_total{backend}` with hit-rate calc.
- Polish
`frontend/lib/diagnostics.js` — NEW (~180 LOC)
PerformanceObserver setup, fetch-wrap counter, mark/measure helpers.
- Polish
`frontend/lib/api.js` — added `api.diagnostics()` introspection (cache hit/miss/
`frontend/lib/api.js` — added `api.diagnostics()` introspection (cache hit/miss/eviction/primed counters).
- Polish
`frontend/lib/auth-context.js` — invokes `initDiagnostics()` once on first mount
`frontend/lib/auth-context.js` — invokes `initDiagnostics()` once on first mount.
- New
`frontend/app/(app)/admin/diagnostics/page.js` — NEW (~270 LOC)
Read-only dashboard.
- Polish
`frontend/app/(app)/layout.js` — Skip link + `<main>` landmark with `id` and `ta
`frontend/app/(app)/layout.js` — Skip link + `<main>` landmark with `id` and `tabIndex={-1}`.
- Polish
`frontend/tests/e2e/07-diagnostics-a11y.spec.js` — NEW
3 tests:
- Polish
Skip link is keyboard-reachable, becomes visible on focus, anchors to `#app-main
Skip link is keyboard-reachable, becomes visible on focus, anchors to `#app-main` on activation.
- Polish
`/admin/diagnostics` redirects non-admin users.
`/admin/diagnostics` redirects non-admin users.
- Polish
`/admin/diagnostics` renders for admins with all expected sections + zero PII leaks (asserts no email/JWT/session_token strings in HTML)
Skipped in production-like preview env where admin login is disabled.
v3.10.20
Apr 26, 2026Critical bug fixes from deep-dive code reviewReal bugs fixed
- New
`/planning` prefetched but doesn't exist.
R16's `TOP_ROUTES` array in `(app)/layout.js` listed `/planning`, which has no Next.js page. Every dashboard mount fired a wasted `router.prefetch("/planning")` and a 404 for the page-data warm. Replaced with `/budget` (real route, high traffic).
- New
Unbounded `getCache` Map in `lib/api.js`.
No size cap → long-running tabs leaked memory. Added `MAX_CACHE_ENTRIES = 250` with insertion-order LRU eviction (drops oldest 25% on cap-hit).
- Speed
Non-deterministic cache keys.
`cacheKey(url, config)` used `JSON.stringify(config.params)` which preserves object insertion order. `{a:1, b:2}` and `{b:2, a:1}` produced different cache entries → cache hit rate dropped silently for any page that built params from variable-key sources. Replaced with a recursive `_stableStringify` that sorts object keys at every depth (arrays preserve order — semantically meaningful).
High-impact gates added
- Polish
R16 idle prefetch now respects network/visibility.
Previously the idle-prefetch burst fired regardless of `saveData`, `effectiveType`, `document.visibilityState`, or `navigator.onLine`. Added `shouldSkipPrefetchBurst()` mirroring the gates `prefetchPageData` already enforces. Backgrounded tabs and 2G clients no longer eat unnecessary network/CPU.
- Speed
Admin perf-dashboard polling pauses on hidden.
`app/(app)/admin/perf/page.js` had a raw `setInterval(load, 5_000)` that kept polling indefinitely on background tabs. Added a `visibilitychange` listener: pauses when hidden, fires one immediate `load()` on visibility-resume, then resumes the interval. Eliminates needless backend pressure from unattended admin tabs.
Docs
- Speed
`docs/adr-005-frontend-perf-rules.md` — NEW canonical rules of the road
Codifies the policies established across R10–R21 (no `<Link prefetch=true>`, hover triggers two warmers, idle prefetch gated, cache keys stable, bundle hygiene, visibility-aware polling, perf budgets, observability). Designed so a future contributor can keep wins compounding instead of regressing.
v3.10.19
Apr 26, 2026Critical bug fixes from deep-dive code reviewWhat shipped
- Speed
Sequential warm: 92-151 ms (cache hits at ~92 ms).
Sequential warm: 92-151 ms (cache hits at ~92 ms).
- Fix
10× concurrent stress: 157-220 ms (matches R18, no regression).
10× concurrent stress: 157-220 ms (matches R18, no regression).
- Polish
Prometheus exports `viably_bootstrap_cache_hits_total{backend="local"}` and `...
Prometheus exports `viably_bootstrap_cache_hits_total{backend="local"}` and `..._misses_total{backend=...}` — verified live (`hits=7` after 7 cache hits).
- Speed
Logout flow correctly invalidates the cache.
Logout flow correctly invalidates the cache.
- Polish
`/api/bootstrap` returns all 26 fan-in fields with no `_errors`.
`/api/bootstrap` returns all 26 fan-in fields with no `_errors`.
- Polish
`/api/shifts/bootstrap` returns 4 expected fields.
`/api/shifts/bootstrap` returns 4 expected fields.
- Polish
`/api/reports/bootstrap` returns 3 expected fields.
`/api/reports/bootstrap` returns 3 expected fields.
- Polish
Quick-start (50-100 users, 1-5 min).
Quick-start (50-100 users, 1-5 min).
- Speed
Perf targets per endpoint.
Perf targets per endpoint.
- Polish
How to interpret p95 spikes.
How to interpret p95 spikes.
- Polish
Headless CI-mode invocation.
Headless CI-mode invocation.
- Polish
`viably_bootstrap_cache_hits_total{backend}` — labelled `redis` or `local`.
`viably_bootstrap_cache_hits_total{backend}` — labelled `redis` or `local`.
- Polish
`viably_bootstrap_cache_misses_total{backend}` — same label.
`viably_bootstrap_cache_misses_total{backend}` — same label.
- New
Two new metric rows in the table.
Two new metric rows in the table.
- New
A new alert rule: `ViablyBootstrapCacheNotWarming` fires if hit rate drops below
A new alert rule: `ViablyBootstrapCacheNotWarming` fires if hit rate drops below 40% for 15 min (signals Redis down or `REDIS_URL` unset on a multi-pod deploy).
- Polish
Datadog openmetrics list extended.
Datadog openmetrics list extended.
v3.10.18
Apr 26, 2026Critical bug fixes from deep-dive code reviewWhat shipped (P1/P2 items applied immediately)
- Polish
Bounded scheduler cursors
in `services/shift_confirmation.py`. Two `to_list(length=None)` calls were doing full-table scans of stale planned shifts (would scale poorly to 10k+ users). Capped at 10k buckets/shifts per scheduler tick — overflow rolls over to the next tick.
- Polish
PII-safe password reset log line.
`auth.py:620` was logging `Password reset token generated for {email}`. Changed to log only `user_id` so log access can't reveal user emails.
- Polish
Production CORS guard.
`compute_cors_origins()` now raises `RuntimeError` at startup if `CORS_ORIGINS=*` is set with `APP_ENV=production`. The wildcard regex (combined with `allow_credentials=True`) is convenient for dev/preview but would let any third-party site issue authenticated requests in production. Fail-fast on startup beats silent security downgrade.