fix(ui): defensive optional chains on all remaining var?.x.y sites

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).
This commit is contained in:
Marc-André Bombeck committed 2026-05-08 18:47:00 +02:00
1 parent 680b0019c1
commit 0810a859e1
10 files changed
+22 -22

No files matched your search

+5 -5
View File
@@ -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,
},
};
+3 -3
View File
@@ -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,
});
}
+1 -1
View File
@@ -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,
@@ -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<FeedbackStatusType>("OPEN");
const [selected, setSelected] = useState<FeedbackItem | null>(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")}
</span>
</div>
) : !data?.items.length ? (
) : !data?.items?.length ? (
<p className="text-muted-foreground text-sm">
{t("admin.feedback.noEntries")}
</p>
@@ -287,7 +287,7 @@ export function MeasurementList({ onEdit }: MeasurementListProps) {
))}
</SelectContent>
</Select>
{data?.meta.total !== undefined && (
{data?.meta?.total !== undefined && (
<span className="text-muted-foreground text-sm">
{t("measurements.measurementCount", {
count: fmt.integer(data.meta.total),
@@ -362,7 +362,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
<>
<div className="space-y-3">
<div className="flex items-center justify-end">
{data?.meta.total !== undefined && (
{data?.meta?.total !== undefined && (
<span className="text-muted-foreground text-sm">
{t("medications.intakeCount", { count: data.meta.total })}
</span>
@@ -373,7 +373,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
<div className="flex h-32 items-center justify-center">
<Loader2 className="text-primary h-6 w-6 animate-spin" />
</div>
) : !data?.events.length ? (
) : !data?.events?.length ? (
<div className="text-muted-foreground flex h-32 items-center justify-center rounded-lg border border-dashed">
{t("medications.noIntakesYet")}
</div>
@@ -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<string, string> = {
BLOOD_PRESSURE: t("medications.categoryBloodPressure"),
VITAMIN: t("medications.categoryVitamin"),
@@ -259,7 +259,7 @@ export function MedicationForm({
);
const [schedules, setSchedules] = useState<Schedule[]>(
sortSchedules(
initial?.schedules.length
initial?.schedules?.length
? initial.schedules.map((s) => {
const recurrence = {
daysOfWeek: s.daysOfWeek ?? [],
+1 -1
View File
@@ -283,7 +283,7 @@ export function MoodList() {
))}
</SelectContent>
</Select>
{data?.meta.total !== undefined && (
{data?.meta?.total !== undefined && (
<span className="text-muted-foreground text-sm">
{t("mood.entryCount", {
count: fmt.integer(data.meta.total),
+2 -2
View File
@@ -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));
}