mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
merge: admin consolidation — unified coach section, backups layout, danger-zone parity
This commit is contained in:
commit
cdf7fa2d41
13 files changed
+150
-188
No files matched your search
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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-
|
||||
|
||||
Reference in new issue
Block a user