diff --git a/client/index.html b/client/index.html index cb4f111..2970852 100644 --- a/client/index.html +++ b/client/index.html @@ -4,6 +4,12 @@ System Update + + + + + +
diff --git a/client/public/apple-touch-icon.png b/client/public/apple-touch-icon.png new file mode 100644 index 0000000..6ee6466 Binary files /dev/null and b/client/public/apple-touch-icon.png differ diff --git a/client/public/favicon.ico b/client/public/favicon.ico new file mode 100644 index 0000000..fd8641f Binary files /dev/null and b/client/public/favicon.ico differ diff --git a/client/public/favicon.svg b/client/public/favicon.svg new file mode 100644 index 0000000..cc9a682 --- /dev/null +++ b/client/public/favicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/client/public/site.webmanifest b/client/public/site.webmanifest new file mode 100644 index 0000000..ad5b192 --- /dev/null +++ b/client/public/site.webmanifest @@ -0,0 +1,14 @@ +{ + "name": "System Update", + "short_name": "SysUpdate", + "description": "Dashboard de mise à jour distante de machines Linux (SSH agentless).", + "start_url": "/", + "display": "standalone", + "background_color": "#2a231d", + "theme_color": "#fe8019", + "icons": [ + { "src": "/favicon.svg", "type": "image/svg+xml", "sizes": "any", "purpose": "any" }, + { "src": "/web-app-manifest-192x192.png", "type": "image/png", "sizes": "192x192", "purpose": "any maskable" }, + { "src": "/web-app-manifest-512x512.png", "type": "image/png", "sizes": "512x512", "purpose": "any maskable" } + ] +} diff --git a/client/public/web-app-manifest-192x192.png b/client/public/web-app-manifest-192x192.png new file mode 100644 index 0000000..6311549 Binary files /dev/null and b/client/public/web-app-manifest-192x192.png differ diff --git a/client/public/web-app-manifest-512x512.png b/client/public/web-app-manifest-512x512.png new file mode 100644 index 0000000..640274d Binary files /dev/null and b/client/public/web-app-manifest-512x512.png differ diff --git a/client/src/features/machines/AddMachineModal.tsx b/client/src/features/machines/AddMachineModal.tsx index 8de0713..3250e1e 100644 --- a/client/src/features/machines/AddMachineModal.tsx +++ b/client/src/features/machines/AddMachineModal.tsx @@ -1,12 +1,33 @@ // client/src/features/machines/AddMachineModal.tsx import { useEffect, useState } from "react"; +import type { AptProxyMode, MachineKind, OsFamily } from "@shared/types.js"; import type { DefaultAptProxy } from "../../lib/api.js"; import { api } from "../../lib/api.js"; interface Props { onClose: () => void; onCreated: () => void; } +const OS_OPTIONS: { value: OsFamily; label: string }[] = [ + { value: "debian", label: "Debian" }, + { value: "ubuntu", label: "Ubuntu" }, + { value: "proxmox", label: "Proxmox VE" }, + { value: "raspbian", label: "Raspberry Pi OS" }, + { value: "unknown", label: "Autre / auto" }, +]; +const KIND_OPTIONS: { value: MachineKind; label: string }[] = [ + { value: "vm", label: "VM" }, + { value: "physical", label: "Physique" }, + { value: "proxmox_host", label: "Hôte Proxmox" }, + { value: "lxc", label: "LXC / conteneur" }, + { value: "raspberry_pi", label: "Raspberry Pi" }, + { value: "workstation", label: "Workstation / GPU" }, + { value: "unknown", label: "Inconnu" }, +]; + export function AddMachineModal({ onClose, onCreated }: Props) { - const [form, setForm] = useState({ name: "", hostname: "", port: 22, username: "", password: "", sudoPassword: "" }); + const [form, setForm] = useState({ + name: "", hostname: "", port: 22, username: "", password: "", sudoPassword: "", + osFamily: "debian" as OsFamily, machineKind: "vm" as MachineKind, + }); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const [proxyDefault, setProxyDefault] = useState(null); @@ -46,6 +67,18 @@ export function AddMachineModal({ onClose, onCreated }: Props) { set(k, e.target.value)} /> ))} set("port", e.target.value)} /> + + set("password", e.target.value)} /> set("sudoPassword", e.target.value)} /> {proxyDefault?.url && ( @@ -54,6 +87,9 @@ export function AddMachineModal({ onClose, onCreated }: Props) { Proxy APT par défaut {proxyDefault.url} )} +
+ « Autre / auto » détecte l'OS via os-release. Détection complète (type, virt) ensuite via ⚙ Sonder. +
{error &&
{error}
}
diff --git a/client/src/features/machines/MachineTile.tsx b/client/src/features/machines/MachineTile.tsx index 0167e6d..151b1aa 100644 --- a/client/src/features/machines/MachineTile.tsx +++ b/client/src/features/machines/MachineTile.tsx @@ -6,6 +6,7 @@ import { api, type DockerSettingsView, type DockerStackRow, + type MachineHardwareView, type ProbeResultView, type ProfileManifestView, type ProfileValues, @@ -49,8 +50,9 @@ export function MachineTile({ }: Props) { const [dockerOpen, setDockerOpen] = useState(false); const [postOpen, setPostOpen] = useState(false); + const [hwOpen, setHwOpen] = useState(false); const [configOpen, setConfigOpen] = useState(false); - const expanded = dockerOpen || postOpen; + const expanded = dockerOpen || postOpen || hwOpen; const isError = machine.status === "error" || machine.status === "unknown"; return ( @@ -154,6 +156,14 @@ export function MachineTile({ onToggle={() => setPostOpen((value) => !value)} /> {postOpen && } + + setHwOpen((value) => !value)} + /> + {hwOpen && }
); @@ -776,6 +786,48 @@ function PostInstallSection({ machine, onSelect }: { machine: MachineView; onSel ); } +function HardwareSection({ machineId }: { machineId: string }) { + const [hw, setHw] = useState(null); + const [err, setErr] = useState(null); + useEffect(() => { + void (async () => { + try { + setHw(await api.machineHardware(machineId)); + } catch (e) { + setErr((e as Error).message); + } + })(); + }, [machineId]); + + if (err) return
{err}
; + if (!hw) return
Chargement…
; + + return ( +
+
+ + + + + +
+ {hw.gpus.length > 0 && ( +
+ GPU + {hw.gpus.map((g, i) => {g})} +
+ )} + {hw.network.length > 0 && ( +
+ Réseau + {hw.network.map((n, i) => )} +
+ )} + {!hw.probed && Données limitées — lance ⚙ Sonder pour détecter GPU/réseau.} +
+ ); +} + function ProfileFieldInput({ field, value, @@ -888,6 +940,7 @@ export function MachineDetailPanel({ // Mode liste : sections dépliées par défaut (inverse du mode tuile). const [dockerOpen, setDockerOpen] = useState(true); const [postOpen, setPostOpen] = useState(true); + const [hwOpen, setHwOpen] = useState(true); const [configOpen, setConfigOpen] = useState(false); const isError = machine.status === "error" || machine.status === "unknown"; @@ -936,6 +989,8 @@ export function MachineDetailPanel({ {dockerOpen && } setPostOpen((v) => !v)} /> {postOpen && } + setHwOpen((v) => !v)} /> + {hwOpen && } {configOpen && ( diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index 467b22d..112430d 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -59,6 +59,7 @@ export const api = { updateMachine: (id: string, body: UpdateMachineBody) => req(`/machines/${id}`, { method: "PATCH", body: JSON.stringify(body) }), probe: (id: string) => req(`/machines/${id}/probe`, { method: "POST" }), + machineHardware: (id: string) => req(`/machines/${id}/hardware`), // --- Docker --- dockerSettings: (id: string) => req(`/machines/${id}/docker/settings`), @@ -182,6 +183,17 @@ export interface UpdateMachineBody { aptProxyUrl?: string | null; } +export interface MachineHardwareView { + osFamily: string; + osVersion: string | null; + arch: string | null; + machineKind: string | null; + virtualization: string | null; + gpus: string[]; + network: { iface: string; addr: string }[]; + probed: boolean; +} + export interface ProbeResultView { probe: { osId: string | null; diff --git a/server/routes/machines.ts b/server/routes/machines.ts index 3059232..4e5495c 100644 --- a/server/routes/machines.ts +++ b/server/routes/machines.ts @@ -2,6 +2,7 @@ import { Hono } from "hono"; import { listMachines, createMachine, deleteMachine, updateMachine, getMachineRow, getCreds, testConnection, + getMachineHardware, type CreateMachineInput, type UpdateMachineInput, } from "../services/machines.js"; import { refreshMachine, getLatestSnapshot } from "../services/refresh.js"; @@ -53,6 +54,14 @@ machinesRoutes.patch("/:id", async (c) => { } }); +machinesRoutes.get("/:id/hardware", (c) => { + try { + return c.json(getMachineHardware(c.req.param("id"))); + } catch (err) { + return c.json({ error: (err as Error).message }, 404); + } +}); + // Sonde synchrone (lecture seule) : renvoie faits + proposition de correction. machinesRoutes.post("/:id/probe", async (c) => { try { diff --git a/server/services/machines.ts b/server/services/machines.ts index c7bb693..9f98c40 100644 --- a/server/services/machines.ts +++ b/server/services/machines.ts @@ -17,6 +17,8 @@ export interface CreateMachineInput { sudoPassword?: string | null; aptProxyMode?: AptProxyMode; aptProxyUrl?: string | null; + osFamily?: OsFamily; // choix manuel ; sinon auto-détecté via os-release + machineKind?: MachineKind; } type MachineRow = typeof schema.machines.$inferSelect; @@ -118,11 +120,11 @@ export async function createMachine(input: CreateMachineInput): Promise auto osVersion: os.version || null, osCodename: null, arch: null, - machineKind: null, + machineKind: input.machineKind ?? null, virtualization: null, hardwareProfile: null, username: input.username, @@ -146,6 +148,26 @@ export function deleteMachine(id: string): void { db.delete(schema.machines).where(eq(schema.machines.id, id)).run(); } +/** Faits matériels d'une machine (machine_hardware rempli par machine_probe + colonnes machines). */ +export function getMachineHardware(id: string) { + const m = getMachineRow(id); + if (!m) throw new Error("Machine introuvable"); + const hw = db.select().from(schema.machineHardware).where(eq(schema.machineHardware.machineId, id)).get(); + const parse = (j: string | null | undefined): T[] => { + try { return j ? (JSON.parse(j) as T[]) : []; } catch { return []; } + }; + return { + osFamily: m.osFamily, + osVersion: m.osVersion, + arch: m.arch, + machineKind: m.machineKind, + virtualization: m.virtualization, + gpus: parse(hw?.gpusJson), + network: parse<{ iface: string; addr: string }>(hw?.networkJson), + probed: !!hw, + }; +} + /** Applique un proxy APT à toutes les machines. Renvoie le nombre de machines modifiées. */ export function applyProxyToAllMachines(mode: AptProxyMode, url: string | null): number { const res = db