Files
steep/front/js/api.js
T
qpismont 7493de6952
ci/woodpecker/push/tests Pipeline was successful
Add live dashboard with SSE job events
Expose /jobs, /dedup, and /events routes backed by a new EventBus that
broadcasts job lifecycle and per-job progress over SSE. Serve a static
dashboard from front/ that lists pending and in-progress jobs, streams
agent activity, and shows completed reviews.

Also add migrate-apply/migrate-generate scripts that load .env, update
the Containerfile to ship the front assets and use the apply script, and
refresh the roadmap.
2026-10-13 16:14:48 +00:00

79 lines
2.3 KiB
JavaScript

// Accès aux routes exposées par le serveur axum et petits utilitaires partagés
// par les web components du dashboard.
export const endpoints = {
jobs: "/jobs",
reviews: "/dedup",
lifecycle: "/events",
jobEvents: (id) => `/jobs/${id}/events`,
};
async function getJson(url) {
const response = await fetch(url, { headers: { accept: "application/json" } });
if (!response.ok) {
throw new Error(`${url} → ${response.status}`);
}
return response.json();
}
export const fetchJobs = () => getJson(endpoints.jobs);
export const fetchReviews = () => getJson(endpoints.reviews);
// Ouvre un flux SSE et route les évènements nommés vers les callbacks fournis.
// Les évènements du serveur sont nommés (`lifecycle`, `progress`, `gap`), donc
// `addEventListener("message")` ne suffit pas : il faut écouter par nom.
export function subscribe(url, handlers = {}) {
const source = new EventSource(url);
const onData = (name, handler) => {
if (handler) {
source.addEventListener(name, (event) => handler(JSON.parse(event.data)));
}
};
if (handlers.onOpen) source.addEventListener("open", () => handlers.onOpen());
if (handlers.onError) source.addEventListener("error", () => handlers.onError());
onData("lifecycle", handlers.onLifecycle);
onData("progress", handlers.onProgress);
if (handlers.onGap) {
source.addEventListener("gap", (event) => handlers.onGap(Number(event.data)));
}
return source;
}
export function escapeHtml(value) {
return String(value ?? "").replace(/[&<>"']/g, (char) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
})[char]);
}
// Horodatage relatif (« il y a 3 min »), tolérant sur les valeurs absentes.
export function relativeTime(value) {
if (!value) return "—";
const then = Date.parse(value);
if (Number.isNaN(then)) return "—";
const seconds = Math.round((then - Date.now()) / 1000);
const steps = [
["year", 60 * 60 * 24 * 365],
["month", 60 * 60 * 24 * 30],
["day", 60 * 60 * 24],
["hour", 60 * 60],
["minute", 60],
["second", 1],
];
const formatter = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
for (const [unit, size] of steps) {
if (Math.abs(seconds) >= size || unit === "second") {
return formatter.format(Math.round(seconds / size), unit);
}
}
return "—";
}