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[]; 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. /** Queue-Status pro Kontext — was gerade arbeitet, was wartet.
* Key "__main__" = Hauptchat, sonst project_id. */ * Key "__main__" = Hauptchat, sonst project_id. */
export interface QueueContextStatus { export interface QueueContextStatus {
@@ -626,6 +638,23 @@ export const brainApi = {
return _send(`/projects/${encodeURIComponent(projectId)}/file?path=${encodeURIComponent(path)}`); 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). */ /** Projekt verstecken / wieder sichtbar machen (bleibt voll nutzbar). */
setProjectHidden(projectId: string, hidden: boolean): Promise<Project> { setProjectHidden(projectId: string, hidden: boolean): Promise<Project> {
return _send(`/projects/${encodeURIComponent(projectId)}`, { return _send(`/projects/${encodeURIComponent(projectId)}`, {
+2 -2
View File
@@ -16,7 +16,7 @@ import { useWorkspaceLayout } from './useWorkspaceLayout';
import WorkspaceDock from './WorkspaceDock'; import WorkspaceDock from './WorkspaceDock';
import ChatTile from './tiles/ChatTile'; import ChatTile from './tiles/ChatTile';
import CodeEditorTile from './tiles/CodeEditorTile'; import CodeEditorTile from './tiles/CodeEditorTile';
import VncTile from './tiles/VncTile'; import DesktopTile from './tiles/DesktopTile';
interface Props { interface Props {
projectId: string; projectId: string;
@@ -58,7 +58,7 @@ const WorkspaceDeck: React.FC<Props> = ({ projectId, panels, badges }) => {
switch (id) { switch (id) {
case 'chat': return <ChatTile />; case 'chat': return <ChatTile />;
case 'editor': return <CodeEditorTile projectId={projectId} />; case 'editor': return <CodeEditorTile projectId={projectId} />;
case 'vnc': return <VncTile projectId={projectId} focused={active === 'vnc'} />; case 'vnc': return <DesktopTile projectId={projectId} focused={active === 'vnc'} />;
default: return null; default: return null;
} }
}; };
+154
View File
@@ -0,0 +1,154 @@
/**
* DesktopTile — das Desktop-Panel eines Code-Projekts.
*
* Zeigt die (pro Projekt gefuehrte) QEMU-VM-Liste: leer, bis ARIA per
* vm_register eine VM eintraegt. Pro VM: Start / Stop / Verbinden. „Verbinden"
* oeffnet die noVNC-Ansicht (VncTile) fuer den VNC-Port dieser VM.
*/
import React, { useCallback, useEffect, useState } from 'react';
import { ActivityIndicator, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import brainApi, { ProjectVm } from '../../services/brainApi';
import VncTile from './VncTile';
interface Props {
projectId: string;
focused: boolean;
}
const DesktopTile: React.FC<Props> = ({ projectId, focused }) => {
const [vms, setVms] = useState<ProjectVm[]>([]);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState('');
const [busy, setBusy] = useState(''); // VM-Name, der gerade bootet/stoppt
const [connected, setConnected] = useState<ProjectVm | null>(null);
const load = useCallback(() => {
setLoading(true); setErr('');
brainApi.listProjectVms(projectId)
.then(r => setVms(r.vms || []))
.catch(e => setErr(String(e?.message || e)))
.finally(() => setLoading(false));
}, [projectId]);
useEffect(() => {
if (focused && !connected) load();
}, [focused, projectId, connected, load]);
const boot = useCallback((vm: ProjectVm) => {
setBusy(vm.name);
brainApi.bootProjectVm(projectId, vm.name)
.then(() => load())
.catch(e => setErr(String(e?.message || e)))
.finally(() => setBusy(''));
}, [projectId, load]);
const stop = useCallback((vm: ProjectVm) => {
setBusy(vm.name);
brainApi.stopProjectVm(projectId, vm.name)
.then(() => load())
.catch(e => setErr(String(e?.message || e)))
.finally(() => setBusy(''));
}, [projectId, load]);
if (!focused) {
return (
<View style={styles.placeholder}>
<Text style={styles.icon}>🖥</Text>
<Text style={styles.text}>Desktop</Text>
<Text style={styles.sub}>Panel öffnen für VM-Liste</Text>
</View>
);
}
// Verbunden → noVNC-Ansicht der VM + Zurück-Leiste.
if (connected) {
return (
<View style={styles.container}>
<View style={styles.bar}>
<TouchableOpacity onPress={() => setConnected(null)} style={styles.barBtn}>
<Text style={styles.barBtnText}> VMs</Text>
</TouchableOpacity>
<Text style={styles.barTitle} numberOfLines={1}>{connected.name} · :{connected.vnc_display}</Text>
</View>
<VncTile projectId={projectId} focused port={connected.vnc_port || (5900 + (connected.vnc_display || 1))} />
</View>
);
}
return (
<View style={styles.container}>
<View style={styles.bar}>
<Text style={styles.barTitle}>Virtuelle Maschinen</Text>
<TouchableOpacity onPress={load} style={styles.barBtn}><Text style={styles.barBtnText}></Text></TouchableOpacity>
</View>
<ScrollView contentContainerStyle={{ padding: 12 }}>
{loading && vms.length === 0 ? (
<ActivityIndicator color="#0096FF" style={{ marginTop: 20 }} />
) : err ? (
<Text style={styles.err}>{err}</Text>
) : vms.length === 0 ? (
<Text style={styles.empty}>
Noch keine VM in diesem Projekt.{'\n'}
Sag ARIA z.B. bau eine QEMU-VM zum Testen" — sie registriert sie hier,
dann kannst du sie starten und verbinden.
</Text>
) : (
vms.map(vm => {
const isBusy = busy === vm.name;
return (
<View key={vm.name} style={styles.vmRow}>
<View style={{ flex: 1 }}>
<Text style={styles.vmName}>
<Text style={{ color: vm.running ? '#34C759' : '#555570' }}>●</Text> {vm.name}
</Text>
<Text style={styles.vmMeta}>{vm.arch} · Display :{vm.vnc_display} · {vm.running ? 'läuft' : 'gestoppt'}</Text>
</View>
<View style={styles.vmBtns}>
{isBusy ? (
<ActivityIndicator color="#0096FF" />
) : vm.running ? (
<>
<TouchableOpacity onPress={() => setConnected(vm)} style={[styles.vmBtn, { borderColor: '#0096FF' }]}>
<Text style={[styles.vmBtnText, { color: '#0096FF' }]}>Verbinden</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => stop(vm)} style={[styles.vmBtn, { borderColor: '#E55C5C' }]}>
<Text style={[styles.vmBtnText, { color: '#E55C5C' }]}>Stop</Text>
</TouchableOpacity>
</>
) : (
<TouchableOpacity onPress={() => boot(vm)} style={[styles.vmBtn, { borderColor: '#34C759' }]}>
<Text style={[styles.vmBtnText, { color: '#34C759' }]}>Start</Text>
</TouchableOpacity>
)}
</View>
</View>
);
})
)}
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0D0D1A' },
placeholder: { flex: 1, backgroundColor: '#000', alignItems: 'center', justifyContent: 'center' },
icon: { fontSize: 64, marginBottom: 16 },
text: { color: '#FFFFFF', fontSize: 18, fontWeight: '700' },
sub: { color: '#9090B0', fontSize: 14, marginTop: 8 },
bar: { height: 40, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, backgroundColor: '#12122A', borderBottomColor: '#1E1E2E', borderBottomWidth: 1 },
barTitle: { color: '#E0E0F0', fontSize: 14, fontWeight: '700', flex: 1 },
barBtn: { paddingHorizontal: 10, paddingVertical: 4 },
barBtnText: { color: '#0096FF', fontSize: 14, fontWeight: '700' },
empty: { color: '#8888AA', fontSize: 13, lineHeight: 20, textAlign: 'center', marginTop: 24 },
err: { color: '#FF6E6E', fontSize: 13, marginTop: 16 },
vmRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#12122A', borderRadius: 10, padding: 12, marginBottom: 8 },
vmName: { color: '#E0E0F0', fontSize: 15, fontWeight: '700' },
vmMeta: { color: '#8888AA', fontSize: 12, marginTop: 3 },
vmBtns: { flexDirection: 'row', gap: 6, alignItems: 'center' },
vmBtn: { borderWidth: 1, borderRadius: 8, paddingHorizontal: 10, paddingVertical: 6 },
vmBtnText: { fontSize: 12, fontWeight: '700' },
});
export default DesktopTile;
+4 -3
View File
@@ -16,9 +16,10 @@ import { NOVNC_HTML } from '../assets/novncHtml';
interface Props { interface Props {
projectId: string; projectId: string;
focused: boolean; focused: boolean;
port?: number; // VNC-Port der zu verbindenden VM (Default 5901 = Display :1)
} }
const VncTile: React.FC<Props> = ({ projectId, focused }) => { const VncTile: React.FC<Props> = ({ projectId, focused, port = 5901 }) => {
const webRef = useRef<WebView>(null); const webRef = useRef<WebView>(null);
const [status, setStatus] = useState<'idle' | 'connecting' | 'connected' | 'disconnected'>('idle'); const [status, setStatus] = useState<'idle' | 'connecting' | 'connected' | 'disconnected'>('idle');
const unsubDataRef = useRef<null | (() => void)>(null); const unsubDataRef = useRef<null | (() => void)>(null);
@@ -46,7 +47,7 @@ const VncTile: React.FC<Props> = ({ projectId, focused }) => {
const js = `window.ariaVnc && window.ariaVnc.onData(${JSON.stringify(b64)}); true;`; const js = `window.ariaVnc && window.ariaVnc.onData(${JSON.stringify(b64)}); true;`;
webRef.current?.injectJavaScript(js); webRef.current?.injectJavaScript(js);
}); });
desktop.openVnc(projectId); desktop.openVnc(projectId, port);
} else if (m.event === 'vnc_send') { } else if (m.event === 'vnc_send') {
desktop.sendInput(m.b64); desktop.sendInput(m.b64);
} else if (m.event === 'vnc_close') { } else if (m.event === 'vnc_close') {
@@ -55,7 +56,7 @@ const VncTile: React.FC<Props> = ({ projectId, focused }) => {
if (m.state === 'connected') setStatus('connected'); if (m.state === 'connected') setStatus('connected');
else if (m.state === 'disconnected') setStatus('disconnected'); else if (m.state === 'disconnected') setStatus('disconnected');
} }
}, [projectId]); }, [projectId, port]);
if (!focused) { if (!focused) {
return ( return (
+56
View File
@@ -37,6 +37,7 @@ import triggers as triggers_mod
import watcher as watcher_mod import watcher as watcher_mod
import oauth as oauth_mod import oauth as oauth_mod
import projects as projects_mod import projects as projects_mod
import project_vms as project_vms_mod
BRIDGE_URL = os.environ.get("BRIDGE_URL", "http://aria-bridge:8090") BRIDGE_URL = os.environ.get("BRIDGE_URL", "http://aria-bridge:8090")
# SearXNG (self-hosted Meta-Suche) — Backend fuers web_search-Tool (B1b). # SearXNG (self-hosted Meta-Suche) — Backend fuers web_search-Tool (B1b).
@@ -1161,6 +1162,38 @@ META_TOOLS = [
}, },
}, },
}, },
{
"type": "function",
"function": {
"name": "vm_register",
"description": (
"Traegt eine QEMU-VM in die VM-Liste des AKTUELLEN Projekts ein, "
"damit sie in Stefans Desktop-Panel (Cockpit) erscheint und er sie "
"starten/stoppen/verbinden kann. Rufe das auf, NACHDEM Du mit aria-vm "
"eine VM gebaut/gebootet hast (z.B. bei einem OS-Bau-Projekt). "
"vnc_display bestimmt den VNC-Port (Port = 5900+display)."
),
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "VM-Name wie bei aria-vm (a-z0-9_-)."},
"arch": {"type": "string", "description": "z.B. i386, x86_64, aarch64, mips."},
"iso": {"type": "string", "description": "optional: Boot-ISO-Pfad auf dem Host."},
"vnc_display": {"type": "integer", "description": "VNC-Display (Default 1 → Port 5901)."},
"mem": {"type": "integer", "description": "RAM in MB (Default 1024)."},
},
"required": ["name", "arch"],
},
},
},
{
"type": "function",
"function": {
"name": "vm_list",
"description": "Listet die registrierten QEMU-VMs des aktuellen Projekts (mit Lauf-Status).",
"parameters": {"type": "object", "properties": {}},
},
},
] ]
@@ -2797,6 +2830,29 @@ class Agent:
return f"OK — Projekt '{updated['name']}' ist wieder ein normaler Chat." return f"OK — Projekt '{updated['name']}' ist wieder ein normaler Chat."
if name in ("satellite_list", "satellite_devices", "satellite_command"): if name in ("satellite_list", "satellite_devices", "satellite_command"):
return self._dispatch_satellite(name, arguments) return self._dispatch_satellite(name, arguments)
if name == "vm_register":
pid = (project_id or "").strip()
if not pid:
return "Kein aktives Projekt — VMs werden pro Projekt gefuehrt. Erst ein Projekt betreten."
try:
vm = project_vms_mod.add_vm(
pid, (arguments.get("name") or "").strip(),
(arguments.get("arch") or "i386").strip(),
(arguments.get("iso") or "").strip(),
int(arguments.get("vnc_display") or 1),
int(arguments.get("mem") or 1024),
)
except (ValueError, TypeError) as exc:
return f"FEHLER: {exc}"
return (f"OK — VM '{vm['name']}' ({vm['arch']}, Display :{vm['vnc_display']}) "
f"in Projekt '{pid}' registriert. Erscheint in Stefans Desktop-Panel.")
if name == "vm_list":
pid = (project_id or "").strip()
vms = project_vms_mod.list_vms(pid)
if not vms:
return f"Projekt '{pid or 'Hauptchat'}' hat noch keine registrierten VMs."
return "VMs:\n" + "\n".join(
f"- {v['name']} ({v.get('arch')}, Display :{v.get('vnc_display', 1)})" for v in vms)
return f"Unbekanntes Tool: {name}" return f"Unbekanntes Tool: {name}"
except Exception as exc: except Exception as exc:
logger.exception("Tool '%s' fehlgeschlagen", name) logger.exception("Tool '%s' fehlgeschlagen", name)
+93
View File
@@ -39,6 +39,7 @@ import background as background_mod
import oauth as oauth_mod import oauth as oauth_mod
import seed_rules as seed_rules_mod import seed_rules as seed_rules_mod
import projects as projects_mod import projects as projects_mod
import project_vms as project_vms_mod
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
logger = logging.getLogger("aria-brain") logger = logging.getLogger("aria-brain")
@@ -894,6 +895,98 @@ def project_file(project_id: str, path: str):
return {"projectId": project_id, "path": path, "content": content} return {"projectId": project_id, "path": path, "content": content}
# ── QEMU-VMs pro Projekt ────────────────────────────────────────────
# Registry (project_vms) + echter Start/Stop via `aria-vm` auf dem Host (SSH
# aria-wohnung). Das Desktop-Panel der App zeigt pro Projekt die Liste.
_ARIA_VM_HOST = os.environ.get("ARIA_VM_SSH_HOST", "aria-wohnung")
def _ssh_aria_vm(*args: str, timeout: int = 25):
import subprocess
cmd = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=8",
_ARIA_VM_HOST, "aria-vm", *[str(a) for a in args]]
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
return r.returncode, r.stdout or "", r.stderr or ""
except Exception as exc:
return 1, "", str(exc)
def _vm_running_names() -> set:
rc, out, _err = _ssh_aria_vm("list", timeout=15)
names = set()
if rc == 0:
for line in out.splitlines():
parts = line.split()
if parts and "laeuft" in line:
names.add(parts[0])
return names
class VmAddBody(BaseModel):
name: str
arch: str = "i386"
iso: str = ""
vnc_display: int = 1
mem: int = 1024
create_disk: bool = False
size: str = "10G"
@app.get("/projects/{project_id}/vms")
def project_vms_list(project_id: str):
vms = [dict(v) for v in project_vms_mod.list_vms(project_id)]
running = _vm_running_names()
for v in vms:
v["running"] = v.get("name") in running
v["vnc_port"] = 5900 + int(v.get("vnc_display", 1))
return {"projectId": project_id, "vms": vms}
@app.post("/projects/{project_id}/vms")
def project_vm_add(project_id: str, body: VmAddBody):
try:
vm = project_vms_mod.add_vm(project_id, body.name, body.arch, body.iso,
body.vnc_display, body.mem)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
if body.create_disk:
rc, out, err = _ssh_aria_vm("create", body.name, body.arch, body.size)
vm["create_result"] = out.strip() or err.strip()
vm["create_ok"] = (rc == 0)
return vm
@app.delete("/projects/{project_id}/vms/{name}")
def project_vm_remove(project_id: str, name: str, purge: bool = False):
ok = project_vms_mod.remove_vm(project_id, name)
if not ok:
raise HTTPException(status_code=404, detail=f"VM '{name}' nicht in Projekt {project_id}")
if purge:
_ssh_aria_vm("rm", name)
return {"ok": True, "name": name}
@app.post("/projects/{project_id}/vms/{name}/boot")
def project_vm_boot(project_id: str, name: str):
vm = project_vms_mod.get_vm(project_id, name)
if not vm:
raise HTTPException(status_code=404, detail=f"VM '{name}' nicht gefunden")
args = ["boot", name, "--vnc-display", str(vm.get("vnc_display", 1)),
"--mem", str(vm.get("mem", 1024))]
if vm.get("iso"):
args += ["--iso", vm["iso"]]
rc, out, err = _ssh_aria_vm(*args, timeout=40)
return {"ok": rc == 0, "name": name, "vnc_port": 5900 + int(vm.get("vnc_display", 1)),
"output": (out.strip() or err.strip())[:500]}
@app.post("/projects/{project_id}/vms/{name}/stop")
def project_vm_stop(project_id: str, name: str):
rc, out, err = _ssh_aria_vm("stop", name, timeout=25)
return {"ok": rc == 0, "name": name, "output": (out.strip() or err.strip())[:500]}
@app.get("/conversation/stats") @app.get("/conversation/stats")
def conversation_stats(): def conversation_stats():
return conversation().stats() return conversation().stats()
+86
View File
@@ -0,0 +1,86 @@
"""
project_vms Registry der QEMU-VMs PRO PROJEKT.
Persistenz: /shared/config/project_vms.json { project_id: [ {vm}, ... ] }.
Eine VM = {name, arch, iso, vnc_display, mem, created_at, updated_at}. Der echte
Start/Stop laeuft ueber `aria-vm` auf dem Host (SSH aria-wohnung, siehe main.py);
diese Datei haelt nur die Zuordnung VM Projekt + die Startparameter, damit die
Liste im Desktop-Panel der App pro Projekt erscheint (auch wenn leer).
"""
from __future__ import annotations
import json
import os
import re
import time
from pathlib import Path
from typing import Optional
VMS_FILE = Path(os.environ.get("PROJECT_VMS_FILE", "/shared/config/project_vms.json"))
NAME_RE = re.compile(r"^[a-zA-Z0-9_-]{1,40}$")
VALID_ARCH = {"x86_64", "amd64", "i386", "i686", "x86", "arm", "aarch64",
"mips", "mipsel", "mips64", "ppc", "ppc64", "riscv64", "sparc"}
def _load() -> dict:
if not VMS_FILE.exists():
return {}
try:
data = json.loads(VMS_FILE.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else {}
except Exception:
return {}
def _save(data: dict) -> None:
VMS_FILE.parent.mkdir(parents=True, exist_ok=True)
tmp = VMS_FILE.with_suffix(".tmp")
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
tmp.replace(VMS_FILE)
def list_vms(project_id: str) -> list[dict]:
return _load().get(project_id or "", [])
def get_vm(project_id: str, name: str) -> Optional[dict]:
for v in list_vms(project_id):
if v.get("name") == name:
return v
return None
def add_vm(project_id: str, name: str, arch: str, iso: str = "",
vnc_display: int = 1, mem: int = 1024) -> dict:
if not NAME_RE.match(name or ""):
raise ValueError(f"Ungueltiger VM-Name: {name!r} (nur a-z0-9_-, max 40)")
if arch not in VALID_ARCH:
raise ValueError(f"Unbekannte Architektur: {arch!r}")
data = _load()
lst = data.setdefault(project_id or "", [])
now = int(time.time())
for v in lst:
if v.get("name") == name:
v.update({"arch": arch, "iso": iso, "vnc_display": int(vnc_display),
"mem": int(mem), "updated_at": now})
_save(data)
return v
vm = {"name": name, "arch": arch, "iso": iso, "vnc_display": int(vnc_display),
"mem": int(mem), "created_at": now, "updated_at": now}
lst.append(vm)
_save(data)
return vm
def remove_vm(project_id: str, name: str) -> bool:
data = _load()
lst = data.get(project_id or "")
if not lst:
return False
new = [v for v in lst if v.get("name") != name]
if len(new) == len(lst):
return False
data[project_id or ""] = new
_save(data)
return True
+5
View File
@@ -428,6 +428,11 @@ SEED_RULES: List[dict] = [
"Stefan mit [FILE:] schicken).\n" "Stefan mit [FILE:] schicken).\n"
" - `aria-vm list` / `aria-vm stop <name>` / `aria-vm rm <name>`.\n" " - `aria-vm list` / `aria-vm stop <name>` / `aria-vm rm <name>`.\n"
"\n" "\n"
"WICHTIG: Nachdem Du eine VM gebaut/gebootet hast, registriere sie mit "
"`vm_register(name, arch, vnc_display, ...)` — dann erscheint sie in Stefans "
"Desktop-Panel im Cockpit, wo er sie starten/stoppen/verbinden kann. Ohne "
"vm_register bleibt seine VM-Liste leer, obwohl die VM laeuft.\n"
"\n"
"Der VNC-Stream kommt AUTOMATISCH ueber den RVS-Tunnel der Bridge in " "Der VNC-Stream kommt AUTOMATISCH ueber den RVS-Tunnel der Bridge in "
"Stefans App-Desktop-Kachel (Display 1 / Port 5901) — dort kann er die " "Stefans App-Desktop-Kachel (Display 1 / Port 5901) — dort kann er die "
"VM live mit Maus/Tastatur bedienen. Du musst nur die VM booten und ggf. " "VM live mit Maus/Tastatur bedienen. Du musst nur die VM booten und ggf. "