i18n: migrate from German-first to English-first default locale

Switch defaultLocale from "de" to "en" and make English the code-level
language throughout. All ~145 hardcoded German strings translated:

- API error messages (52 routes) now return English
- Zod validation messages in English
- Classification/analytics functions return English categories
- Notification type labels in English
- Component strings use i18n t() calls (heatmap, mood chart,
  medication form, intake history, password strength, insights card)
- ~50 new i18n keys in both de.json and en.json

Route migration:
- /stimmung → /mood, /zielwerte → /targets (new pages)
- 301 redirects in proxy.ts for legacy URLs
- Sidebar + bottom nav updated

Bump version to 1.0.1.
This commit is contained in:
Marc-André Bombeck committed 2026-03-12 21:31:48 +01:00
1 parent 38309932f3
commit 0c0c345e82
74 files changed
+871 -402

No files matched your search

+2 -2
View File
@@ -30,11 +30,11 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
const body = await request.json();
confirm = typeof body?.confirm === "string" ? body.confirm : "";
} catch {
return apiError("Ungültige Anfrage", 422);
return apiError("Invalid request", 422);
}
if (confirm !== "DELETE ALL") {
return apiError("Bestätigung fehlt", 422);
return apiError("Confirmation missing", 422);
}
const result = await prisma.$transaction(async (tx) => {
@@ -12,10 +12,10 @@ export const POST = apiHandler(async () => {
const settings = await getGlitchtipSettings();
if (!settings.glitchtipEnabled) {
return apiError("Glitchtip ist deaktiviert", 422);
return apiError("Glitchtip is disabled", 422);
}
if (!settings.glitchtipDsn) {
return apiError("Glitchtip DSN fehlt", 422);
return apiError("Glitchtip DSN is missing", 422);
}
const delivery = await sendGlitchtipEvent({
@@ -32,13 +32,13 @@ export const POST = apiHandler(async () => {
if (!delivery.ok) {
getEvent()?.addWarning("Glitchtip test event rejected: " + delivery.method + " " + delivery.status + " " + delivery.details);
return apiError(
`Glitchtip-Testevent wurde abgelehnt (HTTP ${delivery.status ?? 502})`,
`Glitchtip test event rejected (HTTP ${delivery.status ?? 502})`,
502,
);
}
return apiSuccess({
sent: true,
message: "Glitchtip-Testevent wurde gesendet",
message: "Glitchtip test event sent",
});
});
@@ -51,18 +51,18 @@ export const POST = apiHandler(async (request: NextRequest) => {
const settings = await getPublicMonitoringSettings();
if (!settings.umamiEnabled) {
return apiError("Umami ist deaktiviert", 422);
return apiError("Umami is disabled", 422);
}
if (!settings.umamiScriptUrl || !settings.umamiWebsiteId) {
return apiError(
"Umami Script-URL und Website-ID müssen konfiguriert sein",
"Umami script URL and website ID must be configured",
422,
);
}
const targetUrls = resolveUmamiSendUrls(settings.umamiScriptUrl);
if (targetUrls.length === 0) {
return apiError("Umami Script-URL ist ungültig", 422);
return apiError("Umami script URL is invalid", 422);
}
const appUrl = resolveAppUrl(request);
@@ -100,7 +100,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
if (upstream.ok) {
return apiSuccess({
sent: true,
message: "Umami-Testevent wurde gesendet",
message: "Umami test event sent",
});
}
@@ -117,7 +117,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
getEvent()?.addWarning("Umami test event rejected: " + lastStatus);
return apiError(
`Umami-Testevent wurde abgelehnt (HTTP ${lastStatus})`,
`Umami test event rejected (HTTP ${lastStatus})`,
502,
);
});
+10 -10
View File
@@ -79,7 +79,7 @@ export const POST = apiHandler(async () => {
if (channels.length === 0) {
return apiSuccess({
sent: false,
message: "Keine aktivierten Benachrichtigungskanäle gefunden",
message: "No enabled notification channels found",
results: [],
});
}
@@ -89,7 +89,7 @@ export const POST = apiHandler(async () => {
userId: user.id,
title: "Test-Notification",
message:
"<b>HealthLog Test:</b> Wenn du diese Nachricht siehst, funktionieren deine Benachrichtigungen!",
"<b>HealthLog Test:</b> If you see this message, your notifications are working!",
};
const results: Array<{
@@ -104,7 +104,7 @@ export const POST = apiHandler(async () => {
results.push({
channel: channel.type,
success: false,
error: "SYSTEM_ALERT deaktiviert in Einstellungen",
error: "SYSTEM_ALERT disabled in settings",
});
continue;
}
@@ -124,7 +124,7 @@ export const POST = apiHandler(async () => {
results.push({
channel: "TELEGRAM",
success: false,
error: `Telegram API hat Fehler gemeldet (chatId: ${config.chatId})`,
error: `Telegram API reported error (chatId: ${config.chatId})`,
});
continue;
}
@@ -139,7 +139,7 @@ export const POST = apiHandler(async () => {
results.push({
channel: "NTFY",
success: false,
error: `Senden fehlgeschlagen (topic: ${config.topic})`,
error: `Send failed (topic: ${config.topic})`,
});
continue;
}
@@ -153,7 +153,7 @@ export const POST = apiHandler(async () => {
results.push({
channel: type,
success: false,
error: "Unbekannter Kanaltyp",
error: "Unknown channel type",
});
continue;
}
@@ -163,7 +163,7 @@ export const POST = apiHandler(async () => {
results.push({
channel: channel.type,
success: false,
error: err instanceof Error ? err.message : "Unbekannter Fehler",
error: err instanceof Error ? err.message : "Unknown error",
});
}
}
@@ -173,11 +173,11 @@ export const POST = apiHandler(async () => {
let message: string;
if (failCount === 0) {
message = `Alle ${successCount} Kanäle erfolgreich`;
message = `All ${successCount} channels successful`;
} else if (successCount === 0) {
message = `Alle ${failCount} Kanäle fehlgeschlagen`;
message = `All ${failCount} channels failed`;
} else {
message = `${successCount} erfolgreich, ${failCount} fehlgeschlagen`;
message = `${successCount} successful, ${failCount} failed`;
}
return apiSuccess({ sent: successCount > 0, message, results });
+1 -1
View File
@@ -180,7 +180,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
}
if (Object.keys(updates).length === 0) {
return apiError("Keine gültigen Felder", 422);
return apiError("No valid fields", 422);
}
const settings = await prisma.appSettings.upsert({
@@ -32,16 +32,16 @@ export const POST = apiHandler(async (
const { password } = body as { password?: string };
if (!password || typeof password !== "string") {
return apiError("Passwort erforderlich", 422);
return apiError("Password required", 422);
}
const target = await prisma.user.findUnique({ where: { id } });
if (!target) return apiError("Benutzer nicht gefunden", 404);
if (!target) return apiError("User not found", 404);
const strength = checkPasswordStrength(password, [target.username]);
if (!strength.isAcceptable) {
return apiError(
strength.feedback[0] || "Passwort zu schwach (Score < 3)",
strength.feedback[0] || "Password too weak (score < 3)",
422,
);
}
+2 -2
View File
@@ -30,13 +30,13 @@ export const PUT = apiHandler(async (
}
const target = await prisma.user.findUnique({ where: { id } });
if (!target) return apiError("Benutzer nicht gefunden", 404);
if (!target) return apiError("User not found", 404);
// Prevent removing the last admin
if (parsed.data.role === "USER" && target.role === "ADMIN") {
const adminCount = await prisma.user.count({ where: { role: "ADMIN" } });
if (adminCount <= 1) {
return apiError("Der letzte Admin kann nicht degradiert werden", 400);
return apiError("The last admin cannot be demoted", 400);
}
}
+4 -4
View File
@@ -17,7 +17,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
return NextResponse.json(
{
data: null,
error: "Zu viele Anmeldeversuche. Bitte später erneut versuchen.",
error: "Too many login attempts. Please try again later.",
},
{ status: 429, headers: rateLimitHeaders(rl) },
);
@@ -31,7 +31,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
const parsed = loginPasswordSchema.safeParse(body);
if (!parsed.success) {
return apiError("Ungültige Anmeldedaten", 422);
return apiError("Invalid credentials", 422);
}
const { email, password } = parsed.data;
@@ -51,7 +51,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
ipAddress: ip,
details: { identifier, reason: "user_not_found_or_no_password" },
});
return apiError("Ungültige Anmeldedaten", 401);
return apiError("Invalid credentials", 401);
}
const valid = await verifyPassword(user.passwordHash, password);
@@ -61,7 +61,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
ipAddress: ip,
details: { reason: "invalid_password" },
});
return apiError("Ungültige Anmeldedaten", 401);
return apiError("Invalid credentials", 401);
}
const ua = request.headers.get("user-agent");
@@ -16,7 +16,7 @@ export const POST = apiHandler(async (request: Request) => {
return NextResponse.json(
{
data: null,
error: "Zu viele Passkey-Anfragen. Bitte später erneut versuchen.",
error: "Too many passkey requests. Please try again later.",
},
{ status: 429, headers: rateLimitHeaders(rl) },
);
@@ -15,7 +15,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
const ip = getClientIp(request);
const rl = await checkRateLimit(`auth:passkey-verify:${ip}`, 10, 15 * 60 * 1000);
if (!rl.allowed) {
return apiError("Zu viele Versuche. Bitte 15 Minuten warten.", 429);
return apiError("Too many attempts. Please wait 15 minutes.", 429);
}
const { data: body, error: jsonError } = await safeJson<Record<string, unknown>>(request);
@@ -25,7 +25,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
const credential = body.credential;
if (!challengeId || !credential) {
return apiError("challengeId und credential erforderlich", 422);
return apiError("challengeId and credential required", 422);
}
const { verification, passkey } = await verifyAuthentication(
@@ -38,7 +38,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
ipAddress: ip,
details: { reason: "passkey_verification_failed" },
});
return apiError("Passkey-Verifizierung fehlgeschlagen", 401);
return apiError("Passkey verification failed", 401);
}
const user = await prisma.user.findUnique({
@@ -46,7 +46,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
});
if (!user) {
return apiError("Benutzer nicht gefunden", 404);
return apiError("User not found", 404);
}
const ua = request.headers.get("user-agent");
@@ -16,13 +16,13 @@ export const POST = apiHandler(async (request: NextRequest) => {
const credential = body.credential as Record<string, unknown> | undefined;
if (!challengeId || !credential) {
return apiError("challengeId und credential erforderlich", 422);
return apiError("challengeId and credential required", 422);
}
const verification = await verifyRegistration(challengeId, credential);
if (!verification.verified || !verification.registrationInfo) {
return apiError("Passkey-Verifizierung fehlgeschlagen", 400);
return apiError("Passkey verification failed", 400);
}
const { registrationInfo } = verification;
+2 -2
View File
@@ -18,7 +18,7 @@ export const DELETE = apiHandler(async (
});
if (!passkey || passkey.userId !== user.id) {
return apiError("Passkey nicht gefunden", 404);
return apiError("Passkey not found", 404);
}
// Check: at least 1 auth method must remain
@@ -29,7 +29,7 @@ export const DELETE = apiHandler(async (
if (passkeyCount <= 1 && !hasPassword) {
return apiError(
"Kann nicht gelöscht werden — es muss mindestens eine Anmeldemethode bestehen bleiben",
"Cannot delete — at least one authentication method must remain",
400,
);
}
+5 -5
View File
@@ -23,7 +23,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
);
if (!rl.allowed) {
return apiError(
"Zu viele Versuche. Bitte 15 Minuten warten.",
"Too many attempts. Please wait 15 minutes.",
429,
);
}
@@ -38,7 +38,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
const { currentPassword, newPassword } = parsed.data;
if (!user.passwordHash) {
return apiError("Für dieses Konto ist kein Passwort gesetzt", 400);
return apiError("No password set for this account", 400);
}
const currentValid = await verifyPassword(
@@ -46,12 +46,12 @@ export const POST = apiHandler(async (request: NextRequest) => {
currentPassword,
);
if (!currentValid) {
return apiError("Aktuelles Passwort ist falsch", 401);
return apiError("Current password is incorrect", 401);
}
if (currentPassword === newPassword) {
return apiError(
"Neues Passwort muss sich vom aktuellen unterscheiden",
"New password must differ from current password",
422,
);
}
@@ -62,7 +62,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
]);
if (!strength.isAcceptable) {
return apiError(
strength.feedback[0] || "Passwort zu schwach (Score < 3)",
strength.feedback[0] || "Password too weak (score < 3)",
422,
);
}
+1 -1
View File
@@ -28,7 +28,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
});
if (existingUser && existingUser.id !== user.id) {
return apiError("E-Mail-Adresse wird bereits verwendet", 409);
return apiError("Email already in use", 409);
}
}
+4 -4
View File
@@ -18,7 +18,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
{
data: null,
error:
"Zu viele Registrierungsversuche. Bitte später erneut versuchen.",
"Too many registration attempts. Please try again later.",
},
{ status: 429, headers: rateLimitHeaders(rl) },
);
@@ -42,7 +42,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
// Table may not exist yet; allow registration
}
if (!registrationEnabled) {
return apiError("Registrierung ist deaktiviert", 403);
return apiError("Registration is disabled", 403);
}
const { data: body, error: jsonError } = await safeJson(request);
@@ -62,14 +62,14 @@ export const POST = apiHandler(async (request: NextRequest) => {
prisma.user.findUnique({ where: { username } }),
]);
if (existingEmail || existingUsername) {
return apiError("Benutzername oder E-Mail-Adresse bereits vergeben", 409);
return apiError("Username or email already taken", 409);
}
// Validate password strength
const strength = checkPasswordStrength(password, [username, email]);
if (!strength.isAcceptable) {
return apiError(
strength.feedback[0] || "Passwort zu schwach (Score < 3)",
strength.feedback[0] || "Password too weak (score < 3)",
422,
);
}
+11 -11
View File
@@ -9,10 +9,10 @@ import { NextRequest } from "next/server";
import { z } from "zod/v4";
const bugReportSchema = z.object({
description: z.string().min(10, "Beschreibung zu kurz").max(5000),
description: z.string().min(10, "Description too short").max(5000),
screenshot: z
.string()
.max(7_000_000, "Screenshot zu groß (max. 5 MB)")
.max(7_000_000, "Screenshot too large (max 5 MB)")
.optional(),
});
@@ -21,7 +21,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
const rl = await checkRateLimit(`bugreport:${user.id}`, 3, 60 * 60 * 1000);
if (!rl.allowed) {
return apiError("Maximal 3 Bugreports pro Stunde", 429);
return apiError("Maximum 3 bug reports per hour", 429);
}
annotate({ action: { name: "bugreport.submit" } });
@@ -48,7 +48,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
if (!ghToken || !ghRepo) {
return apiError(
"Bugreport ist nicht konfiguriert (GitHub-Issue-Token/Repository in Admin-Einstellungen fehlt)",
"Bug report not configured (GitHub issue token/repository missing in admin settings)",
500,
);
}
@@ -75,15 +75,15 @@ export const POST = apiHandler(async (request: NextRequest) => {
.slice(0, 5000);
// Build issue body
let issueBody = `**Gemeldet von:** ${safeUsername}\n`;
issueBody += `**Datum:** ${dateStr}\n\n`;
issueBody += `## Beschreibung\n\n${safeDescription}\n`;
issueBody += `\n---\n*Erstellt über die HealthLog Bugreport-Funktion*`;
let issueBody = `**Reported by:** ${safeUsername}\n`;
issueBody += `**Date:** ${dateStr}\n\n`;
issueBody += `## Description\n\n${safeDescription}\n`;
issueBody += `\n---\n*Created via HealthLog bug report*`;
// If screenshot provided, upload to GitHub as a gist or include as base64
// For simplicity, we'll note there's a screenshot but GitHub issues don't support inline base64
if (screenshot) {
issueBody += `\n\n**Screenshot:** Ein Screenshot wurde angehängt (siehe Kommentar).`;
issueBody += `\n\n**Screenshot:** A screenshot was attached (see comment).`;
}
// Create the issue
@@ -107,7 +107,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
if (!issueRes.ok) {
const errBody = await issueRes.text();
getEvent()?.addWarning("GitHub issue creation failed: " + errBody);
return apiError("Issue konnte nicht erstellt werden", 500);
return apiError("Failed to create issue", 500);
}
const issue = (await issueRes.json()) as {
@@ -123,7 +123,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
// Upload screenshot as issue comment with embedded image
// GitHub doesn't support base64 in comments, so we note the limitation
const commentBody = `### Screenshot\n\n![Screenshot](data:image/${ext};base64,${base64Data.slice(0, 100)}...)\n\n*Hinweis: Der vollständige Screenshot wurde aus Größengründen gekürzt. Bitte den Nutzer direkt kontaktieren.*`;
const commentBody = `### Screenshot\n\n![Screenshot](data:image/${ext};base64,${base64Data.slice(0, 100)}...)\n\n*Note: The full screenshot was truncated due to size limits. Please contact the user directly.*`;
await fetch(
`https://api.github.com/repos/${ghRepo}/issues/${issue.number}/comments`,
+1 -1
View File
@@ -16,7 +16,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
const rl = await checkRateLimit(`doctor-report:${user.id}`, 10, 60 * 60 * 1000);
if (!rl.allowed) {
return apiError("Maximal 10 Reports pro Stunde", 429);
return apiError("Maximum 10 reports per hour", 429);
}
const { data: body, error: jsonError } = await safeJson(request);
+3 -3
View File
@@ -26,7 +26,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
const rl = await checkRateLimit(`export:${user.id}`, 10, 60 * 60 * 1000);
if (!rl.allowed) {
return apiError("Maximal 10 Exports pro Stunde", 429);
return apiError("Maximum 10 exports per hour", 429);
}
annotate({ action: { name: "export.download" } });
@@ -36,10 +36,10 @@ export const GET = apiHandler(async (request: NextRequest) => {
const type = searchParams.get("type") ?? "all";
if (!["csv", "json"].includes(format)) {
return apiError("Format muss csv oder json sein", 422);
return apiError("Format must be csv or json", 422);
}
if (!["measurements", "medications", "intake", "mood", "all"].includes(type)) {
return apiError("Typ muss measurements, medications, intake, mood oder all sein", 422);
return apiError("Type must be measurements, medications, intake, mood, or all", 422);
}
const userId = user.id;
+3 -3
View File
@@ -19,7 +19,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
annotate({ action: { name: "ingest.medication" } });
if (!(await isApiGloballyEnabled())) {
return apiError("API ist global deaktiviert", 403);
return apiError("API is globally disabled", 403);
}
const ip = getClientIp(request);
@@ -109,12 +109,12 @@ export const POST = apiHandler(async (request: NextRequest) => {
});
if (!medication) {
return apiError("Medikament nicht gefunden", 404);
return apiError("Medication not found", 404);
}
const medicationScope = `medication:${medication.id}:ingest`;
if (!apiToken.permissions.includes(medicationScope)) {
return apiError("API-Endpoint fuer dieses Medikament ist deaktiviert", 403);
return apiError("API endpoint for this medication is disabled", 403);
}
const event = await prisma.medicationIntakeEvent.create({
+6 -6
View File
@@ -27,7 +27,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
const rl = await checkRateLimit(`insights:${userId}`, 2, 60 * 60 * 1000);
if (!rl.allowed) {
return apiError(
"Maximal 2 Insights-Generierungen pro Stunde. Bitte spaeter erneut versuchen.",
"Maximum 2 insight generations per hour. Please try again later.",
429,
);
}
@@ -44,7 +44,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
if (!dbUser?.openaiKeyEncrypted) {
return apiError(
"Kein OpenAI API-Key hinterlegt. Bitte in den Einstellungen konfigurieren.",
"No OpenAI API key configured. Please set it in settings.",
422,
);
}
@@ -105,23 +105,23 @@ export const POST = apiHandler(async (request: NextRequest) => {
const errBody = await openaiRes.text();
annotate({ meta: { openai_status: openaiRes.status, openai_error: errBody } });
if (openaiRes.status === 401) {
return apiError("Ungueltiger OpenAI API-Key", 422);
return apiError("Invalid OpenAI API key", 422);
}
return apiError("OpenAI-Anfrage fehlgeschlagen", 502);
return apiError("OpenAI request failed", 502);
}
const openaiJson = await openaiRes.json();
const content = openaiJson.choices?.[0]?.message?.content;
if (!content) {
return apiError("Keine Antwort von OpenAI erhalten", 502);
return apiError("No response from OpenAI", 502);
}
let insights: InsightsOutput;
try {
insights = JSON.parse(content);
} catch {
return apiError("OpenAI-Antwort konnte nicht verarbeitet werden", 502);
return apiError("Failed to parse OpenAI response", 502);
}
// Cache the result
+3 -3
View File
@@ -51,7 +51,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
} else {
if (!key.startsWith("sk-")) {
return apiError(
"Ungueltiges API-Key-Format (muss mit sk- beginnen)",
"Invalid API key format (must start with sk-)",
422,
);
}
@@ -63,7 +63,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
if (typeof body.privacyMode === "string") {
const mode = body.privacyMode as string;
if (!["aggregated", "raw"].includes(mode)) {
return apiError("Ungueltiger Privacy-Modus", 422);
return apiError("Invalid privacy mode", 422);
}
data.insightsPrivacyMode = mode;
// Clear cache when privacy mode changes
@@ -72,7 +72,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
}
if (Object.keys(data).length === 0) {
return apiError("Keine Aenderungen", 422);
return apiError("No changes", 422);
}
await prisma.user.update({
+24 -24
View File
@@ -161,7 +161,7 @@ export const GET = apiHandler(async () => {
}
targets.push({
type: "WEIGHT",
label: "Gewicht",
label: "Weight",
current: latestByType.WEIGHT ?? null,
average30: avg30ByType.WEIGHT ?? null,
trend: computeTrend("WEIGHT"),
@@ -186,7 +186,7 @@ export const GET = apiHandler(async () => {
}
targets.push({
type: "BLOOD_PRESSURE",
label: "Blutdruck",
label: "Blood pressure",
current: latestByType.BLOOD_PRESSURE_SYS ?? null,
average30: avg30ByType.BLOOD_PRESSURE_SYS ?? null,
trend: computeTrend("BLOOD_PRESSURE_SYS"),
@@ -227,7 +227,7 @@ export const GET = apiHandler(async () => {
targets.push({
type: "BLOOD_PRESSURE_IN_TARGET",
label: "Blutdruck im Zielbereich",
label: "Blood pressure on target",
current: bpInTargetRate,
average30: bpInTargetRate,
trend: null,
@@ -236,10 +236,10 @@ export const GET = apiHandler(async () => {
classification:
bpInTargetRate != null
? bpInTargetRate >= 70
? { category: "Gut", color: "#50fa7b" }
? { category: "Good", color: "#50fa7b" }
: bpInTargetRate >= 40
? { category: "Moderat", color: "#f1fa8c" }
: { category: "Niedrig", color: "#ff5555" }
? { category: "Moderate", color: "#f1fa8c" }
: { category: "Low", color: "#ff5555" }
: null,
source: "ESC/ESH 2018",
});
@@ -254,7 +254,7 @@ export const GET = apiHandler(async () => {
}
targets.push({
type: "PULSE",
label: "Ruhepuls",
label: "Resting pulse",
current: latestByType.PULSE ?? null,
average30: avg30ByType.PULSE ?? null,
trend: computeTrend("PULSE"),
@@ -273,7 +273,7 @@ export const GET = apiHandler(async () => {
}
targets.push({
type: "SLEEP_DURATION",
label: "Schlafdauer",
label: "Sleep duration",
current: latestByType.SLEEP_DURATION ?? null,
average30: avg30ByType.SLEEP_DURATION ?? null,
trend: computeTrend("SLEEP_DURATION"),
@@ -335,7 +335,7 @@ export const GET = apiHandler(async () => {
}
targets.push({
type: "BODY_FAT",
label: "Koerperfett",
label: "Body fat",
current: latestByType.BODY_FAT ?? null,
average30: avg30ByType.BODY_FAT ?? null,
trend: computeTrend("BODY_FAT"),
@@ -354,11 +354,11 @@ export const GET = apiHandler(async () => {
}
targets.push({
type: "ACTIVITY_STEPS",
label: "Schritte/Tag",
label: "Steps/day",
current: latestByType.ACTIVITY_STEPS ?? null,
average30: avg30ByType.ACTIVITY_STEPS ?? null,
trend: computeTrend("ACTIVITY_STEPS"),
unit: "Schritte",
unit: "steps",
range: stepsRange,
classification: stepsClassification,
source: "WHO",
@@ -453,7 +453,7 @@ export const GET = apiHandler(async () => {
targets.push({
type: "MEDICATION_COMPLIANCE",
label: "Einnahmetreue",
label: "Medication compliance",
current: complianceRate7,
average30: complianceRate30,
trend: null,
@@ -462,12 +462,12 @@ export const GET = apiHandler(async () => {
classification:
complianceRate7 != null
? complianceRate7 >= 90
? { category: "Sehr gut", color: "#50fa7b" }
? { category: "Very good", color: "#50fa7b" }
: complianceRate7 >= 70
? { category: "Gut", color: "#f1fa8c" }
: { category: "Niedrig", color: "#ff5555" }
? { category: "Good", color: "#f1fa8c" }
: { category: "Low", color: "#ff5555" }
: null,
source: "7-Tage",
source: "7-day",
details: {
medications: medicationStats.map((medication) => ({
name: medication.name,
@@ -523,15 +523,15 @@ export const GET = apiHandler(async () => {
const moodClassification =
latestMoodScore != null
? latestMoodScore >= 3.5
? { category: "Gut", color: "#50fa7b" }
? { category: "Good", color: "#50fa7b" }
: latestMoodScore >= 2
? { category: "Moderat", color: "#f1fa8c" }
: { category: "Niedrig", color: "#ff5555" }
? { category: "Moderate", color: "#f1fa8c" }
: { category: "Low", color: "#ff5555" }
: null;
targets.push({
type: "MOOD_SCORE",
label: "Stimmung",
label: "Mood",
current: latestMoodScore,
average30: moodAvg30,
trend: moodTrend,
@@ -551,14 +551,14 @@ export const GET = apiHandler(async () => {
const stabilityClassification =
stdDev <= 0.5
? { category: "Sehr stabil", color: "#50fa7b" }
? { category: "Very stable", color: "#50fa7b" }
: stdDev <= 1.0
? { category: "Stabil", color: "#f1fa8c" }
: { category: "Schwankend", color: "#ff5555" };
? { category: "Stable", color: "#f1fa8c" }
: { category: "Fluctuating", color: "#ff5555" };
targets.push({
type: "MOOD_STABILITY",
label: "Stimmungsstabilitaet",
label: "Mood stability",
current: stdDev,
average30: stdDev,
trend: null,
@@ -17,7 +17,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
select: { moodLogGlobal: true },
});
if (appSettings && !appSettings.moodLogGlobal) {
return apiError("moodLog-Integration ist deaktiviert", 403);
return apiError("moodLog integration is disabled", 403);
}
let fullSync = false;
+3 -3
View File
@@ -18,7 +18,7 @@ export const GET = apiHandler(async (_request: NextRequest, { params }: RoutePar
});
if (!measurement || measurement.userId !== user.id) {
return apiError("Messwert nicht gefunden", 404);
return apiError("Measurement not found", 404);
}
annotate({
@@ -42,7 +42,7 @@ export const PUT = apiHandler(async (request: NextRequest, { params }: RoutePara
});
if (!existing || existing.userId !== user.id) {
return apiError("Messwert nicht gefunden", 404);
return apiError("Measurement not found", 404);
}
const { data: body, error: jsonError } = await safeJson(request);
@@ -90,7 +90,7 @@ export const DELETE = apiHandler(async (request: NextRequest, { params }: RouteP
});
if (!existing || existing.userId !== user.id) {
return apiError("Messwert nicht gefunden", 404);
return apiError("Measurement not found", 404);
}
await prisma.measurement.delete({ where: { id } });
+1 -1
View File
@@ -134,7 +134,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
});
} catch (err) {
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
return apiError("Ein Messwert mit diesen Daten existiert bereits", 409);
return apiError("A measurement with this data already exists", 409);
}
throw err;
}
@@ -17,13 +17,13 @@ function medicationScope(medicationId: string): string {
export const GET = apiHandler(async (_request: NextRequest, { params }: RouteParams) => {
const { user } = await requireAuth();
if (!(await isApiGloballyEnabled())) {
return apiError("API ist global deaktiviert", 403);
return apiError("API is globally disabled", 403);
}
const { id } = await params;
const medication = await prisma.medication.findUnique({ where: { id } });
if (!medication || medication.userId !== user.id) {
return apiError("Medikament nicht gefunden", 404);
return apiError("Medication not found", 404);
}
const scope = medicationScope(id);
@@ -56,13 +56,13 @@ export const GET = apiHandler(async (_request: NextRequest, { params }: RoutePar
export const PUT = apiHandler(async (request: NextRequest, { params }: RouteParams) => {
const { user } = await requireAuth();
if (!(await isApiGloballyEnabled())) {
return apiError("API ist global deaktiviert", 403);
return apiError("API is globally disabled", 403);
}
const { id } = await params;
const medication = await prisma.medication.findUnique({ where: { id } });
if (!medication || medication.userId !== user.id) {
return apiError("Medikament nicht gefunden", 404);
return apiError("Medication not found", 404);
}
let enabled = false;
@@ -70,7 +70,7 @@ export const PUT = apiHandler(async (request: NextRequest, { params }: RoutePara
const body = await request.json();
enabled = body?.enabled === true;
} catch {
return apiError("Ungültige Anfrage", 422);
return apiError("Invalid request", 422);
}
const scope = medicationScope(id);
@@ -20,7 +20,7 @@ export const GET = apiHandler(async (_request: Request, { params }: RouteParams)
});
if (!medication || medication.userId !== user.id) {
return apiError("Medikament nicht gefunden", 404);
return apiError("Medication not found", 404);
}
const events = await prisma.medicationIntakeEvent.findMany({
@@ -22,7 +22,7 @@ export const PUT = apiHandler(async (request: NextRequest, { params }: RoutePara
event.userId !== user.id ||
event.medicationId !== id
) {
return apiError("Einnahme nicht gefunden", 404);
return apiError("Intake not found", 404);
}
const { data: body, error: jsonError } = await safeJson(request);
@@ -77,7 +77,7 @@ export const DELETE = apiHandler(async (request: NextRequest, { params }: RouteP
event.userId !== user.id ||
event.medicationId !== id
) {
return apiError("Einnahme nicht gefunden", 404);
return apiError("Intake not found", 404);
}
await prisma.medicationIntakeEvent.delete({ where: { id: eventId } });
@@ -22,7 +22,7 @@ export const POST = apiHandler(async (request: NextRequest, { params }: RoutePar
const { id } = await params;
const medication = await prisma.medication.findUnique({ where: { id } });
if (!medication || medication.userId !== user.id) {
return apiError("Medikament nicht gefunden", 404);
return apiError("Medication not found", 404);
}
const { data: body, error: jsonError } = await safeJson(request);
@@ -39,7 +39,7 @@ export const POST = apiHandler(async (request: NextRequest, { params }: RoutePar
const parsed = importSchema.safeParse(payload);
if (!parsed.success) {
return apiError(
`Ungültiges Format: ${parsed.error.issues[0].message}`,
`Invalid format: ${parsed.error.issues[0].message}`,
422,
);
}
@@ -13,7 +13,7 @@ export const DELETE = apiHandler(async (request: NextRequest, { params }: RouteP
const { id } = await params;
const medication = await prisma.medication.findUnique({ where: { id } });
if (!medication || medication.userId !== user.id) {
return apiError("Medikament nicht gefunden", 404);
return apiError("Medication not found", 404);
}
const { count } = await prisma.medicationIntakeEvent.deleteMany({
+2 -2
View File
@@ -17,7 +17,7 @@ export const POST = apiHandler(async (request: NextRequest, { params }: RoutePar
const { id } = await params;
const medication = await prisma.medication.findUnique({ where: { id } });
if (!medication || medication.userId !== user.id) {
return apiError("Medikament nicht gefunden", 404);
return apiError("Medication not found", 404);
}
const { data: body, error: jsonError } = await safeJson(request);
@@ -108,7 +108,7 @@ export const GET = apiHandler(async (request: NextRequest, { params }: RoutePara
const { id } = await params;
const medication = await prisma.medication.findUnique({ where: { id } });
if (!medication || medication.userId !== user.id) {
return apiError("Medikament nicht gefunden", 404);
return apiError("Medication not found", 404);
}
const searchParams = Object.fromEntries(request.nextUrl.searchParams);
@@ -16,7 +16,7 @@ export const GET = apiHandler(async (_request: NextRequest, { params }: RoutePar
select: { userId: true },
});
if (!medication || medication.userId !== user.id) {
return apiError("Medikament nicht gefunden", 404);
return apiError("Medication not found", 404);
}
const config = await prisma.reminderPhaseConfig.findUnique({
@@ -55,7 +55,7 @@ export const PUT = apiHandler(async (request: NextRequest, { params }: RoutePara
select: { userId: true },
});
if (!medication || medication.userId !== user.id) {
return apiError("Medikament nicht gefunden", 404);
return apiError("Medication not found", 404);
}
const { data: body, error: jsonError } = await safeJson(request);
@@ -63,7 +63,7 @@ export const PUT = apiHandler(async (request: NextRequest, { params }: RoutePara
if (jsonError) return jsonError;
const parsed = phaseConfigSchema.safeParse(body);
if (!parsed.success) {
return apiError("Ungültige Eingabe", 400);
return apiError("Invalid input", 400);
}
const config = await prisma.reminderPhaseConfig.upsert({
@@ -95,7 +95,7 @@ export const DELETE = apiHandler(async (_request: NextRequest, { params }: Route
select: { userId: true },
});
if (!medication || medication.userId !== user.id) {
return apiError("Medikament nicht gefunden", 404);
return apiError("Medication not found", 404);
}
await prisma.reminderPhaseConfig.deleteMany({
+3 -3
View File
@@ -24,7 +24,7 @@ export const GET = apiHandler(async (_request: NextRequest, { params }: RoutePar
});
if (!medication || medication.userId !== user.id) {
return apiError("Medikament nicht gefunden", 404);
return apiError("Medication not found", 404);
}
let category = "OTHER";
@@ -52,7 +52,7 @@ export const PUT = apiHandler(async (request: NextRequest, { params }: RoutePara
const { id } = await params;
const existing = await prisma.medication.findUnique({ where: { id } });
if (!existing || existing.userId !== user.id) {
return apiError("Medikament nicht gefunden", 404);
return apiError("Medication not found", 404);
}
const { data: body, error: jsonError } = await safeJson(request);
@@ -171,7 +171,7 @@ export const DELETE = apiHandler(async (request: NextRequest, { params }: RouteP
const { id } = await params;
const existing = await prisma.medication.findUnique({ where: { id } });
if (!existing || existing.userId !== user.id) {
return apiError("Medikament nicht gefunden", 404);
return apiError("Medication not found", 404);
}
// Revoke API tokens scoped to this medication
+3 -3
View File
@@ -32,11 +32,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
try {
body = await request.json();
} catch {
return apiError("Ungültige JSON-Daten", 422);
return apiError("Invalid JSON data", 422);
}
const parsed = clientErrorSchema.safeParse(body);
if (!parsed.success) return apiError("Ungültige Fehlerdaten", 422);
if (!parsed.success) return apiError("Invalid error data", 422);
const delivery = await sendGlitchtipEvent({
dsn: settings.glitchtipDsn,
@@ -54,7 +54,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
if (!delivery.ok) {
getEvent()?.addWarning("Glitchtip event rejected: " + delivery.method + " " + delivery.status + " " + delivery.details);
return apiError("Glitchtip konnte Fehler nicht annehmen", 502);
return apiError("Glitchtip could not accept error", 502);
}
return apiSuccess({ sent: true });
+3 -3
View File
@@ -34,7 +34,7 @@ export const GET = apiHandler(async (_request: NextRequest, { params }: RoutePar
const entry = await prisma.moodEntry.findUnique({ where: { id } });
if (!entry || entry.userId !== user.id) {
return apiError("Stimmungseintrag nicht gefunden", 404);
return apiError("Mood entry not found", 404);
}
annotate({ action: { name: "mood-entries.get" }, meta: { moodEntryId: id } });
@@ -50,7 +50,7 @@ export const PUT = apiHandler(async (request: NextRequest, { params }: RoutePara
const existing = await prisma.moodEntry.findUnique({ where: { id } });
if (!existing || existing.userId !== user.id) {
return apiError("Stimmungseintrag nicht gefunden", 404);
return apiError("Mood entry not found", 404);
}
const { data: body, error: jsonError } = await safeJson(request);
@@ -100,7 +100,7 @@ export const DELETE = apiHandler(async (request: NextRequest, { params }: RouteP
const existing = await prisma.moodEntry.findUnique({ where: { id } });
if (!existing || existing.userId !== user.id) {
return apiError("Stimmungseintrag nicht gefunden", 404);
return apiError("Mood entry not found", 404);
}
await prisma.moodEntry.delete({ where: { id } });
+1 -1
View File
@@ -113,7 +113,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
} catch (err) {
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
return apiError(
"Ein Stimmungseintrag mit diesen Daten existiert bereits",
"A mood entry with this data already exists",
409,
);
}
@@ -98,18 +98,18 @@ export const PUT = apiHandler(async (request: NextRequest) => {
try {
body = await request.json();
} catch {
return apiError("Ungültige JSON-Daten", 422);
return apiError("Invalid JSON data", 422);
}
const parsed = notificationPreferenceSchema.safeParse(body);
if (!parsed.success) return apiError("Ungültige Daten", 422);
if (!parsed.success) return apiError("Invalid data", 422);
const { channelId, eventType, enabled } = parsed.data;
const channel = await prisma.notificationChannel.findFirst({
where: { id: channelId, userId: user.id },
});
if (!channel) return apiError("Kanal nicht gefunden", 404);
if (!channel) return apiError("Channel not found", 404);
const preference = await prisma.notificationPreference.upsert({
where: {
+4 -4
View File
@@ -30,11 +30,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
try {
body = await request.json();
} catch {
return apiError("Ungültige JSON-Daten", 422);
return apiError("Invalid JSON data", 422);
}
const parsed = subscribeSchema.safeParse(body);
if (!parsed.success) return apiError("Ungültige Daten", 422);
if (!parsed.success) return apiError("Invalid data", 422);
const { endpoint, keys } = parsed.data;
const userAgent = request.headers.get("user-agent") ?? undefined;
@@ -92,11 +92,11 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
try {
body = await request.json();
} catch {
return apiError("Ungültige JSON-Daten", 422);
return apiError("Invalid JSON data", 422);
}
const parsed = unsubscribeSchema.safeParse(body);
if (!parsed.success) return apiError("Ungültige Daten", 422);
if (!parsed.success) return apiError("Invalid data", 422);
await prisma.pushSubscription.deleteMany({
where: {
+1 -1
View File
@@ -24,7 +24,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
if (jsonError) return jsonError;
const result = z.safeParse(onboardingSchema, body);
if (!result.success) {
return apiError("Ungültige Eingabe", 422);
return apiError("Invalid input", 422);
}
const data: Record<string, unknown> = {
+3 -3
View File
@@ -20,12 +20,12 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
const body = await request.json();
confirm = typeof body?.confirm === "string" ? body.confirm : "";
} catch {
return apiError("Ungueltige Anfrage", 422);
return apiError("Invalid request", 422);
}
if (confirm !== "DELETE_ACCOUNT") {
return apiError(
"Bestaetigung fehlt. Sende { confirm: 'DELETE_ACCOUNT' }",
"Confirmation missing. Send { confirm: 'DELETE_ACCOUNT' }",
422,
);
}
@@ -35,7 +35,7 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
const adminCount = await prisma.user.count({ where: { role: "ADMIN" } });
if (adminCount <= 1) {
return apiError(
"Der letzte Admin-Account kann nicht geloescht werden",
"The last admin account cannot be deleted",
400,
);
}
+2 -2
View File
@@ -18,11 +18,11 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
const body = await request.json();
confirm = typeof body?.confirm === "string" ? body.confirm : "";
} catch {
return apiError("Ungueltige Anfrage", 422);
return apiError("Invalid request", 422);
}
if (confirm !== "DELETE") {
return apiError("Bestaetigung fehlt", 422);
return apiError("Confirmation missing", 422);
}
const userId = user.id;
+1 -1
View File
@@ -21,7 +21,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
const parsed = moodLogCredentialsSchema.safeParse(body);
if (!parsed.success) {
return apiError("Ungueltige Eingabe", 422);
return apiError("Invalid input", 422);
}
const { url, apiKey } = parsed.data;
+2 -2
View File
@@ -51,13 +51,13 @@ export const PUT = apiHandler(async (request: NextRequest) => {
if (jsonError) return jsonError;
const parsed = ntfySettingsSchema.safeParse(body);
if (!parsed.success) return apiError("Ungueltige Daten", 422);
if (!parsed.success) return apiError("Invalid data", 422);
const { serverUrl, topic, authToken, enabled } = parsed.data;
if (enabled && (!serverUrl || !topic)) {
return apiError(
"Server-URL und Topic sind erforderlich, wenn ntfy aktiviert ist",
"Server URL and topic are required when ntfy is enabled",
422,
);
}
+5 -5
View File
@@ -15,7 +15,7 @@ export const POST = apiHandler(async () => {
const rl = await checkRateLimit(`ntfy-test:${user.id}`, 5, 5 * 60 * 1000);
if (!rl.allowed) {
return apiError("Maximal 5 Tests in 5 Minuten", 429);
return apiError("Maximum 5 tests in 5 minutes", 429);
}
const channel = await prisma.notificationChannel.findUnique({
@@ -25,13 +25,13 @@ export const POST = apiHandler(async () => {
});
if (!channel) {
return apiError("ntfy ist nicht konfiguriert", 400);
return apiError("ntfy is not configured", 400);
}
const config = JSON.parse(decrypt(channel.config)) as NtfyChannelConfig;
if (!config.serverUrl || !config.topic) {
return apiError("Server-URL und Topic sind erforderlich", 400);
return apiError("Server URL and topic are required", 400);
}
const success = await sendViaNtfy(config, {
@@ -39,11 +39,11 @@ export const POST = apiHandler(async () => {
userId: user.id,
title: "HealthLog Test",
message:
"HealthLog: Verbindung erfolgreich! ntfy-Benachrichtigungen sind aktiv.",
"HealthLog: Connection successful! ntfy notifications are active.",
});
if (!success) {
return apiError("Testnachricht konnte nicht gesendet werden", 500);
return apiError("Failed to send test message", 500);
}
annotate({ action: { name: "settings.ntfy.test" }, meta: { success: true } });
+9 -9
View File
@@ -47,14 +47,14 @@ export const PUT = apiHandler(async (request: NextRequest) => {
telegramEnabled: true,
},
});
if (!current) return apiError("Benutzer nicht gefunden", 404);
if (!current) return apiError("User not found", 404);
const { data: body, error: jsonError } = await safeJson(request);
if (jsonError) return jsonError;
const result = z.safeParse(telegramSettingsSchema, body);
if (!result.success) {
return apiError("Ungueltige Eingabe", 422);
return apiError("Invalid input", 422);
}
const { botToken, chatId, enabled } = result.data;
@@ -68,7 +68,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
if (enabled && (!hasTokenAfter || !hasChatIdAfter)) {
return apiError(
"Fuer aktiviertes Telegram werden Bot-Token und Chat-ID benoetigt",
"Bot token and chat ID are required when Telegram is enabled",
422,
);
}
@@ -82,7 +82,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
const appUrl = process.env.APP_URL ?? process.env.NEXT_PUBLIC_APP_URL;
if (!appUrl) {
return apiError(
"Server-Konfiguration fehlerhaft: APP_URL (oder NEXT_PUBLIC_APP_URL) fehlt.",
"Server configuration error: APP_URL (or NEXT_PUBLIC_APP_URL) is missing.",
500,
);
}
@@ -92,14 +92,14 @@ export const PUT = apiHandler(async (request: NextRequest) => {
appBaseUrl = new URL(appUrl);
} catch {
return apiError(
"Server-Konfiguration fehlerhaft: APP_URL ist ungueltig.",
"Server configuration error: APP_URL is invalid.",
500,
);
}
if (appBaseUrl.protocol !== "https:") {
return apiError(
"Telegram-Webhook benoetigt eine oeffentliche HTTPS-URL (kein http/localhost).",
"Telegram webhook requires a public HTTPS URL (no http/localhost).",
422,
);
}
@@ -107,7 +107,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
const localHosts = new Set(["localhost", "127.0.0.1", "::1"]);
if (localHosts.has(appBaseUrl.hostname.toLowerCase())) {
return apiError(
"Telegram-Webhook benoetigt eine oeffentlich erreichbare Domain (localhost ist nicht erlaubt).",
"Telegram webhook requires a publicly reachable domain (localhost is not allowed).",
422,
);
}
@@ -117,7 +117,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
!["80", "88", "443", "8443"].includes(appBaseUrl.port)
) {
return apiError(
"Telegram-Webhook erlaubt nur die Ports 80, 88, 443 oder 8443.",
"Telegram webhook only allows ports 80, 88, 443, or 8443.",
422,
);
}
@@ -131,7 +131,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
);
if (!ok) {
return apiError(
"Telegram-Webhook konnte nicht gesetzt werden. Pruefe Bot-Token und Erreichbarkeit.",
"Failed to set Telegram webhook. Check bot token and reachability.",
422,
);
}
+4 -4
View File
@@ -14,7 +14,7 @@ export const POST = apiHandler(async () => {
const rl = await checkRateLimit(`telegram-test:${user.id}`, 5, 5 * 60 * 1000);
if (!rl.allowed) {
return apiError("Maximal 5 Tests in 5 Minuten", 429);
return apiError("Maximum 5 tests in 5 minutes", 429);
}
const dbUser = await prisma.user.findUnique({
@@ -24,7 +24,7 @@ export const POST = apiHandler(async () => {
if (!dbUser?.telegramBotToken || !dbUser?.telegramChatId) {
return apiError(
"Bot-Token und Chat-ID muessen zuerst gespeichert werden",
"Bot token and chat ID must be saved first",
422,
);
}
@@ -33,12 +33,12 @@ export const POST = apiHandler(async () => {
const ok = await sendTelegramMessage(
botToken,
dbUser.telegramChatId,
"HealthLog: Verbindung erfolgreich! Telegram-Benachrichtigungen sind aktiv.",
"HealthLog: Connection successful! Telegram notifications are active.",
);
if (!ok) {
return apiError(
"Nachricht konnte nicht gesendet werden. Pruefe Bot-Token und Chat-ID.",
"Failed to send message. Check bot token and chat ID.",
422,
);
}
+2 -2
View File
@@ -14,14 +14,14 @@ export const DELETE = apiHandler(async (_request: Request, { params }: RoutePara
annotate({ action: { name: "tokens.revoke" } });
if (!(await isApiGloballyEnabled())) {
return apiError("API ist global deaktiviert", 403);
return apiError("API is globally disabled", 403);
}
const { id } = await params;
const token = await prisma.apiToken.findUnique({ where: { id } });
if (!token || token.userId !== user.id) {
return apiError("Token nicht gefunden", 404);
return apiError("Token not found", 404);
}
await prisma.apiToken.update({
+3 -3
View File
@@ -9,7 +9,7 @@ import { NextRequest } from "next/server";
import { z } from "zod/v4";
const createTokenSchema = z.object({
name: z.string().min(1, "Name erforderlich").max(100),
name: z.string().min(1, "Name required").max(100),
expiresInDays: z.number().int().min(1).max(365).optional(),
});
@@ -18,7 +18,7 @@ export const GET = apiHandler(async () => {
annotate({ action: { name: "tokens.list" } });
if (!(await isApiGloballyEnabled())) {
return apiError("API ist global deaktiviert", 403);
return apiError("API is globally disabled", 403);
}
const tokens = await prisma.apiToken.findMany({
@@ -43,7 +43,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
annotate({ action: { name: "tokens.create" } });
if (!(await isApiGloballyEnabled())) {
return apiError("API ist global deaktiviert", 403);
return apiError("API is globally disabled", 403);
}
const { data: body, error: jsonError } = await safeJson(request);
+1 -1
View File
@@ -41,7 +41,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
if (jsonError) return jsonError;
const result = z.safeParse(withingsCredentialsSchema, body);
if (!result.success) {
return apiError("Client-ID und Client-Secret sind erforderlich", 422);
return apiError("Client ID and Client Secret are required", 422);
}
await prisma.user.update({
+1 -1
View File
@@ -19,7 +19,7 @@ export const POST = apiHandler(async () => {
});
if (!connection) {
return apiError("Keine Withings-Verbindung vorhanden", 404);
return apiError("No Withings connection", 404);
}
// Try to unsubscribe webhook (best-effort)