feat(probe): sonde enrichie CPU/RAM/disques + recommandations de profils (tâche 4)

- template machine-probe : lscpu Model name + nproc, MemTotal, lsblk disques
- parseProbe étendu (cpuModel/cpuCores/memoryBytes/disks) + buildRecommendations
  (KVM/QEMU → vm_guest_tools) ; tests TDD
- runProbe persiste cpu/mem/disks dans machine_hardware ; /probe renvoie recommendations
- popup Sonde affiche cpu/ram/disks + profils conseillés

tsc 0 · 110 tests · build OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 18:36:49 +02:00
parent c390addadb
commit e3e824185f
6 changed files with 123 additions and 14 deletions
+52 -12
View File
@@ -22,6 +22,10 @@ export interface ProbeResult {
isRpi: boolean;
gpus: string[];
net: { iface: string; addr: string }[];
cpuModel: string | null;
cpuCores: number | null;
memoryBytes: number | null;
disks: { name: string; sizeBytes: number }[];
}
export interface CorrectionProposal {
@@ -30,6 +34,11 @@ export interface CorrectionProposal {
virtualization: string;
}
export interface ProfileRecommendation {
profileId: string;
reason: string;
}
function section(raw: string, start: string, end?: string): string {
const i = raw.indexOf(start);
if (i < 0) return "";
@@ -51,7 +60,10 @@ export function parseProbe(raw: string): ProbeResult {
const prox = section(raw, "===SU:PROBE_PROXMOX===", "===SU:PROBE_RPI===");
const rpi = section(raw, "===SU:PROBE_RPI===", "===SU:PROBE_GPU===");
const gpuBlock = section(raw, "===SU:PROBE_GPU===", "===SU:PROBE_NET===");
const netBlock = section(raw, "===SU:PROBE_NET===", "===SU:EXIT=");
const netBlock = section(raw, "===SU:PROBE_NET===", "===SU:PROBE_CPU===");
const cpuBlock = section(raw, "===SU:PROBE_CPU===", "===SU:PROBE_MEM===");
const memBlock = section(raw, "===SU:PROBE_MEM===", "===SU:PROBE_DISK===");
const diskBlock = section(raw, "===SU:PROBE_DISK===", "===SU:EXIT=");
const gpus = gpuBlock
.split("\n")
@@ -66,6 +78,17 @@ export function parseProbe(raw: string): ProbeResult {
}
}
const cpuModelMatch = /^MODEL=(.+)$/m.exec(cpuBlock);
const coresMatch = /^\s*(\d+)\s*$/m.exec(cpuBlock);
const memMatch = /^MemTotal:\s+(\d+)\s*kB/m.exec(memBlock);
const disks: ProbeResult["disks"] = [];
for (const line of diskBlock.split("\n")) {
if (!line.startsWith("DISK\t")) continue;
const [, name, size] = line.split("\t");
if (name) disks.push({ name, sizeBytes: Number(size) || 0 });
}
return {
osId: osReleaseValue(os, "ID"),
osVersion: osReleaseValue(os, "VERSION_ID"),
@@ -77,9 +100,24 @@ export function parseProbe(raw: string): ProbeResult {
isRpi: /RPI=1/.test(rpi),
gpus,
net,
cpuModel: cpuModelMatch?.[1]?.trim() || null,
cpuCores: coresMatch?.[1] ? Number(coresMatch[1]) : null,
memoryBytes: memMatch?.[1] ? Number(memMatch[1]) * 1024 : null,
disks,
};
}
/** Recommandations de profils post-install déduites de la sonde. */
export function buildRecommendations(p: ProbeResult): ProfileRecommendation[] {
const recs: ProfileRecommendation[] = [];
if (p.virt === "kvm" || p.virt === "qemu") {
recs.push({ profileId: "vm_guest_tools", reason: "QEMU/KVM détecté → qemu-guest-agent" });
} else if (p.virt === "vmware") {
recs.push({ profileId: "vm_guest_tools", reason: "VMware détecté → open-vm-tools" });
}
return recs;
}
const VM_VIRTS = new Set(["kvm", "qemu", "vmware", "oracle", "microsoft", "xen", "bochs", "parallels"]);
const LXC_VIRTS = new Set(["lxc", "lxc-libvirt", "openvz", "systemd-nspawn", "docker", "podman"]);
@@ -112,6 +150,7 @@ export function proposeCorrections(p: ProbeResult): CorrectionProposal {
export interface ProbeOutcome {
probe: ProbeResult;
proposal: CorrectionProposal;
recommendations: ProfileRecommendation[];
raw: string;
changes: string[]; // diff entre l'actuel et la proposition (pour l'UI)
}
@@ -129,17 +168,18 @@ export async function runProbe(machineId: string, onData?: (c: string) => void):
const proposal = proposeCorrections(probe);
const now = new Date().toISOString();
const hwFields = {
cpuModel: probe.cpuModel,
cpuCores: probe.cpuCores,
memoryBytes: probe.memoryBytes,
disksJson: JSON.stringify(probe.disks),
gpusJson: JSON.stringify(probe.gpus),
networkJson: JSON.stringify(probe.net),
updatedAt: now,
};
db.insert(schema.machineHardware)
.values({
machineId,
gpusJson: JSON.stringify(probe.gpus),
networkJson: JSON.stringify(probe.net),
updatedAt: now,
})
.onConflictDoUpdate({
target: schema.machineHardware.machineId,
set: { gpusJson: JSON.stringify(probe.gpus), networkJson: JSON.stringify(probe.net), updatedAt: now },
})
.values({ machineId, ...hwFields })
.onConflictDoUpdate({ target: schema.machineHardware.machineId, set: hwFields })
.run();
const changes: string[] = [];
@@ -151,5 +191,5 @@ export async function runProbe(machineId: string, onData?: (c: string) => void):
changes.push(`virtualization: ${m.virtualization ?? "—"}${proposal.virtualization}`);
}
return { probe, proposal, raw, changes };
return { probe, proposal, recommendations: buildRecommendations(probe), raw, changes };
}