diff --git a/frontend/src/components/WindowsPanel.jsx b/frontend/src/components/WindowsPanel.jsx new file mode 100644 index 0000000..7a89e33 --- /dev/null +++ b/frontend/src/components/WindowsPanel.jsx @@ -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 ( +
+
+ {children} +
+
+ ) +} + +// ── 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 ( + + ) + + return ( +
+ + + +
+ ) +} + +// ── 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
Laden...
+ if (!data) return
Nicht gefunden
+ + 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 + const t = activeSubTab + if (t === 'installed') return + if (t === 'winget') return + if (t === 'wupdates') return + if (t === 'services') return + if (t === 'tunnel') return + if (t === 'tasks') return + if (t === 'agent') return + return null + } + + return ( + + {/* Header */} +
+
+
+
+ +

{agent.name}

+ +
+ +
+ {win?.hostname || agent.hostname} + {win?.os_version && · {win.os_version}} + {win?.domain && · {win.domain}} + · IP: {agent.ip} +
+
+
+ + +
+
+ + {/* Haupt-Tabs */} +
+ {mainCategories.map(cat => ( + + ))} +
+
+ + {/* Content + Sidebar */} +
+ {/* Sub-Navigation */} + {mainTab !== 'uebersicht' && currentSubs.length > 0 && ( +
+
+ + {mainCategories.find(c => c.id === mainTab)?.label} + +
+ {currentSubs.map(item => { + const Icon = item.icon + const isActive = activeSubTab === item.id + return ( + + ) + })} +
+ )} + + {/* Inhalt */} +
+ {renderContent()} +
+
+
+ ) +} + +// ── 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
Keine Systemdaten — Agent noch nicht verbunden oder erster Heartbeat ausstehend
+ + const memUsed = win.memory?.total_bytes - win.memory?.available_bytes + const memPct = win.memory?.used_percent ?? 0 + + return ( +
+ {/* Stats-Karten */} +
+ + + + +
+ + {/* CPU-Details */} +
+
Prozessor
+
{win.cpu?.model || '—'}
+
+ {win.cpu?.cores ?? '—'} Kerne + {win.cpu?.logical_cores ?? '—'} logische Kerne +
+
+ +
+
+ + {/* Arbeitsspeicher */} +
+
Arbeitsspeicher
+
+ Belegt: {fmt(memUsed)} + Gesamt: {fmt(win.memory?.total_bytes)} +
+ 85 ? 'bg-red-500' : memPct > 65 ? 'bg-yellow-500' : 'bg-purple-500'} /> +
+ + {/* Festplatten */} + {win.disks?.length > 0 && ( +
+
Laufwerke
+
+ {win.disks.map(d => ( +
+
+ {d.drive} + {fmt(d.total_bytes - d.free_bytes)} / {fmt(d.total_bytes)} ({d.used_percent?.toFixed(0)}%) +
+ 90 ? 'bg-red-500' : d.used_percent > 75 ? 'bg-yellow-500' : 'bg-green-600'} /> +
+ ))} +
+
+ )} +
+ ) +} + +// ── 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 ( +
+
+
+ + 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" /> +
+ +
+ {loading ? ( +
Lade Liste...
+ ) : ( +
+          {filtered.join('\n') || 'Keine Eintraege'}
+        
+ )} +
+ ) +} + +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 ( +
+
+ + +
+ {(running || upgradeAll) &&
Bitte warten...
} + {output && ( +
+          {output}
+        
+ )} +
+ ) +} + +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 ( +
+
+ + +
+ {output && ( +
+          {output}
+        
+ )} +
+ ) +} + +// ── 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
Keine Dienstdaten verfuegbar
+ + return ( +
+ 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" /> +
+ {filtered.map(s => ( +
+ + {s.display_name || s.name} + {s.start_type} +
+ ))} +
+
+ ) +} + +function TunnelTab({ agentId, agent }) { + const [showTerminal, setShowTerminal] = useState(false) + const [showScript, setShowScript] = useState(false) + return ( +
+
+ + +
+ {showTerminal && setShowTerminal(false)} />} + {showScript && setShowScript(false)} />} +
+ ) +} + +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 ( +
+
+ 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" /> + +
+ {output !== null && ( +
+          {output || '(keine Ausgabe)'}
+        
+ )} +
+ ) +} + +function AgentInfoTab({ agent, status }) { + return ( +
+
+ {[ + ['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]) => ( +
+ {k} + {v} +
+ ))} +
+
+ ) +} + +// ── Hilfskomponenten ────────────────────────────────────────────────────────── + +function StatCard({ icon: Icon, label, value, sub, color, pct }) { + return ( +
+
+ + {label} +
+
{value}
+ {sub &&
{sub}
} + {pct !== undefined && } +
+ ) +} + +function UsageBar({ pct, color, className = '' }) { + return ( +
+
+
+ ) +} diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 0a2a6e8..cbbcf43 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -3,7 +3,8 @@ import api from '../api/client' import StatusBadge from '../components/StatusBadge' import AgentPanel from '../components/AgentPanel' 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() { const [agents, setAgents] = useState([]) @@ -11,7 +12,7 @@ export default function Dashboard() { const [agentDetails, setAgentDetails] = useState({}) const [loading, setLoading] = useState(true) const [selectedId, setSelectedId] = useState(null) - const [selectedType, setSelectedType] = useState(null) // 'device' or 'proxmox' + const [selectedType, setSelectedType] = useState(null) // 'device' | 'proxmox' | 'windows' const reload = () => { Promise.all([api.getAgents(), api.getCustomers()]) @@ -34,6 +35,11 @@ export default function Dashboard() { const d = agentDetails[agentId] 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(() => { reload() @@ -89,7 +95,7 @@ export default function Dashboard() {
{customer.agents.map((agent) => ( - { setSelectedId(agent.id); setSelectedType(isProxmox(agent.id) ? 'proxmox' : 'device') }} selected={selectedId === agent.id} /> + { setSelectedId(agent.id); setSelectedType(getAgentType(agent)) }} selected={selectedId === agent.id} /> ))}
@@ -105,14 +111,14 @@ export default function Dashboard() { {agents .filter((a) => !a.customer_id) .map((agent) => ( - { setSelectedId(agent.id); setSelectedType(isProxmox(agent.id) ? 'proxmox' : 'device') }} selected={selectedId === agent.id} /> + { setSelectedId(agent.id); setSelectedType(getAgentType(agent)) }} selected={selectedId === agent.id} /> ))} )} - {/* Detail Panel — Geraet-Detail */} + {/* Detail Panels */} {selectedId && selectedType === 'proxmox' && ( )} - {selectedId && selectedType !== 'proxmox' && ( + {selectedId && selectedType === 'windows' && ( + { setSelectedId(null); setSelectedType(null) }} + onReload={reload} + /> + )} + {selectedId && selectedType === 'device' && (
- - {agent.platform === 'linux' ? 'LNX' : 'OPN'} + + {agent.platform === 'linux' ? 'LNX' : agent.platform === 'windows' ? 'WIN' : 'OPN'} {agent.name}