mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
Merge fix/v125-coach-nudge into release/v1.25.0
Proactive nudge rework + PUT /api/auth/me/locale.
This commit is contained in:
commit
0989ed4f20
18 files changed
+908
-237
No files matched your search
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* PUT /api/auth/me/locale
|
||||
*
|
||||
* v1.25.0 — persist the user's UI locale onto `User.locale`.
|
||||
*
|
||||
* The language switcher historically wrote the choice to localStorage,
|
||||
* the `healthlog-locale` cookie and `<html lang>` ONLY — never to the
|
||||
* user row. Server-side background work (the proactive Coach nudge, the
|
||||
* Telegram test message, …) has no request cookie to read, so it falls
|
||||
* back to `User.locale`; with the column left null those messages went
|
||||
* out in English regardless of the language the user reads the app in.
|
||||
*
|
||||
* This setter closes the gap: the client persists the active locale here
|
||||
* (on every switch, plus a one-time backfill on mount) so the column
|
||||
* reliably mirrors the UI language and the cron paths render in it.
|
||||
*
|
||||
* Auth: cookie session OR Bearer token.
|
||||
* Validation: `{ "locale": "de" }` — must be one of the supported
|
||||
* locales; anything else is rejected at the surface so the column never
|
||||
* holds a value the resolver would later have to defend against.
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { apiError, apiSuccess, safeJson } from "@/lib/api-response";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { locales, type Locale } from "@/lib/i18n/config";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
function isLocale(value: unknown): value is Locale {
|
||||
return (
|
||||
typeof value === "string" && (locales as readonly string[]).includes(value)
|
||||
);
|
||||
}
|
||||
|
||||
export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAuth();
|
||||
annotate({ action: { name: "user.locale.update" } });
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
maxBytes: 4 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const locale = (body as { locale?: unknown } | null)?.locale;
|
||||
if (!isLocale(locale)) {
|
||||
return apiError("Not a supported locale.", 422);
|
||||
}
|
||||
|
||||
// Idempotent: skip the write (and the annotation noise) when the column
|
||||
// already matches — the mount-time backfill fires on every page load.
|
||||
if (user.locale !== locale) {
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { locale },
|
||||
});
|
||||
annotate({ meta: { locale_next: locale } });
|
||||
}
|
||||
|
||||
return apiSuccess({ locale });
|
||||
});
|
||||
@@ -103,6 +103,7 @@ describe("GET /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
ambientSuggestions: true,
|
||||
nudgeAiComposed: false,
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
@@ -139,6 +140,7 @@ describe("GET /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
ambientSuggestions: true,
|
||||
nudgeAiComposed: false,
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
@@ -177,6 +179,7 @@ describe("GET /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
ambientSuggestions: true,
|
||||
nudgeAiComposed: false,
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
@@ -226,6 +229,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
ambientSuggestions: true,
|
||||
nudgeAiComposed: false,
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
@@ -251,6 +255,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
ambientSuggestions: true,
|
||||
nudgeAiComposed: false,
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
@@ -280,6 +285,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
ambientSuggestions: true,
|
||||
nudgeAiComposed: false,
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
@@ -301,6 +307,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
ambientSuggestions: true,
|
||||
nudgeAiComposed: false,
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
@@ -375,6 +382,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
ambientSuggestions: true,
|
||||
nudgeAiComposed: false,
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
@@ -414,6 +422,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
ambientSuggestions: true,
|
||||
nudgeAiComposed: false,
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
@@ -439,6 +448,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
ambientSuggestions: true,
|
||||
nudgeAiComposed: false,
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
@@ -483,6 +493,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
ambientSuggestions: true,
|
||||
nudgeAiComposed: false,
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
|
||||
Reference in new issue
Block a user