Fas 1a punkt 3+4: worker-härdning (DLQ, larm, multi-instance), AAMOS training export

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 23:32:18 +07:00
parent d1455fb581
commit b2b6601f54
15 changed files with 9355 additions and 32 deletions
+204
View File
@@ -0,0 +1,204 @@
import { useEffect, useState } from "react";
import { api } from "../api.js";
interface JobCounts {
wait: number;
active: number;
delayed: number;
completed: number;
failed: number;
paused?: number;
}
interface WorkerInfo {
id: string;
addr: string;
concurrency: number;
lastSeenAt: number;
}
interface SchedulerInfo {
id: string;
pattern?: string;
every?: number;
}
interface HealthResponse {
ok: boolean;
queues: {
jobs: JobCounts;
deadLetter: JobCounts;
};
workers: WorkerInfo[];
schedulers: SchedulerInfo[];
}
interface DlqJob {
id: string;
name: string;
data: unknown;
failedReason?: string;
attemptsMade: number;
timestamp: number;
}
export function WorkersPage() {
const [health, setHealth] = useState<HealthResponse | null>(null);
const [dlq, setDlq] = useState<DlqJob[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [actionId, setActionId] = useState<string | null>(null);
const load = async () => {
setLoading(true);
try {
const [h, d] = await Promise.all([
api<HealthResponse>("/admin/v1/workers/health"),
api<{ jobs: DlqJob[] }>("/admin/v1/workers/dead-letter?limit=50"),
]);
setHealth(h);
setDlq(d.jobs);
setError(null);
} catch (e) {
setError(String((e as Error).message));
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
const t = setInterval(load, 10_000);
return () => clearInterval(t);
}, []);
const retry = async (id: string) => {
setActionId(id);
try {
await api(`/admin/v1/workers/dead-letter/${id}/retry`, { method: "POST" });
await load();
} catch (e) {
setError(String((e as Error).message));
} finally {
setActionId(null);
}
};
const discard = async (id: string) => {
if (!confirm("Ta bort jobbet permanent?")) return;
setActionId(id);
try {
await api(`/admin/v1/workers/dead-letter/${id}/discard`, { method: "POST" });
await load();
} catch (e) {
setError(String((e as Error).message));
} finally {
setActionId(null);
}
};
return (
<div className="page">
<h2>Workers & köer</h2>
{error && <div className="alert err">{error}</div>}
{loading && !health && <div>Laddar </div>}
{health && (
<>
<section className="card">
<h3>Jobbkö</h3>
<div className="metrics">
<Metric label="Väntar" value={health.queues.jobs.wait} />
<Metric label="Aktiva" value={health.queues.jobs.active} />
<Metric label="Fördröjda" value={health.queues.jobs.delayed} />
<Metric label="Misslyckade" value={health.queues.jobs.failed} />
<Metric label="Klara" value={health.queues.jobs.completed} />
</div>
</section>
<section className="card">
<h3>Dead letter ({health.queues.deadLetter.wait + health.queues.deadLetter.failed})</h3>
{dlq.length === 0 ? (
<p>Inga jobb i dead letter.</p>
) : (
<table>
<thead>
<tr>
<th>Jobb</th>
<th>Fel</th>
<th>Försök</th>
<th>Tid</th>
<th>Åtgärder</th>
</tr>
</thead>
<tbody>
{dlq.map((job) => (
<tr key={job.id}>
<td>{job.name}</td>
<td className="ellipsis" title={job.failedReason}>
{job.failedReason ?? ""}
</td>
<td>{job.attemptsMade}</td>
<td>{new Date(job.timestamp).toLocaleString("sv-SE")}</td>
<td>
<button
onClick={() => retry(job.id)}
disabled={actionId === job.id}
className="btn small"
>
Kör igen
</button>{" "}
<button
onClick={() => discard(job.id)}
disabled={actionId === job.id}
className="btn small danger"
>
Kasta
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</section>
<section className="card">
<h3>Aktiva worker-instanser</h3>
{health.workers.length === 0 ? (
<p className="err">Inga aktiva workers!</p>
) : (
<ul>
{health.workers.map((w) => (
<li key={w.id}>
{w.id} @ {w.addr} (concurrency {w.concurrency})
</li>
))}
</ul>
)}
</section>
<section className="card">
<h3>Scheduler</h3>
<ul>
{health.schedulers.map((s) => (
<li key={s.id}>
{s.id}: {s.pattern ?? `every ${s.every}ms`}
</li>
))}
</ul>
</section>
</>
)}
</div>
);
}
function Metric({ label, value }: { label: string; value: number }) {
return (
<div className="metric">
<div className="value">{value}</div>
<div className="label">{label}</div>
</div>
);
}