final touches for beta skymoney (at least i think)
This commit is contained in:
@@ -1,18 +1,218 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api } from "../api/client";
|
||||
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: () => api.get<{ok:true}>("/health") });
|
||||
const db = useQuery({ queryKey: ["health","db"], queryFn: () => api.get<{ok:true; nowISO:string; latencyMs:number}>("/health/db") });
|
||||
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="card max-w-lg">
|
||||
<h2 className="section-title">Health</h2>
|
||||
<ul className="stack">
|
||||
<li>API: {app.isLoading ? "…" : app.data?.ok ? "OK" : "Down"}</li>
|
||||
<li>DB: {db.isLoading ? "…" : db.data?.ok ? `OK (${db.data.latencyMs} ms)` : "Down"}</li>
|
||||
<li>Server Time: {db.data?.nowISO ? new Date(db.data.nowISO).toLocaleString() : "…"}</li>
|
||||
</ul>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user