From fa73ab07b07f6cad1d3aa48ef93853948a2e3be3 Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Sat, 6 Jun 2026 19:36:06 +0200 Subject: [PATCH] =?UTF-8?q?feat(events):=20timeline=20d'=C3=A9v=C3=A9nemen?= =?UTF-8?q?ts=20machine=20(t=C3=A2che=205=20backlog)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - listMachineEvents (machine_events, 30 derniers, desc) + route GET /machines/:id/events - api machineEvents ; section repliable « Timeline » dans le panneau détail (badge sévérité + horodatage), exploite les events déjà enregistrés par recordEvent tsc 0 · 118 tests · build OK. Co-Authored-By: Claude Opus 4.8 --- client/src/features/machines/MachineTile.tsx | 24 ++++++++++++++++++++ client/src/lib/api.ts | 9 ++++++++ server/routes/machines.ts | 4 ++++ server/services/machineState.ts | 12 ++++++++++ 4 files changed, 49 insertions(+) diff --git a/client/src/features/machines/MachineTile.tsx b/client/src/features/machines/MachineTile.tsx index 601f48a..a338cc6 100644 --- a/client/src/features/machines/MachineTile.tsx +++ b/client/src/features/machines/MachineTile.tsx @@ -7,6 +7,7 @@ import { type DockerSettingsView, type DockerStackRow, type ImportantMessageView, + type MachineEventView, type MachineHardwareView, type ProbeResultView, type ProfileManifestView, @@ -835,6 +836,26 @@ function fmtBytes(b: number | null): string { return `${b} o`; } +function TimelineSection({ machineId }: { machineId: string }) { + const [events, setEvents] = useState(null); + useEffect(() => { + void api.machineEvents(machineId).then(setEvents).catch(() => setEvents([])); + }, [machineId]); + if (events === null) return
Chargement…
; + if (events.length === 0) return
Aucun événement.
; + return ( +
+ {events.map((e) => ( +
+ + {e.message ?? e.eventType} + {new Date(e.createdAt).toLocaleString("fr-FR", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" })} +
+ ))} +
+ ); +} + function MessagesCard({ machineId }: { machineId: string }) { const [msgs, setMsgs] = useState([]); const load = () => { void api.machineMessages(machineId).then(setMsgs).catch(() => setMsgs([])); }; @@ -1047,6 +1068,7 @@ export function MachineDetailPanel({ const [dockerOpen, setDockerOpen] = useState(true); const [postOpen, setPostOpen] = useState(true); const [hwOpen, setHwOpen] = useState(true); + const [tlOpen, setTlOpen] = useState(false); const [configOpen, setConfigOpen] = useState(false); const isError = machine.status === "error" || machine.status === "unknown"; @@ -1099,6 +1121,8 @@ export function MachineDetailPanel({ {postOpen && } setHwOpen((v) => !v)} /> {hwOpen && } + setTlOpen((v) => !v)} /> + {tlOpen && } {configOpen && ( diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index 24ebd16..0288a03 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -68,6 +68,7 @@ export const api = { probe: (id: string) => req(`/machines/${id}/probe`, { method: "POST" }), machineHardware: (id: string) => req(`/machines/${id}/hardware`), machineMessages: (id: string) => req(`/machines/${id}/messages`), + machineEvents: (id: string) => req(`/machines/${id}/events`), ackMessage: (id: string, msgId: string) => req<{ ok: boolean }>(`/machines/${id}/messages/${msgId}/ack`, { method: "POST" }), latestMetrics: (id: string) => req(`/machines/${id}/metrics`), collectMetrics: (id: string) => req(`/machines/${id}/metrics/collect`, { method: "POST" }), @@ -195,6 +196,14 @@ export interface UpdateMachineBody { aptProxyUrl?: string | null; } +export interface MachineEventView { + id: string; + eventType: string; + severity: "info" | "warning" | "error"; + createdAt: string; + message: string | null; +} + export interface ImportantMessageView { id: string; source: string; diff --git a/server/routes/machines.ts b/server/routes/machines.ts index 8f897d2..b43ffb6 100644 --- a/server/routes/machines.ts +++ b/server/routes/machines.ts @@ -10,6 +10,7 @@ import { runProbe } from "../services/machineProbe.js"; import { collectMetrics, getLatestMetrics } from "../services/machineMetrics.js"; import { analyzeMachineRepositories } from "../services/aptRepositories.js"; import { listImportantMessages, acknowledgeMessage } from "../services/importantMessages.js"; +import { listMachineEvents } from "../services/machineState.js"; export const machinesRoutes = new Hono(); @@ -78,6 +79,9 @@ machinesRoutes.post("/:id/apt-repositories", async (c) => { } }); +// Timeline d'événements machine. +machinesRoutes.get("/:id/events", (c) => c.json(listMachineEvents(c.req.param("id")))); + // Messages importants (warnings/erreurs/évolutions) extraits des sorties. machinesRoutes.get("/:id/messages", (c) => c.json(listImportantMessages(c.req.param("id")))); machinesRoutes.post("/:id/messages/:msgId/ack", (c) => { diff --git a/server/services/machineState.ts b/server/services/machineState.ts index aba06dc..5d99e44 100644 --- a/server/services/machineState.ts +++ b/server/services/machineState.ts @@ -1,8 +1,20 @@ // server/services/machineState.ts import { randomUUID } from "node:crypto"; +import { desc, eq } from "drizzle-orm"; import { db, schema } from "../db/client.js"; import type { UpdateSnapshot } from "@shared/types.js"; +/** Derniers événements d'une machine (timeline), du plus récent au plus ancien. */ +export function listMachineEvents(machineId: string, limit = 30) { + return db + .select() + .from(schema.machineEvents) + .where(eq(schema.machineEvents.machineId, machineId)) + .orderBy(desc(schema.machineEvents.createdAt)) + .limit(limit) + .all(); +} + export interface AptDerivedState { status: string; aptStatus: string;