diff --git a/frontend/src/pages/Windows.jsx b/frontend/src/pages/Windows.jsx index 21bdba5..197b311 100644 --- a/frontend/src/pages/Windows.jsx +++ b/frontend/src/pages/Windows.jsx @@ -1,149 +1,300 @@ -import { useEffect, useState, useMemo } from 'react' +import { useEffect, useState, useMemo, useRef } from 'react' import api from '../api/client' import StatusBadge from '../components/StatusBadge' import WindowsPanel from '../components/WindowsPanel' -import { Search, Monitor } from 'lucide-react' +import { Search, ChevronUp, ChevronDown, Settings2, X } from 'lucide-react' -function fmtPct(v) { - if (v === null || v === undefined) return '—' - return `${Number(v).toFixed(0)}%` +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 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 saveColumns(cols) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(cols)) } -function fmtBytes(b) { - if (!b) return '—' - if (b >= 1073741824) return `${(b / 1073741824).toFixed(1)} GB` - return `${(b / 1048576).toFixed(0)} MB` +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 [details, setDetails] = useState({}) - const [search, setSearch] = useState('') - const [selectedId, setSelectedId] = useState(null) - const [loading, setLoading] = useState(true) + 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]) => { - 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 }))) + Promise.all([api.getAgents(), api.getCustomers()]) + .then(([a, c]) => { + setAgents((a || []).filter(ag => ag.platform === 'windows')) + setCustomers(c || []) }) - }) + .finally(() => setLoading(false)) } - useEffect(() => { reload() }, []) + 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]) + 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 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 memUsed = win?.memory?.total_bytes > 0 - ? ((win.memory.total_bytes - win.memory.available_bytes) / win.memory.total_bytes * 100) + + 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 { - 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' }) - : '—', + ...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, details, customerMap]) + }), [agents, agentDetails, 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]) + 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]) - if (loading) return
| {h} | - ))} +|||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| + {isColVisible('customer') && | HOSTNAME | } + {isColVisible('os') &&IP | } + {isColVisible('uptime') &&LETZTER KONTAKT | }||||||||||||||||||
|
- |
- {r.name} | -{r.customer} | -{r.os} | -{r.version ? `v${r.version}` : '—'} | -{r.cpu} | -{r.ram} | -{r.disk} | -{r.uptime} | -{r.lastHB} | ++ {a.customer + ? {a.customerNumber} + : —} + | + )} + {isColVisible('name') &&{a.name} | } + {isColVisible('hostname') &&{a.hostname || '—'} | } + {isColVisible('os') &&{a.os} | } + {isColVisible('version') &&{a.agent_version ? `v${a.agent_version}` : '—'} | } + {isColVisible('ip') &&{a.ip || '—'} | } + {isColVisible('uptime') &&{formatUptime(a.uptime)} | } + {isColVisible('cpu') &&+ {a.last_heartbeat ? new Date(a.last_heartbeat).toLocaleString('de-DE') : '—'} + | + )}