mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
Twenty-six of twenty-eight admin files already used the Settings card and header. What made the console read as its own system was everything on top of that: three hand-rolled cards, a header slot that meant five different things, and a heading the frame took but never painted. - `SectionFrame` drops `title` and `subtitle`. Seventeen call sites passed both into a component that has rendered neither since v1.18.6.1 — the shell reads the same keys itself. A prop contract nothing honours is a promise the next reader believes. - The invite card, the four host-metric headers, and the `/admin` welcome card stop hand-rolling `bg-card border rounded-xl` shells with their own padding steps and their own heading weights. `text-sm` as a card title on the first card of a page, directly above a correct one, was the first thing the eye landed on. - The header status slot carries status. The backups run, the invite create, the danger-zone wipe, and the user filters come out of it; the app-log refresh stays but reads as the secondary it is. - Loading states paint their card and header. Encryption returned a bare spinner row and then a card, so the section jumped when the status landed — the fix `coach-feedback-section.tsx` already carried, applied where it was missing. Same for the notification-status card. - One label/value tile. `StatusItem`, the encryption section's private `Stat`, the user-management inline panels and the backups upload box were four fills and three paddings for one role. - The overview stopped printing the running version, build SHA and build time twice, thirty pixels apart. The version tile owns build identity — it is the one that checks for a newer release; the snapshot owns what is running. - `ai-server-key` had three paragraphs stacked in a muted `text-xs` slot; one sentence stays there and the two that carry information move into the body as foreground. The central-codex consent copy moves the same way, and its disconnect is `variant="destructive"` — which `ConfirmButton` refused to accept, so every destructive confirm in the tree had been hand-tinting an outline instead. - Leftovers: the `-mt-2` correcting the card's own rhythm, two alpha hovers on text, `text-[10px]` badges, the redundant `space-y-6` wrapper, the empty state repeating the header's CTA, and the `mt-6` a card added inside a frame that already spaces its children.
113 lines
3.9 KiB
TypeScript
113 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* `<SystemStatusSummary>` — compact at-a-glance system snapshot for the
|
|
* `/admin` overview landing page.
|
|
*
|
|
* The full breakdown lives in `<SystemStatusSection>` at
|
|
* `/admin/system-status`; this component only surfaces the handful of
|
|
* facts an admin wants on the overview screen: app version, database
|
|
* up/down, worker running, image build SHA + timestamp, and the
|
|
* server-process start time. Same `useSystemStatus()` data source so we
|
|
* don't re-fetch.
|
|
*/
|
|
|
|
import { Clock, Cog, Database, Globe, Loader2, Server } from "lucide-react";
|
|
import { formatDateTime } from "@/lib/format";
|
|
import { SettingsCard } from "@/components/settings/settings-card";
|
|
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
import { StatusItem, usePublicVersion, useSystemStatus } from "./_shared";
|
|
|
|
export function SystemStatusSummary() {
|
|
const { t } = useTranslations();
|
|
const { data: status, isError } = useSystemStatus();
|
|
const { data: version } = usePublicVersion();
|
|
|
|
return (
|
|
<SettingsCard
|
|
as="section"
|
|
aria-labelledby="admin-overview-snapshot-heading"
|
|
>
|
|
<SettingsCardHeader
|
|
icon={Server}
|
|
titleId="admin-overview-snapshot-heading"
|
|
title={t("admin.overview.snapshotTitle")}
|
|
/>
|
|
|
|
{status ? (
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
<StatusItem
|
|
icon={Database}
|
|
label={t("admin.overview.snapshotDatabase")}
|
|
value={
|
|
status.database === "connected"
|
|
? t("admin.databaseConnected")
|
|
: t("admin.databaseError")
|
|
}
|
|
className={
|
|
status.database === "connected"
|
|
? "text-success"
|
|
: "text-destructive"
|
|
}
|
|
/>
|
|
<StatusItem
|
|
icon={Cog}
|
|
label={t("admin.overview.snapshotWorker")}
|
|
value={
|
|
status.worker.running
|
|
? t("admin.workerRunning")
|
|
: t("admin.workerStopped")
|
|
}
|
|
className={
|
|
status.worker.running ? "text-success" : "text-destructive"
|
|
}
|
|
/>
|
|
<StatusItem
|
|
icon={Clock}
|
|
label={t("admin.overview.snapshotStarted")}
|
|
value={formatDateTime(status.startTime)}
|
|
/>
|
|
{/* v1.4.27 R5 — surface the offline-geo state so the maintainer
|
|
spots the missing MAXMIND_LICENSE_KEY without crawling logs.
|
|
The field is undefined on legacy responses; the row only
|
|
renders when /api/version answers the new shape. */}
|
|
{version?.offlineGeoEnabled !== undefined && (
|
|
<StatusItem
|
|
icon={Globe}
|
|
label={t("admin.overview.snapshotOfflineGeo")}
|
|
value={
|
|
version.offlineGeoEnabled
|
|
? t("admin.overview.snapshotOfflineGeoOn")
|
|
: t("admin.overview.snapshotOfflineGeoOff", {
|
|
host: version.geoProviderHost ?? "ipwho.is",
|
|
})
|
|
}
|
|
className={
|
|
version.offlineGeoEnabled ? "text-success" : "text-warning"
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
) : isError ? (
|
|
<div
|
|
role="alert"
|
|
className="text-destructive bg-destructive/10 border-destructive/30 rounded-md border px-3 py-2 text-sm"
|
|
>
|
|
{t("admin.overview.snapshotLoadError")}
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-2">
|
|
<Loader2
|
|
className="text-muted-foreground h-4 w-4 animate-spin motion-reduce:animate-none"
|
|
aria-hidden="true"
|
|
/>
|
|
<span className="text-muted-foreground text-sm">
|
|
{t("admin.overview.snapshotLoading")}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</SettingsCard>
|
|
);
|
|
}
|