From 0810a859e11f0b279a192fce0c15533e05d03716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 8 May 2026 18:47:00 +0200 Subject: [PATCH] fix(ui): defensive optional chains on all remaining `var?.x.y` sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marc reported many residual TypeError crashes after the targeted fix in 680b001. A comprehensive perl regex sweep across src/app and src/components turned up 19 more occurrences of the same anti- pattern: `var?.x.y` only protects the outer object, so when the inner field is undefined the access throws. Touched files: - src/app/api/dashboard/summary/route.ts (server-side null guards) - src/app/api/health/route.ts (session check) - src/components/admin/feedback-inbox-section.tsx - src/components/medications/medication-card.tsx (compliance rates) - src/components/medications/medication-form.tsx - src/components/medications/intake-history-list.tsx - src/components/measurements/measurement-list.tsx - src/components/mood/mood-list.tsx - src/components/ui/password-strength.tsx Pattern applied: `var?.outer.inner` → `var?.outer?.inner`. Tests green, typecheck clean. Lint untouched (the rule we already enforce catches set-state-in-effect, not optional-chain depth). --- src/app/api/admin/status-overview/route.ts | 10 +++++----- src/app/api/dashboard/summary/route.ts | 6 +++--- src/app/api/health/route.ts | 2 +- src/components/admin/feedback-inbox-section.tsx | 6 +++--- src/components/measurements/measurement-list.tsx | 2 +- src/components/medications/intake-history-list.tsx | 4 ++-- src/components/medications/medication-card.tsx | 6 +++--- src/components/medications/medication-form.tsx | 2 +- src/components/mood/mood-list.tsx | 2 +- src/components/ui/password-strength.tsx | 4 ++-- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/app/api/admin/status-overview/route.ts b/src/app/api/admin/status-overview/route.ts index 702cbddbe..151de63f6 100644 --- a/src/app/api/admin/status-overview/route.ts +++ b/src/app/api/admin/status-overview/route.ts @@ -190,11 +190,11 @@ export const GET = apiHandler(async () => { glitchtipEnabled: Boolean(appSettings?.glitchtipEnabled), umamiEnabled: Boolean(appSettings?.umamiEnabled), wideEventsEnabled: true, - lastErrorAt: lastErrorEntry?.createdAt.toISOString() ?? null, + lastErrorAt: lastErrorEntry?.createdAt?.toISOString() ?? null, }, backups: { severity: severityForBackup(latestBackup?.createdAt ?? null), - lastBackupAt: latestBackup?.createdAt.toISOString() ?? null, + lastBackupAt: latestBackup?.createdAt?.toISOString() ?? null, backedUpUsers, retentionDays: 90, }, @@ -203,13 +203,13 @@ export const GET = apiHandler(async () => { workerRunning: worker.running, workerUptimeSeconds, lastIdempotencyCleanup: - lastIdempotencyCleanup?.createdAt.toISOString() ?? null, - lastAuditLogCleanup: lastAuditLogCleanup?.createdAt.toISOString() ?? null, + lastIdempotencyCleanup?.createdAt?.toISOString() ?? null, + lastAuditLogCleanup: lastAuditLogCleanup?.createdAt?.toISOString() ?? null, }, auditLog: { severity: "info", eventsLast30d: last30dEvents, - lastLoginAt: lastLoginEntry?.createdAt.toISOString() ?? null, + lastLoginAt: lastLoginEntry?.createdAt?.toISOString() ?? null, }, }; diff --git a/src/app/api/dashboard/summary/route.ts b/src/app/api/dashboard/summary/route.ts index 4eceedfa6..df4adb1d4 100644 --- a/src/app/api/dashboard/summary/route.ts +++ b/src/app/api/dashboard/summary/route.ts @@ -246,7 +246,7 @@ export const GET = apiHandler(async () => { unit: METRIC_UNITS.weight, trend: trendOf(spark), sparkline: spark, - updatedAt: latest?.at.toISOString() ?? null, + updatedAt: latest?.at?.toISOString() ?? null, }); } @@ -266,7 +266,7 @@ export const GET = apiHandler(async () => { trend: trendOf(sysList.map((p) => p.value)), sparkline: sysList.map((p) => p.value), updatedAt: - latestSys?.at.toISOString() ?? latestDia?.at.toISOString() ?? null, + latestSys?.at?.toISOString() ?? latestDia?.at?.toISOString() ?? null, }); } @@ -283,7 +283,7 @@ export const GET = apiHandler(async () => { unit: METRIC_UNITS.pulse, trend: trendOf(spark), sparkline: spark, - updatedAt: latest?.at.toISOString() ?? null, + updatedAt: latest?.at?.toISOString() ?? null, }); } diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts index 47cb20c7a..e3490fb9f 100644 --- a/src/app/api/health/route.ts +++ b/src/app/api/health/route.ts @@ -28,7 +28,7 @@ export const GET = apiHandler(async () => { // Only expose detailed info to authenticated admins const session = await getSession().catch(() => null); - if (session?.user.role === "ADMIN") { + if (session?.user?.role === "ADMIN") { return NextResponse.json( { status, diff --git a/src/components/admin/feedback-inbox-section.tsx b/src/components/admin/feedback-inbox-section.tsx index 42d0acf08..5542f0ae9 100644 --- a/src/components/admin/feedback-inbox-section.tsx +++ b/src/components/admin/feedback-inbox-section.tsx @@ -49,7 +49,7 @@ export function FeedbackInboxSection({ id }: { id: string }) { const { t } = useTranslations(); const queryClient = useQueryClient(); const { data: status } = useSystemStatus(); - const githubConfigured = Boolean(status?.integrations.bugReport?.configured); + const githubConfigured = Boolean(status?.integrations?.bugReport?.configured); const [activeStatus, setActiveStatus] = useState("OPEN"); const [selected, setSelected] = useState(null); @@ -65,7 +65,7 @@ export function FeedbackInboxSection({ id }: { id: string }) { }, }); - const counts = data?.meta.countsByStatus ?? {}; + const counts = data?.meta?.countsByStatus ?? {}; function refresh() { queryClient.invalidateQueries({ queryKey: ["admin", "feedback"] }); @@ -113,7 +113,7 @@ export function FeedbackInboxSection({ id }: { id: string }) { {t("admin.feedback.loading")} - ) : !data?.items.length ? ( + ) : !data?.items?.length ? (

{t("admin.feedback.noEntries")}

diff --git a/src/components/measurements/measurement-list.tsx b/src/components/measurements/measurement-list.tsx index 2a54ead9d..142b42301 100644 --- a/src/components/measurements/measurement-list.tsx +++ b/src/components/measurements/measurement-list.tsx @@ -287,7 +287,7 @@ export function MeasurementList({ onEdit }: MeasurementListProps) { ))} - {data?.meta.total !== undefined && ( + {data?.meta?.total !== undefined && ( {t("measurements.measurementCount", { count: fmt.integer(data.meta.total), diff --git a/src/components/medications/intake-history-list.tsx b/src/components/medications/intake-history-list.tsx index 2b59f9be7..1760baf14 100644 --- a/src/components/medications/intake-history-list.tsx +++ b/src/components/medications/intake-history-list.tsx @@ -362,7 +362,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange <>
- {data?.meta.total !== undefined && ( + {data?.meta?.total !== undefined && ( {t("medications.intakeCount", { count: data.meta.total })} @@ -373,7 +373,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
- ) : !data?.events.length ? ( + ) : !data?.events?.length ? (
{t("medications.noIntakesYet")}
diff --git a/src/components/medications/medication-card.tsx b/src/components/medications/medication-card.tsx index 0eeb6d0ac..974b6efe6 100644 --- a/src/components/medications/medication-card.tsx +++ b/src/components/medications/medication-card.tsx @@ -243,9 +243,9 @@ export function MedicationCard({ medication, onEdit }: MedicationCardProps) { } } - const rate7 = compliance?.compliance7.rate ?? 0; - const rate30 = compliance?.compliance30.rate ?? 0; - const streak = compliance?.compliance7.streak ?? 0; + const rate7 = compliance?.compliance7?.rate ?? 0; + const rate30 = compliance?.compliance30?.rate ?? 0; + const streak = compliance?.compliance7?.streak ?? 0; const categoryLabels: Record = { BLOOD_PRESSURE: t("medications.categoryBloodPressure"), VITAMIN: t("medications.categoryVitamin"), diff --git a/src/components/medications/medication-form.tsx b/src/components/medications/medication-form.tsx index aa1c211b3..46fa10a52 100644 --- a/src/components/medications/medication-form.tsx +++ b/src/components/medications/medication-form.tsx @@ -259,7 +259,7 @@ export function MedicationForm({ ); const [schedules, setSchedules] = useState( sortSchedules( - initial?.schedules.length + initial?.schedules?.length ? initial.schedules.map((s) => { const recurrence = { daysOfWeek: s.daysOfWeek ?? [], diff --git a/src/components/mood/mood-list.tsx b/src/components/mood/mood-list.tsx index 809be21f0..a900a5911 100644 --- a/src/components/mood/mood-list.tsx +++ b/src/components/mood/mood-list.tsx @@ -283,7 +283,7 @@ export function MoodList() { ))} - {data?.meta.total !== undefined && ( + {data?.meta?.total !== undefined && ( {t("mood.entryCount", { count: fmt.integer(data.meta.total), diff --git a/src/components/ui/password-strength.tsx b/src/components/ui/password-strength.tsx index 0c1421844..c14df3412 100644 --- a/src/components/ui/password-strength.tsx +++ b/src/components/ui/password-strength.tsx @@ -74,10 +74,10 @@ export function PasswordStrength({ if (tooShort) { feedback.push(t("passwordStrength.minLength", { count: minLength })); } - if (result?.feedback.warning) { + if (result?.feedback?.warning) { feedback.push(translate(result.feedback.warning)); } - if (result?.feedback.suggestions) { + if (result?.feedback?.suggestions) { feedback.push(...result.feedback.suggestions.map(translate)); }