// server/services/refresh.ts import { randomUUID } from "node:crypto"; import { eq, desc } from "drizzle-orm"; import { db, schema } from "../db/client.js"; import { getMachineRow, getCreds } from "./machines.js"; import { renderTemplate, resolveTemplate } from "../templates/render.js"; import { reduceAptLines } from "../templates/aptReduce.js"; import { runScriptSudo } from "../ssh/client.js"; import { buildAptSnapshotDetail } from "./aptParse.js"; import { outputHub } from "../ws/outputHub.js"; import { deriveAptState, upsertMachineState, recordEvent } from "./machineState.js"; import { extractImportantMessages, recordImportantMessages } from "./importantMessages.js"; import type { UpdateSnapshot, MachineStatus, AptSnapshotDetail } from "@shared/types.js"; /** Extrait la section entre deux marqueurs ===SU:X=== d'une sortie de script. */ export function extractSection(raw: string, start: string, end: string): string { const s = raw.indexOf(start); if (s === -1) return ""; const from = s + start.length; const e = raw.indexOf(end, from); return raw.slice(from, e === -1 ? undefined : e).trim(); } export async function refreshMachine(machineId: string): Promise { const m = getMachineRow(machineId); if (!m) throw new Error("Machine introuvable"); db.update(schema.machines).set({ status: "running" }).where(eq(schema.machines.id, machineId)).run(); outputHub.clear(machineId); const proxy = m.aptProxyMode === "runtime" ? m.aptProxyUrl : null; const script = renderTemplate(resolveTemplate("update-analyze", m.osFamily), { aptProxy: proxy }); let raw = ""; try { const res = await runScriptSudo(getCreds(m), script, (c) => { raw += c; outputHub.publish(machineId, c); }); raw = res.stdout; } catch (err) { db.update(schema.machines).set({ status: "error" }).where(eq(schema.machines.id, machineId)).run(); throw err; } const updateExit = /===SU:EXIT=(\d+)===/.exec(raw); const detail: AptSnapshotDetail = buildAptSnapshotDetail({ upgradeSim: extractSection(raw, "===SU:APT_SIM_UPGRADE===", "===SU:APT_SIM_DISTUPGRADE==="), distUpgradeSim: extractSection(raw, "===SU:APT_SIM_DISTUPGRADE===", "===SU:APT_HELD==="), heldRaw: extractSection(raw, "===SU:APT_HELD===", "===SU:REBOOT==="), rebootRaw: extractSection(raw, "===SU:REBOOT===", "===SU:EXIT"), updateFailed: updateExit ? Number(updateExit[1]) !== 0 : false, }); // MachineStatus n'a pas "warning" : warning => updates_available côté machine. const status: MachineStatus = detail.status === "error" ? "error" : detail.count > 0 || detail.status === "warning" ? "updates_available" : "ok"; const checkedAt = new Date().toISOString(); const snapshot: UpdateSnapshot = { machineId, hostname: m.hostname, os: { family: m.osFamily as UpdateSnapshot["os"]["family"], version: m.osVersion ?? "" }, checkedAt, status, apt: detail, schemaVersion: 1, kind: "apt_update_analyze", rawHints: { logImportantLines: reduceAptLines(raw) }, }; const snapshotId = randomUUID(); db.insert(schema.snapshots).values({ id: snapshotId, machineId, kind: "apt_update_analyze", schemaVersion: 1, checkedAt, status, payloadJson: JSON.stringify(snapshot), importantJson: JSON.stringify(snapshot.rawHints?.logImportantLines ?? []), }).run(); db.update(schema.machines).set({ status, lastCheckedAt: checkedAt }).where(eq(schema.machines.id, machineId)).run(); upsertMachineState(machineId, deriveAptState(snapshot)); recordImportantMessages(machineId, extractImportantMessages(raw, "apt"), { snapshotId }); recordEvent({ machineId, eventType: "apt_refresh", severity: "info", snapshotId, message: `Refresh APT : ${snapshot.apt.count} mise(s) à jour`, }); return snapshot; } export function getLatestSnapshot(machineId: string): UpdateSnapshot | null { const row = db .select() .from(schema.snapshots) .where(eq(schema.snapshots.machineId, machineId)) .orderBy(desc(schema.snapshots.checkedAt)) .get(); return row ? (JSON.parse(row.payloadJson) as UpdateSnapshot) : null; }