feat: VM-Liste pro Projekt (Desktop-Panel) — Start/Stop/Verbinden
- Brain: project_vms.py (Registry /shared/config/project_vms.json pro Projekt) + Endpoints GET/POST/DELETE /projects/<id>/vms + boot/stop (via SSH aria-wohnung aria-vm auf dem Host; Status aus `aria-vm list`). - ARIA-Tools vm_register/vm_list (aufs Request-Projekt) + Seed-Regel: nach dem VM-Bau registrieren, damit sie in Stefans Desktop-Panel auftaucht. - App: brainApi VM-Methoden + ProjectVm-Typ. Neues DesktopTile — pro Projekt die VM-Liste (leer bis registriert), Start/Stop/Verbinden; Verbinden oeffnet noVNC (VncTile mit dem VNC-Port der VM). Deck rendert DesktopTile statt VncTile. py/tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 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>
|
||||
<Text style={styles.vmMeta}>{vm.arch} · Display :{vm.vnc_display} · {vm.running ? 'läuft' : 'gestoppt'}</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, marginTop: 3 },
|
||||
vmBtns: { flexDirection: 'row', gap: 6, alignItems: 'center' },
|
||||
vmBtn: { borderWidth: 1, borderRadius: 8, paddingHorizontal: 10, paddingVertical: 6 },
|
||||
vmBtnText: { fontSize: 12, fontWeight: '700' },
|
||||
});
|
||||
|
||||
export default DesktopTile;
|
||||
Reference in New Issue
Block a user