added to remote repo

This commit is contained in:
Philipp committed 2026-05-21 08:18:29 +02:00
1 parent 8697c9f405
commit cc599b6e18
8049 files changed
+1096323

No files matched your search

+126
View File
@@ -0,0 +1,126 @@
import { useEffect, useMemo, useState } from 'react';
import { AlertTriangle, DatabaseBackup, RefreshCw, Settings as SettingsIcon } from 'lucide-react';
import { api, errorMessage } from './api.js';
import { Dashboard } from './components/Dashboard.jsx';
import { Settings } from './components/Settings.jsx';
import { VMDetail } from './components/VMDetail.jsx';
export default function App() {
const [health, setHealth] = useState(null);
const [vms, setVms] = useState([]);
const [jobs, setJobs] = useState([]);
const [selectedVm, setSelectedVm] = useState(null);
const [view, setView] = useState('dashboard');
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
async function refresh() {
setError('');
try {
const [healthResult, vmsResult, jobsResult] = await Promise.allSettled([
api.get('/health'),
api.get('/vms'),
api.get('/jobs'),
]);
if (healthResult.status === 'fulfilled') {
setHealth(healthResult.value.data);
} else {
setHealth(healthResult.reason.response?.data || { ok: false, checks: {} });
}
if (vmsResult.status === 'fulfilled') setVms(vmsResult.value.data);
if (jobsResult.status === 'fulfilled') setJobs(jobsResult.value.data);
const firstFailure = [healthResult, vmsResult, jobsResult].find((result) => result.status === 'rejected');
if (firstFailure) setError(errorMessage(firstFailure.reason));
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setLoading(false);
}
}
useEffect(() => {
refresh();
const timer = window.setInterval(refresh, 5000);
return () => window.clearInterval(timer);
}, []);
const currentVm = useMemo(
() => vms.find((vm) => vm.name === selectedVm) || null,
[selectedVm, vms],
);
return (
<main className="min-h-screen bg-zinc-950 text-zinc-100">
<header className="border-b border-zinc-800 bg-zinc-950/95">
<div className="mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-4 sm:px-6">
<button
className="flex items-center gap-3 text-left"
onClick={() => {
setSelectedVm(null);
setView('dashboard');
}}
type="button"
>
<span className="flex h-9 w-9 items-center justify-center rounded-md border border-zinc-800 bg-zinc-900">
<DatabaseBackup className="h-5 w-5 text-cyan-300" />
</span>
<span>
<span className="block text-sm font-semibold tracking-wide text-zinc-100">Incus Backup Control</span>
<span className="block text-xs text-zinc-500">ZFS block-level Restic operations</span>
</span>
</button>
<div className="flex items-center gap-2">
<button
className="inline-flex h-9 items-center gap-2 rounded-md border border-zinc-800 bg-zinc-900 px-3 text-sm text-zinc-200 hover:border-zinc-700"
onClick={() => {
setSelectedVm(null);
setView('settings');
}}
type="button"
>
<SettingsIcon className="h-4 w-4" />
Settings
</button>
<button
className="inline-flex h-9 items-center gap-2 rounded-md border border-zinc-800 bg-zinc-900 px-3 text-sm text-zinc-200 hover:border-zinc-700"
onClick={refresh}
type="button"
>
<RefreshCw className="h-4 w-4" />
Refresh
</button>
</div>
</div>
</header>
{error ? (
<div className="mx-auto mt-4 flex max-w-7xl items-center gap-2 px-4 text-sm text-amber-300 sm:px-6">
<AlertTriangle className="h-4 w-4" />
{error}
</div>
) : null}
<div className="mx-auto max-w-7xl px-4 py-6 sm:px-6">
{view === 'settings' ? (
<Settings onChanged={refresh} />
) : currentVm ? (
<VMDetail vm={currentVm} jobs={jobs} onBack={() => setSelectedVm(null)} onChanged={refresh} />
) : (
<Dashboard
health={health}
jobs={jobs}
loading={loading}
onManage={(vmName) => {
setSelectedVm(vmName);
setView('dashboard');
}}
vms={vms}
/>
)}
</div>
</main>
);
}