feat: socle BDD (tâche 1.9 Phase 1-2) + moteur APT (tâche 2 SJ-0→3) + WIP capabilities/auth/Rust

Checkpoint multi-chantiers (arbre vert : tsc 0 erreur, 70 tests, build OK).
- tâche 1.9 Phase 1 : schéma socle (machine_state/events/reports/raw_artifacts/
  hardware/metrics + colonnes étendues) + wiring refresh/execute. Migration 0002.
- tâche 1.9 Phase 2 : machine_credentials + machine_host_keys (non destructif,
  dual-read + backfill). Migration 0003. Fix séquence journal de migration.
- tâche 2 : SJ-0 (types étendus rétro-compatibles, réducteur Docker, resolveTemplate),
  SJ-1 (update-analyze enrichi), SJ-2 (apply + diff dpkg + timeout inactivité SSH),
  SJ-3 (reboot vérifié boot_id).
- WIP parallèle inclus : /api/capabilities, auth/apiTokens/apiClients, system metrics,
  scaffold app_rust, ajustements frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 19:50:25 +02:00
co-authored by Claude Opus 4.8
parent 0fbca06d3d
commit 08919752e3
69 changed files with 7785 additions and 102 deletions
+19 -1
View File
@@ -1,6 +1,6 @@
// server/templates/aptReduce.test.ts
import { describe, it, expect } from "vitest";
import { reduceAptLines } from "./aptReduce.js";
import { reduceAptLines, reduceLines } from "./aptReduce.js";
describe("reduceAptLines", () => {
it("ne garde que les lignes utiles", () => {
@@ -26,4 +26,22 @@ describe("reduceAptLines", () => {
it("retourne un tableau vide si rien d'utile", () => {
expect(reduceAptLines("Reading package lists...\nDone")).toEqual([]);
});
it("garde aussi les lignes Docker utiles", () => {
const raw = [
"Pulling jellyfin ...",
"Status: Downloaded newer image for jellyfin/jellyfin:latest",
"Recreating jellyfin ...",
"Started jellyfin",
"blabla inutile",
"Total reclaimed space: 1.2GB",
].join("\n");
expect(reduceLines(raw)).toEqual([
"Pulling jellyfin ...",
"Status: Downloaded newer image for jellyfin/jellyfin:latest",
"Recreating jellyfin ...",
"Started jellyfin",
"Total reclaimed space: 1.2GB",
]);
});
});
+15 -4
View File
@@ -1,11 +1,22 @@
// server/templates/aptReduce.ts
const PREFIXES = ["Inst ", "Conf ", "Remv ", "Err ", "E:", "W:", "dpkg:"];
const CONTAINS = ["reboot-required", "REBOOT_REQUIRED"];
const PREFIXES = [
// APT / dpkg (jalon 1)
"Inst ", "Conf ", "Remv ", "Err ", "E:", "W:", "dpkg:",
// Docker (SJ-0)
"Pulling", "Digest", "Status", "Downloaded newer image", "Recreating", "Started", "Error",
];
const CONTAINS = [
"reboot-required", "REBOOT_REQUIRED",
"deleted", "Total reclaimed space",
];
/** Garde uniquement les lignes informatives d'une sortie APT brute. */
export function reduceAptLines(raw: string): string[] {
/** Garde uniquement les lignes informatives (APT + Docker) d'une sortie brute. */
export function reduceLines(raw: string): string[] {
return raw
.split("\n")
.map((l) => l.trimEnd())
.filter((l) => PREFIXES.some((p) => l.startsWith(p)) || CONTAINS.some((c) => l.includes(c)));
}
/** Alias rétro-compatible (jalon 1) : même comportement, conserve les imports existants. */
export const reduceAptLines = reduceLines;
+6
View File
@@ -14,4 +14,10 @@ describe("renderTemplate", () => {
const out = renderTemplate("apt/check.sh.tpl", { aptProxy: "http://cache:3142" });
expect(out).toContain('http_proxy="http://cache:3142"');
});
it("rend update-analyze.sh.tpl avec les sections attendues", () => {
const out = renderTemplate("apt/update-analyze.sh.tpl", { aptProxy: null });
expect(out).toContain("===SU:APT_SIM_DISTUPGRADE===");
expect(out).toContain("apt-mark showhold");
});
});
+21 -1
View File
@@ -1,6 +1,6 @@
// server/templates/render.ts
import Mustache from "mustache";
import { readFileSync } from "node:fs";
import { readFileSync, existsSync } from "node:fs";
import { resolve } from "node:path";
const TEMPLATES_ROOT = resolve(process.cwd(), "templates");
@@ -14,3 +14,23 @@ export function renderTemplate(relPath: string, vars: TemplateVars): string {
// Mustache échappe le HTML par défaut; on désactive (ce sont des scripts shell).
return Mustache.render(tpl, vars, {}, { escape: (s) => s });
}
/** Existence par défaut d'un template relatif à templates/. */
function defaultExists(rel: string): boolean {
return existsSync(resolve(TEMPLATES_ROOT, rel));
}
/**
* Résout le chemin de template le plus spécifique pour (action, OS) :
* `<osFamily>/<action>.sh.tpl` s'il existe, sinon fallback base `apt/<action>.sh.tpl`.
* `exists` est injectable pour les tests.
*/
export function resolveTemplate(
action: string,
osFamily: string,
exists: (rel: string) => boolean = defaultExists,
): string {
const specific = `${osFamily}/${action}.sh.tpl`;
if (osFamily !== "unknown" && osFamily !== "apt" && exists(specific)) return specific;
return `apt/${action}.sh.tpl`;
}
+20
View File
@@ -0,0 +1,20 @@
import { describe, it, expect } from "vitest";
import { resolveTemplate } from "./render.js";
describe("resolveTemplate", () => {
it("retombe sur apt/ quand aucun dossier OS spécifique n'existe (fonction exists fournie)", () => {
const noneExist = () => false;
expect(resolveTemplate("full-upgrade", "proxmox", noneExist)).toBe("apt/full-upgrade.sh.tpl");
expect(resolveTemplate("update-analyze", "debian", noneExist)).toBe("apt/update-analyze.sh.tpl");
});
it("choisit le template OS spécifique quand il existe", () => {
const proxmoxExists = (rel: string) => rel === "proxmox/full-upgrade.sh.tpl";
expect(resolveTemplate("full-upgrade", "proxmox", proxmoxExists)).toBe("proxmox/full-upgrade.sh.tpl");
});
it("unknown retombe toujours sur apt/", () => {
const all = () => true;
expect(resolveTemplate("clean", "unknown", all)).toBe("apt/clean.sh.tpl");
});
});