feat: VM-Liste pro Projekt (Desktop-Panel) — Start/Stop/Verbinden

- Brain: project_vms.py (Registry /shared/config/project_vms.json pro Projekt) +
  Endpoints GET/POST/DELETE /projects/<id>/vms + boot/stop (via SSH aria-wohnung
  aria-vm auf dem Host; Status aus `aria-vm list`).
- ARIA-Tools vm_register/vm_list (aufs Request-Projekt) + Seed-Regel: nach dem
  VM-Bau registrieren, damit sie in Stefans Desktop-Panel auftaucht.
- App: brainApi VM-Methoden + ProjectVm-Typ. Neues DesktopTile — pro Projekt die
  VM-Liste (leer bis registriert), Start/Stop/Verbinden; Verbinden oeffnet noVNC
  (VncTile mit dem VNC-Port der VM). Deck rendert DesktopTile statt VncTile.

py/tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 23:25:55 +02:00
co-authored by Claude Opus 4.8
parent 5fa5d79ad8
commit 0efb8d0848
8 changed files with 429 additions and 5 deletions
+29
View File
@@ -176,6 +176,18 @@ export interface ProjectStatus {
projects: Project[];
}
/** QEMU-VM eines Projekts (Registry + Live-Status). */
export interface ProjectVm {
name: string;
arch: string;
iso?: string;
vnc_display: number;
mem: number;
running?: boolean;
vnc_port?: number;
created_at?: number;
}
/** Queue-Status pro Kontext — was gerade arbeitet, was wartet.
* Key "__main__" = Hauptchat, sonst project_id. */
export interface QueueContextStatus {
@@ -626,6 +638,23 @@ export const brainApi = {
return _send(`/projects/${encodeURIComponent(projectId)}/file?path=${encodeURIComponent(path)}`);
},
// ── QEMU-VMs pro Projekt ─────────────────────────────────────────
listProjectVms(projectId: string): Promise<{ projectId: string; vms: ProjectVm[] }> {
return _send(`/projects/${encodeURIComponent(projectId)}/vms`, { timeoutMs: 20000 });
},
addProjectVm(projectId: string, body: { name: string; arch?: string; iso?: string; vnc_display?: number; mem?: number; create_disk?: boolean; size?: string }): Promise<ProjectVm> {
return _send(`/projects/${encodeURIComponent(projectId)}/vms`, { method: 'POST', body, timeoutMs: 30000 });
},
removeProjectVm(projectId: string, name: string, purge = false): Promise<{ ok: boolean }> {
return _send(`/projects/${encodeURIComponent(projectId)}/vms/${encodeURIComponent(name)}?purge=${purge ? 'true' : 'false'}`, { method: 'DELETE' });
},
bootProjectVm(projectId: string, name: string): Promise<{ ok: boolean; vnc_port: number; output: string }> {
return _send(`/projects/${encodeURIComponent(projectId)}/vms/${encodeURIComponent(name)}/boot`, { method: 'POST', timeoutMs: 45000 });
},
stopProjectVm(projectId: string, name: string): Promise<{ ok: boolean; output: string }> {
return _send(`/projects/${encodeURIComponent(projectId)}/vms/${encodeURIComponent(name)}/stop`, { method: 'POST', timeoutMs: 30000 });
},
/** Projekt verstecken / wieder sichtbar machen (bleibt voll nutzbar). */
setProjectHidden(projectId: string, hidden: boolean): Promise<Project> {
return _send(`/projects/${encodeURIComponent(projectId)}`, {