79 lines
2.3 KiB
JavaScript
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: "/reviews",
|
|
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) => ({
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'",
|
|
})[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 "—";
|
|
}
|