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:
@@ -1,5 +1,6 @@
|
||||
// client/src/features/machines/AddMachineModal.tsx
|
||||
import { useState } from "react";
|
||||
import { api } from "../../lib/api.js";
|
||||
|
||||
interface Props { onClose: () => void; onCreated: () => void; }
|
||||
|
||||
@@ -12,11 +13,7 @@ export function AddMachineModal({ onClose, onCreated }: Props) {
|
||||
async function submit() {
|
||||
setBusy(true); setError(null);
|
||||
try {
|
||||
const res = await fetch("/api/machines", {
|
||||
method: "POST", headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ...form, port: Number(form.port), sudoPassword: form.sudoPassword || null }),
|
||||
});
|
||||
if (!res.ok) throw new Error((await res.json()).error ?? "Échec");
|
||||
await api.createMachine({ ...form, port: Number(form.port), sudoPassword: form.sudoPassword || null });
|
||||
onCreated(); onClose();
|
||||
} catch (e) { setError((e as Error).message); } finally { setBusy(false); }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// client/src/features/machines/MachineTile.tsx
|
||||
import type { MachineView } from "@shared/types.js";
|
||||
import { useState } from "react";
|
||||
import type { MachineStatus, MachineView } from "@shared/types.js";
|
||||
import { Button, Icon, IconButton, StatusLed } from "../../components/ui-kit.js";
|
||||
|
||||
interface Props {
|
||||
machine: MachineView;
|
||||
@@ -10,30 +12,195 @@ interface Props {
|
||||
onReboot: (id: string) => void;
|
||||
}
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
ok: "var(--ok)", updates_available: "var(--warn)", error: "var(--err)",
|
||||
running: "var(--info)", unknown: "var(--ink-4)",
|
||||
const STATUS_LED: Record<MachineStatus, "ok" | "warn" | "err" | "info" | "off"> = {
|
||||
ok: "ok",
|
||||
updates_available: "warn",
|
||||
error: "err",
|
||||
running: "info",
|
||||
unknown: "off",
|
||||
};
|
||||
|
||||
export function MachineTile({ machine, packageCount, onSelect, onRefresh, onUpgrade, onReboot }: Props) {
|
||||
const STATUS_TEXT: Record<MachineStatus, string> = {
|
||||
ok: "OK",
|
||||
updates_available: "Updates",
|
||||
error: "Erreur",
|
||||
running: "Action en cours",
|
||||
unknown: "Inconnu",
|
||||
};
|
||||
|
||||
export function MachineTile({
|
||||
machine,
|
||||
packageCount,
|
||||
onSelect,
|
||||
onRefresh,
|
||||
onUpgrade,
|
||||
onReboot,
|
||||
}: Props) {
|
||||
const [dockerOpen, setDockerOpen] = useState(false);
|
||||
const [postOpen, setPostOpen] = useState(false);
|
||||
const expanded = dockerOpen || postOpen;
|
||||
const isError = machine.status === "error" || machine.status === "unknown";
|
||||
|
||||
return (
|
||||
<div className="glass" style={{ padding: 16, borderRadius: 10 }} onClick={() => onSelect(machine.id)}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span style={{ width: 10, height: 10, borderRadius: 999, background: STATUS_COLOR[machine.status] }} />
|
||||
<strong>{machine.name}</strong>
|
||||
<article
|
||||
className={`machine-tile glass ${expanded ? "machine-tile-expanded" : ""}`}
|
||||
onClick={() => onSelect(machine.id)}
|
||||
>
|
||||
<header className="machine-tile-head">
|
||||
<div className="machine-title-row">
|
||||
<StatusLed status={STATUS_LED[machine.status]} size={10} pulse={machine.status === "running"} />
|
||||
<div className="machine-title-text">
|
||||
<strong>{machine.name}</strong>
|
||||
<span className="mono">{machine.hostname}:{machine.port} · {machine.osFamily}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`machine-status-pill ${isError ? "machine-status-danger" : ""}`}>
|
||||
{STATUS_TEXT[machine.status]}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div className="machine-summary">
|
||||
<Metric label="Updates" value={packageCount.toString()} tone={packageCount > 0 ? "warn" : "ok"} />
|
||||
<Metric label="Reboot" value="-" />
|
||||
<Metric label="Dernier check" value={formatDate(machine.lastCheckedAt)} />
|
||||
</div>
|
||||
<div className="mono" style={{ color: "var(--ink-3)", fontSize: 12 }}>
|
||||
{machine.hostname}:{machine.port} · {machine.osFamily}
|
||||
|
||||
{isError && (
|
||||
<div className="machine-alert">
|
||||
<Icon name="alert" size={14} style={undefined} />
|
||||
<span>État machine à vérifier avant toute action sensible.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="machine-actions" onClick={(event) => event.stopPropagation()}>
|
||||
<IconButton
|
||||
icon="refresh"
|
||||
label="Update + analyse"
|
||||
active={false}
|
||||
danger={false}
|
||||
primary={false}
|
||||
onClick={() => onRefresh(machine.id)}
|
||||
/>
|
||||
<IconButton
|
||||
icon="upgrade"
|
||||
label="Upgrade système"
|
||||
active={false}
|
||||
danger={false}
|
||||
primary={packageCount > 0}
|
||||
onClick={() => onUpgrade(machine.id)}
|
||||
/>
|
||||
<IconButton
|
||||
icon="power"
|
||||
label="Reboot"
|
||||
active={false}
|
||||
danger
|
||||
primary={false}
|
||||
onClick={() => onReboot(machine.id)}
|
||||
/>
|
||||
<IconButton
|
||||
icon="terminal"
|
||||
label="Ouvrir les logs machine"
|
||||
active={false}
|
||||
danger={false}
|
||||
primary={false}
|
||||
onClick={() => onSelect(machine.id)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ margin: "10px 0", fontSize: 13 }}>
|
||||
<span className="label">UPDATES</span>{" "}
|
||||
<span className="mono">{packageCount}</span>
|
||||
|
||||
<div className="machine-sections" onClick={(event) => event.stopPropagation()}>
|
||||
<SectionToggle
|
||||
icon="docker"
|
||||
title="Docker"
|
||||
open={dockerOpen}
|
||||
onToggle={() => setDockerOpen((value) => !value)}
|
||||
/>
|
||||
{dockerOpen && <DockerSection />}
|
||||
|
||||
<SectionToggle
|
||||
icon="script"
|
||||
title="Post-install"
|
||||
open={postOpen}
|
||||
onToggle={() => setPostOpen((value) => !value)}
|
||||
/>
|
||||
{postOpen && <PostInstallSection />}
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }} onClick={(e) => e.stopPropagation()}>
|
||||
<button className="interactive" onClick={() => onRefresh(machine.id)}>Refresh</button>
|
||||
<button className="interactive" onClick={() => onUpgrade(machine.id)}>Upgrade</button>
|
||||
<button className="interactive" onClick={() => onReboot(machine.id)}>Reboot</button>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function Metric({ label, value, tone }: { label: string; value: string; tone?: "ok" | "warn" }) {
|
||||
return (
|
||||
<div className="machine-metric">
|
||||
<span className="label">{label}</span>
|
||||
<span className={`mono ${tone === "warn" ? "machine-metric-warn" : tone === "ok" ? "machine-metric-ok" : ""}`}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionToggle({
|
||||
icon,
|
||||
title,
|
||||
open,
|
||||
onToggle,
|
||||
}: {
|
||||
icon: string;
|
||||
title: string;
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button className="machine-section-toggle interactive" onClick={onToggle}>
|
||||
<span className="machine-section-title">
|
||||
<Icon name={icon} size={14} style={undefined} />
|
||||
<span>{title}</span>
|
||||
</span>
|
||||
<Icon name={open ? "chevD" : "chevR"} size={12} style={undefined} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function DockerSection() {
|
||||
return (
|
||||
<div className="machine-section-body">
|
||||
<div className="machine-section-row">
|
||||
<span className="mono">Docker non scanné</span>
|
||||
<Button icon="cog" size="sm" onClick={() => undefined}>Paramètres</Button>
|
||||
</div>
|
||||
<div className="machine-placeholder">
|
||||
Roots compose, stacks, upgrades image et prune seront affichés ici dès que le backend Docker sera disponible.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PostInstallSection() {
|
||||
return (
|
||||
<div className="machine-section-body">
|
||||
<label className="machine-check-row">
|
||||
<input type="checkbox" />
|
||||
<span>Profil network tools</span>
|
||||
</label>
|
||||
<label className="machine-check-row">
|
||||
<input type="checkbox" />
|
||||
<span>Profil partage Samba/NFS</span>
|
||||
</label>
|
||||
<div className="machine-placeholder">
|
||||
Les champs dynamiques seront dépliés ici selon les profils sélectionnés.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatDate(value: string | null): string {
|
||||
if (!value) return "-";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return "-";
|
||||
return date.toLocaleString("fr-FR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user