From dadb6e2d34800db16dd2737ae3841eb5e64376c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 16 Jun 2026 23:12:36 +0200 Subject: [PATCH] fix(settings): finish the D0-D8 settings/admin frame cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop the leading blurb paragraph from the Channels + Integrations connection panels so each body starts at its first card (D0 alignment). - Correct three stale CORE/always-on comments: medications has been a fail-open toggleable module since D3; CORE is now weight/BP/pulse only. - Remove the dead variant/isSubtab branch from SourcesSection — no callers after the D4 split; render the standalone header unconditionally. - Render the admin AI-quality card frame + header once and branch only the body across loading/error/empty/data, matching the coach-feedback card. - Rename the Coach activation card's section id from the legacy settings-ai-* namespace to settings-coach-activate-* after the S5 move. - Gate the medication low-stock reminder card on the medications module so it hides when the account turns medications off, mirroring mood/coach. --- src/components/admin/ai-quality-section.tsx | 93 +++++++++---------- .../settings/ai/disable-coach-card.tsx | 8 +- .../integrations/connections-panel.tsx | 4 - .../notification-channels-panel.tsx | 6 -- .../settings/medications-section.tsx | 8 +- .../settings/notifications-section.tsx | 19 ++-- src/components/settings/sources-section.tsx | 61 +++++------- src/lib/modules/registry.ts | 22 +++-- 8 files changed, 99 insertions(+), 122 deletions(-) diff --git a/src/components/admin/ai-quality-section.tsx b/src/components/admin/ai-quality-section.tsx index 4a026e59f..ddcd6473f 100644 --- a/src/components/admin/ai-quality-section.tsx +++ b/src/components/admin/ai-quality-section.tsx @@ -65,64 +65,56 @@ export function AiQualitySection() { }, }); - if (query.isLoading) { - return ( -
- - - {t("admin.aiQuality.loading")} - -
- ); - } - - if (query.isError) { - return ( -
- {t("admin.aiQuality.loadError")} -
- ); - } - - const summary = query.data; - - if (!summary || summary.buckets.length === 0) { - return ( -
- -

- {t("admin.aiQuality.empty")} -

-
- ); - } + const summary = query.data ?? null; + const buckets = summary?.buckets ?? []; + const hasData = !!summary && buckets.length > 0; + // v1.18.1 — keep the outer card + header rendered across every query + // state (loading / error / empty / data) so the heading baseline stays at + // a constant Y-offset and the section never snaps downward when the data + // resolves. Mirrors (header outside the + // fetch-state branch). return (
- {t("admin.aiQuality.windowLabel", { - days: String(summary.windowDays), - })} - {" · "} - {t("admin.aiQuality.generatedAtLabel")}{" "} - {fmt.dateTime(summary.generatedAt)} -

