v1.4.41 W-DELETED-2 — achievement progress now scopes the measurement
read to deletedAt: null so deleting a row immediately rolls back any
streak or count badge it had earned, matching the analytics + dashboard
sweeps from v1.4.40.
Every endpoint that mutates a measurement / mood / medication /
intake / workout / dashboard-widget row now calls the matching
invalidateUser* helper after its database commit so the next cached
read paints fresh data instead of waiting out the TTL. The admin
app-settings PUT also flushes the bug-report-status singleton cache.
Endpoints wired (per blueprint §6 matrix):
POST/DELETE /api/measurements
PUT/DELETE /api/measurements/[id]
POST /api/measurements/batch
DELETE /api/measurements/by-external-ids
POST/PUT/DELETE /api/mood-entries[/id]
POST /api/mood-entries/bulk
POST/PUT/DELETE /api/medications[/id]
POST /api/medications/[id]/intake
PUT/DELETE /api/medications/[id]/intake/[eventId]
POST /api/medications/intake/bulk
POST /api/workouts/batch
PUT/DELETE /api/dashboard/widgets
PUT /api/admin/settings (bug-report toggle path)
Batch endpoints gate invalidation on insertedCount > 0 so a
duplicates-only ingest stays a no-op. Route-level tests reset every
cache in beforeEach so re-using a fixed userId doesn't carry response
state across cases.
/api/analytics — wraps both the slim (?slice=summaries) and default
slice paths through caches.analytics, keyed on ${userId}|${slice}.
TTL 60s. The default builder lifts into buildAnalyticsResponse() so
the cache miss / hit boundary stays at the route entry.
/api/gamification/achievements — wraps the locale-agnostic web result
through caches.achievements keyed on userId. TTL 60s. The iOS-format
locale transform runs after the cache read so the cache stays format-
neutral and the dashboard's duplicate mount (HAR-captured) coalesces
through single-flight.
/api/medications/intake?scope=compliance — wraps the per-day
compliance bucket build through caches.medicationsIntake keyed on
${userId}|compliance|${days}|${userTz}. TTL 15min because daily
buckets are slow-moving. The POST handler invalidates the per-user
medications/compliance/achievement caches after committing the dose
status change.
Each cached() call hands annotate() to the wrapper so cache.<name>.
outcome (hit|miss|stampede) + cache.<name>.key_hash land on every
request's wide event.
The medication compliance route and the achievement perfect-day
heuristic both bucket the classifier output into `on_time` / `late` /
`very_late`. After the classifier started emitting `early`, both
paths would have silently dropped those doses into `very_late`
(via the trailing `else`). Treat `early` as compliant alongside
`on_time` so the heatmap stays green and the perfect-day streak
survives a proactive logger. The compliance route also surfaces a
dedicated `early` count on `DailyComplianceEntry` for any future
consumer that wants to break it out from `on_time`.
Updates the cadence-pairing docstring to reflect the widened grace.
Pre-existing format drift accumulated across v1.4.x; running
prettier brings the whole tree back to baseline so future diffs
read clean. No behaviour change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Seven mechanical cleanups, no behaviour changes:
- Drop the duplicate ChartOverlayPrefs type + DEFAULT in
chart-overlay-controls; re-export the canonical pair from
dashboard-layout so a future fourth toggle can't drift between
the UI and persistence layers.
- useChartOverlayPrefs accepts null/undefined chartKey and short-
circuits the dashboard-layout fetch when none is supplied. Drops
the chartKey ?? "bp" fallback and the three discard-the-result
ternaries in health-chart; mini-mode renders no longer subscribe
to the BP chart's persisted prefs.
- /api/analytics no longer pair-and-counts BP twice — the windowed
helper already produces last30Days, so the headline reads it back
instead of running computeBpInTargetPct on the same input.
- Drop the redundant moodEntries.map shape in the achievements
route; the prisma select already returns the matching shape.
- Merge the five hidden-metric cases into the no-precondition block
of isEarnable; the defensive comment admitted they couldn't fire.
- Remove the deprecated getAchievementCategory wrapper (zero callers).
- Strip six "v1.4.18 gradient removed" what-comments scattered
across the chart wrappers; the polish tests pin the absence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hidden Easter-eggs leaked their trigger semantics through the JSON
response: a curious user with DevTools open could read metric names
(nightOwlCount, leapDayCount, etc.), i18n keys (hiddenNightOwl,
hiddenLeapDay, etc.), icon names, and target values without unlocking
anything. The opaque-card UI was already correct, but the wire shape
spoiled the surprise.
Project hidden+locked entries to a sentinel shape before serialising
(achievements.hiddenCard.title key, HelpCircle icon, target 0,
metric totalTakenIntakes), and drop hidden-only metric counters from
the metrics block unless the matching achievement is unlocked. The
iOS branch reuses the same redacted view so native clients can't
short-circuit the surprise via the resolved-strings shape.
Once an Easter-egg unlocks the real fields ship — the unlock IS the
reveal moment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two trigger fixes so the new achievements actually fire end-to-end.
The doctor-PDF hidden Easter-egg used to watch a non-existent
`doctor-report.export` audit action. The real route emits two:
`doctor-report.generate` (the JSON helper) and
`doctor-report.pdf.generate` (the PDF route). Either now counts.
The locale-flip hidden Easter-egg used to watch
`settings.locale.update`, which no endpoint emits — the locale
field shares the catch-all `profile.update` audit row with every
other profile field. Profile-update now emits a separate
`settings.locale.update` row when (and only when) the locale value
actually changes (compares the new value to the prior persisted
locale, suppresses the row when they match), with the from/to
values in the details payload for traceability.
Already-existing TanStack Query invalidation map covers the new
metrics: `measurementDependentKeys`, `moodDependentKeys`, and
`medicationDependentKeys` already include
`queryKeys.gamificationAchievements()`, so any measurement / mood /
intake mutation triggers a refetch and the route persists the new
unlock to `UserAchievement` (existing v1.4.15 mechanism).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the v1.4.18 expansion metrics into the gamification route.
- Fetches `MoodEntry` rows alongside the existing measurement /
intake / passkey / audit fetches so the mood badges have data.
- Extends the audit-log query with `doctor-report.export` and
`settings.locale.update` (the new hidden Easter-egg triggers).
- Pushes the raw rows through `buildExpansionMetricValues` —
mood streak / improvement, weight/BP/pulse counts, consistent-month,
entry day-streak, weekend warrior, and the hidden trigger counts.
- Computes `EarnabilityFlags` from the same numbers and applies
`applyDiscoveryFilter` to the evaluated achievement list. Locked
public badges that the user has *no path* to earn yet are filtered
out; hidden Easter-eggs and already-unlocked badges always pass.
- Recomputes the headline summary (`unlockedCount`, `totalCount`,
`earnedPoints`, `totalPoints`, `nextAchievement`) from the visible
set so the dashboard counters match the rendered list. The
`nextAchievement` selector skips hidden cards so we never tease
the user with a "next goal: ?" suggestion.
The iOS format mock had to grow a `moodEntry.findMany` mock; that's
a one-line pure-test change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Long-overdue prettier sweep; prettier had drifted vs. checked-in code
across ~130 files (mostly long-argument line-break wrapping in TS and
table-padding in markdown). Pure formatting — no semantic changes —
and required to clear the v1.4.6 release gate's `pnpm format:check`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an opt-in `?format=ios` query parameter to
`GET /api/gamification/achievements`. When set, the response collapses
the existing `{ achievements, summary }` envelope into a flat
`Achievement[]` with translated `title`/`description` (server-side `t()`
honouring `Accept-Language` + `user.locale`) and `progress` normalised
to 0..1.
Default response (no query param) is unchanged. Existing web client
keeps receiving the wrapped `{ achievements, summary }` plus i18n keys
exactly as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HealthLog — personal health-tracking PWA with weight, blood pressure,
pulse, mood tracking, medication compliance, Withings integration,
doctor report PDF export, and Dracula-themed UI.