diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index b6a6e08..6cc17e8 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -55,6 +55,13 @@ export const api = { req("/settings/apt-proxy", { method: "PUT", body: JSON.stringify(body) }), applyProxyToAll: () => req<{ ok: boolean; updated: number }>("/settings/apt-proxy/apply-all", { method: "POST" }), + // --- Automatisations planifiées --- + getSchedules: () => req("/schedules"), + createSchedule: (body: ScheduleInput) => req("/schedules", { method: "POST", body: JSON.stringify(body) }), + updateSchedule: (id: string, body: Partial) => req(`/schedules/${id}`, { method: "PATCH", body: JSON.stringify(body) }), + deleteSchedule: (id: string) => req<{ ok: boolean }>(`/schedules/${id}`, { method: "DELETE" }), + runScheduleNow: (id: string) => req<{ ok: boolean }>(`/schedules/${id}/run`, { method: "POST" }), + // --- Profil machine (SJ-7) --- updateMachine: (id: string, body: UpdateMachineBody) => req(`/machines/${id}`, { method: "PATCH", body: JSON.stringify(body) }), @@ -219,6 +226,31 @@ export interface ProbeResultView { changes: string[]; } +export type ScheduleAction = "apt_update_analyze" | "machine_metrics_simple" | "docker_scan"; + +export interface ScheduleView { + id: string; + name: string; + enabled: boolean; + cron: string; + timezone: string | null; + scope: { machineIds: "all" | string[] }; + actions: ScheduleAction[]; + concurrency: number; + lastRunAt: string | null; + lastStatus: string | null; +} + +export interface ScheduleInput { + name: string; + cron: string; + timezone?: string | null; + enabled?: boolean; + scope?: { machineIds: "all" | string[] }; + actions: ScheduleAction[]; + concurrency?: number; +} + export type StackStatus = "candidate" | "enabled" | "ignored" | "error"; export interface DockerSettingsView { diff --git a/client/src/panels/SettingsModal.tsx b/client/src/panels/SettingsModal.tsx index c7545a3..b3c1026 100644 --- a/client/src/panels/SettingsModal.tsx +++ b/client/src/panels/SettingsModal.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import type { AptProxyMode } from "@shared/types.js"; import { Icon, Popup, Button } from "../components/ui-kit.js"; -import { api, type DbInfo } from "../lib/api.js"; +import { api, type DbInfo, type ScheduleView, type ScheduleAction } from "../lib/api.js"; interface Props { open: boolean; @@ -14,6 +14,7 @@ type SettingsTab = | "tiles" | "layout" | "proxy" + | "automation" | "docker" | "scripts" | "hermes" @@ -26,6 +27,7 @@ const TABS: Array<{ id: SettingsTab; label: string; icon: string }> = [ { id: "tiles", label: "Tuiles", icon: "grid" }, { id: "layout", label: "Volets", icon: "collapse" }, { id: "proxy", label: "Proxy APT", icon: "network" }, + { id: "automation", label: "Automatisations", icon: "clock" }, { id: "docker", label: "Docker", icon: "docker" }, { id: "scripts", label: "Scripts", icon: "script" }, { id: "hermes", label: "Hermes", icon: "node" }, @@ -71,6 +73,7 @@ export function SettingsModal({ open, onClose }: Props) { {active === "tiles" && } {active === "layout" && } {active === "proxy" && } + {active === "automation" && } {active === "docker" && } {active === "scripts" && } {active === "hermes" && } @@ -239,6 +242,111 @@ function RetentionSettings() { ); } +const SCHEDULE_ACTIONS: { id: ScheduleAction; label: string }[] = [ + { id: "apt_update_analyze", label: "Analyse APT" }, + { id: "machine_metrics_simple", label: "Métriques" }, + { id: "docker_scan", label: "Scan Docker" }, +]; + +function AutomationSettings() { + const [schedules, setSchedules] = useState([]); + const [busy, setBusy] = useState(null); + const [msg, setMsg] = useState<{ kind: "ok" | "error"; text: string } | null>(null); + const [name, setName] = useState("Analyse quotidienne"); + const [cron, setCron] = useState("0 6 * * *"); + const [actions, setActions] = useState(["apt_update_analyze", "machine_metrics_simple"]); + + async function load() { + try { + setSchedules(await api.getSchedules()); + } catch (e) { + setMsg({ kind: "error", text: (e as Error).message }); + } + } + useEffect(() => { + void load(); + }, []); + + async function withBusy(key: string, fn: () => Promise) { + setBusy(key); + setMsg(null); + try { + await fn(); + } catch (e) { + setMsg({ kind: "error", text: (e as Error).message }); + } finally { + setBusy(null); + } + } + + const toggleAction = (a: ScheduleAction) => + setActions((prev) => (prev.includes(a) ? prev.filter((x) => x !== a) : [...prev, a])); + + const create = () => + withBusy("create", async () => { + if (!actions.length) throw new Error("Sélectionne au moins une action."); + await api.createSchedule({ name, cron, actions, scope: { machineIds: "all" } }); + await load(); + setMsg({ kind: "ok", text: "Automatisation créée." }); + }); + + return ( + +
+ {schedules.length === 0 && Aucune automatisation. Crée-en une ci-dessous.} + {schedules.map((s) => ( +
+
+ {s.name} + {s.enabled ? "actif" : "off"} + {s.cron} +
+
+ {s.actions.join(" · ")} · toutes machines{s.lastRunAt ? ` · dernier ${new Date(s.lastRunAt).toLocaleString("fr-FR")} (${s.lastStatus})` : ""} +
+
+ + + +
+
+ ))} +
+ +
+ Nouvelle automatisation +
+ setName(e.target.value)} /> + setCron(e.target.value)} placeholder="0 6 * * *" /> +
+ +
+ {SCHEDULE_ACTIONS.map((a) => ( + + ))} +
+
+
+ +
+
+ + {msg &&

{msg.text}

} +
+ ); +} + function ProxyDefaultSettings() { const [mode, setMode] = useState("direct"); const [url, setUrl] = useState(""); diff --git a/docs/superpowers/plans/2026-06-06-tache5-scheduler.md b/docs/superpowers/plans/2026-06-06-tache5-scheduler.md new file mode 100644 index 0000000..389d7c8 --- /dev/null +++ b/docs/superpowers/plans/2026-06-06-tache5-scheduler.md @@ -0,0 +1,35 @@ +# Tâche 5 — Automatisations planifiées (scheduler croner) + +> Statut : **implémenté** (2026-06-06). tsc 0 · 113 tests · build OK · boot OK. +> Réf. : `tache5.md §4` (automatisations backend), `validation_tache5.md`. + +## Périmètre livré (1re tranche tâche 5) + +Planificateur piloté par la BDD : exécuter `apt_update_analyze` / `machine_metrics_simple` / +`docker_scan` sur un périmètre de machines à heure fixe (cron), avec concurrence + verrou. + +- **Table `schedules`** (migration 0007) : name, enabled, cron, timezone, scope_json, + actions_json, concurrency, notify_on_json, last_run_at, last_status. +- **`server/services/scheduler.ts`** : + - CRUD (validation cron via `new Cron()` à la création/maj). + - `runSchedule` : résout le scope (`all` ou liste), exécute les actions par machine avec + **pool de concurrence** + **verrou par machine** (in-process Set, évite 2 actions + simultanées), met à jour last_run_at/last_status, `recordEvent` sur échec. + - mapping actions → `refreshMachine` / `collectMetrics` / `scanDockerStacks`. + - `reloadSchedules` : (ré)enregistre les crons actifs via croner (timezone par schedule). +- **`worker.ts`** : `startWorker` = `reloadSchedules()` (remplace le refresh 30 min en dur). +- **Routes** `/api/schedules` : list / create / get / patch / delete / `:id/run` (lancement immédiat). +- **UI** : onglet Paramètres « Automatisations » — liste (cron, actions, actif, dernier run), + activer/désactiver, lancer maintenant, supprimer, et formulaire de création (nom, cron, + cases d'actions, toutes machines). + +## Vérifié + +CRUD via API, cron invalide rejeté proprement (message croner), init scheduler sans erreur, +migration 0007 appliquée. Verrou empêche les exécutions concurrentes sur une même machine. + +## Reste tâche 5 (backlog) + +Notifications (`notifyOn`), tags de scope, retries persistants, extraction structurée des +**messages importants** (E:/W:/dépréciations → `important_messages` + tuile), timeline +d'événements machine, politiques de rétention. (pg-boss = piste future si jobs distribués.) diff --git a/server/db/migrations/0007_bizarre_doctor_faustus.sql b/server/db/migrations/0007_bizarre_doctor_faustus.sql new file mode 100644 index 0000000..0366282 --- /dev/null +++ b/server/db/migrations/0007_bizarre_doctor_faustus.sql @@ -0,0 +1,15 @@ +CREATE TABLE `schedules` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `enabled` integer DEFAULT 1 NOT NULL, + `cron` text NOT NULL, + `timezone` text, + `scope_json` text NOT NULL, + `actions_json` text NOT NULL, + `concurrency` integer DEFAULT 2 NOT NULL, + `notify_on_json` text, + `last_run_at` text, + `last_status` text, + `created_at` text NOT NULL, + `updated_at` text NOT NULL +); diff --git a/server/db/migrations/meta/0007_snapshot.json b/server/db/migrations/meta/0007_snapshot.json new file mode 100644 index 0000000..d4b68fc --- /dev/null +++ b/server/db/migrations/meta/0007_snapshot.json @@ -0,0 +1,2320 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b3f53e38-0dbd-445c-a3a7-527fed8b00df", + "prevId": "b6542545-9bb8-448c-95f7-4feef066f128", + "tables": { + "action_requests": { + "name": "action_requests", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "requested_by_type": { + "name": "requested_by_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "requested_by_id": { + "name": "requested_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "risk": { + "name": "risk", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "approved_at": { + "name": "approved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "action_requests_machine_id_machines_id_fk": { + "name": "action_requests_machine_id_machines_id_fk", + "tableFrom": "action_requests", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_clients": { + "name": "api_clients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scopes_json": { + "name": "scopes_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "api_clients_token_hash_unique": { + "name": "api_clients_token_hash_unique", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_settings": { + "name": "app_settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "docker_compose_roots": { + "name": "docker_compose_roots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "scan_depth": { + "name": "scan_depth", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "docker_compose_roots_machine_id_machines_id_fk": { + "name": "docker_compose_roots_machine_id_machines_id_fk", + "tableFrom": "docker_compose_roots", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "docker_compose_stacks": { + "name": "docker_compose_stacks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "working_dir": { + "name": "working_dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compose_files_json": { + "name": "compose_files_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_file": { + "name": "env_file", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "detected_by": { + "name": "detected_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_scan_at": { + "name": "last_scan_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_update_at": { + "name": "last_update_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "docker_compose_stacks_machine_id_machines_id_fk": { + "name": "docker_compose_stacks_machine_id_machines_id_fk", + "tableFrom": "docker_compose_stacks", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "docker_image_events": { + "name": "docker_image_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stack_id": { + "name": "stack_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_ref": { + "name": "image_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "from_image_id": { + "name": "from_image_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "to_image_id": { + "name": "to_image_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "from_digest": { + "name": "from_digest", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "to_digest": { + "name": "to_digest", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bytes_reclaimed": { + "name": "bytes_reclaimed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "docker_image_events_execution_id_executions_id_fk": { + "name": "docker_image_events_execution_id_executions_id_fk", + "tableFrom": "docker_image_events", + "tableTo": "executions", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "docker_settings": { + "name": "docker_settings", + "columns": { + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "scan_depth": { + "name": "scan_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 4 + }, + "prune_mode": { + "name": "prune_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'safe'" + }, + "last_scan_at": { + "name": "last_scan_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_pull_check_at": { + "name": "last_pull_check_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "docker_settings_machine_id_machines_id_fk": { + "name": "docker_settings_machine_id_machines_id_fk", + "tableFrom": "docker_settings", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "docker_stack_services": { + "name": "docker_stack_services", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "stack_id": { + "name": "stack_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_ref": { + "name": "image_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "current_image_id": { + "name": "current_image_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "current_digest": { + "name": "current_digest", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "candidate_image_id": { + "name": "candidate_image_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "candidate_digest": { + "name": "candidate_digest", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version_label": { + "name": "version_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "docker_stack_services_stack_id_docker_compose_stacks_id_fk": { + "name": "docker_stack_services_stack_id_docker_compose_stacks_id_fk", + "tableFrom": "docker_stack_services", + "tableTo": "docker_compose_stacks", + "columnsFrom": [ + "stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "executions": { + "name": "executions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "important_json": { + "name": "important_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "report_path": { + "name": "report_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_log_path": { + "name": "raw_log_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "report_id": { + "name": "report_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_kind": { + "name": "error_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "executions_machine_id_machines_id_fk": { + "name": "executions_machine_id_machines_id_fk", + "tableFrom": "executions", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "important_messages": { + "name": "important_messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "component": { + "name": "component", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw_line_ref": { + "name": "raw_line_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acknowledged": { + "name": "acknowledged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acknowledged_by": { + "name": "acknowledged_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "important_messages_machine_id_machines_id_fk": { + "name": "important_messages_machine_id_machines_id_fk", + "tableFrom": "important_messages", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "machine_credentials": { + "name": "machine_credentials", + "columns": { + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "auth_method": { + "name": "auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enc_password": { + "name": "enc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enc_sudo_password": { + "name": "enc_sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enc_private_key": { + "name": "enc_private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enc_key_passphrase": { + "name": "enc_key_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_mode": { + "name": "sudo_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_test_at": { + "name": "last_test_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "machine_credentials_machine_id_machines_id_fk": { + "name": "machine_credentials_machine_id_machines_id_fk", + "tableFrom": "machine_credentials", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "machine_events": { + "name": "machine_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "machine_events_machine_id_machines_id_fk": { + "name": "machine_events_machine_id_machines_id_fk", + "tableFrom": "machine_events", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "machine_hardware": { + "name": "machine_hardware", + "columns": { + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "probe_snapshot_id": { + "name": "probe_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_model": { + "name": "cpu_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_cores": { + "name": "cpu_cores", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_bytes": { + "name": "memory_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "gpus_json": { + "name": "gpus_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disks_json": { + "name": "disks_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_json": { + "name": "network_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "firmware_json": { + "name": "firmware_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "driver_json": { + "name": "driver_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "warnings_json": { + "name": "warnings_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "machine_hardware_machine_id_machines_id_fk": { + "name": "machine_hardware_machine_id_machines_id_fk", + "tableFrom": "machine_hardware", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "machine_host_keys": { + "name": "machine_host_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fingerprint_sha256": { + "name": "fingerprint_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "machine_host_keys_machine_id_machines_id_fk": { + "name": "machine_host_keys_machine_id_machines_id_fk", + "tableFrom": "machine_host_keys", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "machine_metrics_latest": { + "name": "machine_metrics_latest", + "columns": { + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpu_load1": { + "name": "cpu_load1", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_load5": { + "name": "cpu_load5", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_cores": { + "name": "cpu_cores", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total_bytes": { + "name": "memory_total_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_used_bytes": { + "name": "memory_used_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_available_bytes": { + "name": "memory_available_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_used_percent": { + "name": "memory_used_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "filesystems_json": { + "name": "filesystems_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "root_used_percent": { + "name": "root_used_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "warnings_json": { + "name": "warnings_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "machine_metrics_latest_machine_id_machines_id_fk": { + "name": "machine_metrics_latest_machine_id_machines_id_fk", + "tableFrom": "machine_metrics_latest", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "machine_state": { + "name": "machine_state", + "columns": { + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "apt_status": { + "name": "apt_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "apt_updates_count": { + "name": "apt_updates_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "apt_reboot_required": { + "name": "apt_reboot_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "apt_last_analyze_at": { + "name": "apt_last_analyze_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_status": { + "name": "docker_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_installed": { + "name": "docker_installed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "docker_stacks_count": { + "name": "docker_stacks_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "docker_updates_count": { + "name": "docker_updates_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "docker_prune_available": { + "name": "docker_prune_available", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "post_install_status": { + "name": "post_install_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metrics_last_collected_at": { + "name": "metrics_last_collected_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_load1": { + "name": "cpu_load1", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_used_percent": { + "name": "memory_used_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "root_used_percent": { + "name": "root_used_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_warnings_count": { + "name": "disk_warnings_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "hardware_warnings_count": { + "name": "hardware_warnings_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "running_job_id": { + "name": "running_job_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_kind": { + "name": "last_error_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "machine_state_machine_id_machines_id_fk": { + "name": "machine_state_machine_id_machines_id_fk", + "tableFrom": "machine_state", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "machines": { + "name": "machines", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 22 + }, + "os_family": { + "name": "os_family", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "os_version": { + "name": "os_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "os_codename": { + "name": "os_codename", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "arch": { + "name": "arch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "machine_kind": { + "name": "machine_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "virtualization": { + "name": "virtualization", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hardware_profile": { + "name": "hardware_profile", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enc_password": { + "name": "enc_password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enc_sudo_password": { + "name": "enc_sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "apt_proxy_mode": { + "name": "apt_proxy_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'direct'" + }, + "apt_proxy_url": { + "name": "apt_proxy_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "raw_artifacts": { + "name": "raw_artifacts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pinned": { + "name": "pinned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "redacted": { + "name": "redacted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delete_reason": { + "name": "delete_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "raw_artifacts_machine_id_machines_id_fk": { + "name": "raw_artifacts_machine_id_machines_id_fk", + "tableFrom": "raw_artifacts", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pinned": { + "name": "pinned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "summary_json": { + "name": "summary_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "reports_machine_id_machines_id_fk": { + "name": "reports_machine_id_machines_id_fk", + "tableFrom": "reports", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedules": { + "name": "schedules", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_json": { + "name": "scope_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actions_json": { + "name": "actions_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "concurrency": { + "name": "concurrency", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2 + }, + "notify_on_json": { + "name": "notify_on_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_status": { + "name": "last_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "snapshots": { + "name": "snapshots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'apt_update_analyze'" + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "checked_at": { + "name": "checked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "important_json": { + "name": "important_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_log_path": { + "name": "raw_log_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_artifact_id": { + "name": "raw_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_job_id": { + "name": "source_job_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "snapshots_machine_id_machines_id_fk": { + "name": "snapshots_machine_id_machines_id_fk", + "tableFrom": "snapshots", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/server/db/migrations/meta/_journal.json b/server/db/migrations/meta/_journal.json index 4f5be15..95e562f 100644 --- a/server/db/migrations/meta/_journal.json +++ b/server/db/migrations/meta/_journal.json @@ -50,6 +50,13 @@ "when": 1780724800966, "tag": "0006_many_northstar", "breakpoints": true + }, + { + "idx": 7, + "version": "6", + "when": 1780766513336, + "tag": "0007_bizarre_doctor_faustus", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/db/schema.ts b/server/db/schema.ts index 3d3fb92..02f1b00 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -319,3 +319,20 @@ export const appSettings = sqliteTable("app_settings", { value: text("value"), updatedAt: text("updated_at").notNull(), }); + +// Automatisations planifiées (cron) : analyse/metrics/scan sur un périmètre de machines. +export const schedules = sqliteTable("schedules", { + id: text("id").primaryKey(), + name: text("name").notNull(), + enabled: integer("enabled").notNull().default(1), + cron: text("cron").notNull(), + timezone: text("timezone"), + scopeJson: text("scope_json").notNull(), // {"machineIds":"all"|string[]} + actionsJson: text("actions_json").notNull(), // ["apt_update_analyze","machine_metrics_simple",...] + concurrency: integer("concurrency").notNull().default(2), + notifyOnJson: text("notify_on_json"), + lastRunAt: text("last_run_at"), + lastStatus: text("last_status"), + createdAt: text("created_at").notNull(), + updatedAt: text("updated_at").notNull(), +}); diff --git a/server/jobs/worker.ts b/server/jobs/worker.ts index 1fdc2e3..473330b 100644 --- a/server/jobs/worker.ts +++ b/server/jobs/worker.ts @@ -1,24 +1,11 @@ // server/jobs/worker.ts -import { Cron } from "croner"; -import { listMachines } from "../services/machines.js"; -import { refreshMachine } from "../services/refresh.js"; +import { reloadSchedules, stopSchedules } from "../services/scheduler.js"; -let job: Cron | null = null; - -/** Rafraîchit toutes les machines toutes les 30 minutes (tâche de fond). */ +/** Démarre le planificateur : enregistre les automatisations actives (cron) depuis la BDD. */ export function startWorker(): void { - job = new Cron("*/30 * * * *", async () => { - for (const m of listMachines()) { - try { - await refreshMachine(m.id); - } catch (err) { - console.error(`[worker] refresh échoué pour ${m.id}:`, (err as Error).message); - } - } - }); + reloadSchedules(); } export function stopWorker(): void { - job?.stop(); - job = null; + stopSchedules(); } diff --git a/server/routes/index.ts b/server/routes/index.ts index d996422..02cabc0 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -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); diff --git a/server/routes/schedules.ts b/server/routes/schedules.ts new file mode 100644 index 0000000..f22d5a3 --- /dev/null +++ b/server/routes/schedules.ts @@ -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; + 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); +}); diff --git a/server/services/scheduler.ts b/server/services/scheduler.ts new file mode 100644 index 0000000..19b100b --- /dev/null +++ b/server/services/scheduler.ts @@ -0,0 +1,206 @@ +// server/services/scheduler.ts +import { randomUUID } from "node:crypto"; +import { Cron } from "croner"; +import { eq } from "drizzle-orm"; +import { db, schema } from "../db/client.js"; +import { listMachines } from "./machines.js"; +import { refreshMachine } from "./refresh.js"; +import { collectMetrics } from "./machineMetrics.js"; +import { recordEvent } from "./machineState.js"; + +export type ScheduleAction = "apt_update_analyze" | "machine_metrics_simple" | "docker_scan"; + +export interface ScheduleScope { + machineIds: "all" | string[]; +} + +export interface ScheduleView { + id: string; + name: string; + enabled: boolean; + cron: string; + timezone: string | null; + scope: ScheduleScope; + actions: ScheduleAction[]; + concurrency: number; + lastRunAt: string | null; + lastStatus: string | null; +} + +type ScheduleRow = typeof schema.schedules.$inferSelect; + +function toView(r: ScheduleRow): ScheduleView { + return { + id: r.id, + name: r.name, + enabled: !!r.enabled, + cron: r.cron, + timezone: r.timezone, + scope: JSON.parse(r.scopeJson) as ScheduleScope, + actions: JSON.parse(r.actionsJson) as ScheduleAction[], + concurrency: r.concurrency, + lastRunAt: r.lastRunAt, + lastStatus: r.lastStatus, + }; +} + +// --------------------------------------------------------------------------- +// CRUD +// --------------------------------------------------------------------------- + +export function listSchedules(): ScheduleView[] { + return db.select().from(schema.schedules).all().map(toView); +} + +export function getSchedule(id: string): ScheduleView | null { + const r = db.select().from(schema.schedules).where(eq(schema.schedules.id, id)).get(); + return r ? toView(r) : null; +} + +export interface ScheduleInput { + name: string; + cron: string; + timezone?: string | null; + enabled?: boolean; + scope?: ScheduleScope; + actions: ScheduleAction[]; + concurrency?: number; +} + +export function createSchedule(input: ScheduleInput): ScheduleView { + // Valide l'expression cron (lève si invalide), sans planifier. + new Cron(input.cron).stop(); + const id = randomUUID(); + const now = new Date().toISOString(); + db.insert(schema.schedules).values({ + id, + name: input.name, + enabled: input.enabled === false ? 0 : 1, + cron: input.cron, + timezone: input.timezone ?? "Europe/Paris", + scopeJson: JSON.stringify(input.scope ?? { machineIds: "all" }), + actionsJson: JSON.stringify(input.actions), + concurrency: input.concurrency ?? 2, + createdAt: now, + updatedAt: now, + }).run(); + reloadSchedules(); + return getSchedule(id)!; +} + +export function updateSchedule(id: string, input: Partial): ScheduleView { + const cur = db.select().from(schema.schedules).where(eq(schema.schedules.id, id)).get(); + if (!cur) throw new Error("Schedule introuvable"); + if (input.cron) new Cron(input.cron).stop(); // valide sans planifier + db.update(schema.schedules).set({ + ...(input.name !== undefined ? { name: input.name } : {}), + ...(input.enabled !== undefined ? { enabled: input.enabled ? 1 : 0 } : {}), + ...(input.cron !== undefined ? { cron: input.cron } : {}), + ...(input.timezone !== undefined ? { timezone: input.timezone } : {}), + ...(input.scope !== undefined ? { scopeJson: JSON.stringify(input.scope) } : {}), + ...(input.actions !== undefined ? { actionsJson: JSON.stringify(input.actions) } : {}), + ...(input.concurrency !== undefined ? { concurrency: input.concurrency } : {}), + updatedAt: new Date().toISOString(), + }).where(eq(schema.schedules.id, id)).run(); + reloadSchedules(); + return getSchedule(id)!; +} + +export function deleteSchedule(id: string): void { + db.delete(schema.schedules).where(eq(schema.schedules.id, id)).run(); + reloadSchedules(); +} + +// --------------------------------------------------------------------------- +// Exécution +// --------------------------------------------------------------------------- + +const locked = new Set(); + +function resolveMachineIds(scope: ScheduleScope): string[] { + const all = listMachines().map((m) => m.id); + return scope.machineIds === "all" ? all : scope.machineIds.filter((id) => all.includes(id)); +} + +async function runActionOnMachine(machineId: string, action: ScheduleAction): Promise { + if (action === "apt_update_analyze") { + await refreshMachine(machineId); + } else if (action === "machine_metrics_simple") { + await collectMetrics(machineId); + } else if (action === "docker_scan") { + const { scanDockerStacks } = await import("./dockerScan.js"); + await scanDockerStacks(machineId); + } +} + +/** Exécute un schedule : actions sur le périmètre, avec verrou par machine et concurrence. */ +export async function runSchedule(id: string): Promise<{ ran: number; errors: number }> { + const sched = getSchedule(id); + if (!sched) throw new Error("Schedule introuvable"); + const machineIds = resolveMachineIds(sched.scope); + let ran = 0; + let errors = 0; + + const queue = [...machineIds]; + const worker = async () => { + for (;;) { + const machineId = queue.shift(); + if (!machineId) break; + if (locked.has(machineId)) continue; // une action tourne déjà sur cette machine + locked.add(machineId); + try { + for (const action of sched.actions) { + await runActionOnMachine(machineId, action); + } + ran++; + } catch (err) { + errors++; + recordEvent({ + machineId, + eventType: "schedule_action_failed", + severity: "warning", + message: `Schedule « ${sched.name} » : ${(err as Error).message}`, + }); + } finally { + locked.delete(machineId); + } + } + }; + + const pool = Math.max(1, Math.min(sched.concurrency, machineIds.length || 1)); + await Promise.all(Array.from({ length: pool }, () => worker())); + + db.update(schema.schedules) + .set({ lastRunAt: new Date().toISOString(), lastStatus: errors ? `partial (${errors} err)` : "ok" }) + .where(eq(schema.schedules.id, id)) + .run(); + return { ran, errors }; +} + +// --------------------------------------------------------------------------- +// Enregistrement croner +// --------------------------------------------------------------------------- + +let jobs: Cron[] = []; + +export function reloadSchedules(): void { + for (const j of jobs) j.stop(); + jobs = []; + for (const s of listSchedules()) { + if (!s.enabled) continue; + try { + jobs.push( + new Cron(s.cron, { timezone: s.timezone ?? undefined, name: s.id }, () => { + runSchedule(s.id).catch((err) => console.error(`[scheduler] ${s.name}:`, (err as Error).message)); + }), + ); + } catch (err) { + console.error(`[scheduler] cron invalide pour ${s.name}:`, (err as Error).message); + } + } +} + +export function stopSchedules(): void { + for (const j of jobs) j.stop(); + jobs = []; +}