Files
HealthLog/src/lib/validations/admin.ts
T
Marc-André BombeckandMarc-André Bombeck a6524721da fix(i18n): localise medication reminders and dashboard streak per user (#124)
Closes the v3 audit "Locale Mixing" CRIT plus three of the cousin
findings the v1.4 discovery audit added:

1. **`getPhaseMessage` and `getPhaseKeyboard` were German-only.** Every
   medication reminder shipped via Telegram, ntfy, and Web Push read
   "Erinnerung", "Bald fällig", etc. regardless of the recipient's
   locale. This is the user-facing surface most affected by the v3
   "Locale Mixing" finding because the strings hit users on a 15-minute
   cron, not just an admin / settings page they might never open. The
   reminder worker now passes `med.user.locale` through; messages and
   button labels resolve from `messages/{de,en}.json` under
   `medicationReminders.*`. `callback_data` keys stay stable English
   identifiers so the bot dispatcher keeps matching them across locale
   changes.

2. **`/api/dashboard/summary` ships a hard-coded "Tage in Folge" label
   and "Hi, X" salutation.** Both now come from
   `dashboard.streakLabel` / `dashboard.greetingSalutation` resolved
   server-side against the user's locale.

3. **Mixed-locale Zod messages.** `validations/measurement.ts:69` had
   "Wert muss zwischen … liegen" while line 86 had "Value out of
   plausible range" — the response envelope leaked German into English
   sessions and vice versa. `validations/admin.ts` had four German
   messages (web-push subject, Umami URL, Glitchtip DSN, GitHub
   bug-report repo). All now resolve to a single English wording (the
   app is English-first; the German UI translates the field labels
   client-side, the server-side validation message is treated as a
   developer-facing string).

Adds 13 new tests in `reminder-phases.test.ts` covering English,
German, and unknown-locale fallbacks for messages and keyboards;
updates `validations/measurement.test.ts` to match the unified English
message. The audit's remaining items
(`classifications.ts` server-returned EN category strings,
`pulse-targets.ts`, `targets/route.ts` labels,
`/api/admin/notifications/reminder-check`) ride a follow-up i18n PR —
those are admin-only or client-renderable surfaces with lower
user-facing priority.

Tests: 358 → 371 (+13). Typecheck clean. Locale-integrity test still
green.

Co-authored-by: Marc-André Bombeck <noreply@anthropic.com>
2026-05-08 12:11:44 +02:00

79 lines
2.4 KiB
TypeScript

import { z } from "zod/v4";
/**
* Schema for admin settings update (PUT /api/admin/settings).
* All fields are optional — partial updates are supported.
*/
export const adminSettingsSchema = z
.object({
registrationEnabled: z.boolean().optional(),
defaultLocale: z.enum(["de", "en"]).optional(),
telegramGlobal: z.boolean().optional(),
ntfyGlobal: z.boolean().optional(),
webPushGlobal: z.boolean().optional(),
webPushVapidPublicKey: z.string().optional(),
webPushVapidSubject: z
.string()
.refine((v) => !v.trim() || /^mailto:.+@.+$/.test(v.trim()), {
message:
"webPushVapidSubject must be in mailto:address@example.com format",
})
.optional(),
webPushVapidPrivateKey: z.string().optional(),
clearWebPushVapidPrivateKey: z.boolean().optional(),
apiGlobal: z.boolean().optional(),
umamiEnabled: z.boolean().optional(),
umamiScriptUrl: z
.string()
.refine(
(v) => {
const trimmed = v.trim();
if (!trimmed) return true;
try {
const parsed = new URL(trimmed);
return ["https:", "http:"].includes(parsed.protocol);
} catch {
return false;
}
},
{ message: "Umami script URL is invalid" },
)
.optional(),
umamiWebsiteId: z.string().optional(),
glitchtipEnabled: z.boolean().optional(),
glitchtipDsn: z
.string()
.refine(
(v) => {
const trimmed = v.trim();
if (!trimmed) return true;
try {
const parsed = new URL(trimmed);
return ["https:", "http:"].includes(parsed.protocol);
} catch {
return false;
}
},
{ message: "Glitchtip DSN is invalid" },
)
.optional(),
glitchtipEnvironment: z.string().optional(),
bugReportRepo: z
.string()
.refine(
(v) => {
const trimmed = v.trim();
if (!trimmed) return true;
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(trimmed);
},
{ message: "GitHub repository must be in owner/repo format" },
)
.optional(),
bugReportToken: z.string().optional(),
clearBugReportToken: z.boolean().optional(),
reminderLateMinutes: z.number().int().min(15).max(480).optional(),
reminderMissedMinutes: z.number().int().min(30).max(720).optional(),
moodLogGlobal: z.boolean().optional(),
})
.strict();