fix(vm): Screenshot ohne /root-Rechte + VNC-Tastatur robuster

Screenshot: aria-vm lief als User 'aria', SHOT_DIR war aber /root/... →
"mkdir: cannot create directory /root". Jetzt schreibt aria-vm das PNG ins
VM-Verzeichnis (aria-schreibbar) und der Brain holt es per SSH (base64,
_ssh_host) — unabhaengig von Volume-Rechten. End-to-end validiert (gueltiges
PNG). Wird weiter ins Projekt (screenshots/) kopiert.

VNC-Tastatur: verstecktes Input-Feld war off-screen (opacity:0, left:-1000px) →
Android oeffnete die Tastatur oft nicht / lieferte keine Events. Jetzt on-screen
(bottom, 1px, opacity:0, pointer-events:none) + beforeinput als primaerer
Handler (Android-robust) mit input-Fallback. Strg+Alt+Entf ging schon.

py/bash/tsc clean. aria-vm bereits live. Deploy: brain rebuild + APK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 00:45:41 +02:00
co-authored by Claude Opus 4.8
parent 25abd220ad
commit 2005e9b85e
3 changed files with 57 additions and 30 deletions
+29 -23
View File
@@ -963,17 +963,21 @@ def _docker_gateway() -> str:
return ""
def _ssh_aria_vm(*args: str, timeout: int = 25):
def _ssh_host(*cmd: 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]]
full = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=8",
_ARIA_VM_HOST, *[str(c) for c in cmd]]
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
r = subprocess.run(full, 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 _ssh_aria_vm(*args: str, timeout: int = 25):
return _ssh_host("aria-vm", *args, timeout=timeout)
def _vm_running_names() -> set:
rc, out, _err = _ssh_aria_vm("list", timeout=15)
names = set()
@@ -1074,11 +1078,13 @@ def project_vm_stop(project_id: str, name: str):
@app.post("/projects/{project_id}/vms/{name}/screenshot")
def project_vm_screenshot(project_id: str, name: str):
"""Macht einen Screenshot der laufenden VM (aria-vm screenshot → PNG in
/shared/uploads) und liefert ihn als Base64 zurueck. So sieht Stefan den
VM-Bildschirm auch ohne Live-VNC (genau wie ARIA es beim Testen macht)."""
"""Macht einen Screenshot der laufenden VM und liefert ihn als Base64.
aria-vm schreibt das PNG ins VM-Verzeichnis (dem aria-User gehoerend — nicht
ins /root-Shared-Volume, wo der aria-User keinen Zugriff hat). Der Brain holt
die Datei danach per SSH (base64) — funktioniert unabhaengig von Volume-
Rechten. Zusaetzlich wird das PNG ins Projekt kopiert (Dateien-Panel)."""
import base64
import time as _t
rc, out, err = _ssh_aria_vm("screenshot", name, timeout=30)
if rc != 0:
raise HTTPException(status_code=400, detail=f"Screenshot fehlgeschlagen: {(err or out).strip()[:200]}")
@@ -1088,28 +1094,28 @@ def project_vm_screenshot(project_id: str, name: str):
path = line.split("=", 1)[1].strip()
if not path:
raise HTTPException(status_code=500, detail=f"Kein Screenshot-Pfad: {out.strip()[:200]}")
local = os.path.join("/shared/uploads", os.path.basename(path))
for _ in range(10): # kurze Bind-Mount-Latenz abfangen
if os.path.isfile(local):
break
_t.sleep(0.2)
if not os.path.isfile(local):
raise HTTPException(status_code=500, detail=f"Screenshot-Datei nicht gefunden: {local}")
with open(local, "rb") as f:
data = f.read()
# Auch ins Projekt kopieren → taucht im Dateien-Panel auf.
# PNG per SSH als Base64 holen (kein Shared-Volume noetig).
rc2, b64, err2 = _ssh_host("base64", "-w0", path, timeout=20)
if rc2 != 0 or not b64.strip():
raise HTTPException(status_code=500, detail=f"Screenshot konnte nicht gelesen werden: {(err2 or 'leer').strip()[:200]}")
b64 = b64.strip()
try:
data = base64.b64decode(b64)
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Base64 ungueltig: {exc}")
fname = os.path.basename(path)
# Ins Projekt kopieren → taucht im Dateien-Panel auf.
proj_rel = ""
try:
shots_dir = os.path.join(_project_dir(project_id), "screenshots")
os.makedirs(shots_dir, exist_ok=True)
with open(os.path.join(shots_dir, os.path.basename(local)), "wb") as f:
with open(os.path.join(shots_dir, fname), "wb") as f:
f.write(data)
proj_rel = "screenshots/" + os.path.basename(local)
proj_rel = "screenshots/" + fname
except Exception:
proj_rel = ""
return {"ok": True, "name": name, "filename": os.path.basename(local),
"projectPath": proj_rel,
"base64": base64.b64encode(data).decode("ascii")}
return {"ok": True, "name": name, "filename": fname,
"projectPath": proj_rel, "base64": b64}
@app.get("/conversation/stats")