mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
feat(admin): surface the running version on the overview
Add a dedicated version tile to the admin overview showing the running version, short build SHA, and build timestamp, alongside a best-effort 'update available' hint computed against the latest GitHub release tag. The hint reuses the existing /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 and never blocks the render. Cookie-admin only by virtue of the admin layout.
This commit is contained in:
1 parent
4be7d8ac5b
commit
7b171cc5e3
9 files changed
+173
No files matched your search
@@ -4242,6 +4242,12 @@
|
||||
"snapshotOfflineGeo": "Geodaten",
|
||||
"snapshotOfflineGeoOn": "Offline (GeoLite2)",
|
||||
"snapshotOfflineGeoOff": "Fallback (ipwho.is)",
|
||||
"versionTileTitle": "Version",
|
||||
"versionTileVersion": "Laufende Version",
|
||||
"versionTileBuildSha": "Build-SHA",
|
||||
"versionTileBuiltAt": "Gebaut am",
|
||||
"versionTileUpdateAvailable": "Update verfügbar: {tag}",
|
||||
"versionTileUpToDate": "Aktuell",
|
||||
"snapshotLoadError": "System-Snapshot konnte nicht geladen werden.",
|
||||
"snapshotLoading": "Snapshot wird geladen...",
|
||||
"auditTitle": "Letzte Aktivitäten",
|
||||
|
||||
@@ -4242,6 +4242,12 @@
|
||||
"snapshotOfflineGeo": "Geo data",
|
||||
"snapshotOfflineGeoOn": "Offline (GeoLite2)",
|
||||
"snapshotOfflineGeoOff": "Fallback (ipwho.is)",
|
||||
"versionTileTitle": "Version",
|
||||
"versionTileVersion": "Running version",
|
||||
"versionTileBuildSha": "Build SHA",
|
||||
"versionTileBuiltAt": "Built at",
|
||||
"versionTileUpdateAvailable": "Update available: {tag}",
|
||||
"versionTileUpToDate": "Up to date",
|
||||
"snapshotLoadError": "Failed to load system snapshot.",
|
||||
"snapshotLoading": "Loading snapshot...",
|
||||
"auditTitle": "Recent activity",
|
||||
|
||||
@@ -4242,6 +4242,12 @@
|
||||
"snapshotOfflineGeo": "Datos geográficos",
|
||||
"snapshotOfflineGeoOn": "Sin conexión (GeoLite2)",
|
||||
"snapshotOfflineGeoOff": "Respaldo (ipwho.is)",
|
||||
"versionTileTitle": "Versión",
|
||||
"versionTileVersion": "Versión en ejecución",
|
||||
"versionTileBuildSha": "SHA de compilación",
|
||||
"versionTileBuiltAt": "Compilado el",
|
||||
"versionTileUpdateAvailable": "Actualización disponible: {tag}",
|
||||
"versionTileUpToDate": "Actualizado",
|
||||
"snapshotLoadError": "No se pudo cargar la instantánea del sistema.",
|
||||
"snapshotLoading": "Cargando instantánea...",
|
||||
"auditTitle": "Actividad reciente",
|
||||
|
||||
@@ -4242,6 +4242,12 @@
|
||||
"snapshotOfflineGeo": "Données géographiques",
|
||||
"snapshotOfflineGeoOn": "Hors ligne (GeoLite2)",
|
||||
"snapshotOfflineGeoOff": "Fallback (ipwho.is)",
|
||||
"versionTileTitle": "Version",
|
||||
"versionTileVersion": "Version en cours",
|
||||
"versionTileBuildSha": "SHA de build",
|
||||
"versionTileBuiltAt": "Compilé le",
|
||||
"versionTileUpdateAvailable": "Mise à jour disponible : {tag}",
|
||||
"versionTileUpToDate": "À jour",
|
||||
"snapshotLoadError": "Échec du chargement de l'instantané du système.",
|
||||
"snapshotLoading": "Chargement de l'instantané...",
|
||||
"auditTitle": "Activité récente",
|
||||
|
||||
@@ -4242,6 +4242,12 @@
|
||||
"snapshotOfflineGeo": "Dati geografici",
|
||||
"snapshotOfflineGeoOn": "Offline (GeoLite2)",
|
||||
"snapshotOfflineGeoOff": "Fallback (ipwho.is)",
|
||||
"versionTileTitle": "Versione",
|
||||
"versionTileVersion": "Versione in esecuzione",
|
||||
"versionTileBuildSha": "SHA build",
|
||||
"versionTileBuiltAt": "Compilato il",
|
||||
"versionTileUpdateAvailable": "Aggiornamento disponibile: {tag}",
|
||||
"versionTileUpToDate": "Aggiornato",
|
||||
"snapshotLoadError": "Impossibile caricare l'istantanea del sistema.",
|
||||
"snapshotLoading": "Caricamento istantanea...",
|
||||
"auditTitle": "Attività recente",
|
||||
|
||||
@@ -4242,6 +4242,12 @@
|
||||
"snapshotOfflineGeo": "Dane geograficzne",
|
||||
"snapshotOfflineGeoOn": "Offline (GeoLite2)",
|
||||
"snapshotOfflineGeoOff": "Awaryjne (ipwho.is)",
|
||||
"versionTileTitle": "Wersja",
|
||||
"versionTileVersion": "Uruchomiona wersja",
|
||||
"versionTileBuildSha": "SHA kompilacji",
|
||||
"versionTileBuiltAt": "Skompilowano",
|
||||
"versionTileUpdateAvailable": "Dostępna aktualizacja: {tag}",
|
||||
"versionTileUpToDate": "Aktualne",
|
||||
"snapshotLoadError": "Nie udało się załadować migawki systemu.",
|
||||
"snapshotLoading": "Ładowanie migawki...",
|
||||
"auditTitle": "Ostatnia aktywność",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { AdminShell } from "@/components/admin/admin-shell";
|
||||
import { SystemStatusSummary } from "@/components/admin/system-status-summary";
|
||||
import { VersionTileSection } from "@/components/admin/version-tile-section";
|
||||
import { RecentAuditPreview } from "@/components/admin/recent-audit-preview";
|
||||
import { useAdminSettings } from "@/components/admin/_shared";
|
||||
|
||||
@@ -61,6 +62,8 @@ export default function AdminOverviewPage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<VersionTileSection />
|
||||
|
||||
<SystemStatusSummary />
|
||||
|
||||
<RecentAuditPreview />
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
"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 { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
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> {
|
||||
const res = await fetch("/api/version/check-updates");
|
||||
if (!res.ok) {
|
||||
return { status: "unknown", current: "", reason: `http_${res.status}` };
|
||||
}
|
||||
return (await res.json()).data as CheckUpdatesResult;
|
||||
}
|
||||
|
||||
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-6"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Tag className="text-primary h-5 w-5" aria-hidden="true" />
|
||||
<h2
|
||||
id="admin-overview-version-heading"
|
||||
className="text-lg font-semibold"
|
||||
>
|
||||
{t("admin.overview.versionTileTitle")}
|
||||
</h2>
|
||||
|
||||
{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 ml-auto 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 ml-auto 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>
|
||||
|
||||
<div className="mt-4 grid gap-3 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>
|
||||
);
|
||||
}
|
||||
@@ -297,6 +297,9 @@ export const queryKeys = {
|
||||
|
||||
apiVersion: () => ["api", "version"] as const,
|
||||
publicVersion: () => ["public", "version"] as const,
|
||||
/** v1.15.12 H1 — admin overview "update available" check against the
|
||||
* latest GitHub release tag (best-effort, day-stale). */
|
||||
versionUpdateCheck: () => ["version", "update-check"] as const,
|
||||
researchMode: () => ["research-mode"] as const,
|
||||
moodlogStatus: () => ["moodlog-status"] as const,
|
||||
integrationsStatus: () => ["integrations", "status"] as const,
|
||||
|
||||
Reference in new issue
Block a user