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;