mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
The admin system snapshot hardcoded "ipwho.is" for the offline-geo-off state, so an operator who points IP_GEO_LOOKUP_URL at another provider still read the wrong host. Resolve the actual provider host from the env and surface it through /api/version, so the snapshot reads "Online (ip-api.com)" — the host it really queries.
225 lines
7.6 KiB
TypeScript
225 lines
7.6 KiB
TypeScript
"use client";
|
|
|
|
import dynamic from "next/dynamic";
|
|
import {
|
|
Activity,
|
|
AlertTriangle,
|
|
Bell,
|
|
BellRing,
|
|
Clock,
|
|
Cog,
|
|
Database,
|
|
Globe,
|
|
Key,
|
|
Loader2,
|
|
Map,
|
|
RotateCw,
|
|
Server,
|
|
Users,
|
|
} from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { formatDateTime } from "@/lib/format";
|
|
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
|
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
|
import { StatusItem, usePublicVersion, useSystemStatus } from "./_shared";
|
|
|
|
// v1.4.16 phase B3: Recharts is ~108 KiB Brotli — defer-load the host
|
|
// metrics chart so the system-status page only ships it when an admin
|
|
// actually opens this view. Splitting at the wrapper boundary (instead
|
|
// of per-primitive) keeps Recharts' `findAllByType` reconciliation
|
|
// working — same pattern the insights page uses for the scatter chart.
|
|
const HostMetricsChart = dynamic(
|
|
() =>
|
|
import("./host-metrics-chart").then((mod) => ({
|
|
default: mod.HostMetricsChart,
|
|
})),
|
|
{
|
|
ssr: false,
|
|
loading: () => (
|
|
<Skeleton className="bg-muted/40 h-[200px] w-full rounded-xl" />
|
|
),
|
|
},
|
|
);
|
|
|
|
export function SystemStatusSection() {
|
|
const { t } = useTranslations();
|
|
const fmt = useFormatters();
|
|
const { data: status, isError, refetch, isFetching } = useSystemStatus();
|
|
const { data: version } = usePublicVersion();
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<HostMetricsChart />
|
|
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
|
<SettingsCardHeader
|
|
icon={Server}
|
|
title={t("admin.systemStatus")}
|
|
description={t("admin.systemStatusDescription")}
|
|
/>
|
|
{status ? (
|
|
<div className="mt-4 grid gap-4 pl-7 sm:grid-cols-2 lg:grid-cols-4">
|
|
<StatusItem
|
|
icon={Database}
|
|
label={t("admin.database")}
|
|
value={
|
|
status.database === "connected"
|
|
? t("admin.databaseConnected")
|
|
: t("admin.databaseError")
|
|
}
|
|
className={
|
|
status.database === "connected"
|
|
? "text-dracula-green"
|
|
: "text-destructive"
|
|
}
|
|
/>
|
|
<StatusItem
|
|
icon={Clock}
|
|
label={t("admin.startedAt")}
|
|
value={formatDateTime(status.startTime)}
|
|
/>
|
|
<StatusItem
|
|
icon={Users}
|
|
label={t("admin.users")}
|
|
value={String(status.counts.users)}
|
|
/>
|
|
<StatusItem
|
|
icon={Activity}
|
|
label={t("admin.measurementsCount")}
|
|
value={fmt.integer(status.counts.measurements)}
|
|
/>
|
|
<StatusItem
|
|
icon={Key}
|
|
label={t("admin.activeTokens")}
|
|
value={String(status.counts.activeTokens)}
|
|
/>
|
|
<StatusItem
|
|
icon={Globe}
|
|
label={t("admin.activeSessions")}
|
|
value={String(status.counts.activeSessions)}
|
|
/>
|
|
<StatusItem
|
|
icon={Cog}
|
|
label={t("admin.workerStatus")}
|
|
value={
|
|
status.worker.running
|
|
? t("admin.workerRunning")
|
|
: t("admin.workerStopped")
|
|
}
|
|
className={
|
|
status.worker.running
|
|
? "text-dracula-green"
|
|
: "text-destructive"
|
|
}
|
|
/>
|
|
{status.worker.lastReminderCheck && (
|
|
<StatusItem
|
|
icon={Bell}
|
|
label={t("admin.lastReminderCheck")}
|
|
value={formatDateTime(status.worker.lastReminderCheck)}
|
|
/>
|
|
)}
|
|
{status.integrations.umami && (
|
|
<StatusItem
|
|
icon={Activity}
|
|
label="Umami"
|
|
value={
|
|
status.integrations.umami.enabled
|
|
? t("common.active")
|
|
: t("common.disabled")
|
|
}
|
|
className={
|
|
status.integrations.umami.enabled
|
|
? "text-dracula-green"
|
|
: "text-destructive"
|
|
}
|
|
/>
|
|
)}
|
|
{status.integrations.glitchtip && (
|
|
<StatusItem
|
|
icon={AlertTriangle}
|
|
label="GlitchTip"
|
|
value={
|
|
status.integrations.glitchtip.enabled
|
|
? t("common.active")
|
|
: t("common.disabled")
|
|
}
|
|
className={
|
|
status.integrations.glitchtip.enabled
|
|
? "text-dracula-green"
|
|
: "text-destructive"
|
|
}
|
|
/>
|
|
)}
|
|
{status.integrations.webPush && (
|
|
<StatusItem
|
|
icon={BellRing}
|
|
label={t("admin.integrationWebPush")}
|
|
value={t("admin.configured")}
|
|
className="text-dracula-green"
|
|
/>
|
|
)}
|
|
{/* v1.4.27 R5 — offline GeoLite2 availability. Renders only
|
|
when /api/version returns the new field so legacy
|
|
responses do not produce a placeholder row. */}
|
|
{version?.offlineGeoEnabled !== undefined && (
|
|
<StatusItem
|
|
icon={Map}
|
|
label={t("admin.offlineGeoLabel")}
|
|
value={
|
|
version.offlineGeoEnabled
|
|
? t("admin.offlineGeoEnabled")
|
|
: t("admin.offlineGeoFallback", {
|
|
host: version.geoProviderHost ?? "ipwho.is",
|
|
})
|
|
}
|
|
className={
|
|
version.offlineGeoEnabled
|
|
? "text-dracula-green"
|
|
: "text-dracula-yellow"
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
) : isError ? (
|
|
// P19: surface load failures inline instead of leaving the
|
|
// section spinning forever. The hook throws on non-OK
|
|
// responses, so isError is true on 500s, network errors, etc.
|
|
// v1.4.16 Wave-C MED — pair the alert with a Retry button so a
|
|
// transient 500 (rolling deploy, DB blip) doesn't require a
|
|
// full page reload to recover.
|
|
<div
|
|
role="alert"
|
|
className="text-destructive bg-destructive/10 border-destructive/30 mt-4 flex flex-col items-start gap-2 rounded-md border px-3 py-2 text-sm sm:flex-row sm:items-center sm:justify-between"
|
|
>
|
|
<span>{t("admin.systemStatusLoadError")}</span>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={isFetching}
|
|
onClick={() => void refetch()}
|
|
className="min-h-9"
|
|
data-testid="system-status-retry"
|
|
>
|
|
{isFetching ? (
|
|
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
|
) : (
|
|
<RotateCw className="h-3.5 w-3.5" />
|
|
)}
|
|
{t("admin.systemStatusRetry")}
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="mt-4 flex items-center gap-2">
|
|
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin motion-reduce:animate-none" />
|
|
<span className="text-muted-foreground text-sm">
|
|
{t("admin.loadingStatus")}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|