feat: ProxmoxPanel mit verschachtelter Navigation wie AgentPanel

Haupt-Tabs: Uebersicht / Virtualisierung / Speicher / System
Sub-Tabs: VMs+Container | Storage+ZFS+Backups+PBS | Dienste+Updates+Aufgaben+Tunnel+Agent
This commit is contained in:
cynfo3000 2026-03-10 00:53:37 +01:00
parent 81669c59a5
commit 67e15040fb
2 changed files with 87 additions and 52 deletions

View File

@ -492,7 +492,7 @@ func (d *Database) writeMetrics(tx *sql.Tx, agentID string, ts time.Time, data *
}
func (d *Database) GetAgents() ([]models.Agent, error) {
rows, err := d.db.Query("SELECT id, name, hostname, ip, opnsense_version, agent_version, platform, registered_at, last_heartbeat, customer_id, update_requested FROM agents ORDER BY name")
rows, err := d.db.Query("SELECT id, name, hostname, ip, opnsense_version, agent_version, platform, registered_at, last_heartbeat, customer_id, update_requested, last_api_key FROM agents ORDER BY name")
if err != nil {
return nil, err
}
@ -504,7 +504,7 @@ func (d *Database) GetAgents() ([]models.Agent, error) {
var lastHB sql.NullTime
var custID sql.NullInt64
if err := rows.Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.AgentVersion, &a.Platform, &a.RegisteredAt, &lastHB, &custID, &a.UpdateRequested); err != nil {
if err := rows.Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.AgentVersion, &a.Platform, &a.RegisteredAt, &lastHB, &custID, &a.UpdateRequested, &a.LastAPIKey); err != nil {
return nil, err
}
if lastHB.Valid {

View File

@ -11,32 +11,51 @@ import {
MonitorSmartphone, Terminal, Cable, ExternalLink, Plus, Unplug, Search, Shield, Play, FileCode,
} from 'lucide-react'
const buildTabs = (hasPBS) => [
{ id: 'overview', label: 'Uebersicht' },
{ id: 'vms', label: 'Virtuelle Maschinen' },
const mainCategories = [
{ id: 'uebersicht', label: 'Uebersicht' },
{ id: 'virtualisierung', label: 'Virtualisierung' },
{ id: 'speicher', label: 'Speicher' },
{ id: 'system', label: 'System' },
]
const buildSubTabs = (hasPBS) => ({
virtualisierung: [
{ id: 'vms', label: 'VMs' },
{ id: 'containers', label: 'Container' },
],
speicher: [
{ id: 'storage', label: 'Storage' },
{ id: 'zfs', label: 'ZFS' },
{ id: 'backups', label: 'Backups' },
...(hasPBS ? [{ id: 'pbs', label: 'Backup Server' }] : []),
{ id: 'tunnel', label: 'Tunnel' },
],
system: [
{ id: 'services', label: 'Dienste' },
{ id: 'updates', label: 'Updates' },
{ id: 'tasks', label: 'Aufgaben' },
{ id: 'backups', label: 'Backups' },
{ id: 'tunnel', label: 'Tunnel' },
{ id: 'agent', label: 'Agent' },
]
],
})
export default function ProxmoxPanel({ agentId, customers, onClose, onReload }) {
const [data, setData] = useState(null)
const [tab, setTab] = useState('overview')
const [mainTab, setMainTab] = useState('uebersicht')
const [subTab, setSubTab] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(true)
setTab('overview')
setMainTab('uebersicht')
setSubTab(null)
api.getAgent(agentId).then(setData).finally(() => setLoading(false))
}, [agentId])
const handleMainTab = (id, subTabs) => {
setMainTab(id)
setSubTab(subTabs?.[id]?.[0]?.id ?? null)
}
useEffect(() => {
if (!data) return
const iv = setInterval(() => {
@ -52,12 +71,32 @@ export default function ProxmoxPanel({ agentId, customers, onClose, onReload })
const proxmox = sys?.proxmox
const pbs = sys?.pbs
const cust = agent.customer_id ? customers.find((c) => c.id === agent.customer_id) : null
const tabs = buildTabs(pbs?.available)
const subs = buildSubTabs(pbs?.available)
const currentSubs = mainTab !== 'uebersicht' ? (subs[mainTab] || []) : []
const activeSubTab = currentSubs.find(s => s.id === subTab)?.id ?? currentSubs[0]?.id
if (!proxmox?.available) {
return <Panel onClose={onClose}><div className="p-6 text-red-400">Keine Proxmox-Daten verfuegbar</div></Panel>
}
const renderContent = () => {
if (!sys) return <div className="text-gray-500">Keine Systemdaten</div>
if (mainTab === 'uebersicht') return <OverviewTab sys={sys} proxmox={proxmox} />
const t = activeSubTab
if (t === 'vms') return <VmsTab proxmox={proxmox} agentId={agentId} />
if (t === 'containers') return <ContainersTab proxmox={proxmox} agentId={agentId} />
if (t === 'storage') return <StorageTab proxmox={proxmox} />
if (t === 'zfs') return <ZfsTab proxmox={proxmox} />
if (t === 'backups') return <BackupsTab sys={sys} />
if (t === 'pbs') return <PBSTab pbs={pbs} agentId={agentId} sys={sys} />
if (t === 'services') return <ServicesTab sys={sys} />
if (t === 'updates') return <UpdatesTab sys={sys} />
if (t === 'tasks') return <LinuxTasksTab agentId={agentId} agentName={data?.agent?.name || ''} />
if (t === 'tunnel') return <TunnelTab agentId={agentId} agent={data?.agent} webguiPort={8006} />
if (t === 'agent') return <AgentTab agent={data?.agent} status={data?.status} />
return null
}
return (
<Panel onClose={onClose}>
{/* Header */}
@ -92,53 +131,49 @@ export default function ProxmoxPanel({ agentId, customers, onClose, onReload })
</div>
</div>
{/* Tabs */}
<div className="flex gap-1 mt-4 overflow-x-auto text-xs">
{tabs.map((t) => (
{/* Haupt-Tabs */}
<div className="flex gap-1 mt-4 text-xs">
{mainCategories.map((cat) => (
<button
key={t.id}
onClick={() => setTab(t.id)}
key={cat.id}
onClick={() => handleMainTab(cat.id, subs)}
className={`px-3 py-1.5 rounded-t whitespace-nowrap transition-colors ${
tab === t.id
mainTab === cat.id
? 'bg-gray-900 text-orange-400 border-t border-x border-gray-700'
: 'text-gray-400 hover:text-gray-200'
}`}
>
{t.label}
{cat.label}
</button>
))}
</div>
</div>
{/* Sub-Tabs */}
{currentSubs.length > 0 && (
<div className="px-6 py-2 bg-gray-900/60 border-b border-gray-800 flex items-center gap-1 text-xs">
<span className="text-gray-600 mr-2">
{mainCategories.find(c => c.id === mainTab)?.label}
</span>
{currentSubs.map((s) => (
<button
key={s.id}
onClick={() => setSubTab(s.id)}
className={`px-2.5 py-1 rounded whitespace-nowrap transition-colors ${
activeSubTab === s.id
? 'bg-gray-700 text-white'
: 'text-gray-500 hover:text-gray-300'
}`}
>
{s.label}
</button>
))}
</div>
)}
{/* Content */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{!sys ? (
<div className="text-gray-500">Keine Systemdaten</div>
) : tab === 'overview' ? (
<OverviewTab sys={sys} proxmox={proxmox} />
) : tab === 'vms' ? (
<VmsTab proxmox={proxmox} agentId={agentId} />
) : tab === 'containers' ? (
<ContainersTab proxmox={proxmox} agentId={agentId} />
) : tab === 'storage' ? (
<StorageTab proxmox={proxmox} />
) : tab === 'zfs' ? (
<ZfsTab proxmox={proxmox} />
) : tab === 'tunnel' ? (
<TunnelTab agentId={agentId} agent={data?.agent} webguiPort={8006} />
) : tab === 'services' ? (
<ServicesTab sys={sys} />
) : tab === 'updates' ? (
<UpdatesTab sys={sys} />
) : tab === 'tasks' ? (
<LinuxTasksTab agentId={agentId} agentName={data?.agent?.name || ''} />
) : tab === 'pbs' ? (
<PBSTab pbs={pbs} agentId={agentId} sys={sys} />
) : tab === 'backups' ? (
<BackupsTab sys={sys} />
) : tab === 'agent' ? (
<AgentTab agent={data?.agent} status={data?.status} />
) : null}
{renderContent()}
</div>
</Panel>
)