100 lines
2.7 KiB
TypeScript
100 lines
2.7 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { api } from "../api.js";
|
||
|
||
interface Health {
|
||
checks: Record<string, { ok: boolean; detail?: string }>;
|
||
}
|
||
interface JobsOverview {
|
||
scanJobs: Array<{ status: string; count: number }>;
|
||
queue: Record<string, number>;
|
||
outboxPending: number;
|
||
}
|
||
|
||
export function DashboardPage() {
|
||
const [health, setHealth] = useState<Health | null>(null);
|
||
const [jobs, setJobs] = useState<JobsOverview | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
const load = () => {
|
||
api<Health>("/admin/v1/system/health")
|
||
.then(setHealth)
|
||
.catch((e) => setError(String(e.message)));
|
||
api<JobsOverview>("/admin/v1/jobs/overview")
|
||
.then(setJobs)
|
||
.catch(() => undefined);
|
||
};
|
||
load();
|
||
const timer = setInterval(load, 15_000);
|
||
return () => clearInterval(timer);
|
||
}, []);
|
||
|
||
return (
|
||
<div>
|
||
<h2>Systemöversikt</h2>
|
||
{error && <div className="error-text">{error}</div>}
|
||
|
||
<div className="stat-grid">
|
||
{health &&
|
||
Object.entries(health.checks).map(([name, check]) => (
|
||
<div className="stat" key={name}>
|
||
<div className="value">
|
||
<span className={`badge ${check.ok ? "ok" : "err"}`}>
|
||
{check.ok ? "OK" : "FEL"}
|
||
</span>
|
||
</div>
|
||
<div className="label">
|
||
{name} {check.detail ? `– ${check.detail}` : ""}
|
||
</div>
|
||
</div>
|
||
))}
|
||
{jobs && (
|
||
<div className="stat">
|
||
<div className="value">{jobs.outboxPending}</div>
|
||
<div className="label">Outbox väntar</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{jobs && (
|
||
<div className="panel" style={{ marginTop: "1.25rem" }}>
|
||
<h3>Jobbköer</h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Kö-status</th>
|
||
<th>Antal</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{Object.entries(jobs.queue).map(([status, count]) => (
|
||
<tr key={status}>
|
||
<td>{status}</td>
|
||
<td>{count}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
<h3>Skanningsjobb</h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Status</th>
|
||
<th>Antal</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{jobs.scanJobs.map((row) => (
|
||
<tr key={row.status}>
|
||
<td>{row.status}</td>
|
||
<td>{row.count}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|