- Brain: /projects/status + /list liefern has_files + file_count pro Projekt (Scan /shared/projects/<id>/). Ersetzt das manuelle Code-Flag als primaeren Indikator. VM-Liste liefert boot_cmd (lesbarer aria-vm-Startbefehl). - App: 📄-Symbol (+ Anzahl) an Projekten mit Dateien im ProjectsBrowser. DesktopTile zeigt pro VM den Start-Befehl als Wert dahinter. - Diagnostic: 📄-Symbol (+ Anzahl, Tooltip) an Projekten mit Dateien. Hinweis (kein Code): der VNC-Stream laeuft komplett durch RVS — der Port ist nur der interne QEMU-Display-Port, den die Bridge lokal auf dem Host nutzt; die App oeffnet nie einen Port (firewall-unabhaengig). py/tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
157 lines
6.6 KiB
TypeScript
157 lines
6.6 KiB
TypeScript
/**
|
||
* 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 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={() => 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, 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 },
|
||
vmBtnText: { fontSize: 12, fontWeight: '700' },
|
||
});
|
||
|
||
export default DesktopTile;
|