Files
ARIA-AGENT/android/src/workspace/tiles/DesktopTile.tsx
T
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

193 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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, Image, Modal, 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 [shotBusy, setShotBusy] = useState('');
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 || []))
.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]);
const screenshot = useCallback((vm: ProjectVm) => {
setShotBusy(vm.name); setErr('');
brainApi.screenshotProjectVm(projectId, vm.name)
.then(r => setShot({ name: vm.name, b64: r.base64 }))
.catch(e => setErr(String(e?.message || e)))
.finally(() => setShotBusy(''));
}, [projectId]);
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>
);
}
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>
) : !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'}
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 style={styles.vmMeta}> {vm.arch} · {vm.running ? 'läuft' : 'gestoppt'}</Text>
</Text>
<Text style={styles.vmCmd} numberOfLines={2}>{vm.boot_cmd || `aria-vm boot ${vm.name} --vnc-display ${vm.vnc_display}`}</Text>
</View>
<View style={styles.vmBtns}>
{isBusy ? (
<ActivityIndicator color="#0096FF" />
) : vm.running ? (
<>
<TouchableOpacity onPress={() => screenshot(vm)} style={[styles.vmBtn, { borderColor: '#8888AA' }]} disabled={shotBusy === vm.name}>
{shotBusy === vm.name
? <ActivityIndicator color="#8888AA" size="small" />
: <Text style={[styles.vmBtnText, { color: '#C8C8E0' }]}>📷</Text>}
</TouchableOpacity>
<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>
<Modal visible={!!shot} transparent animationType="fade" onRequestClose={() => setShot(null)}>
<TouchableOpacity style={styles.shotOverlay} activeOpacity={1} onPress={() => setShot(null)}>
<Text style={styles.shotTitle}>{shot?.name} — Screenshot</Text>
{shot && (
<Image
source={{ uri: `data:image/png;base64,${shot.b64}` }}
style={styles.shotImg}
resizeMode="contain"
/>
)}
<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>
);
};
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, fontWeight: '400' },
vmCmd: { color: '#6A9BD0', fontSize: 11, fontFamily: 'monospace', marginTop: 4 },
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' },
shotHint: { color: '#8888AA', fontSize: 12, marginTop: 12 },
});
export default DesktopTile;