Files
HealthLog/src/components/admin/version-tile-section.tsx
T
Marc-André Bombeck 40f7196c20 feat(medications,coach,admin,perf): era corrections, a truthful weekly status and one card language everywhere
The dashboard snapshot fires the moment login succeeds instead of
waiting for the page chunk (start ~670 to ~200 ms throttled). Guided
follow-ups are personalised from the user's snapshot and an answer gets
the coach's streamed reaction before the next question. Archived
schedule eras can be corrected through a superseding revision that
keeps the original as an audit trail, era writes serialise per
medication, and duplicate dose times collapse in validation. A rolling
medication whose next dose is tomorrow can no longer read as heavily
overdue — the card status derives strictly from the served next-due —
and the adherence bars keep a fixed geometry in every state. The
server-wide assistant key gains its admin card, the one duplicated
coach preference collapses onto a single owner, twenty-seven admin and
settings card headers move onto the shared pattern with sixty
containers normalised, and the remaining hydration warnings are gone.
2026-06-11 13:57:15 +02:00

139 lines
4.6 KiB
TypeScript

"use client";
/**
* `<VersionTileSection>` — admin-overview version card.
*
* v1.15.12 H1 — a dedicated tile that surfaces the running build's
* version, short build SHA, and build timestamp, plus a best-effort
* "update available" hint computed against the latest GitHub release
* tag. The release comparison reuses the existing
* `GET /api/version/check-updates` proxy (server-side `safeFetch` past
* the CSP), so no new route or OpenAPI schema is introduced. The check
* is day-stale (`staleTime`) and never blocks the render: on any
* failure the tile simply omits the update hint.
*
* Cookie-admin only by construction — it lives under the admin layout
* (`/admin`), which gates on `requireAdmin()`.
*/
import {
ArrowUpCircle,
CheckCircle2,
Clock,
GitCommit,
Tag,
} from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { formatDateTime } from "@/lib/format";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { ApiError, apiGet } from "@/lib/api/api-fetch";
import { StatusItem, usePublicVersion } from "./_shared";
type CheckUpdatesResult =
| {
status: "up_to_date";
current: string;
latest_tag: string;
checked_at: string;
}
| {
status: "newer_available";
current: string;
latest_tag: string;
html_url: string | null;
published_at: string | null;
checked_at: string;
}
| { status: "unknown"; current: string; reason: string };
async function fetchUpdateCheck(): Promise<CheckUpdatesResult> {
try {
return await apiGet<CheckUpdatesResult>("/api/version/check-updates");
} catch (err) {
const reason =
err instanceof ApiError ? `http_${err.status}` : "network_error";
return { status: "unknown", current: "", reason };
}
}
function shortSha(sha: string | null): string | null {
if (!sha) return null;
return sha.length > 7 ? sha.slice(0, 7) : sha;
}
export function VersionTileSection() {
const { t } = useTranslations();
const { data: version } = usePublicVersion();
// Best-effort, day-stale, never throws into the render. The route is
// auth-gated and proxies the GitHub Releases API through the server so
// the production CSP doesn't block the call.
const { data: update } = useQuery({
queryKey: queryKeys.versionUpdateCheck(),
queryFn: fetchUpdateCheck,
staleTime: 24 * 60 * 60_000,
retry: false,
});
const newer = update?.status === "newer_available";
const upToDate = update?.status === "up_to_date";
const sha = shortSha(version?.buildSha ?? null);
return (
<section
aria-labelledby="admin-overview-version-heading"
className="bg-card border-border rounded-xl border p-4 sm:p-6"
>
<SettingsCardHeader
icon={Tag}
titleId="admin-overview-version-heading"
title={t("admin.overview.versionTileTitle")}
status={
newer ? (
<a
href={
(update.status === "newer_available" && update.html_url) ||
"https://github.com/MBombeck/HealthLog/releases/latest"
}
target="_blank"
rel="noopener noreferrer"
className="bg-dracula-yellow/15 text-dracula-yellow border-dracula-yellow/30 inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium transition-opacity hover:opacity-80"
>
<ArrowUpCircle className="h-3.5 w-3.5" aria-hidden="true" />
{t("admin.overview.versionTileUpdateAvailable", {
tag:
update.status === "newer_available" ? update.latest_tag : "",
})}
</a>
) : upToDate ? (
<span className="text-dracula-green inline-flex items-center gap-1.5 text-xs font-medium">
<CheckCircle2 className="h-3.5 w-3.5" aria-hidden="true" />
{t("admin.overview.versionTileUpToDate")}
</span>
) : null
}
/>
<div className="mt-4 grid gap-3 pl-7 sm:grid-cols-2 lg:grid-cols-3">
<StatusItem
icon={Tag}
label={t("admin.overview.versionTileVersion")}
value={version?.version ?? "—"}
/>
<StatusItem
icon={GitCommit}
label={t("admin.overview.versionTileBuildSha")}
value={sha ?? "—"}
/>
<StatusItem
icon={Clock}
label={t("admin.overview.versionTileBuiltAt")}
value={version?.builtAt ? formatDateTime(version.builtAt) : "—"}
/>
</div>
</section>
);
}