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>
Three new layers of coverage for the v1.4.18 expansion set:
- Unit (expansion-metrics.test.ts) — drives synthetic data through
every new evaluator (count by type, mood mean improvement, weekend
warrior, hidden Easter-egg triggers including the Berlin TZ
classifier for night-owl / early-bird) and the earnability flag
builder. 13 new assertions.
- Integration (achievements-expansion.test.ts) — round-trips real
prisma rows (MoodEntry's YYYY-MM-DD `date` string, Measurement
type discrimination, audit log row) through
`buildExpansionMetricValues` so a schema drift in any of the row
shapes surfaces here rather than at runtime. We don't drive the
full HTTP route in this spec because of pre-existing
medication_schedules.days_of_week migration drift.
- E2E (achievements.spec.ts) — the existing sidebar smoke gets a
sibling describe-block exercising all four card states (unlocked /
locked-earnable / hidden-locked / hidden-unlocked) and asserting
hidden-locked cards never leak their real strings to the rendered
HTML, which is the entire point of the "hidden" state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>