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 (
+
+ )
+}
+
+// ── 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}