feat: Windows Agent Frontend Panel

- WindowsPanel.jsx: Uebersicht (CPU/RAM/Disk), Software (winget), System
- Dashboard: Windows-Agents mit WIN-Badge, oeffnet WindowsPanel
- Gleiche Navigation wie OPNsense/Proxmox (Sidebar-Menue)
This commit is contained in:
cynfo3000 2026-03-10 01:28:30 +01:00
parent 8116b71b48
commit 4f71bd1520
2 changed files with 612 additions and 8 deletions

View File

@ -0,0 +1,586 @@
import { useEffect, useState } from 'react'
import api from '../api/client'
import { useSettingsStore } from '../stores/settings'
import StatusBadge from './StatusBadge'
import TerminalModal from './TerminalModal'
import ScriptModal from './ScriptModal'
import {
X, Cpu, MemoryStick, HardDrive, Monitor, Server, Settings,
Download, Trash2, RefreshCw, Package, Search, Play,
ShieldCheck, Bot, Cable, CalendarClock, ChevronRight,
} from 'lucide-react'
// Navigation
const mainCategories = [
{ id: 'uebersicht', label: 'Uebersicht' },
{ id: 'software', label: 'Software' },
{ id: 'system', label: 'System' },
]
const subTabs = {
software: [
{ id: 'installed', label: 'Installiert', icon: Package },
{ id: 'winget', label: 'winget Updates', icon: RefreshCw },
{ id: 'wupdates', label: 'Windows Updates', icon: ShieldCheck },
],
system: [
{ id: 'services', label: 'Dienste', icon: Settings },
{ id: 'tunnel', label: 'Tunnel', icon: Cable },
{ id: 'tasks', label: 'Aufgaben', icon: CalendarClock },
{ id: 'agent', label: 'Agent', icon: Bot },
],
}
// Panel Shell
function Panel({ onClose, children }) {
return (
<div className="fixed inset-0 z-50 flex items-stretch justify-end bg-black/50">
<div className="w-full max-w-5xl bg-gray-900 flex flex-col shadow-2xl border-l border-gray-700 overflow-hidden">
{children}
</div>
</div>
)
}
// CustomerAssign
function CustomerAssign({ agent, customers, current, onReload }) {
const [editing, setEditing] = useState(false)
const [val, setVal] = useState(current?.id?.toString() || '')
const save = async () => {
if (val === '') await api.unassignAgentCustomer(agent.id)
else await api.assignAgentCustomer(agent.id, parseInt(val))
setEditing(false)
if (onReload) onReload()
}
if (!editing) return (
<button onClick={() => setEditing(true)}
className="text-xs text-gray-500 hover:text-orange-400 transition-colors mt-0.5 flex items-center gap-1">
{current ? `${current.number}${current.name}` : 'Kein Kunde'}
<ChevronRight className="w-3 h-3" />
</button>
)
return (
<div className="flex items-center gap-2 mt-1">
<select value={val} onChange={e => setVal(e.target.value)}
className="bg-gray-800 border border-gray-700 rounded px-2 py-0.5 text-xs text-white focus:outline-none">
<option value="">Kein Kunde</option>
{customers.map(c => <option key={c.id} value={c.id}>{c.number} {c.name}</option>)}
</select>
<button onClick={save} className="text-green-400 text-xs">OK</button>
<button onClick={() => setEditing(false)} className="text-gray-500 text-xs">Abbrechen</button>
</div>
)
}
// Main Component
export default function WindowsPanel({ agentId, customers, onClose, onReload }) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [mainTab, setMainTab] = useState('uebersicht')
const [subTab, setSubTab] = useState(null)
useEffect(() => {
setLoading(true)
setMainTab('uebersicht')
setSubTab(null)
api.getAgent(agentId).then(setData).finally(() => setLoading(false))
}, [agentId])
useEffect(() => {
if (!data) return
const iv = setInterval(() => api.getAgent(agentId).then(setData), 30000)
return () => clearInterval(iv)
}, [agentId, data])
if (loading) return <Panel onClose={onClose}><div className="p-6 text-gray-500">Laden...</div></Panel>
if (!data) return <Panel onClose={onClose}><div className="p-6 text-red-400">Nicht gefunden</div></Panel>
const { agent, system_data: sys, status } = data
const win = sys?.windows
const cust = agent.customer_id ? customers.find(c => c.id === agent.customer_id) : null
const currentSubs = mainTab !== 'uebersicht' ? (subTabs[mainTab] || []) : []
const activeSubTab = currentSubs.find(s => s.id === subTab)?.id ?? currentSubs[0]?.id
const handleMainTab = (id) => {
setMainTab(id)
setSubTab(subTabs[id]?.[0]?.id ?? null)
}
const renderContent = () => {
if (mainTab === 'uebersicht') return <OverviewTab win={win} agent={agent} status={status} />
const t = activeSubTab
if (t === 'installed') return <WingetListTab agentId={agentId} />
if (t === 'winget') return <WingetUpgradeTab agentId={agentId} />
if (t === 'wupdates') return <WindowsUpdatesTab agentId={agentId} />
if (t === 'services') return <ServicesTab win={win} />
if (t === 'tunnel') return <TunnelTab agentId={agentId} agent={agent} />
if (t === 'tasks') return <TasksTab agentId={agentId} agentName={agent.name} />
if (t === 'agent') return <AgentInfoTab agent={agent} status={status} />
return null
}
return (
<Panel onClose={onClose}>
{/* Header */}
<div className="px-6 py-4 bg-gradient-to-r from-gray-800 to-gray-900 border-b border-gray-700">
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-3">
<Monitor className="w-5 h-5 text-blue-400" />
<h2 className="text-lg font-bold text-white">{agent.name}</h2>
<StatusBadge status={status} />
</div>
<CustomerAssign agent={agent} customers={customers} current={cust} onReload={onReload} />
<div className="text-xs text-gray-500 mt-1">
{win?.hostname || agent.hostname}
{win?.os_version && <span> &middot; {win.os_version}</span>}
{win?.domain && <span> &middot; {win.domain}</span>}
<span> &middot; IP: {agent.ip}</span>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={async () => {
if (!confirm(`Agent "${agent.name}" wirklich loeschen?`)) return
await api.deleteAgent(agent.id)
if (onReload) onReload()
onClose()
}}
className="text-gray-500 hover:text-red-400 transition-colors"
title="Agent loeschen"
>
<Trash2 className="w-4 h-4" />
</button>
<button onClick={onClose} className="text-gray-400 hover:text-white">
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* Haupt-Tabs */}
<div className="flex gap-1 mt-4 overflow-x-auto text-xs">
{mainCategories.map(cat => (
<button key={cat.id} onClick={() => handleMainTab(cat.id)}
className={`px-4 py-1.5 whitespace-nowrap transition-colors border-b-2 ${
mainTab === cat.id
? 'text-blue-400 border-blue-400'
: 'text-gray-400 hover:text-gray-200 border-transparent'
}`}>
{cat.label}
</button>
))}
</div>
</div>
{/* Content + Sidebar */}
<div className="flex-1 flex overflow-hidden">
{/* Sub-Navigation */}
{mainTab !== 'uebersicht' && currentSubs.length > 0 && (
<div className="w-44 shrink-0 bg-gray-900/50 border-r border-gray-800 overflow-y-auto py-3">
<div className="px-4 mb-2">
<span className="text-[10px] uppercase tracking-wider text-gray-500 font-semibold">
{mainCategories.find(c => c.id === mainTab)?.label}
</span>
</div>
{currentSubs.map(item => {
const Icon = item.icon
const isActive = activeSubTab === item.id
return (
<button key={item.id} onClick={() => setSubTab(item.id)}
className={`w-full flex items-center gap-2.5 px-4 py-2 text-sm transition-colors ${
isActive
? 'text-blue-400 bg-blue-400/5 border-l-2 border-blue-400'
: 'text-gray-400 hover:text-white border-l-2 border-transparent'
}`}>
{Icon && <Icon className="w-4 h-4 shrink-0" />}
<span>{item.label}</span>
</button>
)
})}
</div>
)}
{/* Inhalt */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{renderContent()}
</div>
</div>
</Panel>
)
}
// Uebersicht
function OverviewTab({ win, agent, status }) {
const fmt = (b) => {
if (!b) return '—'
if (b >= 1073741824) return `${(b / 1073741824).toFixed(1)} GB`
return `${(b / 1048576).toFixed(0)} MB`
}
const fmtUptime = (s) => {
if (!s) return '—'
const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60)
return d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m`
}
if (!win) return <div className="text-gray-500">Keine Systemdaten Agent noch nicht verbunden oder erster Heartbeat ausstehend</div>
const memUsed = win.memory?.total_bytes - win.memory?.available_bytes
const memPct = win.memory?.used_percent ?? 0
return (
<div className="space-y-4">
{/* Stats-Karten */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<StatCard icon={Cpu} label="CPU-Last" value={`${win.cpu?.load_percent?.toFixed(1) ?? '—'}%`} sub={win.cpu?.model?.split(' ').slice(-2).join(' ')} color="text-blue-400" pct={win.cpu?.load_percent} />
<StatCard icon={MemoryStick} label="RAM" value={`${memPct.toFixed(0)}%`} sub={`${fmt(memUsed)} / ${fmt(win.memory?.total_bytes)}`} color="text-purple-400" pct={memPct} />
<StatCard icon={Server} label="Uptime" value={fmtUptime(win.uptime_seconds)} sub="seit letztem Start" color="text-green-400" />
<StatCard icon={Monitor} label="OS" value={win.os_version?.replace('Windows ', 'Win ') || '—'} sub={win.os_build ? `Build ${win.os_build}` : ''} color="text-gray-400" />
</div>
{/* CPU-Details */}
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4">
<div className="text-xs text-gray-500 mb-2 font-semibold uppercase tracking-wide">Prozessor</div>
<div className="text-sm text-white mb-2">{win.cpu?.model || '—'}</div>
<div className="flex gap-6 text-xs text-gray-400">
<span>{win.cpu?.cores ?? '—'} Kerne</span>
<span>{win.cpu?.logical_cores ?? '—'} logische Kerne</span>
</div>
<div className="mt-2">
<UsageBar pct={win.cpu?.load_percent ?? 0} color="bg-blue-500" />
</div>
</div>
{/* Arbeitsspeicher */}
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4">
<div className="text-xs text-gray-500 mb-2 font-semibold uppercase tracking-wide">Arbeitsspeicher</div>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-300">Belegt: {fmt(memUsed)}</span>
<span className="text-gray-500">Gesamt: {fmt(win.memory?.total_bytes)}</span>
</div>
<UsageBar pct={memPct} color={memPct > 85 ? 'bg-red-500' : memPct > 65 ? 'bg-yellow-500' : 'bg-purple-500'} />
</div>
{/* Festplatten */}
{win.disks?.length > 0 && (
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4">
<div className="text-xs text-gray-500 mb-3 font-semibold uppercase tracking-wide">Laufwerke</div>
<div className="space-y-3">
{win.disks.map(d => (
<div key={d.drive}>
<div className="flex justify-between text-sm mb-1">
<span className="text-white font-mono">{d.drive}</span>
<span className="text-gray-400 text-xs">{fmt(d.total_bytes - d.free_bytes)} / {fmt(d.total_bytes)} ({d.used_percent?.toFixed(0)}%)</span>
</div>
<UsageBar pct={d.used_percent} color={d.used_percent > 90 ? 'bg-red-500' : d.used_percent > 75 ? 'bg-yellow-500' : 'bg-green-600'} />
</div>
))}
</div>
</div>
)}
</div>
)
}
// Software / winget
function WingetListTab({ agentId }) {
const [output, setOutput] = useState(null)
const [loading, setLoading] = useState(false)
const [search, setSearch] = useState('')
const load = async () => {
setLoading(true)
try {
const res = await api.execCommand(agentId, 'winget list --accept-source-agreements', 30)
setOutput(res?.output || res?.data?.output || '')
} catch (e) {
setOutput(`Fehler: ${e.message}`)
} finally {
setLoading(false)
}
}
useEffect(() => { load() }, [agentId])
const lines = output ? output.split('\n').filter(l => l.trim() && !l.startsWith('-')) : []
const filtered = search ? lines.filter(l => l.toLowerCase().includes(search.toLowerCase())) : lines
return (
<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-2 w-3.5 h-3.5 text-gray-500" />
<input value={search} onChange={e => setSearch(e.target.value)}
placeholder="Software suchen..."
className="w-full pl-8 pr-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500" />
</div>
<button onClick={load} disabled={loading}
className="flex items-center gap-1.5 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded text-xs text-gray-300 transition-colors">
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />
Aktualisieren
</button>
</div>
{loading ? (
<div className="text-gray-500 text-sm">Lade Liste...</div>
) : (
<pre className="bg-gray-950 rounded p-3 text-xs text-gray-300 font-mono overflow-x-auto whitespace-pre max-h-[500px] overflow-y-auto">
{filtered.join('\n') || 'Keine Eintraege'}
</pre>
)}
</div>
)
}
function WingetUpgradeTab({ agentId }) {
const [output, setOutput] = useState(null)
const [running, setRunning] = useState(false)
const [upgradeAll, setUpgradeAll] = useState(false)
const check = async () => {
setRunning(true)
try {
const res = await api.execCommand(agentId, 'winget upgrade --accept-source-agreements', 60)
setOutput(res?.output || res?.data?.output || '')
} catch (e) { setOutput(`Fehler: ${e.message}`) }
finally { setRunning(false) }
}
const doUpgradeAll = async () => {
if (!confirm('Alle verfügbaren winget-Updates installieren?')) return
setUpgradeAll(true)
try {
const res = await api.execCommand(agentId,
'winget upgrade --all --silent --accept-package-agreements --accept-source-agreements', 600)
setOutput(res?.output || res?.data?.output || '')
} catch (e) { setOutput(`Fehler: ${e.message}`) }
finally { setUpgradeAll(false) }
}
useEffect(() => { check() }, [agentId])
return (
<div className="space-y-3">
<div className="flex gap-2">
<button onClick={check} disabled={running}
className="flex items-center gap-1.5 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded text-xs text-gray-300 transition-colors">
<RefreshCw className={`w-3.5 h-3.5 ${running ? 'animate-spin' : ''}`} />
Pruefen
</button>
<button onClick={doUpgradeAll} disabled={running || upgradeAll}
className="flex items-center gap-1.5 px-3 py-1.5 bg-blue-700 hover:bg-blue-600 rounded text-xs text-white transition-colors">
<Download className="w-3.5 h-3.5" />
{upgradeAll ? 'Installiere...' : 'Alle updaten'}
</button>
</div>
{(running || upgradeAll) && <div className="text-gray-400 text-sm">Bitte warten...</div>}
{output && (
<pre className="bg-gray-950 rounded p-3 text-xs text-gray-300 font-mono overflow-x-auto whitespace-pre max-h-[500px] overflow-y-auto">
{output}
</pre>
)}
</div>
)
}
function WindowsUpdatesTab({ agentId }) {
const [output, setOutput] = useState(null)
const [checking, setChecking] = useState(false)
const [installing, setInstalling] = useState(false)
const script = `$s = New-Object -ComObject Microsoft.Update.Session; $r = $s.CreateUpdateSearcher().Search("IsInstalled=0 and Type='Software'"); $r.Updates | ForEach-Object { Write-Output "KB$($_.KBArticleIDs[0]) — $($_.Title)" }; Write-Output "---"; Write-Output "Gesamt: $($r.Updates.Count) Updates ausstehend"`
const check = async () => {
setChecking(true)
try {
const res = await api.execCommand(agentId, `powershell -NonInteractive -Command "${script.replace(/"/g, '\\"')}"`, 120)
setOutput(res?.output || res?.data?.output || 'Keine Ausgabe')
} catch (e) { setOutput(`Fehler: ${e.message}`) }
finally { setChecking(false) }
}
const install = async () => {
if (!confirm('Windows Updates installieren? Der PC kann danach neu starten.')) return
setInstalling(true)
setOutput('Updates werden installiert — das kann mehrere Minuten dauern...')
try {
const installScript = `$s=New-Object -ComObject Microsoft.Update.Session; $r=$s.CreateUpdateSearcher().Search("IsInstalled=0 and Type='Software'"); if($r.Updates.Count -eq 0){Write-Output "Keine Updates verfuegbar"; exit}; $d=$s.CreateUpdateDownloader(); $d.Updates=$r.Updates; $d.Download(); $i=$s.CreateUpdateInstaller(); $i.Updates=$r.Updates; $ir=$i.Install(); Write-Output "Installiert: $($r.Updates.Count), ResultCode: $($ir.ResultCode)"`
const res = await api.execCommand(agentId, `powershell -NonInteractive -Command "${installScript.replace(/"/g, '\\"')}"`, 1800)
setOutput(res?.output || res?.data?.output || '')
} catch (e) { setOutput(`Fehler: ${e.message}`) }
finally { setInstalling(false) }
}
useEffect(() => { check() }, [agentId])
return (
<div className="space-y-3">
<div className="flex gap-2">
<button onClick={check} disabled={checking || installing}
className="flex items-center gap-1.5 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded text-xs text-gray-300 transition-colors">
<RefreshCw className={`w-3.5 h-3.5 ${checking ? 'animate-spin' : ''}`} />
Pruefen
</button>
<button onClick={install} disabled={checking || installing}
className="flex items-center gap-1.5 px-3 py-1.5 bg-blue-700 hover:bg-blue-600 rounded text-xs text-white transition-colors">
<ShieldCheck className="w-3.5 h-3.5" />
{installing ? 'Installiere...' : 'Jetzt installieren'}
</button>
</div>
{output && (
<pre className="bg-gray-950 rounded p-3 text-xs text-gray-300 font-mono overflow-x-auto whitespace-pre max-h-[500px] overflow-y-auto">
{output}
</pre>
)}
</div>
)
}
// System-Tabs
function ServicesTab({ win }) {
const [search, setSearch] = useState('')
const services = win?.services || []
const filtered = search
? services.filter(s => s.name?.toLowerCase().includes(search.toLowerCase()) ||
s.display_name?.toLowerCase().includes(search.toLowerCase()))
: services
if (services.length === 0) return <div className="text-gray-500 text-sm">Keine Dienstdaten verfuegbar</div>
return (
<div className="space-y-3">
<input value={search} onChange={e => setSearch(e.target.value)}
placeholder="Dienste suchen..."
className="w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500" />
<div className="space-y-0.5">
{filtered.map(s => (
<div key={s.name} className="flex items-center gap-2.5 px-2 py-1 rounded hover:bg-gray-800/50">
<span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${
s.status === 'running' ? 'bg-green-400' :
s.status === 'stopped' ? 'bg-gray-600' : 'bg-yellow-400'
}`} />
<span className="text-xs text-gray-300 flex-1 min-w-0 truncate">{s.display_name || s.name}</span>
<span className="text-[10px] text-gray-600 flex-shrink-0">{s.start_type}</span>
</div>
))}
</div>
</div>
)
}
function TunnelTab({ agentId, agent }) {
const [showTerminal, setShowTerminal] = useState(false)
const [showScript, setShowScript] = useState(false)
return (
<div className="space-y-4">
<div className="flex gap-3">
<button onClick={() => setShowTerminal(true)}
className="flex items-center gap-2 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded text-sm text-white transition-colors">
<Play className="w-4 h-4" />
Terminal
</button>
<button onClick={() => setShowScript(true)}
className="flex items-center gap-2 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded text-sm text-white transition-colors">
<Settings className="w-4 h-4" />
Script
</button>
</div>
{showTerminal && <TerminalModal agentId={agentId} agentName={agent?.name || ''} onClose={() => setShowTerminal(false)} />}
{showScript && <ScriptModal agentId={agentId} agentName={agent?.name || ''} onClose={() => setShowScript(false)} />}
</div>
)
}
function TasksTab({ agentId, agentName }) {
const [cmd, setCmd] = useState('')
const [output, setOutput] = useState(null)
const [running, setRunning] = useState(false)
const run = async () => {
if (!cmd.trim()) return
setRunning(true)
try {
const res = await api.execCommand(agentId, cmd, 60)
setOutput(res?.output || res?.data?.output || '')
} catch (e) { setOutput(`Fehler: ${e.message}`) }
finally { setRunning(false) }
}
return (
<div className="space-y-3">
<div className="flex gap-2">
<input value={cmd} onChange={e => setCmd(e.target.value)}
onKeyDown={e => e.key === 'Enter' && run()}
placeholder="Befehl (PowerShell oder cmd)"
className="flex-1 px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white font-mono focus:outline-none focus:border-blue-500" />
<button onClick={run} disabled={running || !cmd.trim()}
className="flex items-center gap-1.5 px-3 py-1.5 bg-blue-700 hover:bg-blue-600 disabled:bg-gray-700 rounded text-sm text-white transition-colors">
<Play className="w-3.5 h-3.5" />
{running ? 'Laeuft...' : 'Ausfuehren'}
</button>
</div>
{output !== null && (
<pre className="bg-gray-950 rounded p-3 text-xs text-gray-300 font-mono overflow-x-auto whitespace-pre max-h-80 overflow-y-auto">
{output || '(keine Ausgabe)'}
</pre>
)}
</div>
)
}
function AgentInfoTab({ agent, status }) {
return (
<div className="space-y-3">
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4 space-y-2">
{[
['Agent-ID', agent.id],
['Name', agent.name],
['Hostname', agent.hostname],
['IP', agent.ip],
['Version', agent.agent_version || '—'],
['Plattform', agent.platform],
['Status', status],
['Registriert', new Date(agent.registered_at).toLocaleString('de-DE')],
].map(([k, v]) => (
<div key={k} className="flex gap-4">
<span className="text-xs text-gray-500 w-28 shrink-0">{k}</span>
<span className="text-xs text-gray-300 font-mono break-all">{v}</span>
</div>
))}
</div>
</div>
)
}
// Hilfskomponenten
function StatCard({ icon: Icon, label, value, sub, color, pct }) {
return (
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4">
<div className="flex items-center gap-2 mb-1">
<Icon className={`w-4 h-4 ${color}`} />
<span className="text-xs text-gray-500">{label}</span>
</div>
<div className={`text-2xl font-bold ${color}`}>{value}</div>
{sub && <div className="text-xs text-gray-600 mt-0.5 truncate">{sub}</div>}
{pct !== undefined && <UsageBar pct={pct} color={color.replace('text-', 'bg-')} className="mt-2" />}
</div>
)
}
function UsageBar({ pct, color, className = '' }) {
return (
<div className={`w-full bg-gray-700 rounded-full h-1.5 ${className}`}>
<div className={`h-1.5 rounded-full transition-all ${color}`}
style={{ width: `${Math.min(100, pct ?? 0).toFixed(0)}%` }} />
</div>
)
}

View File

@ -3,7 +3,8 @@ import api from '../api/client'
import StatusBadge from '../components/StatusBadge' import StatusBadge from '../components/StatusBadge'
import AgentPanel from '../components/AgentPanel' import AgentPanel from '../components/AgentPanel'
import ProxmoxPanel from '../components/ProxmoxPanel' import ProxmoxPanel from '../components/ProxmoxPanel'
import { Server, HardDrive, Users, Activity, AlertTriangle } from 'lucide-react' import WindowsPanel from '../components/WindowsPanel'
import { Server, HardDrive, Users, Activity, AlertTriangle, Monitor } from 'lucide-react'
export default function Dashboard() { export default function Dashboard() {
const [agents, setAgents] = useState([]) const [agents, setAgents] = useState([])
@ -11,7 +12,7 @@ export default function Dashboard() {
const [agentDetails, setAgentDetails] = useState({}) const [agentDetails, setAgentDetails] = useState({})
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [selectedId, setSelectedId] = useState(null) const [selectedId, setSelectedId] = useState(null)
const [selectedType, setSelectedType] = useState(null) // 'device' or 'proxmox' const [selectedType, setSelectedType] = useState(null) // 'device' | 'proxmox' | 'windows'
const reload = () => { const reload = () => {
Promise.all([api.getAgents(), api.getCustomers()]) Promise.all([api.getAgents(), api.getCustomers()])
@ -34,6 +35,11 @@ export default function Dashboard() {
const d = agentDetails[agentId] const d = agentDetails[agentId]
return d?.system_data?.proxmox?.available === true return d?.system_data?.proxmox?.available === true
} }
const getAgentType = (agent) => {
if (isProxmox(agent.id)) return 'proxmox'
if (agent.platform === 'windows') return 'windows'
return 'device'
}
useEffect(() => { useEffect(() => {
reload() reload()
@ -89,7 +95,7 @@ export default function Dashboard() {
</div> </div>
<div className="divide-y divide-gray-800"> <div className="divide-y divide-gray-800">
{customer.agents.map((agent) => ( {customer.agents.map((agent) => (
<AgentRow key={agent.id} agent={agent} isPve={isProxmox(agent.id)} onClick={() => { setSelectedId(agent.id); setSelectedType(isProxmox(agent.id) ? 'proxmox' : 'device') }} selected={selectedId === agent.id} /> <AgentRow key={agent.id} agent={agent} isPve={isProxmox(agent.id)} onClick={() => { setSelectedId(agent.id); setSelectedType(getAgentType(agent)) }} selected={selectedId === agent.id} />
))} ))}
</div> </div>
</div> </div>
@ -105,14 +111,14 @@ export default function Dashboard() {
{agents {agents
.filter((a) => !a.customer_id) .filter((a) => !a.customer_id)
.map((agent) => ( .map((agent) => (
<AgentRow key={agent.id} agent={agent} isPve={isProxmox(agent.id)} onClick={() => { setSelectedId(agent.id); setSelectedType(isProxmox(agent.id) ? 'proxmox' : 'device') }} selected={selectedId === agent.id} /> <AgentRow key={agent.id} agent={agent} isPve={isProxmox(agent.id)} onClick={() => { setSelectedId(agent.id); setSelectedType(getAgentType(agent)) }} selected={selectedId === agent.id} />
))} ))}
</div> </div>
</div> </div>
)} )}
</div> </div>
{/* Detail Panel — Geraet-Detail */} {/* Detail Panels */}
{selectedId && selectedType === 'proxmox' && ( {selectedId && selectedType === 'proxmox' && (
<ProxmoxPanel <ProxmoxPanel
agentId={selectedId} agentId={selectedId}
@ -121,7 +127,15 @@ export default function Dashboard() {
onReload={reload} onReload={reload}
/> />
)} )}
{selectedId && selectedType !== 'proxmox' && ( {selectedId && selectedType === 'windows' && (
<WindowsPanel
agentId={selectedId}
customers={customers}
onClose={() => { setSelectedId(null); setSelectedType(null) }}
onReload={reload}
/>
)}
{selectedId && selectedType === 'device' && (
<AgentPanel <AgentPanel
agentId={selectedId} agentId={selectedId}
customers={customers} customers={customers}
@ -161,8 +175,12 @@ function AgentRow({ agent, isPve, onClick, selected }) {
<StatusBadge status={agent.status} size="dot" /> <StatusBadge status={agent.status} size="dot" />
<div> <div>
<div className="text-sm text-white inline-flex items-center gap-2"> <div className="text-sm text-white inline-flex items-center gap-2">
<span className={`text-[10px] font-mono px-1 rounded ${agent.platform === 'linux' ? 'bg-blue-900/50 text-blue-400' : 'bg-orange-900/50 text-orange-400'}`}> <span className={`text-[10px] font-mono px-1 rounded ${
{agent.platform === 'linux' ? 'LNX' : 'OPN'} agent.platform === 'linux' ? 'bg-blue-900/50 text-blue-400' :
agent.platform === 'windows' ? 'bg-sky-900/50 text-sky-400' :
'bg-orange-900/50 text-orange-400'
}`}>
{agent.platform === 'linux' ? 'LNX' : agent.platform === 'windows' ? 'WIN' : 'OPN'}
</span> </span>
{agent.name} {agent.name}
</div> </div>