noVNC bridge: per-device live screens in dashboard (websockify + TokenFile + VNC button)

This commit is contained in:
drjones
2026-08-13 18:06:14 -07:00
parent 90921f8d9b
commit 2a2d72f288
3 changed files with 63 additions and 1 deletions

41
app.py
View File

@@ -691,6 +691,45 @@ def api_shot_latest(mac):
return jsonify({"error": "no screenshot yet"}), 404
return send_file(path, mimetype="image/png")
VNC_TOKEN_FILE = "/opt/android-fleet/vnc_tokens.json"
def regen_vnc_tokens():
"""Map each online device MAC -> its QEMU VNC port for websockify TokenFile."""
try:
con = db()
rows = con.execute("SELECT mac, vmid FROM devices WHERE vmid IS NOT NULL").fetchall()
con.close()
lines = []
for r in rows:
got = pve("GET", f"/nodes/pve/qemu/{r['vmid']}/config") or {}
cfg = (got.get("data") or {}) or {}
args = cfg.get("args", "") or ""
m = re.search(r"-vnc 0\.0\.0\.0:(\d{4})", args)
if m:
port = int(m.group(1)) + 5900 # QEMU: -vnc :N listens on N+5900
lines.append(f"{r['mac']}: 10.30.20.85:{port}")
with open(VNC_TOKEN_FILE, "w") as f:
f.write("\n".join(lines) + ("\n" if lines else ""))
except Exception as e:
log("WARN", f"vnc token regen: {e}")
def vnc_loop():
while True:
regen_vnc_tokens()
time.sleep(60)
@app.route("/api/device/<mac>/vnc")
def api_vnc(mac):
regen_vnc_tokens()
try:
tokens = open(VNC_TOKEN_FILE).read()
except FileNotFoundError:
tokens = ""
if mac not in tokens:
return jsonify({"error": "device has no VNC mapping (needs vmid + -vnc args)"}), 404
return jsonify({"url": (f"http://10.30.20.31:8081/vnc.html?autoconnect=true"
f"&reconnect=true&resize=scale&path=websockify?token={mac}")})
@app.route("/api/device/<mac>", methods=["DELETE"])
def api_delete_device(mac):
"""Remove a device row (and release its proxy)."""
@@ -841,7 +880,7 @@ if __name__ == "__main__":
subprocess.run(["adb", "start-server"], capture_output=True, timeout=10)
except Exception:
pass
for fn in (discovery_loop, adb_loop, proxy_loop, ws_broadcast_loop, screenshot_loop):
for fn in (discovery_loop, adb_loop, proxy_loop, ws_broadcast_loop, screenshot_loop, vnc_loop):
threading.Thread(target=fn, daemon=True).start()
log("STARTUP", f"godhead up — template {TEMPLATE}, LAN {LAN}")
app.run(host="0.0.0.0", port=8080, threaded=True)