From 9014203d8a9d8e0dadaf231e415248ff9b1348a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 18 Jun 2026 22:42:04 +0200 Subject: [PATCH] fix(a11y): label per-reminder toggles and wire disclosure controls The diabetes PATCH parsed its body without a size cap; bound it through safeJson like the sibling routes. Label each reminder's enable/disable switch by its own name span so screen readers hear the reminder rather than one generic label repeated per row. Point the chronotype and illness disclosure triggers at the regions they reveal via aria-controls. --- src/app/api/auth/me/diabetes/route.ts | 15 ++++++++------- src/components/illness/illness-view.tsx | 5 ++++- src/components/insights/sleep/chronotype-card.tsx | 3 ++- .../measurement-reminders/vorsorge-settings.tsx | 10 ++++++++-- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/app/api/auth/me/diabetes/route.ts b/src/app/api/auth/me/diabetes/route.ts index 56d72da64..2b3e48e15 100644 --- a/src/app/api/auth/me/diabetes/route.ts +++ b/src/app/api/auth/me/diabetes/route.ts @@ -19,12 +19,13 @@ */ import { z } from "zod"; -import { apiHandler, requireAuth, HttpError } from "@/lib/api-handler"; +import { apiHandler, requireAuth } from "@/lib/api-handler"; import { apiError, apiSuccess, getClientIp, returnAllZodIssues, + safeJson, } from "@/lib/api-response"; import { annotate } from "@/lib/logging/context"; import { auditLog } from "@/lib/auth/audit"; @@ -74,12 +75,12 @@ export const PATCH = apiHandler(async (req: Request) => { return response; } - let body: unknown; - try { - body = await req.json(); - } catch { - throw new HttpError(422, "diabetes.body.invalid_json"); - } + // The body is a single boolean — bound the parse so a malformed or + // oversized payload is rejected before it is materialised. + const { data: body, error: jsonError } = await safeJson(req, { + maxBytes: 1024, + }); + if (jsonError) return jsonError; const parsed = patchBodySchema.safeParse(body); if (!parsed.success) { diff --git a/src/components/illness/illness-view.tsx b/src/components/illness/illness-view.tsx index 10101938c..7af11279d 100644 --- a/src/components/illness/illness-view.tsx +++ b/src/components/illness/illness-view.tsx @@ -15,7 +15,7 @@ * per-day timeline and correlation card live. Retrospective-only copy — a * journal, not a medical device, and it does not diagnose. */ -import { useMemo, useState } from "react"; +import { useId, useMemo, useState } from "react"; import Link from "next/link"; import { ChevronDown, Plus, Stethoscope, Wrench } from "lucide-react"; @@ -245,10 +245,12 @@ function EpisodeGroup({ }) { // Hooks must run unconditionally; bail on the empty render afterwards. const [open, setOpen] = useState(false); + const gridId = useId(); if (parents.length === 0) return null; const grid = (
setOpen((v) => !v)} aria-expanded={open} + aria-controls={gridId} className="text-muted-foreground hover:text-foreground -mx-1 flex w-fit items-center gap-1.5 rounded-md px-1 py-0.5 text-xs font-medium uppercase tracking-wide transition-colors" > setShowAdvanced((v) => !v)} aria-expanded={showAdvanced} + aria-controls="chronotype-advanced" className="text-muted-foreground hover:text-foreground flex items-center gap-1 text-xs font-medium transition-colors" > {showAdvanced ? ( @@ -114,7 +115,7 @@ export function ChronotypeCard({ chronotype }: { chronotype: ChronotypeDto }) { {showAdvanced && ( -
+
{chronotype.socialJetlagMinutes != null && (() => { const { hours, minutes } = hoursMinutes( diff --git a/src/components/measurement-reminders/vorsorge-settings.tsx b/src/components/measurement-reminders/vorsorge-settings.tsx index 47710d026..1d2820fa0 100644 --- a/src/components/measurement-reminders/vorsorge-settings.tsx +++ b/src/components/measurement-reminders/vorsorge-settings.tsx @@ -100,7 +100,10 @@ export function VorsorgeSettings() { key={reminder.id} className="flex items-center justify-between gap-3 px-4 py-3" > - + {resolveLabel(reminder, t)}
))}