Compare commits

...
10 Commits
Author SHA1 Message Date
duffyduck 1c7157327c release: bump version to 0.2.2.8 2026-07-21 01:10:26 +02:00
duffyduckandClaude Opus 4.8 6c27097c96 fix(app): gruener Desktop-Punkt nur bei laufender VM im aktiven Projekt
Badge haing bisher am globalen desktop_status (irgendwo ein Desktop
erreichbar) → wurde generell angezeigt. Jetzt aus der projektbezogenen
VM-Liste (listProjectVms → running), pid-Guard, 6s-Refresh fuer Start/Stop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 01:08:37 +02:00
duffyduckandClaude Opus 4.8 a91325a04f fix(vnc): Tastatur ueber echtes RN-TextInput statt WebView-Hidden-Input
Der versteckte WebView-Input oeffnete die Android-Software-Tastatur
unzuverlaessig. Jetzt haelt VncTile ein echtes RN-<TextInput>
(keyboardType=visible-password), der ⌨-Button fokussiert es. Getippte
Zeichen gehen per injectJavaScript an window.ariaVncKey.char/keysym →
rfb.sendKey. Prefix-Diff fuer Druckbares, onKeyPress fuer Backspace/Enter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 01:06:27 +02:00
duffyduck ef826d1ed1 release: bump version to 0.2.2.7 2026-07-21 01:01:54 +02:00
duffyduckandClaude Opus 4.8 933836f0a6 fix(app): kein 404 bei Dateien/Desktop im Hauptchat (leere project_id)
Im Hauptchat (projectId='') riefen FilesTile/DesktopTile /projects//files bzw.
/vms auf → Brain 404. Beide guarden jetzt leere projectId: kein Fetch, klare
Meldung "Kein aktives Projekt — wechsle in ein Projekt".

tsc clean. App-only, Deploy: APK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 00:59:50 +02:00
duffyduck e04d8f360b release: bump version to 0.2.2.6 2026-07-21 00:54:02 +02:00
duffyduckandClaude Opus 4.8 4685632294 feat(vnc): echtes Vollbild beim Verbinden (randlos ueber das ganze Display)
Der ⤢-Knopf war nur der Fit/1:1-Umschalter (Zoom) INNERHALB des eingerahmten
Streifens (Header+Dock drumrum) — sah deshalb wie Zoom aus, fuellte nie das
Display. Jetzt oeffnet "Verbinden" die VM in einem Fullscreen-Modal: randlos
ueber alles (Header + Dock weg), scaleViewport passt den VM-Screen sauber ein,
Landscape erlaubt. Buttons schweben drueber (‹ VMs links, ⌨/Strg+Alt+Entf/⤢
rechts, unter der Statusleiste). Android-Back verlaesst Vollbild.

tsc clean. App-only, Deploy: APK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 00:51:59 +02:00
duffyduck 769025c41b release: bump version to 0.2.2.5 2026-07-21 00:47:46 +02:00
duffyduckandClaude Opus 4.8 2005e9b85e 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>
2026-07-21 00:45:41 +02:00
duffyduckandClaude Opus 4.8 25abd220ad fix(vnc): Bridge nutzt aria-net-Gateway statt host.docker.internal (VNC refused)
Kern des "Verbinde..."-Hangs: host.docker.internal loeste zu 172.17.0.1 (docker0)
auf, QEMUs VNC ist aber ans aria-net-Gateway 192.168.64.1 gebunden (dorthin
bindet der Brain) → Connection refused. Die Bridge ermittelt den VNC-Host jetzt
selbst per _docker_gateway() (/proc/net/route) = dasselbe Gateway wie der Brain.
ARIA_VNC_HOST-Env hat weiter Vorrang.

Live belegt: Bridge->192.168.64.1:5901 liefert RFB-Handshake, ->host.docker.
internal:5901 (172.17.0.1) = refused.

