feat(scheduler): automatisations planifiées (cron) — tâche 5

- table schedules (migration 0007) + service scheduler (croner) : CRUD,
  runSchedule avec scope (all/liste), pool de concurrence et verrou par machine,
  mapping actions → refresh/metrics/docker_scan ; reloadSchedules au boot
- worker = reloadSchedules (remplace le refresh 30 min en dur)
- routes /api/schedules (CRUD + :id/run) ; cron invalide rejeté (validation croner)
- UI Paramètres : onglet « Automatisations » (liste, activer/lancer/supprimer, création)

tsc 0 · 113 tests · build OK · boot OK (migration 0007, CRUD vérifié).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 19:25:44 +02:00
parent bdbe7af55c
commit ff9cfaa9e1
11 changed files with 2796 additions and 18 deletions
+2
View File
@@ -7,6 +7,7 @@ import { dockerRoutes } from "./docker.js";
import { dbRoutes } from "./db.js";
import { settingsRoutes } from "./settings.js";
import { postInstallRoutes } from "./postInstall.js";
import { schedulesRoutes } from "./schedules.js";
import { getServerCapabilities } from "../services/capabilities.js";
import { getSystemMetrics, getSystemStatus } from "../services/system.js";
@@ -16,6 +17,7 @@ api.get("/system/status", (c) => c.json(getSystemStatus()));
api.get("/system/metrics", (c) => c.json(getSystemMetrics()));
api.route("/system/db", dbRoutes);
api.route("/settings", settingsRoutes);
api.route("/schedules", schedulesRoutes);
api.route("/machines", machinesRoutes);
api.route("/machines", actionsRoutes);
api.route("/machines", dockerRoutes);
+49
View File
@@ -0,0 +1,49 @@
// server/routes/schedules.ts
import { Hono } from "hono";
import {
listSchedules,
getSchedule,
createSchedule,
updateSchedule,
deleteSchedule,
runSchedule,
type ScheduleInput,
} from "../services/scheduler.js";
export const schedulesRoutes = new Hono();
schedulesRoutes.get("/", (c) => c.json(listSchedules()));
schedulesRoutes.post("/", async (c) => {
const body = (await c.req.json()) as ScheduleInput;
try {
return c.json(createSchedule(body), 201);
} catch (err) {
return c.json({ error: (err as Error).message }, 400);
}
});
schedulesRoutes.get("/:id", (c) => {
const s = getSchedule(c.req.param("id"));
return s ? c.json(s) : c.json({ error: "Schedule introuvable" }, 404);
});
schedulesRoutes.patch("/:id", async (c) => {
const body = (await c.req.json()) as Partial<ScheduleInput>;
try {
return c.json(updateSchedule(c.req.param("id"), body));
} catch (err) {
return c.json({ error: (err as Error).message }, 400);
}
});
schedulesRoutes.delete("/:id", (c) => {
deleteSchedule(c.req.param("id"));
return c.json({ ok: true });
});
// Lancement immédiat (hors planning).
schedulesRoutes.post("/:id/run", (c) => {
runSchedule(c.req.param("id")).catch((err) => console.error("[schedule run]", (err as Error).message));
return c.json({ ok: true }, 202);
});