import React, { useState, useEffect, useCallback, useRef } from 'react'; import api from '../services/api'; import { toast } from 'react-toastify'; import { useAuth } from '../context/AuthContext'; const ANN_TYPES = { maintenance: { icon: '🔧', label: 'Wartung', color: '#f59e0b' }, warning: { icon: '⚠️', label: 'Warnung', color: '#ef4444' }, info: { icon: 'ℹ️', label: 'Info', color: '#6366f1' } }; const LATEST_AGENT_VERSION = '2.7.0'; const SEVERITY_LABELS = { critical: 'Kritisch', important: 'Wichtig', moderate: 'Moderat', low: 'Niedrig', all: 'Alle' }; const SEVERITY_COLORS = { critical: '#EF4444', important: '#F59E0B', moderate: '#3B82F6', low: '#6B7280', all: '#0D9488' }; const COMMAND_LABELS = { check_updates: '🔍 Update-Scan', install_updates: '⬇️ Installation', reboot: '🔄 Neustart', upgrade_win11: '🪟 Win 11 Upgrade', update_agent: '⬆️ Agent-Update' }; export default function PatchManagementPage() { const { user } = useAuth(); const [tab, setTab] = useState('overview'); const [overview, setOverview] = useState(null); const [groups, setGroups] = useState([]); const [commands, setCommands] = useState([]); const [loading, setLoading] = useState(true); const [groupModal, setGroupModal] = useState(null); const [policyModal, setPolicyModal] = useState(null); const [assignModal, setAssignModal] = useState(null); const [releaseModal, setReleaseModal] = useState(null); const [announcements, setAnnouncements] = useState([]); const [annModal, setAnnModal] = useState(null); // null | { id, title, message, type, target_roles, expires_at } | 'new' const [liveCommands, setLiveCommands] = useState([]); const pollRef = useRef(null); const loadAll = useCallback(async () => { setLoading(true); try { const [ov, gr, cm, ann] = await Promise.all([ api.get('/patch/overview').then(r => r.data), api.get('/patch/groups').then(r => r.data), api.get('/patch/commands').then(r => r.data), api.get('/announcements').then(r => r.data).catch(() => []), ]); setOverview(ov); setGroups(gr); setCommands(cm); setAnnouncements(ann || []); } catch { toast.error('Fehler beim Laden'); } setLoading(false); }, []); useEffect(() => { loadAll(); const t = setInterval(loadAll, 60000); return () => clearInterval(t); }, [loadAll]); // Live-Polling für aktive Commands const startPolling = useCallback(() => { if (pollRef.current) return; pollRef.current = setInterval(async () => { try { const cm = await api.get('/patch/commands').then(r => r.data); setCommands(cm); setLiveCommands(prev => { const updated = prev.map(lc => { const server = cm.find(c => c.id === lc.id); return server ? { ...lc, ...server } : lc; }); // Polling stoppen wenn alle done/failed const allDone = updated.every(c => c.status === 'done' || c.status === 'failed') && updated.length > 0; if (allDone && updated.length > 0) { clearInterval(pollRef.current); pollRef.current = null; loadAll(); // finale Daten laden } return updated; }); } catch {} }, 4000); }, [loadAll]); const addLiveCommand = useCallback((cmd) => { setLiveCommands(prev => { const exists = prev.find(c => c.id === cmd.id); if (exists) return prev; return [{ ...cmd, _ts: Date.now() }, ...prev].slice(0, 20); }); startPolling(); }, [startPolling]); const clearLiveCommands = () => { setLiveCommands([]); if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } }; useEffect(() => () => { if (pollRef.current) clearInterval(pollRef.current); }, []); const triggerCommand = async (agentId, command, hostname) => { try { const r = await api.post('/patch/commands/trigger', { agent_id: agentId, command }); addLiveCommand({ ...r.data, hostname: hostname || agentId }); toast.success(`${COMMAND_LABELS[command]} gesendet — warte auf Agent...`); } catch { toast.error('Fehler'); } }; const triggerGroupCommand = async (groupId, command, agents) => { try { const r = await api.post('/patch/commands/trigger-group', { group_id: groupId, command }); toast.success(`${COMMAND_LABELS[command]} an ${r.data.triggered} Gerät(e) gesendet`); // Live commands nachladen const cm = await api.get('/patch/commands').then(res => res.data); cm.filter(c => c.status === 'pending' || c.status === 'sent').forEach(c => addLiveCommand(c)); } catch { toast.error('Fehler'); } }; const assignAgent = async (agentId, groupId) => { try { await api.post('/patch/assign', { agent_id: agentId, group_id: groupId }); toast.success('Gerät zugewiesen'); setAssignModal(null); loadAll(); } catch { toast.error('Fehler'); } }; const saveGroup = async (form) => { try { if (form.id) await api.put(`/patch/groups/${form.id}`, form); else await api.post('/patch/groups', form); toast.success('Gespeichert'); setGroupModal(null); loadAll(); } catch (e) { toast.error(e.response?.data?.error || 'Fehler'); } }; const deleteGroup = async (id) => { if (!window.confirm('Gruppe löschen?')) return; try { await api.delete(`/patch/groups/${id}`); loadAll(); } catch { toast.error('Fehler'); } }; const openReleaseModal = (groupId, groupName, version, agents, sorted) => { const idx = sorted.findIndex(g => g.id === groupId); const nextGroup = sorted[idx + 1] || null; const affectedCount = agents.filter(a => a.group_id === groupId).length; setReleaseModal({ groupId, groupName, version, affectedCount, nextGroup, isFirst: idx === 0 }); }; const confirmRelease = async () => { if (!releaseModal) return; try { await api.post(`/patch/groups/${releaseModal.groupId}/release`, { version: releaseModal.version }); toast.success(`v${releaseModal.version} für "${releaseModal.groupName}" freigegeben`); setReleaseModal(null); loadAll(); } catch (err) { const msg = err?.response?.data?.error || 'Freigabe fehlgeschlagen'; toast.error(msg); setReleaseModal(null); } }; const saveAnnouncement = async (form) => { try { const payload = { ...form, target_groups: form.target_groups || [] }; if (form.id) await api.put(`/announcements/${form.id}`, payload); else await api.post('/announcements', payload); toast.success('Ankündigung gespeichert'); setAnnModal(null); loadAll(); } catch { toast.error('Fehler'); } }; const deleteAnnouncement = async (id) => { if (!window.confirm('Ankündigung löschen?')) return; try { await api.delete(`/announcements/${id}`); loadAll(); } catch { toast.error('Fehler'); } }; const toggleAnnouncement = async (ann) => { try { await api.put(`/announcements/${ann.id}`, { active: !ann.active }); loadAll(); } catch { toast.error('Fehler'); } }; const savePolicy = async (form) => { try { await api.post('/patch/policies', form); toast.success('Richtlinie gespeichert'); setPolicyModal(null); loadAll(); } catch { toast.error('Fehler'); } }; const deletePolicy = async (id) => { try { await api.delete(`/patch/policies/${id}`); loadAll(); } catch { toast.error('Fehler'); } }; if (loading) return
Laden...
; const stats = overview?.stats || {}; const agents = overview?.agents || []; return (
{/* Header */}

Patch Management

Windows-Updates zentral verwalten und verteilen

{/* Live-Monitor */} {liveCommands.length > 0 && (
c.status === 'pending' || c.status === 'sent') ? 'pulse 1.5s infinite' : 'none' }} /> Live-Monitor — aktualisiert alle 4 Sekunden
{liveCommands.map(c => (
{c.hostname} {COMMAND_LABELS[c.command]}
{c.result &&
âś“ {c.result}
}
{c.created_at ? new Date(c.created_at).toLocaleTimeString('de-DE') : ''}
))}
)} {/* Stats */}
{[ { label: 'Geräte gesamt', value: stats.total || 0, color: 'var(--cereda-primary)' }, { label: 'Konform', value: stats.compliant || 0, color: '#10B981' }, { label: 'Warnung', value: stats.warnings || 0, color: '#F59E0B' }, { label: 'Offline', value: stats.offline || 0, color: '#6B7280' }, { label: 'Ausstehende Updates', value: stats.total_pending_updates || 0, color: '#EF4444' }, ].map(s => (
{s.value}
{s.label}
))}
{/* Tabs */}
{[['overview', '📊 Übersicht'], ['groups', '🗂️ Gruppen'], ['commands', '📋 Verlauf'], ['announcements', `📢 Ankündigungen${announcements.filter(a=>a.active).length > 0 ? ` (${announcements.filter(a=>a.active).length})` : ''}`]].map(([k, l]) => ( ))}
{/* ── Overview Tab ── */} {tab === 'overview' && (
{groups.map(group => { const groupAgents = agents.filter(a => a.group_id === group.id); if (groupAgents.length === 0) return null; const sorted = [...groups].sort((a, b) => (a.sort_order ?? 99) - (b.sort_order ?? 99)); const idx = sorted.findIndex(g => g.id === group.id); const isLatest = group.target_agent_version === LATEST_AGENT_VERSION; const prevDone = idx === 0 || sorted[idx - 1]?.target_agent_version === LATEST_AGENT_VERSION; const canRelease = !isLatest && prevDone; const updatedCount = groupAgents.filter(a => a.agent_version === LATEST_AGENT_VERSION).length; return (

{group.name}

{groupAgents.length} Gerät(e) {/* ── Rollout-Status inline ── */}
{isLatest ? `✓ v${LATEST_AGENT_VERSION}` : group.target_agent_version ? `v${group.target_agent_version}` : `v${LATEST_AGENT_VERSION} verfügbar`} {isLatest && groupAgents.length > 0 && ( · {updatedCount}/{groupAgents.length} aktualisiert )} {canRelease && ( )} {!isLatest && !canRelease && ( ⏳ Wartet auf {sorted[idx - 1]?.name} )}
setAssignModal(a)} groups={groups} />
); })} {/* Ungrouped */} {(() => { const ungrouped = agents.filter(a => !a.group_id); if (!ungrouped.length) return null; return (

Keine Gruppe

{ungrouped.length} Gerät(e)
setAssignModal(a)} groups={groups} />
); })()}
)} {/* ── Groups Tab ── */} {tab === 'groups' && (() => { const sorted = [...groups].sort((a, b) => (a.sort_order ?? 99) - (b.sort_order ?? 99)); const allDone = sorted.every(g => g.target_agent_version === LATEST_AGENT_VERSION); return (
{/* ── Linke Spalte: Gruppen ── */}
{groups.map(g => (
{g.name} {g.agent_count} Gerät(e) {g.target_agent_version && ( v{g.target_agent_version} )}
{g.description &&
{g.description}
} {g.policies?.length > 0 && (
{g.policies.map(p => (
{SEVERITY_LABELS[p.severity]} ≤ {p.max_days} Tage {p.notify_email && 📧}
))}
)} {(!g.policies || g.policies.length === 0) && ( Keine Richtlinien definiert )}
))}
{/* ── Rechte Spalte: Rollout Pipeline ── */}
🚀
Agent Rollout
VerfĂĽgbar: v{LATEST_AGENT_VERSION}
{allDone && ( âś“ Alle aktuell )}
{/* Vertikale Pipeline */}
{sorted.map((g, idx) => { const isLatest = g.target_agent_version === LATEST_AGENT_VERSION; const prevDone = idx === 0 || sorted[idx - 1].target_agent_version === LATEST_AGENT_VERSION; const canRelease = !isLatest && prevDone; const isWaiting = !isLatest && !prevDone; const borderColor = isLatest ? '#34d399' : canRelease ? '#6366f1' : '#6b7280'; const groupAgents = agents.filter(a => a.group_id === g.id); const updatedCount = groupAgents.filter(a => a.agent_version === LATEST_AGENT_VERSION).length; return (
{g.name} {g.target_agent_version ? `v${g.target_agent_version}` : '—'}
{isLatest && groupAgents.length > 0 && (
{updatedCount}/{groupAgents.length} Geräte aktualisiert
)} {isLatest &&
âś“ Freigegeben
} {canRelease && ( )} {isWaiting && (
⏳ Wartet auf {sorted[idx - 1].name}
)}
{idx < sorted.length - 1 && (
↓
)} ); })}
{false && ( )}
); })()} {/* ── Announcements Tab ── */} {tab === 'announcements' && (
{announcements.length === 0 && (
Noch keine AnkĂĽndigungen erstellt.
)} {announcements.map(ann => { const cfg = ANN_TYPES[ann.type] || ANN_TYPES.info; const ackPct = ann.total_agents > 0 ? Math.round((ann.ack_count / ann.total_agents) * 100) : 0; return (
{cfg.icon}
{ann.title} {cfg.label} {!ann.active && Inaktiv} von {ann.created_by_name || '—'} · {new Date(ann.created_at).toLocaleDateString('de-DE')}
{ann.message}
{/* Bestätigungs-Fortschritt */}
{ann.ack_count}/{ann.total_agents} bestätigt
{ann.expires_at && (
Läuft ab: {new Date(ann.expires_at).toLocaleString('de-DE')}
)}
); })}
)} {/* ── Commands Tab ── */} {tab === 'commands' && (
{['Gerät', 'Command', 'Status', 'Ausgelöst von', 'Erstellt', 'Ergebnis'].map(h => ( ))} {commands.map(c => ( ))} {commands.length === 0 && ( )}
{h}
{c.hostname} {COMMAND_LABELS[c.command] || c.command} {c.triggered_by_username || '—'} {c.created_at ? new Date(c.created_at).toLocaleString('de-DE') : '—'} {c.result || '—'}
Noch keine Commands ausgefĂĽhrt
)} {/* ── Modals ── */} {/* ── Announcement Create/Edit Modal ── */} {annModal && (
{ if (e.target === e.currentTarget) setAnnModal(null); }}>

{annModal.id ? 'AnkĂĽndigung bearbeiten' : 'Neue AnkĂĽndigung'}

{Object.entries(ANN_TYPES).map(([k, v]) => ( ))}
setAnnModal(p => ({ ...p, title: e.target.value }))} placeholder="z.B. Wartungsfenster am 06.05.2026" style={{ width: '100%', padding: '9px 12px', borderRadius: 8, border: '1px solid var(--border-color)', background: 'var(--bg-secondary)', color: 'var(--text-primary)', fontSize: 13, boxSizing: 'border-box' }} />