py_compile clean. Deploy: bridge rebuild.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 00:35:26 +02:00
10 changed files with 179 additions and 75 deletions
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 20204
versionName "0.2.2.4"
versionCode 20208
versionName "0.2.2.8"
// Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general'
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aria-cockpit",
"version": "0.2.2.4",
"version": "0.2.2.8",
"private": true,
"scripts": {
"android": "react-native run-android",
+16 -9
View File
@@ -5,13 +5,13 @@
* Cockpit-Modus → Workbench mit Taskleisten-Dock: Chat · Code · Desktop.
*
* Aktivitaets-Badges am Dock: Editor blau, wenn schon Code-Dateien da sind;
* Desktop gruen, wenn eine VM verbunden ist.
* Desktop gruen NUR, wenn im aktiven Projekt eine VM laeuft.
*/
import React, { useEffect, useMemo, useState } from 'react';
import projectFocus, { FocusSnapshot } from '../services/projectFocus';
import codeFile from '../services/codeFile';
import desktop from '../services/desktop';
import brainApi from '../services/brainApi';
import viewMode, { ViewModeValue } from '../services/viewMode';
import ChatScreen from '../screens/ChatScreen';
import { TileId } from './layout';
@@ -29,7 +29,6 @@ const WorkspaceScreen: React.FC = () => {
useEffect(() => projectFocus.subscribe(setFocus), []);
const pid = focus.focusedProjectId;
const kind = projectFocus.getProjectKind(pid);
// Code-Signal: hat der Spiegel schon Dateien fuer dieses Projekt?
useEffect(() => {
@@ -39,13 +38,21 @@ const WorkspaceScreen: React.FC = () => {
});
}, [pid]);
// Desktop-Signal + einmaliger Check beim Betreten eines Code-Projekts.
// Desktop-Signal: gruener Punkt NUR, wenn im AKTIVEN Projekt wirklich eine VM
// laeuft (nicht generell irgendwo). Quelle ist die projektbezogene VM-Liste;
// leichtes Nachfassen, damit Start/Stop sich zeitnah zeigt.
useEffect(() => {
setHasDesktop(desktop.getStatus().available);
const unsub = desktop.subscribeStatus((s) => setHasDesktop(s.available));
if (kind === 'code') desktop.requestCheck(pid);
return unsub;
}, [pid, kind]);
if (!pid) { setHasDesktop(false); return; }
let alive = true;
const check = () => {
brainApi.listProjectVms(pid)
.then(r => { if (alive) setHasDesktop((r.vms || []).some(v => v.running)); })
.catch(() => { if (alive) setHasDesktop(false); });
};
check();
const t = setInterval(check, 6000);
return () => { alive = false; clearInterval(t); };
}, [pid]);
const badges = useMemo(() => ({
editor: hasCode ? '#0096FF' : undefined,
+16 -17
View File
@@ -77,14 +77,13 @@ export const NOVNC_HTML = `<!doctype html><html><head><meta charset="utf-8">
var msg=document.getElementById('msg');
// Verstecktes Eingabefeld → Software-Tastatur des Handys tippt in die VM.
var kbd=document.createElement('input');
kbd.setAttribute('autocomplete','off'); kbd.setAttribute('autocorrect','off');
kbd.setAttribute('autocapitalize','off'); kbd.spellcheck=false;
kbd.style.cssText='position:absolute;left:-1000px;top:0;width:1px;height:1px;opacity:0;';
document.body.appendChild(kbd);
var SPECIAL={Enter:0xff0d,Backspace:0xff08,Tab:0xff09,Escape:0xff1b,Delete:0xffff,
ArrowLeft:0xff51,ArrowUp:0xff52,ArrowRight:0xff53,ArrowDown:0xff54,Home:0xff50,End:0xff57};
// Tastatur laeuft NICHT mehr ueber ein verstecktes WebView-Feld (Android
// oeffnet die Software-Tastatur dafuer unzuverlaessig). Stattdessen haelt die
// App ein echtes RN-<TextInput> und ruft window.ariaVncKey.* per
// injectJavaScript auf → wird unten (nach RFB-Init) definiert.
// cp<0x100 → Keysym == Codepoint (Latin-1)
// sonst → X11-Unicode-Keysym 0x01000000+cp
function cpToKeysym(cp){ return cp < 0x100 ? cp : 0x01000000 + cp; }
import('https://cdn.jsdelivr.net/npm/@novnc/novnc@1.4.0/core/rfb.js').then(function(mod){
var RFB = mod.default;
@@ -99,19 +98,19 @@ export const NOVNC_HTML = `<!doctype html><html><head><meta charset="utf-8">
});
window.__rfb = rfb;
// Tasten aus dem versteckten Feld an die VM schicken.
// Down+Up einer Taste an die VM schicken.
function tap(keysym, code){ try{ rfb.sendKey(keysym, code||null, true); rfb.sendKey(keysym, code||null, false); }catch(_){} }
kbd.addEventListener('keydown', function(e){
if(SPECIAL[e.key]!==undefined){ tap(SPECIAL[e.key], e.code); e.preventDefault(); }
});
kbd.addEventListener('input', function(){
var v=kbd.value; for(var i=0;i<v.length;i++){ tap(v.charCodeAt(i)); } kbd.value='';
});
// Empfaenger-API: die App (RN-<TextInput>) ruft das per injectJavaScript.
// char(cp) druckbares Zeichen (Codepoint)
// keysym(ks) Sondertaste als fertiges X11-Keysym (Backspace/Enter/…)
window.ariaVncKey = {
char: function(cp){ tap(cpToKeysym(cp)); },
keysym: function(ks){ tap(ks); }
};
// Steuerungs-API fuer die App (per injectJavaScript).
window.ariaVncCtl = {
focusKeyboard: function(){ try{ kbd.focus(); }catch(_){} },
blurKeyboard: function(){ try{ kbd.blur(); }catch(_){} },
cad: function(){ try{ rfb.sendCtrlAltDel(); }catch(_){} },
toggleFit: function(){ fit=!fit; rfb.scaleViewport=fit; rfb.clipViewport=!fit; post({event:'vnc_fit', fit:fit}); }
};
+18 -15
View File
@@ -26,6 +26,7 @@ const DesktopTile: React.FC<Props> = ({ projectId, focused }) => {
const [shot, setShot] = useState<{ name: string; b64: string } | null>(null);
const load = useCallback(() => {
if (!projectId) { setVms([]); setErr(''); setLoading(false); return; }
setLoading(true); setErr('');
brainApi.listProjectVms(projectId)
.then(r => setVms(r.vms || []))
@@ -71,21 +72,6 @@ const DesktopTile: React.FC<Props> = ({ projectId, focused }) => {
);
}
// 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}>
@@ -97,6 +83,8 @@ const DesktopTile: React.FC<Props> = ({ projectId, focused }) => {
<ActivityIndicator color="#0096FF" style={{ marginTop: 20 }} />
) : err ? (
<Text style={styles.err}>{err}</Text>
) : !projectId ? (
<Text style={styles.empty}>Kein aktives Projekt wechsle in ein Projekt für dessen VMs.</Text>
) : vms.length === 0 ? (
<Text style={styles.empty}>
Noch keine VM in diesem Projekt.{'\n'}
@@ -157,6 +145,18 @@ const DesktopTile: React.FC<Props> = ({ projectId, focused }) => {
<Text style={styles.shotHint}>Tippen zum Schließen</Text>
</TouchableOpacity>
</Modal>
{/* Vollbild-VNC — randlos ueber das ganze Display (Header + Dock weg). */}
{connected && (
<Modal visible animationType="slide" onRequestClose={() => setConnected(null)} supportedOrientations={['portrait', 'landscape']}>
<View style={styles.fs}>
<VncTile projectId={projectId} focused port={connected.vnc_port || (5900 + (connected.vnc_display || 1))} />
<TouchableOpacity style={styles.fsBack} onPress={() => setConnected(null)} activeOpacity={0.8}>
<Text style={styles.fsBackText}> VMs</Text>
</TouchableOpacity>
</View>
</Modal>
)}
</View>
);
};
@@ -180,6 +180,9 @@ const styles = StyleSheet.create({
vmBtns: { flexDirection: 'row', gap: 6, alignItems: 'center' },
vmBtn: { borderWidth: 1, borderRadius: 8, paddingHorizontal: 10, paddingVertical: 6, minWidth: 34, alignItems: 'center' },
vmBtnText: { fontSize: 12, fontWeight: '700' },
fs: { flex: 1, backgroundColor: '#000000' },
fsBack: { position: 'absolute', top: 34, left: 10, backgroundColor: 'rgba(18,18,42,0.9)', borderColor: '#2A2A3E', borderWidth: 1, borderRadius: 10, paddingHorizontal: 12, paddingVertical: 7 },
fsBackText: { color: '#0096FF', fontSize: 14, fontWeight: '700' },
shotOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.92)', alignItems: 'center', justifyContent: 'center', padding: 12 },
shotTitle: { color: '#E0E0F0', fontSize: 14, fontWeight: '700', marginBottom: 10 },
shotImg: { width: '100%', height: '78%', backgroundColor: '#000' },
+2 -1
View File
@@ -43,6 +43,7 @@ const FilesTile: React.FC<Props> = ({ projectId, focused }) => {
const [previewBusy, setPreviewBusy] = useState('');
const load = useCallback(() => {
if (!projectId) { setFiles([]); setErr(''); setLoading(false); return; }
setLoading(true); setErr('');
brainApi.listProjectFiles(projectId)
.then(r => setFiles((r.files || []).slice().sort((a, b) => a.path.localeCompare(b.path))))
@@ -88,7 +89,7 @@ const FilesTile: React.FC<Props> = ({ projectId, focused }) => {
) : err ? (
<Text style={styles.err}>{err}</Text>
) : files.length === 0 ? (
<Text style={styles.empty}>Noch keine Dateien in diesem Projekt.</Text>
<Text style={styles.empty}>{!projectId ? 'Kein aktives Projekt — wechsle in ein Projekt für dessen Dateien.' : 'Noch keine Dateien in diesem Projekt.'}</Text>
) : (
files.map(f => (
<TouchableOpacity key={f.path} onPress={() => open(f)} style={styles.row} disabled={previewBusy === f.path}>
+65 -3
View File
@@ -8,7 +8,7 @@
*/
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { NativeSyntheticEvent, StyleSheet, Text, TextInput, TextInputChangeEventData, TextInputKeyPressEventData, TouchableOpacity, View } from 'react-native';
import { WebView, WebViewMessageEvent } from 'react-native-webview';
import desktop from '../../services/desktop';
import { NOVNC_HTML } from '../assets/novncHtml';
@@ -19,9 +19,15 @@ interface Props {
port?: number; // VNC-Port der zu verbindenden VM (Default 5901 = Display :1)
}
// X11-Keysyms fuer Sondertasten, die kein druckbares Zeichen liefern.
const KEYSYM = { Backspace: 0xff08, Enter: 0xff0d, Tab: 0xff09 };
const VncTile: React.FC<Props> = ({ projectId, focused, port = 5901 }) => {
const webRef = useRef<WebView>(null);
const kbdRef = useRef<TextInput>(null);
const bufRef = useRef(''); // Spiegel des TextInput-Textes
const [status, setStatus] = useState<'idle' | 'connecting' | 'connected' | 'disconnected'>('idle');
const [kbdOn, setKbdOn] = useState(false);
const unsubDataRef = useRef<null | (() => void)>(null);
const teardown = useCallback(() => {
@@ -38,6 +44,40 @@ const VncTile: React.FC<Props> = ({ projectId, focused, port = 5901 }) => {
webRef.current?.injectJavaScript(`window.ariaVncCtl && window.ariaVncCtl.${fn}(); true;`);
}, []);
const sendChar = useCallback((cp: number) => {
webRef.current?.injectJavaScript(`window.ariaVncKey && window.ariaVncKey.char(${cp}); true;`);
}, []);
const sendKeysym = useCallback((ks: number) => {
webRef.current?.injectJavaScript(`window.ariaVncKey && window.ariaVncKey.keysym(${ks}); true;`);
}, []);
// Tastatur ein-/ausblenden: echtes RN-<TextInput> fokussieren → Android oeffnet
// die Software-Tastatur zuverlaessig (anders als ein verstecktes WebView-Feld).
const toggleKbd = useCallback(() => {
if (kbdOn) { kbdRef.current?.blur(); setKbdOn(false); }
else { setKbdOn(true); setTimeout(() => kbdRef.current?.focus(), 0); }
}, [kbdOn]);
// Druckbare Zeichen: Prefix-Diff des (wachsenden) Feldes → nur neu Getipptes an
// die VM. Loeschungen kommen ueber onKeyPress(Backspace), daher hier nur Inserts.
const onKbdChange = useCallback((e: NativeSyntheticEvent<TextInputChangeEventData>) => {
const text = e.nativeEvent.text || '';
const prev = bufRef.current;
let i = 0;
const min = Math.min(prev.length, text.length);
while (i < min && prev.charCodeAt(i) === text.charCodeAt(i)) i++;
for (const ch of text.slice(i)) { const cp = ch.codePointAt(0); if (cp) sendChar(cp); }
bufRef.current = text;
if (text.length > 200) { bufRef.current = ''; kbdRef.current?.setNativeProps({ text: '' }); }
}, [sendChar]);
// Sondertasten: Backspace/Enter feuern auf Android zuverlaessig als keyPress.
const onKbdKeyPress = useCallback((e: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
const k = e.nativeEvent.key;
if (k === 'Backspace') sendKeysym(KEYSYM.Backspace);
else if (k === 'Enter') sendKeysym(KEYSYM.Enter);
}, [sendKeysym]);
const onMessage = useCallback((e: WebViewMessageEvent) => {
let m: any;
try { m = JSON.parse(e.nativeEvent.data); } catch { return; }
@@ -84,10 +124,29 @@ const VncTile: React.FC<Props> = ({ projectId, focused, port = 5901 }) => {
keyboardDisplayRequiresUserAction={false}
/>
{/* Verstecktes Eingabefeld: fokussiert → Android-Tastatur tippt in die VM.
keyboardType=visible-password schaltet Autokorrektur/Vorschlaege ab und
liefert saubere Einzelzeichen. Offscreen, aber fokussierbar. */}
<TextInput
ref={kbdRef}
style={styles.hiddenInput}
onChange={onKbdChange}
onKeyPress={onKbdKeyPress}
onBlur={() => setKbdOn(false)}
keyboardType="visible-password"
autoCapitalize="none"
autoCorrect={false}
spellCheck={false}
blurOnSubmit={false}
caretHidden
contextMenuHidden
multiline={false}
/>
{/* Steuerungs-Leiste — nur wenn verbunden */}
{connected && (
<View style={styles.ctlBar}>
<TouchableOpacity style={styles.ctlBtn} onPress={() => ctl('focusKeyboard')} activeOpacity={0.7}>
<TouchableOpacity style={[styles.ctlBtn, kbdOn && styles.ctlBtnOn]} onPress={toggleKbd} activeOpacity={0.7}>
<Text style={styles.ctlText}></Text>
</TouchableOpacity>
<TouchableOpacity style={styles.ctlBtn} onPress={() => ctl('cad')} activeOpacity={0.7}>
@@ -119,7 +178,7 @@ const styles = StyleSheet.create({
sub: { color: '#9090B0', fontSize: 14, marginTop: 8 },
ctlBar: {
position: 'absolute',
top: 8,
top: 34,
right: 8,
flexDirection: 'row',
gap: 6,
@@ -135,8 +194,11 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
},
ctlBtnOn: { backgroundColor: 'rgba(0,150,255,0.85)', borderColor: '#0096FF' },
ctlText: { color: '#E0E0F0', fontSize: 16, fontWeight: '700' },
ctlTextSmall: { color: '#E0E0F0', fontSize: 11, fontWeight: '700' },
// Fokussierbar (nicht display:none), aber aus dem Sichtfeld geschoben.
hiddenInput: { position: 'absolute', width: 1, height: 1, top: -100, left: -100, opacity: 0, padding: 0 },
overlay: { position: 'absolute', top: 12, left: 0, right: 0, alignItems: 'center' },
overlayText: { color: '#9090B0', fontSize: 12, backgroundColor: 'rgba(0,0,0,0.6)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 10, overflow: 'hidden' },
});
+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")
+23 -1
View File
@@ -43,6 +43,25 @@ from openwakeword.model import Model as WakeWordModel
from modes import Mode, canonical_id, detect_mode_switch, mode_from_id, should_speak
def _docker_gateway() -> str:
"""Docker-Gateway-IP (= Host-IP auf DIESEM Container-Netz, aria-net) aus
/proc/net/route. Genau die IP, an die der Brain QEMUs VNC bindet — im
Gegensatz zu host.docker.internal, das auf die Default-Bridge (docker0)
zeigt und daher die VM nicht trifft."""
try:
import socket as _sock
import struct as _struct
with open("/proc/net/route") as f:
for line in f.readlines()[1:]:
fields = line.strip().split()
if len(fields) >= 4 and fields[1] == "00000000" and int(fields[3], 16) & 2:
return _sock.inet_ntoa(_struct.pack("<L", int(fields[2], 16)))
except Exception:
pass
return ""
# ── Logging ──────────────────────────────────────────────────
logging.basicConfig(
@@ -738,7 +757,10 @@ class ARIABridge:
# "task": asyncio.Task}. Wir bruecken rohes RFB-TCP (QEMU-VNC auf dem
# Host) <-> RVS (vnc_data/vnc_input, Base64-in-JSON).
self._vnc_sessions: dict[str, dict] = {}
self._vnc_host: str = os.environ.get("ARIA_VNC_HOST", "host.docker.internal")
# VNC-Host: das aria-net-Gateway (dort bindet der Brain QEMUs VNC).
# host.docker.internal zeigt faelschlich auf docker0 (172.17.0.1) → refused.
self._vnc_host: str = (os.environ.get("ARIA_VNC_HOST")
or _docker_gateway() or "host.docker.internal")
# Satelliten (Aussenposten in fremden Netzen). id → {location, caps,
# control, last_seen}. Registrierung via sat_hello. _pending_sat:
# requestId → Future (sat_devices / sat_result), analog _pending_flux.
+7 -3
View File
@@ -147,16 +147,20 @@ cmd_screenshot() {
local d; d="$(vm_dir "${name}")"
vm_running "${name}" || die "VM '${name}' laeuft nicht"
command -v socat >/dev/null || die "socat fehlt (qemu-setup.sh?)"
mkdir -p "${SHOT_DIR}"
# Standard: ins VM-Verzeichnis schreiben (dem aria-User gehoerend) — NICHT
# nach /root/... (da kommt der aria-User nicht hin). Der Brain holt das PNG
# danach per SSH (base64). Ueberschreibbar via ARIA_VM_SHOT_DIR.
local out_dir="${ARIA_VM_SHOT_DIR:-${d}}"
mkdir -p "${out_dir}"
local ts; ts="$(date +%s)"
local ppm="${d}/shot-${ts}.ppm"
printf 'screendump %s\n' "${ppm}" | socat - "unix-connect:${d}/monitor.sock" >/dev/null
sleep 0.3
local out="${SHOT_DIR}/${name}-${ts}.png"
local out="${out_dir}/${name}-${ts}.png"
if command -v convert >/dev/null; then
convert "${ppm}" "${out}" && rm -f "${ppm}"
else
out="${SHOT_DIR}/${name}-${ts}.ppm"; mv "${ppm}" "${out}"
out="${out_dir}/${name}-${ts}.ppm"; mv "${ppm}" "${out}"
fi
echo "screenshot=${out}"
}