merge: admin consolidation — unified coach section, backups layout, danger-zone parity

This commit is contained in:
Marc-André Bombeck committed 2026-06-16 21:42:29 +02:00
commit cdf7fa2d41
13 files changed
+150 -188

No files matched your search

+3 -11
View File
@@ -5283,17 +5283,9 @@
"title": "Integrationen",
"subtitle": "Umami, GlitchTip, Web Push und Bug-Reports konfigurieren."
},
"ai-quality": {
"title": "Auswertungs-Qualität",
"subtitle": "Hilfreichkeit nach Schweregrad und Provider — Nutzer-Feedback der letzten 30 Tage."
},
"assistant": {
"title": "Assistenz-Oberflächen",
"subtitle": "Jede modellgestützte Oberfläche separat ein- oder ausschalten, ohne die Provider-Konfiguration zu entfernen."
},
"coach-feedback": {
"title": "Coach-Feedback",
"subtitle": "Hilfreich-Quote pro Coach-Prompt-Version, Tonfall und Ausführlichkeit der letzten 30 Tage."
"coach": {
"title": "Coach",
"subtitle": "Assistenz-Oberflächen, Server-Schlüssel sowie die Feedback-Qualitäts-Tabellen für Coach und Auswertungen an einem Ort."
},
"feedback": {
"title": "Feedback",
+3 -11
View File
@@ -5283,17 +5283,9 @@
"title": "Integrations",
"subtitle": "Configure Umami, GlitchTip, Web Push, and bug reports."
},
"ai-quality": {
"title": "Insights Quality",
"subtitle": "Helpfulness scores grouped by severity and provider, from the last 30 days of user feedback."
},
"assistant": {
"title": "Assistant Surfaces",
"subtitle": "Toggle each model-driven surface on or off without removing the provider configuration."
},
"coach-feedback": {
"title": "Coach Feedback",
"subtitle": "Helpful-rate per Coach prompt version, tone, and verbosity from the last 30 days."
"coach": {
"title": "Coach",
"subtitle": "Assistant surfaces, server key, and the Coach and Insights feedback-quality tables in one place."
},
"feedback": {
"title": "Feedback",
+3 -11
View File
@@ -5283,17 +5283,9 @@
"title": "Integraciones",
"subtitle": "Configura Umami, GlitchTip, Web Push e informes de errores."
},
"ai-quality": {
"title": "Calidad de los análisis",
"subtitle": "Puntuaciones de utilidad agrupadas por gravedad y proveedor, de los últimos 30 días de comentarios de usuarios."
},
"assistant": {
"title": "Superficies del asistente",
"subtitle": "Activa o desactiva cada superficie impulsada por el modelo sin retirar la configuración del proveedor."
},
"coach-feedback": {
"title": "Comentarios del Coach",
"subtitle": "Tasa de utilidad por versión de prompt, tono y nivel de detalle del Coach de los últimos 30 días."
"coach": {
"title": "Coach",
"subtitle": "Superficies del asistente, clave del servidor y las tablas de calidad de comentarios del Coach y de los análisis, en un solo lugar."
},
"feedback": {
"title": "Comentarios",
+3 -11
View File
@@ -5283,17 +5283,9 @@
"title": "Intégrations",
"subtitle": "Configurez Umami, GlitchTip, Web Push et les rapports de bug."
},
"ai-quality": {
"title": "Qualité des analyses",
"subtitle": "Scores d'utilité regroupés par gravité et fournisseur, sur les 30 derniers jours de retours utilisateurs."
},
"assistant": {
"title": "Surfaces d'assistance",
"subtitle": "Activer ou désactiver chaque surface pilotée par le modèle sans supprimer la configuration du fournisseur."
},
"coach-feedback": {
"title": "Retours sur le Coach",
"subtitle": "Taux d'utilité par version de prompt, ton et niveau de détail du Coach sur les 30 derniers jours."
"coach": {
"title": "Coach",
"subtitle": "Surfaces d'assistance, clé du serveur et les tableaux de qualité des retours du Coach et des analyses, au même endroit."
},
"feedback": {
"title": "Retours",
+3 -11
View File
@@ -5283,17 +5283,9 @@
"title": "Integrazioni",
"subtitle": "Configura Umami, GlitchTip, Web Push e le segnalazioni di bug."
},
"ai-quality": {
"title": "Qualità delle analisi",
"subtitle": "Punteggi di utilità raggruppati per gravità e provider, dagli ultimi 30 giorni di feedback degli utenti."
},
"assistant": {
"title": "Superfici dell'assistente",
"subtitle": "Attiva o disattiva ciascuna superficie guidata dal modello senza rimuovere la configurazione del provider."
},
"coach-feedback": {
"title": "Feedback del Coach",
"subtitle": "Tasso di utilità per versione del prompt, tono e livello di dettaglio del Coach degli ultimi 30 giorni."
"coach": {
"title": "Coach",
"subtitle": "Superfici dell'assistente, chiave del server e le tabelle di qualità del feedback di Coach e analisi, in un unico posto."
},
"feedback": {
"title": "Feedback",
+3 -11
View File
@@ -5283,17 +5283,9 @@
"title": "Integracje",
"subtitle": "Skonfiguruj Umami, GlitchTip, Web Push i zgłoszenia błędów."
},
"ai-quality": {
"title": "Jakość analiz",
"subtitle": "Oceny przydatności pogrupowane według ważności i dostawcy, z ostatnich 30 dni opinii użytkowników."
},
"assistant": {
"title": "Powierzchnie asystenta",
"subtitle": "Włącz lub wyłącz każdą powierzchnię opartą na modelu bez usuwania konfiguracji dostawcy."
},
"coach-feedback": {
"title": "Opinie o Coachu",
"subtitle": "Wskaźnik przydatności według wersji promptu, tonu i szczegółowości Coacha z ostatnich 30 dni."
"coach": {
"title": "Coach",
"subtitle": "Powierzchnie asystenta, klucz serwera oraz tabele jakości opinii Coacha i analiz w jednym miejscu."
},
"feedback": {
"title": "Opinie",
+10 -21
View File
@@ -90,33 +90,20 @@ export function AdminSectionRenderer({
<IntegrationsGroupSection />
</SectionFrame>
);
case "ai-quality":
case "coach":
// v1.18.1 — the former ai-quality, assistant, and coach-feedback
// sections fold into one Coach area: surface toggles + operator
// config first, then the two feedback-quality tables.
return (
<SectionFrame
title={t("admin.section.ai-quality.title")}
subtitle={t("admin.section.ai-quality.subtitle")}
>
<AiQualitySection />
</SectionFrame>
);
case "assistant":
return (
<SectionFrame
title={t("admin.section.assistant.title")}
subtitle={t("admin.section.assistant.subtitle")}
title={t("admin.section.coach.title")}
subtitle={t("admin.section.coach.subtitle")}
>
<AssistantSection />
<ModuleAvailabilitySection />
<AiServerKeySection />
</SectionFrame>
);
case "coach-feedback":
return (
<SectionFrame
title={t("admin.section.coach-feedback.title")}
subtitle={t("admin.section.coach-feedback.subtitle")}
>
<CoachFeedbackSection />
<AiQualitySection />
</SectionFrame>
);
case "feedback":
@@ -207,7 +194,9 @@ export function AdminSectionRenderer({
// v1.4.36 W4e — About section reused as-is from the settings
// surface. The component owns its own heading + cards layout
// so no SectionFrame wrapper.
return <AboutSection />;
// v1.18.1 E5 — hide the onboarding "Replay tour" card on the admin
// surface; it's a user-facing affordance with no admin-side use.
return <AboutSection hideTourReplay />;
default:
slug satisfies never;
return null;
@@ -155,7 +155,10 @@ describe("admin sections — SSR smoke", () => {
expect(html).toContain("No feedback yet");
});
it("admin slug list contains ai-quality (renderer parity)", () => {
expect(ADMIN_SECTION_SLUGS).toContain("ai-quality");
it("admin slug list contains coach (renderer parity)", () => {
// v1.18.1 — ai-quality / assistant / coach-feedback folded into one
// "coach" section. The standalone component still renders above; the
// slug parity assertion tracks the consolidated nav entry.
expect(ADMIN_SECTION_SLUGS).toContain("coach");
});
});
+5 -14
View File
@@ -23,14 +23,12 @@ import {
Info,
KeyRound,
Inbox,
MessageSquare,
Plug,
Radio,
ScrollText,
Server,
Settings,
ShieldAlert,
SlidersHorizontal,
Sparkles,
Ticket,
Users,
@@ -76,21 +74,14 @@ export const ADMIN_SECTIONS: readonly AdminSection[] = [
titleKey: "admin.section.integrations.title",
icon: Plug,
},
// v1.18.1 — single "Coach" entry replacing the former ai-quality,
// assistant, and coach-feedback trio. The section body stacks all
// three surfaces so the coach-quality picture lives in one place.
{
slug: "ai-quality",
titleKey: "admin.section.ai-quality.title",
slug: "coach",
titleKey: "admin.section.coach.title",
icon: Sparkles,
},
{
slug: "assistant",
titleKey: "admin.section.assistant.title",
icon: SlidersHorizontal,
},
{
slug: "coach-feedback",
titleKey: "admin.section.coach-feedback.title",
icon: MessageSquare,
},
{
slug: "feedback",
titleKey: "admin.section.feedback.title",
+23 -15
View File
@@ -333,6 +333,13 @@ export function BackupsSection() {
return (
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
{/* v1.18.1 E3 — the snapshot count leads (numbers first), the
"Run now" button follows, and the explainer + docs link move
into the header's description slot. Previously the description
sat in a separate `mt-1 pl-7` paragraph that crowded the button
directly above it; routing it through the header's `space-y-1`
stack restores breathing room and left-aligns it under the
title. */}
<SettingsCardHeader
icon={Database}
title={t("admin.section.backups.title")}
@@ -358,22 +365,23 @@ export function BackupsSection() {
{t("admin.section.backups.runNow")}
</Button>
}
description={
<p>
{t("admin.section.backups.description")}{" "}
{/* External docs link — `noopener noreferrer` because this
leaves the admin shell. */}
<a
href="https://docs.healthlog.dev/admin/backups"
target="_blank"
rel="noopener noreferrer"
className="text-primary inline-flex items-center gap-1 underline-offset-2 hover:underline"
>
<BookOpen className="h-3 w-3" aria-hidden="true" />
{t("admin.section.backups.docsLink")}
</a>
</p>
}
/>
<p className="text-muted-foreground mt-1 pl-7 text-xs">
{t("admin.section.backups.description")}{" "}
{/* External docs link — Phase E will publish the matching page on
the docs site. `noopener noreferrer` because this leaves the
admin shell. */}
<a
href="https://docs.healthlog.dev/admin/backups"
target="_blank"
rel="noopener noreferrer"
className="text-primary inline-flex items-center gap-1 underline-offset-2 hover:underline"
>
<BookOpen className="h-3 w-3" aria-hidden="true" />
{t("admin.section.backups.docsLink")}
</a>
</p>
{/* Upload card — separate from the table so admins can ingest a
backup file independently of any existing rows. The visible
+24 -22
View File
@@ -15,6 +15,7 @@ import {
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { apiDelete } from "@/lib/api/api-fetch";
@@ -59,24 +60,25 @@ export function DangerZoneSection() {
});
return (
// v1.18.1 E4 — adopt the shared card shape (rounded-xl border + the
// `SettingsCardHeader` icon/title/description/status layout) so the
// Danger Zone reads like every other admin section. The destructive
// tint + AlertTriangle icon keep it visually flagged as dangerous;
// the delete affordance now sits in the header status slot, properly
// aligned beside its label rather than floating below a lone icon.
<div className="bg-destructive/5 border-destructive/30 rounded-xl border p-4 sm:p-6">
{/* v1.4.19 A8 / F-08: page header `admin.section.danger-zone.*`
already names the section, so the card-level title was a
duplicate of the page title. The destructive icon stays so
the card still flags itself as dangerous at a glance. */}
<AlertTriangle className="text-destructive h-5 w-5" aria-hidden="true" />
<div className="mt-4">
<p className="text-sm font-medium">{t("admin.deleteAllData")}</p>
<p className="text-muted-foreground text-xs">
{t("admin.deleteAllDescription")}
</p>
<div className="mt-3">
<SettingsCardHeader
icon={AlertTriangle}
title={t("admin.deleteAllData")}
description={t("admin.deleteAllDescription")}
status={
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="destructive"
size="sm"
disabled={wipeAllData.isPending}
className="min-h-11"
>
{wipeAllData.isPending ? (
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
@@ -111,17 +113,17 @@ export function DangerZoneSection() {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
{wipeMsg && (
<p
className={`mt-2 text-sm ${
wipeAllData.isError ? "text-destructive" : "text-dracula-green"
}`}
>
{wipeMsg}
</p>
)}
</div>
}
/>
{wipeMsg && (
<p
className={`mt-3 pl-7 text-sm ${
wipeAllData.isError ? "text-destructive" : "text-dracula-green"
}`}
>
{wipeMsg}
</p>
)}
</div>
);
}
+7 -7
View File
@@ -14,13 +14,13 @@ export const ADMIN_SECTION_SLUGS = [
"general",
"services",
"integrations",
"ai-quality",
// v1.4.31 — operator-side assistant feature-flag toggles. Sits
// between `ai-quality` (provider config) and `coach-feedback`
// (in-app feedback inbox) per
// `.planning/research/v15-assistant-optional.md` Part B.
"assistant",
"coach-feedback",
// v1.18.1 — the former `ai-quality`, `assistant`, and `coach-feedback`
// sections collapse into one "Coach" area. It holds the assistant
// surface toggles, the server-key + module-availability operator
// config, and both feedback-quality tables (Coach + Insights). Three
// navigation entries for one coherent coach-quality concern read as
// clutter; one card opens the full picture.
"coach",
"feedback",
"reminders",
"users",
+58 -41
View File
@@ -118,7 +118,18 @@ export function UpdateBadge({
);
}
export function AboutSection() {
export interface AboutSectionProps {
/**
* v1.18.1 E5 — suppress the "Replay tour" card. The admin → Über
* surface reuses this component but the onboarding tour is a
* user-facing affordance with no place in the admin console, so the
* admin renderer passes `hideTourReplay`. The Settings → About surface
* leaves it on.
*/
hideTourReplay?: boolean;
}
export function AboutSection({ hideTourReplay = false }: AboutSectionProps = {}) {
const { t } = useTranslations();
const fmt = useFormatters();
// Pull the auth user — only needed so the "Replay the tour" button
@@ -364,49 +375,55 @@ export function AboutSection() {
settings shell), so the button lives here next to version +
links. Mirrors the stack-on-mobile / right-align-on-desktop
contract used by the Account section's "Restart onboarding
tour" card so the two surfaces feel consistent. */}
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-2">
<div className="flex items-start gap-2">
<Compass className="text-muted-foreground mt-0.5 h-5 w-5 shrink-0" />
<div className="space-y-1">
<h2 className="text-lg font-semibold">
{t("settings.about.tourReplay")}
</h2>
<p className="text-muted-foreground text-xs">
{t("settings.about.tourReplayHint")}
</p>
tour" card so the two surfaces feel consistent.
v1.18.1 E5 — hidden on the admin → Über surface (`hideTourReplay`):
the onboarding tour is a user-facing affordance with no place in
the admin console. */}
{!hideTourReplay && (
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-2">
<div className="flex items-start gap-2">
<Compass className="text-muted-foreground mt-0.5 h-5 w-5 shrink-0" />
<div className="space-y-1">
<h2 className="text-lg font-semibold">
{t("settings.about.tourReplay")}
</h2>
<p className="text-muted-foreground text-xs">
{t("settings.about.tourReplayHint")}
</p>
</div>
</div>
<Button
type="button"
variant="outline"
onClick={handleReplayTour}
disabled={replayingTour}
data-testid="about-replay-tour"
className="w-full shrink-0 sm:w-auto"
>
{replayingTour ? (
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Compass className="h-4 w-4" />
)}
{t("settings.about.tourReplay")}
</Button>
</div>
<Button
type="button"
variant="outline"
onClick={handleReplayTour}
disabled={replayingTour}
data-testid="about-replay-tour"
className="w-full shrink-0 sm:w-auto"
>
{replayingTour ? (
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Compass className="h-4 w-4" />
)}
{t("settings.about.tourReplay")}
</Button>
{tourFeedback && (
<p
role="alert"
className={`mt-2 pl-7 text-xs ${
tourFeedback.type === "success"
? "text-success"
: "text-destructive"
}`}
>
{tourFeedback.text}
</p>
)}
</div>
{tourFeedback && (
<p
role="alert"
className={`mt-2 pl-7 text-xs ${
tourFeedback.type === "success"
? "text-success"
: "text-destructive"
}`}
>
{tourFeedback.text}
</p>
)}
</div>
)}
{/* v1.4.36 W4f — the dedicated "Updates" card with the
manual "Check for updates" button is gone. The 24 h auto-