Files
SkyMoney/web/src/pages/HealthPage.tsx

219 lines
6.1 KiB
TypeScript

import { useQuery } from "@tanstack/react-query";
import { http } from "../api/http";
type AppHealth = { ok: true };
type DbHealth = { ok: true; nowISO: string; latencyMs: number };
export default function HealthPage() {
const app = useQuery({
queryKey: ["health"],
queryFn: () => http<AppHealth>("/health"),
});
const db = useQuery({
queryKey: ["health", "db"],
queryFn: () => http<DbHealth>("/health/db"),
});
const appStatus = getStatus(app.isLoading, !!app.data?.ok, !!app.error);
const dbStatus = getStatus(db.isLoading, !!db.data?.ok, !!db.error);
return (
<div className="flex justify-center py-10">
<div className="card w-full max-w-xl space-y-6">
<header className="space-y-1">
<h2 className="section-title">System Health</h2>
<p className="muted text-sm">
Quick status for the SkyMoney API and database. Use this when debugging issues or latency.
</p>
</header>
{/* Status overview */}
<div className="grid gap-4 md:grid-cols-2">
<HealthCard
label="API"
status={appStatus}
description="Core application and auth endpoints."
details={
<>
<StatusLine
label="Status"
value={labelForStatus(appStatus)}
/>
{app.error && (
<StatusLine
label="Error"
value={
app.error instanceof Error
? app.error.message
: "Unknown error"
}
/>
)}
</>
}
onRetry={!app.isLoading ? () => app.refetch() : undefined}
/>
<HealthCard
label="Database"
status={dbStatus}
description="Primary PostgreSQL connection and latency."
details={
<>
<StatusLine
label="Status"
value={labelForStatus(dbStatus)}
/>
<StatusLine
label="Latency"
value={
db.data?.latencyMs != null
? `${db.data.latencyMs} ms`
: db.isLoading
? "Measuring…"
: "—"
}
/>
<StatusLine
label="Server time"
value={
db.data?.nowISO
? new Date(db.data.nowISO).toLocaleString()
: db.isLoading
? "Loading…"
: "—"
}
/>
{db.error && (
<StatusLine
label="Error"
value={
db.error instanceof Error
? db.error.message
: "Unknown error"
}
/>
)}
</>
}
onRetry={!db.isLoading ? () => db.refetch() : undefined}
/>
</div>
{/* Raw data (tiny, for debugging) */}
<section className="border rounded-xl p-3 space-y-2 bg-[--color-panel]">
<div className="row items-center">
<span className="text-xs uppercase tracking-[0.2em] muted">
Debug
</span>
<span className="ml-auto text-xs muted">
Useful for logs / screenshots
</span>
</div>
<pre className="text-xs whitespace-pre-wrap break-all muted">
API: {app.isLoading ? "Loading…" : JSON.stringify(app.data ?? { ok: false })}{"\n"}
DB: {db.isLoading ? "Loading…" : JSON.stringify(db.data ?? { ok: false })}
</pre>
</section>
</div>
</div>
);
}
type Status = "checking" | "up" | "down";
function getStatus(
isLoading: boolean,
ok: boolean,
hasError: boolean,
): Status {
if (isLoading) return "checking";
if (ok && !hasError) return "up";
return "down";
}
function labelForStatus(status: Status) {
if (status === "checking") return "Checking…";
if (status === "up") return "Operational";
return "Unavailable";
}
function HealthCard({
label,
status,
description,
details,
onRetry,
}: {
label: string;
status: Status;
description: string;
details: React.ReactNode;
onRetry?: () => void;
}) {
return (
<section className="border rounded-xl p-4 bg-[--color-panel] space-y-3">
<div className="row items-center gap-2">
<div className="space-y-1">
<div className="text-sm font-semibold">{label}</div>
<p className="text-xs muted">{description}</p>
</div>
<StatusPill status={status} className="ml-auto" />
</div>
<div className="space-y-1 text-xs">{details}</div>
{onRetry && (
<div className="row mt-2">
<button type="button" className="btn text-xs ml-auto" onClick={onRetry}>
Recheck
</button>
</div>
)}
</section>
);
}
function StatusPill({
status,
className = "",
}: {
status: Status;
className?: string;
}) {
let text = "";
let tone = "";
switch (status) {
case "checking":
text = "Checking…";
tone = "bg-amber-500/10 text-amber-100 border border-amber-500/40";
break;
case "up":
text = "OK";
tone = "bg-emerald-500/10 text-emerald-200 border border-emerald-500/40";
break;
case "down":
text = "Down";
tone = "bg-red-500/10 text-red-100 border border-red-500/40";
break;
}
return (
<span className={`badge text-xs ${tone} ${className}`}>
<span className="inline-block w-2 h-2 rounded-full mr-1 bg-current" />
{text}
</span>
);
}
function StatusLine({ label, value }: { label: string; value: string }) {
return (
<div className="row text-xs">
<span className="muted">{label}</span>
<span className="ml-auto text-right">{value}</span>
</div>
);
}