import { useEffect, useState, useMemo, useRef } from 'react' import api from '../api/client' import StatusBadge from '../components/StatusBadge' import WindowsPanel from '../components/WindowsPanel' import { Search, ChevronUp, ChevronDown, Settings2, X } from 'lucide-react' const ALL_COLUMNS = [ { id: 'customer', label: 'Kunde', default: true }, { id: 'name', label: 'Name', default: true, locked: true }, { id: 'hostname', label: 'Hostname', default: false }, { id: 'os', label: 'OS', default: true }, { id: 'version', label: 'Agent Version', default: true }, { id: 'ip', label: 'IP', default: true }, { id: 'uptime', label: 'Uptime', default: true }, { id: 'cpu', label: 'CPU', default: true }, { id: 'ram', label: 'RAM', default: true }, { id: 'disk', label: 'Disk (C:)', default: true }, { id: 'lastresponse', label: 'Letzter Kontakt', default: false }, ] const STORAGE_KEY = 'rmm-windows-columns' function loadColumns() { try { const saved = localStorage.getItem(STORAGE_KEY) if (saved) return JSON.parse(saved) } catch {} return ALL_COLUMNS.filter(c => c.default).map(c => c.id) } function saveColumns(cols) { localStorage.setItem(STORAGE_KEY, JSON.stringify(cols)) } function formatUptime(seconds) { if (!seconds) return '—' const d = Math.floor(seconds / 86400) const h = Math.floor((seconds % 86400) / 3600) const m = Math.floor((seconds % 3600) / 60) if (d > 0) return `${d}d ${h}h` return `${h}h ${m}m` } export default function Windows() { const [agents, setAgents] = useState([]) const [customers, setCustomers] = useState([]) const [agentDetails, setAgentDetails] = useState({}) const [search, setSearch] = useState('') const [loading, setLoading] = useState(true) const [sortKey, setSortKey] = useState('customer') const [sortDir, setSortDir] = useState('asc') const [selectedId, setSelectedId] = useState(null) const [visibleCols, setVisibleCols] = useState(loadColumns) const [showColMenu, setShowColMenu] = useState(false) const colMenuRef = useRef(null) const reload = () => { Promise.all([api.getAgents(), api.getCustomers()]) .then(([a, c]) => { setAgents((a || []).filter(ag => ag.platform === 'windows')) setCustomers(c || []) }) .finally(() => setLoading(false)) } useEffect(() => { reload() const iv = setInterval(reload, 30000) return () => clearInterval(iv) }, []) useEffect(() => { agents.forEach(a => { if (!agentDetails[a.id]) { api.getAgent(a.id).then(d => setAgentDetails(prev => ({ ...prev, [a.id]: d })) ) } }) }, [agents]) // Close column menu on click outside useEffect(() => { const handler = (e) => { if (colMenuRef.current && !colMenuRef.current.contains(e.target)) setShowColMenu(false) } document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) }, []) const customerMap = useMemo(() => Object.fromEntries((customers || []).map(c => [c.id, c])), [customers]) const toggleSort = (key) => { if (sortKey === key) setSortDir(d => d === 'asc' ? 'desc' : 'asc') else { setSortKey(key); setSortDir('asc') } } const toggleCol = (id) => { const col = ALL_COLUMNS.find(c => c.id === id) if (col?.locked) return const next = visibleCols.includes(id) ? visibleCols.filter(c => c !== id) : [...visibleCols, id] setVisibleCols(next) saveColumns(next) } const isColVisible = (id) => visibleCols.includes(id) const enriched = useMemo(() => agents.map(a => { const d = agentDetails[a.id] const win = d?.system_data?.windows const cust = a.customer_id ? customerMap[a.customer_id] : null const memPct = win?.memory?.total_bytes > 0 ? ((win.memory.total_bytes - (win.memory.available_bytes ?? 0)) / win.memory.total_bytes * 100) : null // Ersten FIXED-Disk nehmen (meistens C:) const disk = win?.disks?.[0] const diskPct = disk?.used_percent ?? null return { ...a, customer: cust, customerName: cust?.name || '', customerNumber: cust?.number || '', os: win?.os_version || '—', cpuPct: win?.cpu?.load_percent ?? null, memPct, diskPct, uptime: win?.uptime_seconds || 0, } }), [agents, agentDetails, customerMap]) const filtered = useMemo(() => { let list = enriched if (search) { const q = search.toLowerCase() list = list.filter(a => a.name.toLowerCase().includes(q) || (a.ip || '').toLowerCase().includes(q) || (a.hostname || '').toLowerCase().includes(q) || a.customerName.toLowerCase().includes(q) || a.customerNumber.toLowerCase().includes(q) ) } list = [...list].sort((a, b) => { let va, vb switch (sortKey) { case 'customer': va = a.customerNumber; vb = b.customerNumber; break case 'name': va = a.name; vb = b.name; break case 'os': va = a.os; vb = b.os; break case 'version': va = a.agent_version || ''; vb = b.agent_version || ''; break case 'uptime': return sortDir === 'asc' ? a.uptime - b.uptime : b.uptime - a.uptime case 'cpu': return sortDir === 'asc' ? (a.cpuPct ?? 999) - (b.cpuPct ?? 999) : (b.cpuPct ?? -1) - (a.cpuPct ?? -1) case 'ram': return sortDir === 'asc' ? (a.memPct ?? 999) - (b.memPct ?? 999) : (b.memPct ?? -1) - (a.memPct ?? -1) case 'disk': return sortDir === 'asc' ? (a.diskPct ?? 999) - (b.diskPct ?? 999) : (b.diskPct ?? -1) - (a.diskPct ?? -1) case 'status': va = a.status; vb = b.status; break default: va = a.name; vb = b.name } if (typeof va === 'string') { const cmp = va.localeCompare(vb) return sortDir === 'asc' ? cmp : -cmp } return 0 }) return list }, [enriched, search, sortKey, sortDir]) const SortHeader = ({ label, k, className = '' }) => ( toggleSort(k)} > {label} {sortKey === k && (sortDir === 'asc' ? : )} ) return (

Windows Agents ({agents.length})

setSearch(e.target.value)} className="w-full bg-gray-900 border border-gray-800 rounded pl-10 pr-3 py-2 text-sm text-white placeholder-gray-600 focus:outline-none focus:border-sky-500" />
{/* Spalten-Toggle */}
{showColMenu && (
Sichtbare Spalten
{ALL_COLUMNS.map(col => ( ))}
)}
{loading ? (
Laden...
) : (
{isColVisible('customer') && } {isColVisible('name') && } {isColVisible('hostname') && } {isColVisible('os') && } {isColVisible('version') && } {isColVisible('ip') && } {isColVisible('uptime') && } {isColVisible('cpu') && } {isColVisible('ram') && } {isColVisible('disk') && } {isColVisible('lastresponse') && } {filtered.map(a => ( setSelectedId(a.id)} className={`hover:bg-gray-800/50 transition-colors cursor-pointer ${selectedId === a.id ? 'bg-gray-800/70' : ''}`} > {isColVisible('customer') && ( )} {isColVisible('name') && } {isColVisible('hostname') && } {isColVisible('os') && } {isColVisible('version') && } {isColVisible('ip') && } {isColVisible('uptime') && } {isColVisible('cpu') && } {isColVisible('ram') && } {isColVisible('disk') && } {isColVisible('lastresponse') && ( )} ))}
HOSTNAMEIPLETZTER KONTAKT
{a.customer ? {a.customerNumber} : } {a.name}{a.hostname || '—'}{a.os}{a.agent_version ? `v${a.agent_version}` : '—'}{a.ip || '—'}{formatUptime(a.uptime)} {a.last_heartbeat ? new Date(a.last_heartbeat).toLocaleString('de-DE') : '—'}
{filtered.length === 0 && (
{search ? 'Keine Treffer' : 'Keine Windows Agents registriert'}
)}
)} {selectedId && ( setSelectedId(null)} onReload={reload} /> )}
) } function MiniBar({ value }) { if (value === null || value === undefined) return const color = value > 90 ? 'bg-red-500' : value > 70 ? 'bg-yellow-500' : value > 50 ? 'bg-blue-500' : 'bg-green-500' return (
{value.toFixed(0)}%
) }