The per-day drill-down used measurements.saveError in its fetch-error branch, which surfaced 'Fehler beim Speichern' on a read failure. Adds a measurements.loadError key in every locale and points the GET error path at it.
Mirrors the health-chart and mood-chart fixes. The compliance payload
already carries `scheduled` per day; summing it yields the raw
scheduled-dose count for the window. When the chart aggregates to
< 3 daily buckets but the raw count is ≥ 3, paint the new
"More distinct days needed" copy so the next action matches reality.
Pin both branches with an SSR test that mocks the compliance fetch.
Mirrors the health-chart fix. The mood-analytics endpoint already
exposes `entries[].samples` per calendar day, so summing it yields
the true raw mood-entry count for the window. When chartData
collapses to < 3 daily buckets but the raw count is ≥ 3, paint the
new "More distinct days needed" copy instead of the misleading "log
at least 3 entries" hint.
Pin both branches with an SSR test that mocks the analytics fetch.
The empty-state caption was gated on `chartData.length < 3`, but
chartData is daily-aggregated. A user with 50 blood-pressure readings
on 2 distinct days saw "log at least 3 entries" despite already
logging plenty — only the day-spread was sparse.
Track the raw measurement count during the per-type fetch (using the
rollup endpoint's `count` field when available, falling back to 1 per
row on the raw path) and stash it as a non-enumerable property on the
returned array. The render gate now paints "More distinct days
needed" when chartData is short but the raw count is already ≥ 3, and
the legacy "log more" copy stays for the genuinely sparse case.
Pin both branches with an SSR test that mocks the queryFn return.
Pre-existing empty-state copy ("Erfasse mindestens 3 Einträge, um die
Trendlinie freizuschalten") misleads when a user has logged many
measurements on too few calendar days. Add a second pair of strings
keyed on `charts.needMoreDistinctDays{Title,Description}` so the
upcoming chart-gate split can paint the right call-to-action when raw
count is sufficient but distinct-day count is not.
A misbehaving Withings upstream that returns a multi-MB error body
could bloat any AuditLog.details row that captured the thrown
message. Apply the cap inside the constructor so every downstream
audit, notification, and pg-boss retry path inherits the bound
without touching call sites.
Closes the v1.4.42 L-1 carry-over. A regression test asserts the
1024-char cap for a 4000-char upstreamError while preserving the
"Withings <verb> error: <status>" prefix the legacy regex parser
depends on.
The user_not_found_or_no_password branch wrote the typed email/username
into AuditLog.details, leaking PII into an operator artefact. The reason
field already tells the operator everything they need; the identifier
adds no diagnostic value over hashing or dropping it. Per the v1.4.20
no-PII-in-operator-artefacts directive, drop the field entirely.
A regression test pins the absence of both the literal identifier value
and any "identifier" key in the audit-row JSON.
Mocks `readBestGranularityRollups` with a controllable promise so the
peak in-flight count is observable at the helper boundary. Asserts
peak <= 4 across a 15-type fan-out and re-checks after each drained
resolver so a future regression that floods the pool on completion
would also surface. Mirrors the v1.4.40 W-POOL route-level pin in
`src/app/api/analytics/__tests__/route.test.ts`.
`computeAvg30LastYearMap` in the slim summaries slice ran an unbounded
`Promise.all` over every type the user had data for. On a 15-type
tenant the burst held 15+ Prisma slots simultaneously, and with the
slim and thick `/api/analytics` slices firing in parallel on dashboard
mount the combined fan-out drowned the pg.Pool max=20 raised in
v1.4.40 W-POOL — landing a ~9s server-side wait on every cold mount
since v1.4.40 W-WMY-WIRE.
Cap the per-type fan-out with `p-limit(4)`, mirroring the W-POOL
discipline v1.4.40 applied to the thick route's
`ANALYTICS_TYPE_FETCH_CONCURRENCY`. Same total work, at most 4 slots
held at any moment, leaving headroom for the concurrent thick slice.
Orchestrator hit 78% context with 9 implementation waves dispatched
and running in worktrees. Fresh session picks up at cherry-pick +
W10 QA round + reconcile + release endgame.
Handoff doc inventories every dispatched wave with its agent id and
scope, the post-merge develop+main rebase pattern that recurred in
PR #193, the Marc-voice + PII cleanup checklist for cherry-pick
review, and the docker BuildKit cache verify-loop per the v1.4.42
post-deploy lesson.
Estimated 2-3h fresh session for the closure work.
Carry-over from the v1.4.42 marathon W10 reconcile — the six W10
reviewer reports (code-reviewer, security, design, senior-dev,
product-lead, simplifier-residual) for the release-trail audit-input
that gated the v1.4.42 tag.
Six touch-disjoint waves landed on develop before this release:
- W1-KNIP — exports/types tiers flipped to enforcing-mode on main;
35 unused exports + 52 unused types triaged to 0/0; shadcn surface
+ zod-infer contract types scoped via knip.json ignore-block.
- W2-ZOD-MULTI-ISSUE — `returnAllZodIssues` shared helper +
`/api/dashboard/widgets` 422 now returns every Zod issue + worker
audit-ledger breadcrumb. 41 sibling routes documented for v1.4.43.
- W3-QUERYKEY-LONGTAIL — 40 settings/medications/admin/hooks files
migrated to the queryKeys factory; 9 new factory entries; ESLint
rule + test-guard scope extended in lockstep.
- W4-ARCH-HYGIENE — BERLIN_DAY_FORMATTER consolidated across nine
sites (seven status helpers + bucket-series + bp-in-target);
Suspense double-comment consolidated; doctor-report sanitiser regex
rewritten to escape-form so diffs are reviewable; pr-detection-
worker soft-delete filter; offhost-backup DR-intent comment.
- W5-IOS-WORKOUTS — `dedupeWorkoutBatch` write-time cross-source
dedup helper for the v1.5 iOS HealthKit observer ingest; dashboard
tile-strip placeholder parity with the live TrendCard chrome.
- W6-WITHINGS-OFF + ENV-CHECK — Withings off-response classifier
(transient/reauth/persistent) + WithingsApiError typed throw + the
`pnpm check-env` CLI with the v1.4.40 AP-2 silent-disable manifest
pattern. APNs `allOrNone: true` closes the gap the wave was
conceived to close.
No migration, no env-var change, no API contract break for iOS v0.5.4.
The new `/api/dashboard/widgets` 422 envelope is additively extended.
pnpm test --run green at 4815 passing / 1 skipped.
pnpm typecheck, pnpm lint, pnpm knip (no --include flag) green.
The v1.4.42 senior-dev QA flagged that `dedupeWorkoutBatch` reads the
source ladder from the canonical `DEFAULT_WORKOUT_SOURCE_PRIORITY`
constant unconditionally, while the read-time picker resolves the
per-user `sourcePriorityJson` from the User row. A customised user
who promoted MANUAL above APPLE_HEALTH in Settings -> Sources can
still have their preferred Manual row dropped at write-time before
the read-time picker ever sees it.
Scope-narrow today (most paired users keep the Apple-first default)
but the moment a user customises, the divergence silently overrides
them. The architecturally correct fix (v1.4.43 candidate) is to
accept the small ingest-path cost of one indexed
`User.sourcePriorityJson` lookup and thread the user's ladder into
the picker. Until that lands, this docstring is the load-bearing
operator notice.
The W6 swap from `isWithingsRefreshReauthFailure(message)` to the typed
`classifyError(err)` + `classificationToFailureKind()` chain left two
byte-identical 14-LOC catch-blocks in `sync.ts` — one in
`getValidToken`, one in `syncUserMeasurements`. Extract the shared
recording shape into `recordWithingsSyncFailure(userId, err)` so the
two call sites collapse to a single `await`. Sync-activity / sync-
sleep migration to typed errors (W6 deferred to v1.4.43) gets a single
one-line catch block as a bonus when it lands.
No behaviour change — the 140 withings / integrations unit tests
still pin every failure path.
The W2 `returnAllZodIssues` helper duplicated the meta/headers handling
from `apiError`: same `{headers, ...rest}` destructure, same `metaKeys`
length check, same `...(headers ? {headers} : {})` spread on the
constructor. Six lines that drift the moment one helper gains a feature
(e.g. an `errorId` autoinject for Sentry) the other doesn't.
Extract the shared body into `buildJsonErrorResponse(body, status,
meta)`. Both `apiError` and `returnAllZodIssues` collapse to one-line
constructions of the resource-specific body shape. Wire shape is
byte-identical — the 7-case `api-response-zod` suite and every
downstream `apiError` consumer still see the same envelopes.
The v1.4.42 W5 helper shipped under the same identifier
`pickCanonicalWorkoutRows` as the v1.4.30 read-time picker in
`src/lib/measurements/`. Same name in two modules with different
signatures (write-time takes no user-priority, read-time does) sets
up the next contributor to auto-complete the name in a new route
and silently invoke the wrong picker — no user-priority resolution
on a read, missing the 90-second window on a write.
Rename to `dedupeWorkoutBatch` so the write-time contract is
self-describing. The read-time picker keeps its established name.
One file's export + 12-case test suite + the batch-route caller
update — no algorithm change.
The W3-QUERYKEY-LONGTAIL `medicationIntakeList` factory entry packed
the params as an object at index 4: `["medications", id, "intake",
"list", {sortBy, sortDir, limit, offset, status}]`. TanStack hashes
queryKeys via stable JSON, so the object form happens to work today,
but two sibling factory entries (`chartData`, `adminAuditLogFiltered`)
decompose params into a flat tuple precisely to avoid the deep-equal
trap — a future refactor that reorders the object keys or adds a
transient field would break the hash silently while every existing
caller looks correct.
Flatten the entry to match — the cache key now reads `["medications",
id, "intake", "list", sortBy, sortDir, limit, offset, status]`. The
unit-test walker pins the full tuple shape; the `intake-history-list-v2`
component test seed routes through the factory itself instead of a
hand-built bare literal so the params shape stays in lockstep with the
component's read.
W4-ARCH-HYGIENE's 7-way dedup landed in `src/lib/tz/resolver.ts` but
left two more BERLIN_DAY_FORMATTER declarations alive:
- `src/lib/insights/bucket-series.ts` needed the numeric `{year,
month, day}` parts for `Date.UTC` arithmetic, so it kept its own
formatter and a local `toBerlinYmd` helper.
- `src/lib/analytics/bp-in-target.ts` used a separate en-CA formatter
to skip the part-reassembly when producing the YYYY-MM-DD string.
Export a sibling `toBerlinYmd(date)` helper from the resolver that
returns the formatter's raw string parts so `bucket-series.ts` can
parse them into numbers without instantiating a second formatter.
Switch `bp-in-target.ts` to the existing `toBerlinDayKey` import —
the en-CA shortcut is a microscopic speedup not worth the duplicate
formatter and the divergent contract.
Net: one shared formatter for every Berlin-anchored cache key in the
tree. The 272 insights / analytics / tz unit tests still pass.
W6 widened FailureKind from `transient | reauth_required` to
`+ persistent`. Both formatter branches in `formatAdminAlertPayload`
got the new arm wedged in as a nested ternary, which the style guide
explicitly forbids ("prefer switch statements or if/else chains for
multiple conditions").
Collapse both stacks to a single `FAILURE_KIND_COPY` table lookup keyed
off the union. The table reads as the contract; adding a future fourth
kind is a one-row edit instead of two more arms in two stacks. No
behaviour change — the 28 integration-status tests still pin both the
reason label and the action line verbatim.
Two W10 QA findings on the v1.4.42 env-check wave, both tied to the
v1.4.40 AP-2 silent-disable scenario this script was conceived to
catch:
1. SD-H1 — the APNs group in `scripts/env-manifest.json` shipped
without `allOrNone: true`, so the synthetic `<all-or-none>` required
row never fired. An operator setting three of four `APNS_*` vars
and leaving `APNS_KEY` / `APNS_KEY_FILE` empty would see exit 0 and
green-light the deploy — exactly the v1.4.40 paper-cut the wave was
meant to prevent. Adds the flag.
2. CR-H2 — the renderer printed `[OK] APNS_KEY` even when only
`APNS_KEY_FILE` was set. An operator grepping the env block for
`APNS_KEY` would find nothing and file a ghost issue. The
`CheckResult` now carries `satisfiedBy`, populated to the matched
alternative when it differs from the primary; the renderer prints
`[OK] APNS_KEY (satisfied by APNS_KEY_FILE)`.
Tests pin both fixes — the AP-2 scenario synthetic-row trip
(16 → 19 passing), and the `satisfiedBy` branch coverage on the anyOf
match path.
Captures the gate-flip outcome: 35 unused exports → 0, 52 unused types
→ 0, exit code 0 on the final `pnpm knip` run. Documents the
`ignoreExportsUsedInFile` + `ignoreIssues` ignore-block rationale,
inventories every dropped symbol, and flags the two W3 / W4 reconcile
callouts (`describeInjectionSite` re-export, `listSupportedTimezones`
re-export) along with the merge-order constraint they imply.
Drop the `--include files,dependencies,binaries,unlisted` scope on the
Knip workflow. The tree is now at zero unused exports + zero unused
types, so the gate enforces every issue type knip reports by default.
The `knip.json` ignore-block documents the two muted surfaces
(`src/components/ui/**` for shadcn parity, `src/lib/validations/**`
for the contract-type API surface) so any new addition that breaks
either invariant trips CI immediately.
Every `z.infer<>` source-of-truth type the residual flagged had no
external consumer:
- `MetricSource`, `AIRecommendation`, `AIRecommendationRationale`,
`AICitation`, `AIWarning`, `StoryboardAnnotation` in
`src/lib/ai/schema.ts`
- `InsightFinding` in `src/lib/ai/types.ts`
- `CoachChatRequest` in `src/lib/ai/coach/types.ts`
- `ApplyProfileInput` in `src/lib/auth/profile-update.ts`
- `CreateSideEffectInput`, `ListSideEffectsInput` in
`src/lib/medications/side-effects/validators.ts`
- `AnalyticsSlicePayload` in `src/lib/queries/use-analytics-query.ts`
- `WithingsWebhookAuthOutcome` in `src/lib/withings/webhook-handler.ts`
Plus three stragglers — `ThresholdOverride` (the persisted shape is the
sibling `ThresholdOverridesJson`), `ChartRangePreset` + the now-orphaned
`CHART_RANGE_PRESETS` constant it was derived from (no chart reads the
preset list), and the `WebPushChannelConfig` + `ApnsChannelConfig` +
`ChannelConfig` triplet in `src/lib/notifications/types.ts` (the
dispatcher imports the two real channel configs by name; the empty
records and the union added nothing).
`src/lib/validations/**` keeps its `z.infer<>` types — that's the
external API surface the iOS client matches against. Muted via
`ignoreIssues` so the gate can enforce while the contract surface
stays self-documenting.
Every shadcn UI re-export the simplifier residual flagged
(`AlertDialogMedia/Overlay/Portal`, `AvatarBadge/Group/GroupCount`,
`badgeVariants`, `buttonVariants`, the `Card*` / `Dialog*` /
`DropdownMenu*` / `Select*` / `Sheet*` / `Table*` / `tabsListVariants`
tier) is part of the upstream `new-york-v4` registry — confirmed
against `https://ui.shadcn.com/r/styles/new-york-v4/*.json`. Keeping
them in tree preserves library-shape parity so the next shadcn refresh
applies cleanly. Mute the whole `src/components/ui/**` surface for
the `exports` + `types` issue tiers via `ignoreIssues`.
`ignoreExportsUsedInFile: true` covers the schema-source-of-truth
pattern across `src/lib/ai/**` + `src/lib/validations/**`: the schemas
are exported for the inferred `z.infer<>` type but only referenced
internally for composition. Knip's default behaviour flagged them as
unused exports; the option teaches it that same-file references count.
Two genuine dead helpers go: `requireAdmin` in `src/lib/auth/session.ts`
shadowed the real implementation in `src/lib/api-handler.ts` and had
no callers, and `readDailyMeans` in `src/lib/rollups/measurement-read.ts`
was an unused convenience wrapper around `readRollupBuckets`.
`tokenKind`, `withBackgroundEventSafe`, and `isRollupFresh` had no
callers anywhere in the tree, so the symbol + JSDoc + index re-export
are removed. `PROGRESS_TICK_RECORDS`, `MAINTAINED_LOCALES`, and
`SUB_PAGE_METRIC` lose the `export` keyword — every reader lives in the
same module, so the constants stay local. `isRollupFresh` was deferred
in v1.4.41 pending the persistent-rollup reader landing on develop; the
read-path now carries its own per-tier freshness probe and never
imported the helper.
No behaviour change. Knip drops from 35 unused exports to 29.
W1-KNIP's reconcile callouts identified two re-exports that no caller
ever consumed:
- `src/lib/tz/resolver.ts:51` re-exported `listSupportedTimezones` from
`./format`, but both call-sites (`components/settings/timezone-picker.tsx`
and `components/admin/general-settings-section.tsx`) import directly
from `@/lib/tz/format`.
- `src/components/medications/glp1-medication-card.tsx:595-596`
re-exported `describeInjectionSite` "so the parent doesn't need to
import it", but every real consumer (`therapy-timeline.tsx`,
`injection-site-picker.tsx`, the unit test) imports the source from
`@/lib/medications/injection-sites` directly.
Drops both re-exports and the now-orphaned `describeInjectionSite`
import in `glp1-medication-card.tsx`. Clears the last knip findings so
the gate flip in the next commit lands clean.
The settings / medications / admin / hooks tree still declared
bare-literal `queryKey: [ … ]` arrays at ~70 call sites. Centralise
them through `queryKeys.<entry>()` so mutation-invalidation prefixes
stay symmetrical and a future rename only touches one location.
New factory entries cover per-medication compliance / titration /
cadence / glp1-details / drug-level-chart / intake-list, the
Withings status read, the paginated admin audit-log filter, and the
workouts-recent list opts. Existing entries cover everything else
(passkeys, tokens, settings, telegram, integrations, research-mode,
admin status / users / backups / feedback / host-metrics / app-logs /
ai-quality / assistant-flags, coach-prefs, feature-flags, etc.).
Lockstep updates:
- `eslint-plugins/healthlog/queryKey-factory.js` extends
`GUARDED_DIRECTORIES` to cover `src/components/{settings,
medications,admin}` + `src/hooks`, plus the three guarded files
under `src/app/medications/**` and `src/app/targets/page.tsx`.
- `src/lib/__tests__/query-keys.test.ts` mirrors the guard's
`guardedRoots` and pins the shape of every new factory entry.
Tests stay green: 4 736 unit; the factory walker reports zero
bare-literal `queryKey:` in any newly-guarded surface.
Adds `pnpm check-env` — a CLI tool that reads
`scripts/env-manifest.json` and verifies every required env var is
present + non-empty. Exit code 0 on green, 1 on missing required vars,
2 on a malformed manifest.
Two modes:
pnpm check-env # against process.env
pnpm check-env --file <path> # against a Coolify export / .env.production
The manifest is JSON so it's git-trackable and reviewable. Groups can
be marked `required: true` (boot-blocking) or `required: false`
(silently disable a feature). An `allOrNone: true` group emits a
synthetic <all-or-none> required row when partially populated — this
is the v1.4.40 AP-2 silent-disable pattern (three of four APNS_* set,
.p8 missing) made into a hard failure.
Documents the workflow in docs/ops/env-check.md including the planned
v1.4.43 CI integration. 16 unit tests pin parseEnvFile + checkEnv;
renderResults stays untested as a thin shell over the pure helpers.
This is a CLI tool, not a runtime gate — the app still boots without
this check having run. The point is to make the operator-visible step
between "Coolify env block edited" and "deploy fires" loud about
missing vars before the deploy ships.
Withings sometimes replies HTTP 200 with a non-zero body status, empty
measuregrps, or a transient 503. Pre-v1.4.42 the client surfaced all of
those as identical `Error("Withings <verb> error: <status>")` throws,
and the sync layer used a message regex to bucket reauth vs transient.
That made rate-limit (601) and contract-mismatch (293/294) responses
silently retry forever without ever reaching an admin alert.
Adds `src/lib/withings/response-classifier.ts` — a pure
`(httpStatus, body) -> {success | transient | reauth_required |
persistent}` function plus a `WithingsApiError` subclass carrying the
verdict. The four client entrypoints (exchangeCode, refreshAccessToken,
fetchMeasurements, subscribeWebhook) and both v2-sync paths
(sync-activity, sync-sleep) now throw the typed error; sync.ts reads
err.classification directly via classifyError() and falls back to the
legacy regex when the prototype is lost on a pg-boss retry.
Extends FailureKind with `persistent`. Persistent failures audit with
kind="persistent" and surface a distinct admin-alert label
("persistent error" / "investigate the upstream contract") but keep
state=error_transient so the next sync still runs once the upstream
side fixes the mismatch.
Three test branches pinned (37 new tests, 103 -> 140 passing in
src/lib/withings + src/lib/integrations).
The v1.4.41 W-FRONTEND-FACTORY placeholder rendered as the per-tile
Suspense fallback shipped without the trend-card's flex column or
min-height. The all-suspend edge case (every tile suspending
simultaneously, e.g. a future RSC hoist of every cell) collapsed the
grid row to zero height and the placeholder chrome diverged from the
live tile shell by a couple of layout utilities.
Match the trend-card's container classes byte-for-byte: add flex /
min-w-0 / flex-col for chrome parity and min-h-[6rem] so the fallback
holds the row open during a synchronous all-suspend transition.
Structural Suspense test pin keys on the aria-hidden marker, not the
className, so it stays green without modification.
iOS HealthKit observer queue routinely carries the same logical workout
from two paired sensors (Apple Watch + Withings ScanWatch) in a single
ingest batch. Without write-time dedup both rows persist (their
externalIds differ, so the (userId, source, externalId) unique index
never fires) and the read-time picker has to carry the duplicate
forward on every dashboard render.
Add pickCanonicalWorkoutRows() at src/lib/workouts/canonical-rows.ts as
a pure pre-createMany pass invoked by POST /api/workouts/batch. Groups
by (userId, activityType, startedAt +-90 s); prefers the canonical
source ladder (APPLE_HEALTH > WITHINGS > MANUAL > IMPORT) shared with
the read-time helper; breaks ties on calories > earliest createdAt >
input order. Losers surface to the iOS client as duplicate so the sync
cursor advances past them identically to the externalId case.
pr-detection-worker.ts ran its warm-up count and best-measurement scan
across every Measurement row including soft-deleted ones. A user who
deleted a row that happened to be their PR would not get the next-best
row promoted on the next detection pass. Add `deletedAt: null` to both
where clauses to align with the v1.4.40 soft-delete completeness sweep.
Also document the offhost-backup DR-intent at the matching findMany —
that path deliberately includes soft-deleted rows because the offhost
upload is a disaster-recovery snapshot, not a user-facing export.
src/lib/doctor-report-data.ts was checked in as raw bytes (file(1)
reported "data", git diff reported "Binary files differ") because the
control-character regex carried literal NUL + US + DEL bytes inside the
character class. Rewrite the class as `[\x00-\x1F\x7F]` so the file is
UTF-8 clean and future diffs are readable. Same runtime behaviour — the
existing 18-case test suite still passes.
The trend-card row in src/app/page.tsx carried two adjacent comment
blocks describing the same `<Suspense>` boundary — one from the v1.4.40
W-RSC seed and a second from the v1.4.41 W-FRONTEND-FACTORY fallback
hoist. Collapse into one block keyed to current behaviour with a
one-line history trailer. The sibling chart-row Suspense stayed in
single-block shape so no second consolidation was needed.
Each of the seven src/lib/insights/*-status.ts files carried an identical
20-LOC block defining BERLIN_DAY_FORMATTER plus toBerlinDayKey. Extract
the helper into src/lib/tz/resolver.ts (next to dayKeyForScheduledFor and
friends) and import it everywhere. Same runtime behaviour, 100-LOC drop.
Records the helper landing + widgets-route swap and stages the
v1.4.43 rollout queue (41 sites in src/app/api/** still on the
issues[0].message pattern). Includes the rewrite recipe + iOS-contract
prioritisation so the follow-up wave can pick up without re-discovery.
The PUT /api/dashboard/widgets route surfaced only the first Zod issue
on 422, which forced iOS clients debugging the widget-layout contract
into one round-trip per wrong field. The route now wires the shared
returnAllZodIssues helper so every issue lands in details.issues at
once.
Adds an append-only audit-ledger breadcrumb keyed
dashboard.widgets.validation-failed so the operator can grep
/api/admin/audit for the same trail outside the iOS dev console. The
write is fire-and-forget — the validation response is the contract,
the audit row is best-effort.
Tests cover 2 + 3 simultaneous issues, audit-row shape, params-leak
guard, and audit-write-rejection survival on both the unit and
integration tiers.
Historic pattern was apiError(parsed.error.issues[0].message, 422) which
dropped every issue past the first. Mobile callers debugging a contract
needed one round-trip per wrong field. The new helper returns every
issue under details.issues, additive to the existing envelope so any
client that only reads `error` keeps working.
Sanitises issues to {path, code, message} — issue.params can carry the
raw rejected user input for some Zod codes and must not leak.
Carry-over from the v1.4.41 marathon reconcile — the six W10 reviewer
reports (code-reviewer, security, design, senior-dev, product-lead,
simplifier-residual) were not committed before the squash-merge to
main. Lands them on develop so the next marathon's audit-trail has
the inputs that gated the v1.4.41 tag in one place.
Eight touch-disjoint waves landed on develop before this release:
- W-INSIGHTS-HOT + W-RECONCILE-INSIGHTS — extract the timeout-stub
persist helper and wire all seven *-status routes through it, closing
the recurring ~14 s warm response the iOS client sees on dashboard
open once the AI provider has stalled once that day.
- W-DELETED-2 — close the remaining three soft-delete reader-tier gaps
(export bundle, gamification achievements, doctor-report aggregator)
with a five-route integration-test pin.
- W-ORG — types consolidation into src/types/analytics.ts and
src/types/backups.ts; prompt directory unification.
- W-FRONTEND-FACTORY — queryKey factory expansion + ESLint rule lift.
- W-IOS-COORD — POST /api/auth/check-user four-branch onboarding
discovery, identifier matched verbatim, per-IP rate-limited.
- W-SIMPLIFIER — today-intake projection helper extract; unused tx?
params dropped; 13 knip-flagged dead exports trimmed.
- W-PERF-OPS — v1.4.39 W-SUM legacy-NULL UNION discovery arm retired.
- W-PROCESS-DOCS — pg.Pool sizing operator doc; ESLint rule
formalisation; knip CI gate staged for v1.4.42 flip.
No migration, no env-var change, no API contract break for iOS v0.5.4.
The new check-user endpoint is an additive surface. AP-2 .p8 key is
installed; SB-5 time-sensitive APNs is live as of this deploy.
pnpm test --run green at 4732 passing / 1 skipped.
pnpm typecheck, pnpm lint, pnpm knip green.
Eight touch-disjoint waves landed on develop before this release:
- W-INSIGHTS-HOT + W-RECONCILE-INSIGHTS — extract the timeout-stub
persist helper and wire all seven *-status routes through it, closing
the recurring ~14 s warm response the iOS client sees on dashboard
open once the AI provider has stalled once that day.
- W-DELETED-2 — close the remaining three soft-delete reader-tier gaps
(export bundle, gamification achievements, doctor-report aggregator)
with a five-route integration-test pin.
- W-ORG — types consolidation into src/types/analytics.ts and
src/types/backups.ts; prompt directory unification.
- W-FRONTEND-FACTORY — queryKey factory expansion + ESLint rule lift.
- W-IOS-COORD — POST /api/auth/check-user four-branch onboarding
discovery, identifier matched verbatim, per-IP rate-limited.
- W-SIMPLIFIER — today-intake projection helper extract; unused `tx?`
params dropped; 13 knip-flagged dead exports trimmed.
- W-PERF-OPS — v1.4.39 W-SUM legacy-NULL UNION discovery arm retired.
- W-PROCESS-DOCS — pg.Pool sizing operator doc; ESLint rule
formalisation; knip CI gate staged for v1.4.42 flip.
No migration, no env-var change, no API contract break for iOS v0.5.4.
The new check-user endpoint is an additive surface. AP-2 .p8 key is
installed; SB-5 time-sensitive APNs is live as of this deploy.
pnpm test --run green at 4732 passing / 1 skipped.
pnpm typecheck, pnpm lint, pnpm knip green.
Captures the helper extract + 6 sibling-route wire + cache-shape
reconcile + test pins. Notes the medication-compliance stub-row
recogniser fix and the pulse-status-timeout assertion flip as
risk callouts.
Bring blood-pressure, weight, general, pulse, mood, and
medication-compliance into parity with bmi-status. On a 20 s
provider stall every route now persists a sentinel auditLog row
keyed to today's Berlin day so the next mount short-circuits at
the cache lookup instead of re-racing the same provider call on
every cold visit. Eliminates the ~14 s warm response on every
dashboard open after a one-off provider hiccup.
The seven routes share one persist path through
`persistTimeoutStubAndReturn`; ~120 LOC of duplicate
`auditLog.create` calls collapse into the helper.
medication-compliance returns a richer `{summary, medications}`
envelope rather than the standard `{text}` shape, so it calls
the helper for the persist side-effect and maps `summary` ←
`text` on the way out. Its cache-read also gains a stub-row
recogniser so the second-mount short-circuit fires on the
helper's `text` + `timeout: true` payload.
Tests cover both arms per route: the persist-on-timeout path
asserts a single auditLog.create with `timeout: true`, model
`timeout-stub`, and today's Berlin dateKey; the
short-circuit-on-stub path asserts a subsequent mount returns
the cached stub text without invoking the provider.
Pull the 40-line auditLog.create block out of bmi-status into a
dedicated `persist-timeout-stub.ts` helper so every InsightStatus
route can share one persist path instead of copy-pasting the
sentinel-row shape. The bmi route now calls the helper in a
6-line return; behaviour and persisted body are byte-equivalent
to the inline form.
Sets up the next commit, which wires the six remaining sibling
routes (blood-pressure, weight, general, pulse, mood,
medication-compliance) to the same helper and closes the
last bare-fallback shape on the dashboard hot path.