feat(achievements): worker re-evaluates new + hidden achievements on data change

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>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 10:34:32 +02:00
1 parent 6912d4e2b6
commit 75c74f1c49
5 files changed
+40 -6

No files matched your search

@@ -531,7 +531,8 @@ export const GET = apiHandler(async (request: NextRequest) => {
"auth.login.password",
"bugreport.submit",
// v1.4.18 — hidden Easter-egg triggers
"doctor-report.export",
"doctor-report.generate",
"doctor-report.pdf.generate",
"settings.locale.update",
],
},
+23
View File
@@ -86,6 +86,21 @@ export async function applyProfileUpdate(
if (data.locale !== undefined) updates.locale = data.locale;
if (data.timezone !== undefined) updates.timezone = data.timezone;
// v1.4.18 — capture the prior locale so we can emit a separate
// `settings.locale.update` audit row when the locale actually
// changes. The hidden "polyglot" achievement watches that action,
// and we don't want to fire it on profile updates that happen to
// touch other fields.
const priorLocale =
data.locale !== undefined
? (
await prisma.user.findUnique({
where: { id: userId },
select: { locale: true },
})
)?.locale ?? null
: null;
const updatedUser = await prisma.user.update({
where: { id: userId },
data: updates,
@@ -96,6 +111,14 @@ export async function applyProfileUpdate(
ipAddress: ipAddress ?? null,
});
if (data.locale !== undefined && priorLocale !== data.locale) {
await auditLog("settings.locale.update", {
userId: updatedUser.id,
ipAddress: ipAddress ?? null,
details: { from: priorLocale, to: data.locale ?? null },
});
}
return {
ok: true,
user: {
@@ -235,8 +235,9 @@ describe("getHiddenMetrics", () => {
moodEntries: [],
intakeEvents: [],
auditEvents: [
{ action: "doctor-report.export", createdAt: new Date() },
{ action: "doctor-report.export", createdAt: new Date() },
// The route's auditLog filter accepts both `generate` actions.
{ action: "doctor-report.generate", createdAt: new Date() },
{ action: "doctor-report.pdf.generate", createdAt: new Date() },
{ action: "settings.locale.update", createdAt: new Date() },
{ action: "auth.login.passkey", createdAt: new Date() }, // ignored
],
+10 -1
View File
@@ -276,7 +276,16 @@ export function getHiddenMetrics(input: {
let doctorPdfCount = 0;
let localeFlipCount = 0;
for (const ev of input.auditEvents) {
if (ev.action === "doctor-report.export") doctorPdfCount += 1;
// Both "generate" + "pdf.generate" count — the PDF route emits the
// latter; the JSON-only generate route emits the former. We accept
// either as the trigger so the badge fires the first time the user
// exercises the doctor-report feature in any shape.
if (
ev.action === "doctor-report.generate" ||
ev.action === "doctor-report.pdf.generate"
) {
doctorPdfCount += 1;
}
if (ev.action === "settings.locale.update") localeFlipCount += 1;
}
@@ -159,12 +159,12 @@ describe("achievements expansion — fresh user, real prisma rows", () => {
await prisma.auditLog.create({
data: {
userId: user.id,
action: "doctor-report.export",
action: "doctor-report.pdf.generate",
},
});
const auditEvents = await prisma.auditLog.findMany({
where: { userId: user.id, action: "doctor-report.export" },
where: { userId: user.id, action: "doctor-report.pdf.generate" },
select: { action: true, createdAt: true },
});