+ hasData && summary ? ( +

+ {t("admin.aiQuality.windowLabel", { + days: String(summary.windowDays), + })} + {" · "} + {t("admin.aiQuality.generatedAtLabel")}{" "} + {fmt.dateTime(summary.generatedAt)} +

+ ) : null } /> -
- - + {query.isLoading ? ( +
+ + + {t("admin.aiQuality.loading")} + +
+ ) : query.isError ? ( +
+ {t("admin.aiQuality.loadError")} +
+ ) : !hasData ? ( +

+ {t("admin.aiQuality.empty")} +

+ ) : ( +
+
+ - {summary.buckets.map((bucket, idx) => { + {buckets.map((bucket, idx) => { const ratePct = Math.round(bucket.helpfulRate * 100); return (
{t("admin.aiQuality.colSeverity")} @@ -148,7 +140,7 @@ export function AiQualitySection() {
-
+
+ )} ); } diff --git a/src/components/settings/ai/disable-coach-card.tsx b/src/components/settings/ai/disable-coach-card.tsx index 4a1f8687a..9e1339768 100644 --- a/src/components/settings/ai/disable-coach-card.tsx +++ b/src/components/settings/ai/disable-coach-card.tsx @@ -3,6 +3,10 @@ /** * v1.4.47 W3 — Coach activation toggle card. * + * v1.18.0 (S5) — moved out of Settings → AI into the dedicated Coach + * section; its `
` id namespace is `settings-coach-activate-*` + * accordingly. (The file path stays under `ai/` as pre-existing drift.) + * * v1.18.1 (D7) — polarity flip. The card now reads "Activate Coach" with the * Switch ON by default (the Coach is on for everyone unless the user opts * out). The persisted state is still the single `User.disableCoach` field — @@ -137,14 +141,14 @@ export function DisableCoachCard({ return (
-

- {t("settings.sections.integrations.connections.description")} -

- diff --git a/src/components/settings/integrations/notification-channels-panel.tsx b/src/components/settings/integrations/notification-channels-panel.tsx index 0faf9a4a8..192b0442c 100644 --- a/src/components/settings/integrations/notification-channels-panel.tsx +++ b/src/components/settings/integrations/notification-channels-panel.tsx @@ -18,7 +18,6 @@ import { useQuery } from "@tanstack/react-query"; import { useAuth } from "@/hooks/use-auth"; -import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; import { NotificationStatusCard } from "@/components/settings/notification-status-card"; import { NtfyCard } from "@/components/settings/ntfy-card"; @@ -37,7 +36,6 @@ interface GlobalServiceAvailability { } export function NotificationChannelsPanel() { - const { t } = useTranslations(); const { isAuthenticated } = useAuth(); const { data: globalServices } = useQuery({ @@ -54,10 +52,6 @@ export function NotificationChannelsPanel() { return (
-

- {t("settings.sections.integrations.channels.description")} -

- {/* v1.4.27 MB3 — `scroll-mt-28` keeps each channel card's anchor diff --git a/src/components/settings/medications-section.tsx b/src/components/settings/medications-section.tsx index 54abf90c7..c19d5c261 100644 --- a/src/components/settings/medications-section.tsx +++ b/src/components/settings/medications-section.tsx @@ -19,9 +19,11 @@ import { useMedicationListLayout } from "@/lib/queries/use-medication-list-layou /** * v1.16.10 — the "Medikamente" settings section. v1.18.0 (S5) promoted it - * from a Layout-hub child to its own standalone nav entry (always shown — - * medications is a CORE domain) and gathered the medication-specific - * preferences here. Hosts three blocks: + * from a Layout-hub child to its own standalone nav entry and gathered the + * medication-specific preferences here. v1.18.1 (D3) made medications a + * toggleable fail-open module, so this nav entry hides when the account + * turns the module off (the medication data routes stay live). Hosts three + * blocks: * 1. The list VIEW preference — cards vs table, written optimistically * through the same `PUT /api/medications/layout` the page header * toggle uses (the toggle component is shared). diff --git a/src/components/settings/notifications-section.tsx b/src/components/settings/notifications-section.tsx index 82a06fbb4..ff979d0c5 100644 --- a/src/components/settings/notifications-section.tsx +++ b/src/components/settings/notifications-section.tsx @@ -17,9 +17,9 @@ import { MoodReminderCard } from "@/components/settings/mood-reminder-card"; * * Each reminder-type card is shown ONLY when its module is enabled, read from * the resolved `useAuth().user.modules` map (the same `/auth/me` map nav and - * Insights pills gate off). Mood → `mood`, Coach nudge → `coach`. Low-stock - * runway maps to medications, which fails open (a missing key reads as - * enabled) so a stale `/me` payload never blanks a card. + * Insights pills gate off). Mood → `mood`, Coach nudge → `coach`, low-stock + * runway → `medications` (a toggleable fail-open module since D3, so a missing + * key reads as enabled and a stale `/me` payload never blanks a card). * * v1.18.1 (D5) — the page is intentionally lean: the section blurb, the * channels / inbox cross-links, and the embedded Vorsorge (preventive-care) @@ -35,6 +35,7 @@ export function NotificationsSection() { // so a stale `/me` payload without the module map keeps every card visible. const moodEnabled = user?.modules?.mood !== false; const coachEnabled = user?.modules?.coach !== false; + const medsEnabled = user?.modules?.medications !== false; return (
) : null} - {/* Medication low-stock runway — medications is a CORE domain, so this - is always shown. */} -
- -
+ {/* Medication low-stock runway — only when the medications module is + enabled (toggleable, fail-open, since D3). */} + {medsEnabled ? ( +
+ +
+ ) : null} {/* Proactive Coach nudge — only when the coach module is enabled. */} {coachEnabled ? ( diff --git a/src/components/settings/sources-section.tsx b/src/components/settings/sources-section.tsx index d9ce95c3d..077b6de04 100644 --- a/src/components/settings/sources-section.tsx +++ b/src/components/settings/sources-section.tsx @@ -111,31 +111,16 @@ const DEVICE_TYPE_LABEL_KEYS: Record = { unknown: "settings.sections.sources.deviceLabels.unknown", }; -export interface SourcesSectionProps { - /** - * `"standalone"` (the default) renders the full page header + the - * cross-link back to Integrations — the shape used while Sources had its - * own `/settings/sources` route. `"subtab"` (v1.18.0 S3) suppresses both: - * Sources now renders inside the Integrations → Sources sub-tab, so the - * page header is the parent section's and a cross-link back to the page - * that already contains it would be circular. - */ - variant?: "standalone" | "subtab"; -} - /** * `` — the per-metric source-priority + device-type ladders. * * v1.18.1 (D4) — Sources is a standalone `/settings/sources` left-side entry - * again (split back out of the Integrations sub-tabs). The default - * `variant="standalone"` renders the section header + the cross-link back to - * Integrationen; `variant="subtab"` is retained for any embedded use and - * suppresses the header. + * again (split back out of the Integrations sub-tabs). It renders its own + * section header + the cross-link back to Integrationen. */ -export function SourcesSection({ variant = "standalone" }: SourcesSectionProps = {}) { +export function SourcesSection() { const { t } = useTranslations(); const queryClient = useQueryClient(); - const isSubtab = variant === "subtab"; const { data: remote, isLoading } = useQuery({ queryKey: queryKeys.sourcePriority(), @@ -298,27 +283,25 @@ export function SourcesSection({ variant = "standalone" }: SourcesSectionProps = aria-labelledby="settings-section-sources-title" className="space-y-6" > - {isSubtab ? null : ( - // v1.18.1 (D0/D4) — standalone left-side entry again. The section - // blurb is dropped for consistent top alignment; the cross-link back - // to Integrationen (where the sources this ladder ranks are added / - // removed) stays as a quiet pointer. -
-

- {t("settings.sections.sources.title")} -

-

- {t("settings.sections.sources.integrationsHint")}{" "} - - {t("settings.sections.sources.integrationsHintLink")} - -

-
- )} + {/* v1.18.1 (D0/D4) — standalone left-side entry again. The section + blurb is dropped for consistent top alignment; the cross-link back + to Integrationen (where the sources this ladder ranks are added / + removed) stays as a quiet pointer. */} +
+

+ {t("settings.sections.sources.title")} +

+

+ {t("settings.sections.sources.integrationsHint")}{" "} + + {t("settings.sections.sources.integrationsHintLink")} + +

+