initial commit

This commit is contained in:
2026-05-07 06:41:36 +01:00
commit 783400884a
23 changed files with 2904 additions and 0 deletions

132
src/components/Dashboard.js Normal file
View File

@@ -0,0 +1,132 @@
'use client';
import { useState } from 'react';
import useSWR from 'swr';
import NodeList from './NodeList';
import VMList from './VMList';
import LXCList from './LXCList';
import axios from 'axios';
const POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_API_POLL_INTERVAL || 30000);
/**
* Fetch helper for SWR.
* @param {string} url
* @returns {Promise<{ data: *, error: string | null }>}
*/
async function fetchData(url) {
try {
const res = await axios.get(url);
return res.data;
} catch (err) {
const status = err.response?.status || 'Unknown';
const message = err.response?.data?.message || err.response?.data?.errors || err.message || String(err);
throw new Error(`API error ${status}: ${message}`);
}
}
/** @returns {import('react').ReactNode} */
export default function Dashboard() {
const [activeTab, setActiveTab] = useState('nodes');
const { data: nodesData, error: nodesError } = useSWR('/api/nodes', fetchData, {
refreshInterval: POLL_INTERVAL,
});
const { data: vmsData, error: vmsError } = useSWR('/api/vms', fetchData, {
refreshInterval: POLL_INTERVAL,
});
const { data: lxcData, error: lxcError } = useSWR('/api/lxc', fetchData, {
refreshInterval: POLL_INTERVAL,
});
const { data: summaryData, error: summaryError } = useSWR('/api/status', fetchData, {
refreshInterval: POLL_INTERVAL,
});
const nodes = nodesData?.data ?? [];
const vms = vmsData?.data ?? [];
const lxc = lxcData?.data ?? [];
const summary = summaryData?.data ?? null;
const tabs = [
{ id: 'nodes', label: 'Nodes' },
{ id: 'vms', label: 'VMs' },
{ id: 'lxc', label: 'LXCs' },
];
return (
<div className="min-h-screen p-4 sm:p-6 lg:p-8">
{/* Header */}
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-text-primary">Proxmox Monitor</h1>
<p className="mt-1 text-sm text-text-secondary">
{nodes.length} node{nodes.length !== 1 ? 's' : ''} {summary?.runningVMs ?? 0} running Updated{' '}
{new Date().toLocaleTimeString()}
</p>
</div>
</div>
{/* Summary Cards */}
{summary && !summaryError && (
<div className="mb-6 grid gap-4 sm:grid-cols-4">
<div className="rounded-xl border border-border-card bg-bg-card p-4">
<div className="text-xs uppercase tracking-wider text-text-secondary">Nodes</div>
<div className="mt-1 text-2xl font-bold text-text-primary">{summary.totalNodes}</div>
</div>
<div className="rounded-xl border border-border-card bg-bg-card p-4">
<div className="text-xs uppercase tracking-wider text-text-secondary">Running VMs</div>
<div className="mt-1 text-2xl font-bold text-status-green">{summary.runningVMs}</div>
</div>
<div className="rounded-xl border border-border-card bg-bg-card p-4">
<div className="text-xs uppercase tracking-wider text-text-secondary">Stopped VMs</div>
<div className="mt-1 text-2xl font-bold text-status-red">{summary.stoppedVMs}</div>
</div>
<div className="rounded-xl border border-border-card bg-bg-card p-4">
<div className="text-xs uppercase tracking-wider text-text-secondary">Total Memory</div>
<div className="mt-1 text-xl font-bold text-text-primary">
{summary.totalMemory?.total
? `${(summary.totalMemory.used / (1024 ** 3)).toFixed(1)} / ${(summary.totalMemory.total / (1024 ** 3)).toFixed(1)} GB`
: 'N/A'}
</div>
</div>
</div>
)}
{(summaryError || nodesError) && (
<div className="mb-6 rounded-xl border border-status-red/30 bg-status-red/10 p-4 text-sm text-status-red">
{summaryError || nodesError}
</div>
)}
{/* Tab navigation */}
<div className="mb-4 flex gap-2 border-b border-border-card">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 text-sm font-medium transition-colors ${activeTab === tab.id
? 'border-b-2 border-accent text-text-primary'
: 'text-text-secondary hover:text-text-primary'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Tab content */}
<div>
{activeTab === 'nodes' && <NodeList nodes={nodes} />}
{activeTab === 'vms' && <VMList vms={vms} />}
{activeTab === 'lxc' && <LXCList lxc={lxc} />}
</div>
{/* Footer */}
<div className="mt-8 text-center text-xs text-text-secondary">
Polling every {POLL_INTERVAL / 1000}s Proxmox VE Monitor
</div>
</div>
);
}