diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 5a863a6..1fd349b 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -5,6 +5,7 @@ import AppLayout from './layouts/AppLayout'
import Login from './pages/Login'
import Dashboard from './pages/Dashboard'
import Agents from './pages/Agents'
+import Windows from './pages/Windows'
import AgentDetail from './pages/AgentDetail'
import ProxmoxServers from './pages/ProxmoxServers'
import Customers from './pages/Customers'
@@ -53,6 +54,7 @@ export default function App() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/frontend/src/layouts/AppLayout.jsx b/frontend/src/layouts/AppLayout.jsx
index 4ef0d8e..c2e9d4c 100644
--- a/frontend/src/layouts/AppLayout.jsx
+++ b/frontend/src/layouts/AppLayout.jsx
@@ -14,11 +14,13 @@ import {
Download,
HardDrive,
ClipboardList,
+ Monitor,
} from 'lucide-react'
const navItems = [
{ path: '/', label: 'Dashboard', icon: LayoutDashboard },
{ path: '/agents', label: 'Firewalls', icon: Server },
+ { path: '/windows', label: 'Windows', icon: Monitor },
{ path: '/proxmox', label: 'Proxmox', icon: HardDrive },
{ path: '/tunnels', label: 'Tunnel', icon: Cable },
{ path: '/firmware', label: 'Firmware', icon: Download },
diff --git a/frontend/src/pages/Agents.jsx b/frontend/src/pages/Agents.jsx
index 6576348..007a43d 100644
--- a/frontend/src/pages/Agents.jsx
+++ b/frontend/src/pages/Agents.jsx
@@ -126,7 +126,7 @@ export default function Agents() {
const cust = a.customer_id ? customerMap[a.customer_id] : null
// Nur OPNsense/FreeBSD Agents anzeigen
- if (a.platform === 'linux') return null
+ if (a.platform === 'linux' || a.platform === 'windows') return null
const rootDisk = sys?.disks?.find((d) => d.mount_point === '/')
const diskPct = rootDisk && rootDisk.total_bytes > 0
diff --git a/frontend/src/pages/Windows.jsx b/frontend/src/pages/Windows.jsx
new file mode 100644
index 0000000..21bdba5
--- /dev/null
+++ b/frontend/src/pages/Windows.jsx
@@ -0,0 +1,157 @@
+import { useEffect, useState, useMemo } from 'react'
+import api from '../api/client'
+import StatusBadge from '../components/StatusBadge'
+import WindowsPanel from '../components/WindowsPanel'
+import { Search, Monitor } from 'lucide-react'
+
+function fmtPct(v) {
+ if (v === null || v === undefined) return '—'
+ return `${Number(v).toFixed(0)}%`
+}
+
+function fmtUptime(s) {
+ if (!s) return '—'
+ const d = Math.floor(s / 86400)
+ const h = Math.floor((s % 86400) / 3600)
+ return d > 0 ? `${d}d ${h}h` : `${h}h`
+}
+
+function fmtBytes(b) {
+ if (!b) return '—'
+ if (b >= 1073741824) return `${(b / 1073741824).toFixed(1)} GB`
+ return `${(b / 1048576).toFixed(0)} MB`
+}
+
+export default function Windows() {
+ const [agents, setAgents] = useState([])
+ const [customers, setCustomers] = useState([])
+ const [details, setDetails] = useState({})
+ const [search, setSearch] = useState('')
+ const [selectedId, setSelectedId] = useState(null)
+ const [loading, setLoading] = useState(true)
+
+ const reload = () => {
+ Promise.all([api.getAgents(), api.getCustomers()]).then(([a, c]) => {
+ const win = a.filter(ag => ag.platform === 'windows')
+ setAgents(win)
+ setCustomers(c)
+ setLoading(false)
+ win.forEach(ag => {
+ api.getAgent(ag.id).then(d => setDetails(prev => ({ ...prev, [ag.id]: d })))
+ })
+ })
+ }
+
+ useEffect(() => { reload() }, [])
+
+ const customerMap = useMemo(() =>
+ Object.fromEntries(customers.map(c => [c.id, c])), [customers])
+
+ const rows = useMemo(() => agents.map(a => {
+ const d = details[a.id]
+ const sys = d?.system_data
+ const win = sys?.windows
+ const cust = a.customer_id ? customerMap[a.customer_id] : null
+ const memUsed = win?.memory?.total_bytes > 0
+ ? ((win.memory.total_bytes - win.memory.available_bytes) / win.memory.total_bytes * 100)
+ : null
+ const disk = win?.disks?.[0]
+ return {
+ id: a.id, name: a.name, status: a.status,
+ version: a.agent_version,
+ customer: cust ? `${cust.number} — ${cust.name}` : '—',
+ customer_id: a.customer_id,
+ os: win?.os_version?.replace('Windows ', 'Win ') || '—',
+ cpu: win?.cpu?.load_percent != null ? fmtPct(win.cpu.load_percent) : '—',
+ ram: memUsed != null ? fmtPct(memUsed) : '—',
+ disk: disk?.used_percent != null ? fmtPct(disk.used_percent) : '—',
+ uptime: fmtUptime(win?.uptime_seconds),
+ lastHB: a.last_heartbeat
+ ? new Date(a.last_heartbeat).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
+ : '—',
+ }
+ }), [agents, details, customerMap])
+
+ const filtered = useMemo(() => {
+ if (!search) return rows
+ const q = search.toLowerCase()
+ return rows.filter(r =>
+ r.name.toLowerCase().includes(q) ||
+ r.customer.toLowerCase().includes(q) ||
+ r.os.toLowerCase().includes(q)
+ )
+ }, [rows, search])
+
+ if (loading) return
Laden...
+
+ return (
+
+ {/* Toolbar */}
+
+
+
Windows Agents
+
({agents.length})
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Suchen..."
+ className="pl-8 pr-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-sky-500 w-56"
+ />
+
+
+
+ {/* Tabelle */}
+
+ {filtered.length === 0 ? (
+
+ {agents.length === 0 ? 'Keine Windows Agents registriert' : 'Keine Ergebnisse'}
+
+ ) : (
+
+
+
+ {['Status', 'Name', 'Kunde', 'OS', 'Version', 'CPU', 'RAM', 'Disk', 'Uptime', 'Letzter Kontakt'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {filtered.map(r => (
+ setSelectedId(r.id)}
+ className={`hover:bg-gray-800/50 cursor-pointer transition-colors ${selectedId === r.id ? 'bg-gray-800/70' : ''}`}
+ >
+ |
+
+ |
+ {r.name} |
+ {r.customer} |
+ {r.os} |
+ {r.version ? `v${r.version}` : '—'} |
+ {r.cpu} |
+ {r.ram} |
+ {r.disk} |
+ {r.uptime} |
+ {r.lastHB} |
+
+ ))}
+
+
+ )}
+
+
+ {/* Panel */}
+ {selectedId && (
+
setSelectedId(null)}
+ onReload={reload}
+ />
+ )}
+
+ )
+}