2026-06-01 20:49:07 +02:00
import React , { useState , useEffect , useCallback , useRef } from 'react' ;
import { Link } from 'react-router-dom' ;
import networkMonitorService from '../services/networkMonitorService' ;
import monitoringService from '../services/monitoringService' ;
import unifiService from '../services/unifiService' ;
import externalAlertService from '../services/externalAlertService' ;
import LoadingSpinner from '../components/common/LoadingSpinner' ;
import { toast } from 'react-toastify' ;
// ─── Constants ────────────────────────────────────────────────────────────────
const TYPE _META = {
host : { label : 'Host' , icon : '🖥' } ,
switch : { label : 'Switch' , icon : '🔀' } ,
ap : { label : 'Access Point' , icon : '📶' } ,
router : { label : 'Router' , icon : '🌐' } ,
printer : { label : 'Drucker' , icon : '🖨' } ,
nas : { label : 'NAS' , icon : '💾' } ,
service : { label : 'Service' , icon : '⚙' } ,
} ;
const CHECK _TYPES = [ 'icmp' , 'http' , 'https' , 'tcp' , 'snmp' ] ;
const INTERVALS = [
{ value : 15 , label : '15 Sek' } , { value : 30 , label : '30 Sek' } ,
{ value : 60 , label : '1 Min' } , { value : 300 , label : '5 Min' } ,
{ value : 600 , label : '10 Min' } ,
] ;
const REFRESH _INTERVAL = 30 ;
const MAX _TS _POINTS = 60 ;
// State colors – IT Nexus palette (nicht Checkmk!)
const SC = { ok : '#34d399' , warn : '#f59e0b' , crit : '#ef4444' , unknown : '#6b7280' } ;
// ─── Helpers ──────────────────────────────────────────────────────────────────
const timeAgo = ( iso ) => {
if ( ! iso ) return '– ' ;
const m = Math . floor ( ( Date . now ( ) - new Date ( iso ) . getTime ( ) ) / 60000 ) ;
if ( m < 1 ) return 'gerade eben' ;
if ( m < 60 ) return ` vor ${ m } Min ` ;
if ( m < 1440 ) return ` vor ${ Math . floor ( m / 60 ) } Std ` ;
return ` vor ${ Math . floor ( m / 1440 ) } Tagen ` ;
} ;
const duration = ( iso ) => {
if ( ! iso ) return '– ' ;
const m = Math . floor ( ( Date . now ( ) - new Date ( iso ) . getTime ( ) ) / 60000 ) ;
if ( m < 60 ) return ` ${ m } Min ` ;
const h = Math . floor ( m / 60 ) , rm = m % 60 ;
if ( h < 24 ) return ` ${ h } h ${ rm } min ` ;
return ` ${ Math . floor ( h / 24 ) } d ${ h % 24 } h ` ;
} ;
const pct = ( val , total ) => ( total > 0 ? Math . min ( 100 , Math . round ( ( val / total ) * 100 ) ) : 0 ) ;
const uptimeStr = ( h ) => { if ( ! h ) return '– ' ; if ( h < 24 ) return ` ${ Math . floor ( h ) } h ` ; return ` ${ Math . floor ( h / 24 ) } d ${ Math . floor ( h % 24 ) } h ` ; } ;
const netColor = ( s ) => s === 'up' ? SC . ok : s === 'down' ? SC . crit : SC . unknown ;
const netLabel = ( s ) => s === 'up' ? 'UP' : s === 'down' ? 'DOWN' : 'UNBEKANNT' ;
const fmtTime = ( d ) => d instanceof Date ? d . toLocaleTimeString ( 'de-DE' , { hour : '2-digit' , minute : '2-digit' } ) : '– ' ;
const warnings = ( a ) => {
const w = [ ] ;
if ( a . cpu _usage _percent > 90 ) w . push ( { type : 'error' , msg : ` CPU ${ a . cpu _usage _percent ? . toFixed ( 0 ) } % ` } ) ;
else if ( a . cpu _usage _percent > 80 ) w . push ( { type : 'warn' , msg : ` CPU ${ a . cpu _usage _percent ? . toFixed ( 0 ) } % ` } ) ;
if ( a . disk _free _gb != null && a . disk _free _gb < 5 ) w . push ( { type : 'error' , msg : ` Disk ${ a . disk _free _gb ? . toFixed ( 1 ) } GB ` } ) ;
else if ( a . disk _free _gb != null && a . disk _free _gb < 15 ) w . push ( { type : 'warn' , msg : ` Disk ${ a . disk _free _gb ? . toFixed ( 0 ) } GB ` } ) ;
if ( a . ram _total _gb > 0 && pct ( a . ram _used _gb , a . ram _total _gb ) > 90 ) w . push ( { type : 'warn' , msg : 'RAM voll' } ) ;
if ( a . windows _updates _pending > 20 ) w . push ( { type : 'error' , msg : ` ${ a . windows _updates _pending } Updates ` } ) ;
else if ( a . windows _updates _pending > 5 ) w . push ( { type : 'warn' , msg : ` ${ a . windows _updates _pending } Updates ` } ) ;
if ( a . bitlocker _status === 'off' ) w . push ( { type : 'warn' , msg : 'BitLocker aus' } ) ;
if ( a . defender _enabled === 0 ) w . push ( { type : 'error' , msg : 'Defender inaktiv' } ) ;
else if ( a . defender _signatures _age > 7 ) w . push ( { type : 'warn' , msg : ` Defender ${ a . defender _signatures _age } d alt ` } ) ;
return w ;
} ;
const agentServices = ( a ) => [
{ name : 'Agent Verbindung' , status : a . status === 'online' ? 'ok' : 'crit' ,
value : a . status === 'online' ? 'Online' : 'Offline' , detail : ` Checkin: ${ timeAgo ( a . last _checkin ) } ` , perf : null } ,
{ name : 'CPU Auslastung' , status : a . cpu _usage _percent > 90 ? 'crit' : a . cpu _usage _percent > 80 ? 'warn' : 'ok' ,
value : a . cpu _usage _percent != null ? ` ${ a . cpu _usage _percent . toFixed ( 1 ) } % ` : '– ' ,
detail : a . cpu _model ? . replace ( /\(R\)|\(TM\)/g , '' ) || '' ,
perf : a . cpu _usage _percent != null ? { val : a . cpu _usage _percent , max : 100 , warn : 80 , crit : 90 , unit : '%' } : null } ,
{ name : 'RAM Auslastung' , status : pct ( a . ram _used _gb , a . ram _total _gb ) > 95 ? 'crit' : pct ( a . ram _used _gb , a . ram _total _gb ) > 85 ? 'warn' : 'ok' ,
value : a . ram _used _gb != null ? ` ${ a . ram _used _gb . toFixed ( 1 ) } / ${ a . ram _total _gb ? . toFixed ( 0 ) } GB ` : '– ' ,
detail : ` ${ pct ( a . ram _used _gb , a . ram _total _gb ) } % belegt ` ,
perf : a . ram _total _gb > 0 ? { val : a . ram _used _gb , max : a . ram _total _gb , warn : 85 , crit : 95 , unit : '%' } : null } ,
{ name : 'Festplatte C:' , status : ( a . disk _free _gb ? ? 99 ) < 5 ? 'crit' : ( a . disk _free _gb ? ? 99 ) < 15 ? 'warn' : 'ok' ,
value : a . disk _free _gb != null ? ` ${ a . disk _free _gb . toFixed ( 0 ) } GB frei ` : '– ' ,
detail : ` von ${ a . disk _total _gb ? . toFixed ( 0 ) ? ? '– ' } GB ` ,
perf : a . disk _total _gb > 0 ? { val : a . disk _total _gb - a . disk _free _gb , max : a . disk _total _gb , warn : 75 , crit : 90 , unit : '%' , labelVal : ` ${ a . disk _free _gb ? . toFixed ( 0 ) } GB frei ` } : null } ,
{ name : 'Windows Updates' , status : ( a . windows _updates _pending ? ? 0 ) > 20 ? 'crit' : ( a . windows _updates _pending ? ? 0 ) > 5 ? 'warn' : 'ok' ,
value : ` ${ a . windows _updates _pending ? ? 0 } ausstehend ` , detail : '' , perf : null } ,
] ;
const netDeviceServices = ( d , uptimeStat ) => {
const rtt = d . last _rtt _ms ;
const up = uptimeStat ? . pct ? ? null ;
return [
{ name : 'ICMP / Ping' , status : d . last _status === 'up' ? 'ok' : d . last _status === 'down' ? 'crit' : 'unknown' ,
value : d . last _status === 'up' ? 'UP' : d . last _status === 'down' ? 'DOWN' : 'UNBEKANNT' ,
detail : rtt != null ? ` ${ Math . round ( rtt ) } ms ` : '' , perf : null } ,
{ name : 'Antwortzeit' , status : rtt == null ? 'unknown' : rtt > 1000 ? 'crit' : rtt > 200 ? 'warn' : 'ok' ,
value : rtt != null ? ` ${ Math . round ( rtt ) } ms ` : '– ' , detail : '' ,
perf : rtt != null ? { val : rtt , max : 1000 , warn : 200 , crit : 500 , unit : 'ms' , labelVal : ` ${ Math . round ( rtt ) } ms ` } : null } ,
... ( up != null ? [ { name : 'Uptime (24h)' , status : up >= 99 ? 'ok' : up >= 90 ? 'warn' : 'crit' ,
value : ` ${ up } % ` , detail : '' , perf : { val : 100 - up , max : 100 , warn : 10 , crit : 20 , unit : '%' , labelVal : ` ${ up } % up ` } } ] : [ ] ) ,
] ;
} ;
const proxmoxServices = ( px ) => {
if ( ! px ? . node ) return [ ] ;
const { node , vms = [ ] , lxcs = [ ] , storage = [ ] } = px ;
const cpuPct = ( node . cpu || 0 ) * 100 ;
const ramPct = node . memory ? . total > 0 ? ( node . memory . used / node . memory . total ) * 100 : 0 ;
const fsPct = node . rootfs ? . total > 0 ? ( node . rootfs . used / node . rootfs . total ) * 100 : 0 ;
const load = node . loadavg ? . [ 0 ] || 0 ;
const cpus = node . cpuinfo ? . cpus || 1 ;
const runVMs = vms . filter ( v => v . status === 'running' ) . length ;
const runCTs = lxcs . filter ( c => c . status === 'running' ) . length ;
const svcs = [
{ name : 'CPU Auslastung' , status : cpuPct >= 90 ? 'crit' : cpuPct >= 70 ? 'warn' : 'ok' ,
value : ` ${ cpuPct . toFixed ( 1 ) } % ` , detail : ` ${ node . cpuinfo ? . cpus || '?' } CPUs ` ,
perf : { val : cpuPct , max : 100 , warn : 70 , crit : 90 , unit : '%' } } ,
{ name : 'RAM Auslastung' , status : ramPct >= 90 ? 'crit' : ramPct >= 80 ? 'warn' : 'ok' ,
value : ` ${ ramPct . toFixed ( 1 ) } % ` , detail : ` ${ ( node . memory ? . used / 1073741824 ) . toFixed ( 1 ) } / ${ ( node . memory ? . total / 1073741824 ) . toFixed ( 1 ) } GB ` ,
perf : { val : ramPct , max : 100 , warn : 80 , crit : 90 , unit : '%' } } ,
{ name : 'Root Filesystem' , status : fsPct >= 90 ? 'crit' : fsPct >= 80 ? 'warn' : 'ok' ,
value : ` ${ fsPct . toFixed ( 1 ) } % ` , detail : ` ${ ( node . rootfs ? . avail / 1073741824 ) . toFixed ( 1 ) } GB frei ` ,
perf : { val : fsPct , max : 100 , warn : 80 , crit : 90 , unit : '%' } } ,
{ name : 'Load Average' , status : load > cpus * 0.9 ? 'crit' : load > cpus * 0.7 ? 'warn' : 'ok' ,
value : load . toFixed ( 2 ) , detail : ` ${ node . loadavg ? . map ( l => l . toFixed ( 2 ) ) . join ( ' / ' ) || '' } ` ,
perf : { val : load , max : cpus , warn : cpus * 0.7 , crit : cpus * 0.9 , unit : '' , labelVal : load . toFixed ( 2 ) } } ,
{ name : 'VMs' , status : 'ok' , value : ` ${ runVMs } / ${ vms . length } laufend ` , detail : '' , perf : null } ,
{ name : 'Container' , status : 'ok' , value : ` ${ runCTs } / ${ lxcs . length } laufend ` , detail : '' , perf : null } ,
... storage . filter ( s => s . active && s . total > 0 ) . map ( s => {
const p = ( s . used / s . total ) * 100 ;
return { name : ` Storage: ${ s . storage } ` , status : p >= 90 ? 'crit' : p >= 80 ? 'warn' : 'ok' ,
value : ` ${ p . toFixed ( 1 ) } % ` , detail : ` ${ ( s . avail / 1073741824 ) . toFixed ( 1 ) } GB frei ` ,
perf : { val : p , max : 100 , warn : 80 , crit : 90 , unit : '%' } } ;
} ) ,
] ;
return svcs ;
} ;
// ─── UI Primitives ─────────────────────────────────────────────────────────────
// Checkmk-style State-Square: kleines farbiges Label mit Zustandstext
const StateSquare = ( { state , text , size = 'sm' } ) => {
const color = SC [ state ] || SC . unknown ;
const label = text || state ? . toUpperCase ( ) || '?' ;
return (
< span style = { {
display : 'inline-block' , background : ` ${ color } 22 ` , color ,
border : ` 1px solid ${ color } 60 ` , borderRadius : 3 ,
padding : size === 'lg' ? '3px 10px' : '1px 6px' ,
fontSize : size === 'lg' ? 12 : 10 ,
fontWeight : 700 , letterSpacing : .3 , whiteSpace : 'nowrap' ,
fontFamily : 'monospace' ,
} } > { label } < / span >
) ;
} ;
// Checkmk-style Perf-O-Meter mit WARN/CRIT Schwelllinien
const PerfBar = ( { val , max , warn = 80 , crit = 90 , unit = '%' , labelVal = null } ) => {
if ( val == null || max == null || max === 0 ) return < span style = { { color : 'var(--text-muted)' , fontSize : 11 } } > – < / span > ;
const fillPct = Math . min ( ( val / max ) * 100 , 100 ) ;
const warnPct = warn ;
const critPct = crit ;
const color = fillPct >= critPct ? SC . crit : fillPct >= warnPct ? SC . warn : SC . ok ;
const display = labelVal ? ? ` ${ fillPct . toFixed ( 0 ) } ${ unit } ` ;
return (
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , minWidth : 140 } } >
< div style = { { flex : 1 , position : 'relative' , height : 8 , background : 'rgba(255,255,255,0.07)' , borderRadius : 2 , overflow : 'visible' } } >
{ /* Fill */ }
< div style = { { position : 'absolute' , left : 0 , top : 0 , height : '100%' , width : ` ${ fillPct } % ` , background : color , borderRadius : 2 , transition : 'width .5s' , maxWidth : '100%' } } / >
{ /* WARN threshold line */ }
{ warnPct < 100 && (
< div style = { { position : 'absolute' , left : ` ${ warnPct } % ` , top : - 2 , bottom : - 2 , width : 1.5 , background : ` ${ SC . warn } bb ` , borderRadius : 1 } } / >
) }
{ /* CRIT threshold line */ }
{ critPct < 100 && (
< div style = { { position : 'absolute' , left : ` ${ critPct } % ` , top : - 2 , bottom : - 2 , width : 1.5 , background : ` ${ SC . crit } bb ` , borderRadius : 1 } } / >
) }
< / div >
< span style = { { fontSize : 11 , fontWeight : 700 , color , minWidth : 42 , textAlign : 'right' , fontFamily : 'monospace' } } > { display } < / span >
< / div >
) ;
} ;
// Uptime %-Anzeige (für Netzwerkgeräte)
const UptimePill = ( { pct : p } ) => {
if ( p == null ) return < span style = { { color : 'var(--text-muted)' , fontSize : 11 } } > – < / span > ;
const color = p >= 99 ? SC . ok : p >= 90 ? SC . warn : SC . crit ;
return < span style = { { fontSize : 11 , fontWeight : 700 , color , fontFamily : 'monospace' } } > { p } % < / span > ;
} ;
const NetDot = ( { status , size = 10 } ) => (
< span style = { { display : 'inline-block' , width : size , height : size , borderRadius : '50%' , background : netColor ( status ) , boxShadow : status === 'up' ? ` 0 0 6px ${ netColor ( status ) } ` : 'none' , animation : status === 'up' ? 'nmPulse 2s infinite' : 'none' , flexShrink : 0 } } / >
) ;
const UptimeSparkline = ( { checks } ) => {
if ( ! checks || checks . length === 0 ) return < div style = { { color : 'var(--text-muted)' , fontSize : 11 } } > Keine Daten < / div > ;
const seg = checks . slice ( - 80 ) ;
const w = 240 , h = 28 , sw = w / seg . length ;
return (
< svg width = { w } height = { h } style = { { borderRadius : 4 , overflow : 'hidden' } } >
{ seg . map ( ( c , i ) => < rect key = { i } x = { i * sw } y = { 0 } width = { Math . max ( sw - 0.5 , 1 ) } height = { h } fill = { c . status === 'up' ? SC . ok : SC . crit } opacity = { 0.85 } / > ) }
< / svg >
) ;
} ;
const RttSparkline = ( { checks } ) => {
const vals = checks . filter ( c => c . rtt _ms != null ) . map ( c => c . rtt _ms ) ;
if ( vals . length < 2 ) return < div style = { { color : 'var(--text-muted)' , fontSize : 11 } } > – < / div > ;
const max = Math . max ( ... vals ) ;
const w = 240 , h = 40 ;
const pts = vals . slice ( - 60 ) . map ( ( v , i ) => { const x = ( i / ( vals . slice ( - 60 ) . length - 1 ) ) * w ; const y = h - ( v / max ) * ( h - 4 ) - 2 ; return ` ${ x } , ${ y } ` ; } ) . join ( ' ' ) ;
return < svg width = { w } height = { h } > < polyline points = { pts } fill = "none" stroke = "var(--accent)" strokeWidth = "1.5" / > < / svg > ;
} ;
const WarnBadge = ( { w } ) => (
< span style = { { fontSize : 10 , fontWeight : 700 , padding : '2px 6px' , borderRadius : 4 , background : w . type === 'error' ? ` ${ SC . crit } 22 ` : ` ${ SC . warn } 22 ` , color : w . type === 'error' ? SC . crit : SC . warn , border : ` 1px solid ${ w . type === 'error' ? SC . crit : SC . warn } 44 ` } } > { w . msg } < / span >
) ;
const TimeSeriesChart = ( { data , key1 , key2 , height = 110 } ) => {
const vbW = 400 , vbH = height ;
if ( ! data || data . length < 2 ) return < div style = { { height , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , color : 'var(--text-muted)' , fontSize : 12 } } > Daten werden gesammelt ... < / div > ;
const v1 = data . map ( d => d [ key1 ] || 0 ) , v2 = data . map ( d => d [ key2 ] || 0 ) ;
const maxV = Math . max ( ... v1 , ... v2 , 1 ) ;
const pad = { top : 8 , right : 8 , bottom : 24 , left : 24 } ;
const cw = vbW - pad . left - pad . right , ch = vbH - pad . top - pad . bottom ;
const toX = ( i ) => pad . left + ( i / ( data . length - 1 ) ) * cw ;
const toY = ( v ) => pad . top + ch - ( v / maxV ) * ch ;
const path = ( vals ) => vals . map ( ( v , i ) => ` ${ i === 0 ? 'M' : 'L' } ${ toX ( i ) } , ${ toY ( v ) } ` ) . join ( ' ' ) ;
const area = ( vals ) => { const l = vals . map ( ( v , i ) => ` ${ i === 0 ? 'M' : 'L' } ${ toX ( i ) } , ${ toY ( v ) } ` ) . join ( ' ' ) ; const n = vals . length - 1 ; return ` ${ l } L ${ toX ( n ) } , ${ toY ( 0 ) } L ${ toX ( 0 ) } , ${ toY ( 0 ) } Z ` ; } ;
const xl = data . length > 1 ? [ 0 , Math . floor ( data . length / 2 ) , data . length - 1 ] . map ( i => ( { i , t : fmtTime ( data [ i ] . time ) } ) ) : [ ] ;
return (
< svg viewBox = { ` 0 0 ${ vbW } ${ vbH } ` } style = { { width : '100%' , height } } >
{ [ 0 , Math . ceil ( maxV / 2 ) , maxV ] . map ( v => < line key = { v } x1 = { pad . left } y1 = { toY ( v ) } x2 = { pad . left + cw } y2 = { toY ( v ) } stroke = "var(--border-color)" strokeWidth = "0.7" / > ) }
< path d = { area ( v1 ) } fill = { ` ${ SC . crit } 18 ` } / >
< path d = { area ( v2 ) } fill = { ` ${ SC . warn } 18 ` } / >
< path d = { path ( v1 ) } fill = "none" stroke = { SC . crit } strokeWidth = "1.8" / >
< path d = { path ( v2 ) } fill = "none" stroke = { SC . warn } strokeWidth = "1.8" / >
{ [ 0 , Math . ceil ( maxV / 2 ) , maxV ] . map ( v => < text key = { v } x = { pad . left - 4 } y = { toY ( v ) + 4 } textAnchor = "end" fontSize = "9" fill = { SC . unknown } > { v } < / text > ) }
{ xl . map ( ( { i , t } ) => < text key = { i } x = { toX ( i ) } y = { vbH - 4 } textAnchor = "middle" fontSize = "9" fill = { SC . unknown } > { t } < / text > ) }
< / svg >
) ;
} ;
// ─── Checkmk-style Tactical Overview ─────────────────────────────────────────
const TacticalOverview = ( { devices , agents , problems , acknowledged , countdown , lastUpdated , onTabChange , extAlerts = [ ] } ) => {
const openP = problems . filter ( p => ! acknowledged . has ( p . id ) ) ;
const critN = openP . filter ( p => p . severity <= 1 ) . length ;
const warnN = openP . filter ( p => p . severity === 2 ) . length ;
const hostUp = devices . filter ( d => d . last _status === 'up' ) . length ;
const hostDown = devices . filter ( d => d . last _status === 'down' ) . length ;
const hostUnk = devices . filter ( d => ! d . last _status || d . last _status === 'unknown' ) . length ;
const agOnline = agents . filter ( a => a . status === 'online' ) . length ;
const agOffline = agents . filter ( a => a . status === 'offline' ) . length ;
const agWarn = agents . filter ( a => warnings ( a ) . some ( w => w . type === 'warn' ) && a . status === 'online' ) . length ;
const pxAlerts = extAlerts . filter ( a => a . source === 'proxmox' && ! a . acknowledged ) ;
const pxCrit = pxAlerts . filter ( a => a . severity === 'CRIT' ) . length ;
const pxWarn = pxAlerts . filter ( a => a . severity === 'WARN' ) . length ;
const pxOk = pxCrit === 0 && pxWarn === 0 ;
const CountTile = ( { label , count , color , onClick , sub } ) => {
const active = count > 0 ;
const W = 82 , H = 72 , cx = W / 2 , cy = H / 2 , r = 33 ;
const pts = Array . from ( { length : 6 } , ( _ , i ) => {
const a = ( Math . PI / 3 ) * i ;
return ` ${ ( cx + r * Math . cos ( a ) ) . toFixed ( 1 ) } , ${ ( cy + r * Math . sin ( a ) ) . toFixed ( 1 ) } ` ;
} ) . join ( ' ' ) ;
return (
< svg width = { W } height = { H } onClick = { onClick } style = { { cursor : onClick ? 'pointer' : 'default' , flexShrink : 0 , transition : 'filter .15s' } }
onMouseEnter = { e => { if ( active && onClick ) e . currentTarget . style . filter = ` drop-shadow(0 0 7px ${ color } 99) ` ; } }
onMouseLeave = { e => { e . currentTarget . style . filter = '' ; } } >
< polygon points = { pts }
fill = { active ? ` ${ color } 20 ` : 'rgba(255,255,255,0.03)' }
stroke = { active ? color : 'rgba(255,255,255,0.1)' }
strokeWidth = { active ? 1.5 : 1 } / >
< text x = { cx } y = { cy - ( sub ? 8 : 3 ) } textAnchor = "middle" dominantBaseline = "middle"
fill = { active ? color : 'rgba(255,255,255,0.2)' }
fontSize = "21" fontWeight = "800" fontFamily = "monospace,'Courier New'" > { count } < / text >
< text x = { cx } y = { cy + ( sub ? 9 : 12 ) } textAnchor = "middle" dominantBaseline = "middle"
fill = { active ? color : 'rgba(255,255,255,0.2)' }
fontSize = "7.5" fontWeight = "700" letterSpacing = "0.8" > { label } < / text >
{ sub && < text x = { cx } y = { cy + 20 } textAnchor = "middle" dominantBaseline = "middle"
fill = "rgba(255,255,255,0.3)" fontSize = "6.5" > { sub } < / text > }
< / svg >
) ;
} ;
const Divider = ( ) => < div style = { { width : 1 , alignSelf : 'stretch' , background : 'var(--border-color)' , margin : '0 8px' } } / > ;
return (
< div style = { { background : 'var(--bg-secondary)' , border : '1px solid var(--border-color)' , borderRadius : 10 , padding : '14px 20px' , marginBottom : 20 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 0 , flexWrap : 'wrap' , rowGap : 10 } } >
{ /* Section: Zustand */ }
< div style = { { display : 'flex' , flexDirection : 'column' , marginRight : 16 } } >
< span style = { { fontSize : 9 , fontWeight : 700 , textTransform : 'uppercase' , letterSpacing : .5 , color : 'var(--text-muted)' , marginBottom : 6 , paddingLeft : 2 } } > Gesamtzustand < / span >
< div style = { { display : 'flex' , gap : 6 } } >
< CountTile label = "KRITISCH" count = { critN } color = { SC . crit } onClick = { critN > 0 ? ( ) => onTabChange ( 'problems' ) : null } / >
< CountTile label = "WARNUNG" count = { warnN } color = { SC . warn } onClick = { warnN > 0 ? ( ) => onTabChange ( 'problems' ) : null } / >
< CountTile label = "OK" count = { hostUp + agOnline - agWarn } color = { SC . ok } / >
< / div >
< / div >
< Divider / >
{ /* Section: Netzwerk */ }
< div style = { { display : 'flex' , flexDirection : 'column' , marginRight : 16 } } >
< span style = { { fontSize : 9 , fontWeight : 700 , textTransform : 'uppercase' , letterSpacing : .5 , color : 'var(--text-muted)' , marginBottom : 6 , paddingLeft : 2 } } > Netzwerk - Geräte < / span >
< div style = { { display : 'flex' , gap : 6 } } >
< CountTile label = "UP" count = { hostUp } color = { SC . ok } sub = { ` von ${ devices . length } ` } / >
< CountTile label = "DOWN" count = { hostDown } color = { SC . crit } onClick = { hostDown > 0 ? ( ) => onTabChange ( 'network' ) : null } / >
< CountTile label = "?" count = { hostUnk } color = { SC . unknown } / >
< / div >
< / div >
< Divider / >
{ /* Section: Agents */ }
< div style = { { display : 'flex' , flexDirection : 'column' , marginRight : 16 } } >
< span style = { { fontSize : 9 , fontWeight : 700 , textTransform : 'uppercase' , letterSpacing : .5 , color : 'var(--text-muted)' , marginBottom : 6 , paddingLeft : 2 } } > Windows Agents < / span >
< div style = { { display : 'flex' , gap : 6 } } >
< CountTile label = "ONLINE" count = { agOnline } color = { SC . ok } sub = { ` von ${ agents . length } ` } / >
< CountTile label = "OFFLINE" count = { agOffline } color = { SC . crit } onClick = { agOffline > 0 ? ( ) => onTabChange ( 'agents' ) : null } / >
< CountTile label = "WARNUNG" count = { agWarn } color = { SC . warn } onClick = { agWarn > 0 ? ( ) => onTabChange ( 'agents' ) : null } / >
< / div >
< / div >
< Divider / >
{ /* Section: Proxmox */ }
< div style = { { display : 'flex' , flexDirection : 'column' , marginRight : 16 } } >
< span style = { { fontSize : 9 , fontWeight : 700 , textTransform : 'uppercase' , letterSpacing : .5 , color : 'var(--text-muted)' , marginBottom : 6 , paddingLeft : 2 } } > Proxmox < / span >
< div style = { { display : 'flex' , gap : 6 } } >
< CountTile label = "OK" count = { pxOk ? 1 : 0 } color = { SC . ok } sub = "Infrastruktur" / >
< CountTile label = "KRITISCH" count = { pxCrit } color = { SC . crit } onClick = { pxCrit > 0 ? ( ) => onTabChange ( 'overview' ) : null } / >
< CountTile label = "WARNUNG" count = { pxWarn } color = { SC . warn } onClick = { pxWarn > 0 ? ( ) => onTabChange ( 'overview' ) : null } / >
< / div >
< / div >
{ /* Right: status + timer */ }
< div style = { { marginLeft : 'auto' , display : 'flex' , flexDirection : 'column' , alignItems : 'flex-end' , gap : 6 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , fontSize : 12 } } >
< span style = { { color : 'var(--text-muted)' } } > Nächster Refresh : < / span >
< span style = { { fontWeight : 700 , fontFamily : 'monospace' , color : countdown <= 5 ? SC . warn : 'var(--text-primary)' } } > { countdown } s < / span >
< / div >
< div style = { { fontSize : 11 , color : 'var(--text-muted)' , fontFamily : 'monospace' } } > { lastUpdated } < / div >
< div style = { { fontSize : 10 , color : 'var(--text-muted)' } } > { devices . length + agents . length } Hosts gesamt < / div >
< / div >
< / div >
< / div >
) ;
} ;
// ─── Modals (unverändert) ─────────────────────────────────────────────────────
const DiscoverModal = ( { onClose , onAdd } ) => {
const [ subnet , setSubnet ] = useState ( '192.168.0' ) ;
const [ scanning , setScanning ] = useState ( false ) ;
const [ progress , setProgress ] = useState ( 0 ) ;
const [ results , setResults ] = useState ( null ) ;
const [ selected , setSelected ] = useState ( { } ) ;
const [ typeOverrides , setTypeOverrides ] = useState ( { } ) ;
const [ adding , setAdding ] = useState ( false ) ;
const progressRef = useRef ( null ) ;
const startScan = async ( ) => {
setScanning ( true ) ; setResults ( null ) ; setSelected ( { } ) ; setProgress ( 0 ) ;
let p = 0 ;
progressRef . current = setInterval ( ( ) => { p = Math . min ( p + 1.2 , 90 ) ; setProgress ( p ) ; } , 500 ) ;
try {
const res = await networkMonitorService . discover ( subnet ) ;
clearInterval ( progressRef . current ) ; setProgress ( 100 ) ;
setResults ( res . data || [ ] ) ;
const sel = { } ; ( res . data || [ ] ) . forEach ( d => sel [ d . ip ] = true ) ; setSelected ( sel ) ;
} catch { clearInterval ( progressRef . current ) ; toast . error ( 'Scan fehlgeschlagen' ) ; }
finally { setScanning ( false ) ; }
} ;
const addSelected = async ( ) => {
const toAdd = ( results || [ ] ) . filter ( d => selected [ d . ip ] ) ; if ( ! toAdd . length ) return ;
setAdding ( true ) ; let added = 0 ;
for ( const d of toAdd ) { try { const type = typeOverrides [ d . ip ] || d . type ; const device = await networkMonitorService . create ( { name : d . hostname || d . ip , type , host : d . ip , check _type : d . check _type , interval _sec : 60 , timeout _sec : 5 , enabled : 1 } ) ; onAdd ( device ) ; added ++ ; } catch { } }
toast . success ( ` ${ added } Gerät ${ added !== 1 ? 'e' : '' } hinzugefügt ` ) ; setAdding ( false ) ; onClose ( ) ;
} ;
const toggleAll = ( val ) => { const sel = { } ; ( results || [ ] ) . forEach ( d => sel [ d . ip ] = val ) ; setSelected ( sel ) ; } ;
const inp = { padding : '8px 12px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'var(--bg-secondary)' , color : 'var(--text-primary)' , fontSize : 13 } ;
return (
< div style = { { position : 'fixed' , inset : 0 , background : 'rgba(0,0,0,.65)' , zIndex : 1000 , display : 'flex' , alignItems : 'center' , justifyContent : 'center' } } onClick = { e => e . target === e . currentTarget && ! scanning && onClose ( ) } >
< div style = { { background : 'var(--bg-primary)' , borderRadius : 12 , width : '100%' , maxWidth : 600 , maxHeight : '90vh' , overflow : 'hidden' , display : 'flex' , flexDirection : 'column' , border : '1px solid var(--border-color)' } } >
< div style = { { padding : '16px 20px' , borderBottom : '1px solid var(--border-color)' , display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' } } >
< h2 style = { { fontSize : 16 , fontWeight : 700 } } > 📡 Netzwerk scannen < / h2 >
{ ! scanning && < button onClick = { onClose } style = { { background : 'none' , border : 'none' , color : 'var(--text-muted)' , fontSize : 22 , cursor : 'pointer' } } > × < / button > }
< / div >
< div style = { { padding : 20 , overflowY : 'auto' , flex : 1 } } >
< div style = { { display : 'flex' , gap : 10 , marginBottom : 16 , alignItems : 'flex-end' } } >
< div style = { { flex : 1 } } >
< label style = { { fontSize : 11 , color : 'var(--text-muted)' , fontWeight : 700 , textTransform : 'uppercase' , letterSpacing : '.4px' , display : 'block' , marginBottom : 6 } } > Subnetz ( erste 3 Oktette ) < / label >
< input style = { { ... inp , width : '100%' , boxSizing : 'border-box' } } value = { subnet } onChange = { e => setSubnet ( e . target . value ) } disabled = { scanning } / >
< / div >
< button onClick = { startScan } disabled = { scanning || ! subnet } style = { { padding : '9px 20px' , borderRadius : 7 , border : 'none' , background : 'var(--accent)' , color : '#fff' , cursor : scanning ? 'not-allowed' : 'pointer' , fontWeight : 700 , fontSize : 13 } } > { scanning ? 'Scanne...' : '▶ Scan starten' } < / button >
< / div >
{ ( scanning || results !== null ) && (
< div style = { { marginBottom : 16 } } >
< div style = { { display : 'flex' , justifyContent : 'space-between' , fontSize : 12 , color : 'var(--text-muted)' , marginBottom : 6 } } >
< span > { scanning ? ` Scanne ${ subnet } .1– ${ subnet } .254... ` : ` ${ results ? . length || 0 } Geräte gefunden ` } < / span >
< span > { Math . round ( progress ) } % < / span >
< / div >
< div style = { { background : 'var(--border-color)' , borderRadius : 4 , height : 6 , overflow : 'hidden' } } >
< div style = { { height : '100%' , width : ` ${ progress } % ` , background : progress === 100 ? SC . ok : 'var(--accent)' , borderRadius : 4 , transition : 'width .4s' } } / >
< / div >
< / div >
) }
{ results !== null && results . length === 0 && < div style = { { textAlign : 'center' , color : 'var(--text-muted)' , padding : 30 } } > Keine neuen Geräte im Subnetz { subnet } . x < / div > }
{ results && results . length > 0 && (
< >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 8 } } >
< span style = { { fontSize : 13 , fontWeight : 600 } } > { Object . values ( selected ) . filter ( Boolean ) . length } / { results . length } ausgewählt < / span >
< div style = { { display : 'flex' , gap : 8 } } >
< button onClick = { ( ) => toggleAll ( true ) } style = { { fontSize : 11 , padding : '3px 10px' , borderRadius : 5 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , cursor : 'pointer' } } > Alle < / button >
< button onClick = { ( ) => toggleAll ( false ) } style = { { fontSize : 11 , padding : '3px 10px' , borderRadius : 5 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , cursor : 'pointer' } } > Keine < / button >
< / div >
< / div >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 6 } } >
{ results . map ( d => (
< div key = { d . ip } onClick = { ( ) => setSelected ( s => ( { ... s , [ d . ip ] : ! s [ d . ip ] } ) ) } style = { { display : 'flex' , alignItems : 'center' , gap : 12 , padding : '10px 12px' , borderRadius : 8 , background : selected [ d . ip ] ? ` ${ SC . ok } 10 ` : 'var(--bg-secondary)' , border : ` 1px solid ${ selected [ d . ip ] ? SC . ok + '50' : 'var(--border-color)' } ` , cursor : 'pointer' } } >
< input type = "checkbox" checked = { ! ! selected [ d . ip ] } onChange = { ( ) => { } } style = { { accentColor : 'var(--accent)' } } / >
< span style = { { fontSize : 18 } } > { TYPE _META [ typeOverrides [ d . ip ] || d . type ] ? . icon || '🖥' } < / span >
< div style = { { flex : 1 , minWidth : 0 } } >
< div style = { { fontWeight : 600 , fontSize : 13 } } > { d . ip } < / div >
{ d . hostname && < div style = { { fontSize : 11 , color : 'var(--text-muted)' , overflow : 'hidden' , textOverflow : 'ellipsis' } } > { d . hostname } < / div > }
{ d . open _ports ? . length > 0 && < div style = { { fontSize : 10 , color : 'var(--text-muted)' } } > Ports : { d . open _ports . join ( ', ' ) } < / div > }
< / div >
< select value = { typeOverrides [ d . ip ] || d . type } onChange = { e => { e . stopPropagation ( ) ; setTypeOverrides ( t => ( { ... t , [ d . ip ] : e . target . value } ) ) ; } } onClick = { e => e . stopPropagation ( ) } style = { { fontSize : 11 , padding : '3px 6px' , borderRadius : 5 , border : '1px solid var(--border-color)' , background : 'var(--bg-primary)' , color : 'var(--text-primary)' , cursor : 'pointer' } } >
{ Object . entries ( TYPE _META ) . map ( ( [ k , v ] ) => < option key = { k } value = { k } > { v . icon } { v . label } < / option > ) }
< / select >
< / div >
) ) }
< / div >
< / >
) }
< / div >
{ results && results . length > 0 && (
< div style = { { padding : '14px 20px' , borderTop : '1px solid var(--border-color)' , display : 'flex' , justifyContent : 'flex-end' , gap : 10 } } >
< button onClick = { onClose } disabled = { adding } style = { { padding : '8px 16px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-primary)' , cursor : 'pointer' , fontSize : 13 } } > Abbrechen < / button >
< button onClick = { addSelected } disabled = { adding || ! Object . values ( selected ) . some ( Boolean ) } style = { { padding : '8px 18px' , borderRadius : 7 , border : 'none' , background : 'var(--accent)' , color : '#fff' , cursor : 'pointer' , fontWeight : 700 , fontSize : 13 } } > { adding ? 'Lädt...' : ` ${ Object . values ( selected ) . filter ( Boolean ) . length } hinzufügen ` } < / button >
< / div >
) }
< / div >
< / div >
) ;
} ;
const DeviceFormModal = ( { device , onSave , onClose } ) => {
const [ form , setForm ] = useState ( { name : '' , type : 'host' , host : '' , check _type : 'icmp' , port : '' , http _path : '/' , http _keyword : '' , snmp _community : 'public' , snmp _version : '2c' , interval _sec : 60 , timeout _sec : 5 , enabled : 1 , notify _email : '' , location : '' , ... ( device || { } ) } ) ;
const [ saving , setSaving ] = useState ( false ) ;
const set = ( k , v ) => setForm ( f => ( { ... f , [ k ] : v } ) ) ;
const save = async ( ) => {
if ( ! form . name || ! form . host ) return toast . error ( 'Name und Host sind Pflichtfelder' ) ;
setSaving ( true ) ;
try { const r = device ? await networkMonitorService . update ( device . id , form ) : await networkMonitorService . create ( form ) ; onSave ( r ) ; onClose ( ) ; toast . success ( device ? 'Aktualisiert' : 'Hinzugefügt' ) ; }
catch { toast . error ( 'Fehler beim Speichern' ) ; } finally { setSaving ( false ) ; }
} ;
const inp = { width : '100%' , padding : '8px 10px' , borderRadius : 6 , border : '1px solid var(--border-color)' , background : 'var(--bg-primary)' , color : 'var(--text-primary)' , fontSize : 13 , boxSizing : 'border-box' } ;
const lbl = { fontSize : 11 , color : 'var(--text-muted)' , fontWeight : 700 , marginBottom : 4 , display : 'block' , textTransform : 'uppercase' , letterSpacing : '.4px' } ;
return (
< div style = { { position : 'fixed' , inset : 0 , background : 'rgba(0,0,0,.6)' , zIndex : 1000 , display : 'flex' , alignItems : 'center' , justifyContent : 'center' } } onClick = { e => e . target === e . currentTarget && onClose ( ) } >
< div style = { { background : 'var(--bg-primary)' , borderRadius : 12 , width : '100%' , maxWidth : 540 , maxHeight : '90vh' , overflow : 'auto' , border : '1px solid var(--border-color)' } } >
< div style = { { padding : '18px 20px' , borderBottom : '1px solid var(--border-color)' , display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' } } >
< h2 style = { { fontSize : 16 , fontWeight : 700 } } > { device ? 'Gerät bearbeiten' : 'Neues Gerät' } < / h2 >
< button onClick = { onClose } style = { { background : 'none' , border : 'none' , color : 'var(--text-muted)' , fontSize : 20 , cursor : 'pointer' } } > × < / button >
< / div >
< div style = { { padding : 20 , display : 'grid' , gap : 14 } } >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 } } >
< div > < label style = { lbl } > Name * < / label > < input style = { inp } value = { form . name } onChange = { e => set ( 'name' , e . target . value ) } / > < / div >
< div > < label style = { lbl } > Typ < / label > < select style = { inp } value = { form . type } onChange = { e => set ( 'type' , e . target . value ) } > { Object . entries ( TYPE _META ) . map ( ( [ k , v ] ) => < option key = { k } value = { k } > { v . icon } { v . label } < / option > ) } < / select > < / div >
< / div >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 } } >
< div > < label style = { lbl } > Host / IP * < / label > < input style = { inp } value = { form . host } onChange = { e => set ( 'host' , e . target . value ) } / > < / div >
< div > < label style = { lbl } > Standort < / label > < input style = { inp } value = { form . location || '' } onChange = { e => set ( 'location' , e . target . value ) } / > < / div >
< / div >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr 1fr' , gap : 12 } } >
< div > < label style = { lbl } > Check - Typ < / label > < select style = { inp } value = { form . check _type } onChange = { e => set ( 'check_type' , e . target . value ) } > { CHECK _TYPES . map ( t => < option key = { t } value = { t } > { t . toUpperCase ( ) } < / option > ) } < / select > < / div >
{ [ 'tcp' , 'http' , 'https' ] . includes ( form . check _type ) && < div > < label style = { lbl } > Port < / label > < input style = { inp } type = "number" value = { form . port || '' } onChange = { e => set ( 'port' , e . target . value ) } / > < / div > }
< div > < label style = { lbl } > Intervall < / label > < select style = { inp } value = { form . interval _sec } onChange = { e => set ( 'interval_sec' , parseInt ( e . target . value ) ) } > { INTERVALS . map ( i => < option key = { i . value } value = { i . value } > { i . label } < / option > ) } < / select > < / div >
< / div >
{ [ 'http' , 'https' ] . includes ( form . check _type ) && (
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 } } >
< div > < label style = { lbl } > Pfad < / label > < input style = { inp } value = { form . http _path || '/' } onChange = { e => set ( 'http_path' , e . target . value ) } / > < / div >
< div > < label style = { lbl } > Keyword ( opt . ) < / label > < input style = { inp } value = { form . http _keyword || '' } onChange = { e => set ( 'http_keyword' , e . target . value ) } / > < / div >
< / div >
) }
{ form . check _type === 'snmp' && (
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 } } >
< div > < label style = { lbl } > Community < / label > < input style = { inp } value = { form . snmp _community || 'public' } onChange = { e => set ( 'snmp_community' , e . target . value ) } placeholder = "public" / > < / div >
< div > < label style = { lbl } > Version < / label > < select style = { inp } value = { form . snmp _version || '2c' } onChange = { e => set ( 'snmp_version' , e . target . value ) } > < option value = "2c" > v2c < / option > < option value = "1" > v1 < / option > < / select > < / div >
< / div >
) }
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 } } >
< div > < label style = { lbl } > Timeout ( s ) < / label > < input style = { inp } type = "number" min = "1" max = "30" value = { form . timeout _sec } onChange = { e => set ( 'timeout_sec' , parseInt ( e . target . value ) ) } / > < / div >
< div > < label style = { lbl } > Alert E - Mail < / label > < input style = { inp } value = { form . notify _email || '' } onChange = { e => set ( 'notify_email' , e . target . value ) } / > < / div >
< / div >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 10 } } >
< input type = "checkbox" id = "devEnabled" checked = { ! ! form . enabled } onChange = { e => set ( 'enabled' , e . target . checked ? 1 : 0 ) } / >
< label htmlFor = "devEnabled" style = { { fontSize : 13 , cursor : 'pointer' } } > Monitoring aktiv < / label >
< / div >
< / div >
< div style = { { padding : '14px 20px' , borderTop : '1px solid var(--border-color)' , display : 'flex' , justifyContent : 'flex-end' , gap : 10 } } >
< button onClick = { onClose } style = { { padding : '8px 18px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-primary)' , cursor : 'pointer' , fontSize : 13 } } > Abbrechen < / button >
< button onClick = { save } disabled = { saving } style = { { padding : '8px 18px' , borderRadius : 7 , border : 'none' , background : 'var(--accent)' , color : '#fff' , cursor : 'pointer' , fontSize : 13 , fontWeight : 600 } } > { saving ? '...' : 'Speichern' } < / button >
< / div >
< / div >
< / div >
) ;
} ;
const NetworkDetailModal = ( { device , onClose , onEdit , onDelete , onCheckNow } ) => {
const [ checksData , setChecksData ] = useState ( null ) ;
const [ timeRange , setTimeRange ] = useState ( 24 ) ;
const [ checking , setChecking ] = useState ( false ) ;
useEffect ( ( ) => { networkMonitorService . getChecks ( device . id , timeRange ) . then ( setChecksData ) . catch ( ( ) => { } ) ; } , [ device . id , timeRange ] ) ;
const doCheckNow = async ( ) => { setChecking ( true ) ; try { await onCheckNow ( device . id ) ; toast . success ( 'Check ausgeführt' ) ; const d = await networkMonitorService . getChecks ( device . id , timeRange ) ; setChecksData ( d ) ; } catch { toast . error ( 'Fehlgeschlagen' ) ; } finally { setChecking ( false ) ; } } ;
const meta = TYPE _META [ device . type ] || TYPE _META . host ;
const uptime = checksData ? . uptime , checks = checksData ? . checks || [ ] ;
return (
< div style = { { position : 'fixed' , inset : 0 , background : 'rgba(0,0,0,.6)' , zIndex : 1000 , display : 'flex' , alignItems : 'center' , justifyContent : 'center' } } onClick = { e => e . target === e . currentTarget && onClose ( ) } >
< div style = { { background : 'var(--bg-primary)' , borderRadius : 12 , width : '100%' , maxWidth : 660 , maxHeight : '90vh' , overflow : 'auto' , border : ` 1px solid var(--border-color) ` , borderTop : ` 3px solid ${ netColor ( device . last _status ) } ` } } >
< div style = { { padding : '16px 20px' , display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , borderBottom : '1px solid var(--border-color)' } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 10 } } >
< span style = { { fontSize : 24 } } > { meta . icon } < / span >
< div > < div style = { { fontWeight : 700 , fontSize : 16 } } > { device . name } < / div > < div style = { { color : 'var(--text-muted)' , fontSize : 12 } } > { device . host } · { meta . label } < / div > < / div >
< / div >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 } } >
< StateSquare state = { device . last _status === 'up' ? 'ok' : device . last _status === 'down' ? 'crit' : 'unknown' } text = { netLabel ( device . last _status ) } size = "lg" / >
< button onClick = { onClose } style = { { background : 'none' , border : 'none' , color : 'var(--text-muted)' , fontSize : 22 , cursor : 'pointer' } } > × < / button >
< / div >
< / div >
< div style = { { padding : 20 } } >
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(3,1fr)' , gap : 10 , marginBottom : 20 } } >
{ [ { label : 'Letzter Check' , value : timeAgo ( device . last _checked ) } , { label : 'Antwortzeit' , value : device . last _rtt _ms != null ? ` ${ Math . round ( device . last _rtt _ms ) } ms ` : '– ' } , { label : ` Uptime ${ timeRange } h ` , value : uptime ? . total > 0 ? ` ${ uptime . pct } % ` : '– ' , color : uptime ? . pct >= 99 ? SC . ok : uptime ? . pct >= 90 ? SC . warn : SC . crit } ] . map ( m => (
< div key = { m . label } style = { { background : 'var(--bg-secondary)' , borderRadius : 8 , padding : '10px 14px' } } >
< div style = { { fontSize : 11 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '.4px' , marginBottom : 4 } } > { m . label } < / div >
< div style = { { fontSize : 18 , fontWeight : 700 , color : m . color || 'var(--text-primary)' , fontFamily : 'monospace' } } > { m . value } < / div >
< / div >
) ) }
< / div >
< div style = { { marginBottom : 16 } } >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 8 } } >
< div style = { { fontSize : 11 , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '.4px' } } > Uptime - Verlauf < / div >
< div style = { { display : 'flex' , gap : 4 } } > { [ 24 , 48 , 168 ] . map ( h => < button key = { h } onClick = { ( ) => setTimeRange ( h ) } style = { { padding : '3px 10px' , borderRadius : 5 , fontSize : 11 , fontWeight : 600 , cursor : 'pointer' , border : ` 1px solid ${ timeRange === h ? 'var(--accent)' : 'var(--border-color)' } ` , background : timeRange === h ? 'var(--accent)' : 'var(--bg-secondary)' , color : timeRange === h ? '#fff' : 'var(--text-muted)' } } > { h === 168 ? '7d' : ` ${ h } h ` } < / button > ) } < / div >
< / div >
< div style = { { background : 'var(--bg-secondary)' , borderRadius : 8 , padding : 10 } } >
< UptimeSparkline checks = { checks } / >
< div style = { { display : 'flex' , justifyContent : 'space-between' , fontSize : 10 , color : 'var(--text-muted)' , marginTop : 4 } } >
< span > Grün = Online · Rot = Offline < / span > < span > { uptime ? . up ? ? 0 } / { uptime ? . total ? ? 0 } OK < / span >
< / div >
< / div >
< / div >
{ checks . some ( c => c . rtt _ms != null ) && (
< div style = { { marginBottom : 16 } } >
< div style = { { fontSize : 11 , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '.4px' , marginBottom : 8 } } > Antwortzeit ( ms ) < / div >
< div style = { { background : 'var(--bg-secondary)' , borderRadius : 8 , padding : 10 } } > < RttSparkline checks = { checks } / > < / div >
< / div >
) }
< div style = { { background : 'var(--bg-secondary)' , borderRadius : 8 , padding : '12px 14px' , marginBottom : 16 } } >
< div style = { { fontSize : 11 , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '.4px' , marginBottom : 8 } } > Konfiguration < / div >
< div style = { { display : 'grid' , gridTemplateColumns : '130px 1fr' , gap : '5px 12px' , fontSize : 13 , lineHeight : 1.7 } } >
{ [ [ 'Check-Typ' , device . check _type ? . toUpperCase ( ) ] , [ 'Intervall' , ` ${ device . interval _sec } s ` ] , [ 'Timeout' , ` ${ device . timeout _sec } s ` ] , device . port && [ 'Port' , device . port ] , device . location && [ 'Standort' , device . location ] , device . notify _email && [ 'Alert E-Mail' , device . notify _email ] ] . filter ( Boolean ) . map ( ( [ k , v ] ) => (
< React.Fragment key = { k } > < span style = { { color : 'var(--text-muted)' } } > { k } < / span > < span style = { { fontWeight : 600 } } > { v } < / span > < / React.Fragment >
) ) }
< / div >
< / div >
{ [ ... checks ] . reverse ( ) . slice ( 0 , 15 ) . length > 0 && (
< div >
< div style = { { fontSize : 11 , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '.4px' , marginBottom : 8 } } > Letzte Checks < / div >
< table style = { { width : '100%' , borderCollapse : 'collapse' , fontSize : 12 } } >
< tbody > { [ ... checks ] . reverse ( ) . slice ( 0 , 15 ) . map ( ( c , i ) => (
< tr key = { i } style = { { borderBottom : '1px solid var(--border-color)' } } >
< td style = { { padding : '5px 8px' } } > < StateSquare state = { c . status === 'up' ? 'ok' : 'crit' } text = { c . status === 'up' ? 'UP' : 'DOWN' } / > < / td >
< td style = { { padding : '5px 8px' , color : 'var(--text-muted)' , fontFamily : 'monospace' } } > { c . rtt _ms != null ? ` ${ Math . round ( c . rtt _ms ) } ms ` : '– ' } < / td >
< td style = { { padding : '5px 8px' , color : 'var(--text-muted)' , textAlign : 'right' , fontFamily : 'monospace' , fontSize : 11 } } > { new Date ( c . checked _at ) . toLocaleString ( 'de-DE' , { day : '2-digit' , month : '2-digit' , hour : '2-digit' , minute : '2-digit' } ) } < / td >
{ c . error _msg && < td style = { { padding : '5px 8px' , color : SC . crit , fontSize : 11 } } > { c . error _msg } < / td > }
< / tr >
) ) } < / tbody >
< / table >
< / div >
) }
< / div >
< div style = { { padding : '14px 20px' , borderTop : '1px solid var(--border-color)' , display : 'flex' , gap : 8 } } >
< button onClick = { doCheckNow } disabled = { checking } style = { { padding : '7px 14px' , borderRadius : 7 , border : '1px solid var(--accent)' , background : 'none' , color : 'var(--accent)' , cursor : 'pointer' , fontSize : 13 , fontWeight : 600 } } > { checking ? '...' : '▶ Jetzt prüfen' } < / button >
< button onClick = { ( ) => { onEdit ( device ) ; onClose ( ) ; } } style = { { padding : '7px 14px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-primary)' , cursor : 'pointer' , fontSize : 13 } } > Bearbeiten < / button >
< div style = { { flex : 1 } } / >
< button onClick = { ( ) => onDelete ( device ) } style = { { padding : '7px 14px' , borderRadius : 7 , border : ` 1px solid ${ SC . crit } ` , background : 'none' , color : SC . crit , cursor : 'pointer' , fontSize : 13 } } > Löschen < / button >
< / div >
< / div >
< / div >
) ;
} ;
const AgentDetailModal = ( { agent , onClose , onDelete } ) => {
const [ swSearch , setSwSearch ] = useState ( '' ) ;
const w = warnings ( agent ) ;
const sw = ( agent . installed _software || [ ] ) . filter ( s => ! swSearch || s . toLowerCase ( ) . includes ( swSearch . toLowerCase ( ) ) ) ;
return (
< div style = { { position : 'fixed' , inset : 0 , background : 'rgba(0,0,0,.6)' , zIndex : 1000 , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , padding : 20 } } onClick = { onClose } >
< div style = { { background : 'var(--bg-primary)' , border : '1px solid var(--border-color)' , borderRadius : 14 , width : '100%' , maxWidth : 720 , maxHeight : '90vh' , overflow : 'hidden' , display : 'flex' , flexDirection : 'column' } } onClick = { e => e . stopPropagation ( ) } >
< div style = { { padding : '20px 24px' , borderBottom : '1px solid var(--border-color)' , display : 'flex' , justifyContent : 'space-between' , alignItems : 'flex-start' } } >
< div >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 10 , marginBottom : 4 } } >
< span style = { { fontSize : 20 , fontWeight : 700 , fontFamily : 'monospace' } } > { agent . hostname } < / span >
< StateSquare state = { agent . status === 'online' ? 'ok' : 'crit' } text = { agent . status === 'online' ? 'ONLINE' : 'OFFLINE' } size = "lg" / >
< / div >
< div style = { { fontSize : 13 , color : 'var(--text-muted)' } } > { agent . os _name } { agent . os _version } < / div >
{ w . length > 0 && < div style = { { display : 'flex' , gap : 5 , marginTop : 8 , flexWrap : 'wrap' } } > { w . map ( ( x , i ) => < WarnBadge key = { i } w = { x } / > ) } < / div > }
< / div >
< button onClick = { onClose } style = { { background : 'none' , border : 'none' , color : 'var(--text-muted)' , fontSize : 20 , cursor : 'pointer' } } > ✕ < / button >
< / div >
< div style = { { overflowY : 'auto' , flex : 1 } } >
< div style = { { padding : '16px 24px' , display : 'grid' , gridTemplateColumns : 'repeat(3,1fr)' , gap : 12 , borderBottom : '1px solid var(--border-color)' } } >
{ [ { label : 'CPU' , val : ` ${ agent . cpu _usage _percent ? . toFixed ( 0 ) ? ? '– ' } % ` , sub : agent . cpu _model ? . replace ( /\(R\)|\(TM\)/g , '' ) || '– ' } , { label : 'RAM' , val : ` ${ agent . ram _used _gb ? . toFixed ( 1 ) ? ? '– ' } / ${ agent . ram _total _gb ? . toFixed ( 0 ) ? ? '– ' } GB ` , sub : ` ${ pct ( agent . ram _used _gb , agent . ram _total _gb ) } % belegt ` } , { label : 'Disk C:' , val : ` ${ agent . disk _free _gb ? . toFixed ( 0 ) ? ? '– ' } GB frei ` , sub : ` von ${ agent . disk _total _gb ? . toFixed ( 0 ) ? ? '– ' } GB ` } ] . map ( m => (
< div key = { m . label } style = { { background : 'var(--bg-secondary)' , borderRadius : 8 , padding : '12px 14px' } } >
< div style = { { fontSize : 11 , color : 'var(--text-muted)' , fontWeight : 700 , textTransform : 'uppercase' , letterSpacing : '.5px' , marginBottom : 4 } } > { m . label } < / div >
< div style = { { fontSize : 18 , fontWeight : 700 , fontFamily : 'monospace' } } > { m . val } < / div >
< div style = { { fontSize : 11 , color : 'var(--text-muted)' , marginTop : 2 , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } > { m . sub } < / div >
< / div >
) ) }
< / div >
< div style = { { padding : '16px 24px' , borderBottom : '1px solid var(--border-color)' } } >
< div style = { { fontSize : 11 , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '.5px' , marginBottom : 10 } } > Systeminfos < / div >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 4 } } >
{ [ [ 'IP-Adresse' , agent . ip _address || '– ' ] , [ 'MAC-Adresse' , agent . mac _address || '– ' ] , [ 'Domain' , agent . domain || '– ' ] , [ 'Letzter Benutzer' , agent . last _user || '– ' ] , [ 'Uptime' , uptimeStr ( agent . uptime _hours ) ] , [ 'CPU-Kerne' , agent . cpu _cores || '– ' ] , [ 'Windows Updates' , ` ${ agent . windows _updates _pending ? ? 0 } ausstehend ` ] , [ 'Agent Version' , ` v ${ agent . agent _version || '?' } ` ] , [ 'Letzter Checkin' , timeAgo ( agent . last _checkin ) ] , [ 'Registriert' , new Date ( agent . created _at ) . toLocaleDateString ( 'de-DE' ) ] ] . map ( ( [ k , v ] ) => (
< div key = { k } style = { { display : 'flex' , justifyContent : 'space-between' , padding : '6px 0' , borderBottom : '1px solid var(--border-color)' , fontSize : 13 } } >
< span style = { { color : 'var(--text-muted)' } } > { k } < / span > < span style = { { fontWeight : 500 } } > { v } < / span >
< / div >
) ) }
< / div >
< / div >
< div style = { { padding : '16px 24px' , borderBottom : '1px solid var(--border-color)' } } >
< div style = { { fontSize : 11 , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '.5px' , marginBottom : 10 } } > Sicherheit < / div >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 4 } } >
{ [
[ 'BitLocker' , ( ( ) => {
const s = agent . bitlocker _status ;
if ( s === 'encrypted' ) return { label : '✓ Verschlüsselt' , color : '#22c55e' } ;
if ( s === 'off' ) return { label : '✗ Nicht aktiv' , color : '#ef4444' } ;
return { label : '? Unbekannt' , color : 'var(--text-muted)' } ;
} ) ( ) ] ,
[ 'Microsoft Defender' , ( ( ) => {
if ( agent . defender _enabled == null ) return { label : '? Unbekannt' , color : 'var(--text-muted)' } ;
if ( ! agent . defender _enabled ) return { label : '✗ Inaktiv' , color : '#ef4444' } ;
const age = agent . defender _signatures _age ;
if ( age == null || age < 0 ) return { label : '✓ Aktiv' , color : '#22c55e' } ;
if ( age <= 3 ) return { label : '✓ Aktiv · Signaturen aktuell' , color : '#22c55e' } ;
return { label : ` ⚠ Aktiv · Signaturen ${ age } d alt ` , color : '#f59e0b' } ;
} ) ( ) ] ,
[ 'Seriennummer' , { label : agent . hardware _serial || '– ' , color : agent . hardware _serial ? 'var(--text-primary)' : 'var(--text-muted)' } ] ,
[ 'TPM' , { label : agent . tpm _v2 ? '✓ TPM 2.0' : agent . tpm _present ? ` TPM ${ agent . tpm _version || '?' } ` : '✗ Nicht vorhanden' , color : agent . tpm _v2 ? '#22c55e' : agent . tpm _present ? '#f59e0b' : '#ef4444' } ] ,
[ 'Secure Boot' , { label : agent . secure _boot ? '✓ Aktiv' : '✗ Inaktiv' , color : agent . secure _boot ? '#22c55e' : '#ef4444' } ] ,
[ 'Win 11 Ready' , { label : agent . win11 _ready ? '✓ Ja' : '✗ Nein' , color : agent . win11 _ready ? '#22c55e' : 'var(--text-muted)' } ] ,
] . map ( ( [ k , v ] ) => (
< div key = { k } style = { { display : 'flex' , justifyContent : 'space-between' , padding : '6px 0' , borderBottom : '1px solid var(--border-color)' , fontSize : 13 } } >
< span style = { { color : 'var(--text-muted)' } } > { k } < / span >
< span style = { { fontWeight : 600 , color : v . color } } > { v . label } < / span >
< / div >
) ) }
< / div >
< / div >
{ agent . installed _software ? . length > 0 && (
< div style = { { padding : '16px 24px' } } >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 10 } } >
< div style = { { fontSize : 11 , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '.5px' } } > Software ( { agent . installed _software . length } ) < / div >
< input placeholder = "Suchen..." value = { swSearch } onChange = { e => setSwSearch ( e . target . value ) } style = { { padding : '4px 10px' , borderRadius : 6 , border : '1px solid var(--border-color)' , background : 'var(--bg-secondary)' , color : 'var(--text-primary)' , fontSize : 12 , width : 160 } } / >
< / div >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 4 , maxHeight : 180 , overflowY : 'auto' } } >
{ sw . map ( ( s , i ) => < div key = { i } style = { { fontSize : 12 , color : 'var(--text-muted)' , padding : '4px 8px' , background : 'var(--bg-secondary)' , borderRadius : 4 , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } title = { s } > { s } < / div > ) }
< / div >
< / div >
) }
< / div >
< div style = { { padding : '14px 24px' , borderTop : '1px solid var(--border-color)' , display : 'flex' , justifyContent : 'flex-end' } } >
< button onClick = { ( ) => onDelete ( agent . id , agent . hostname ) } style = { { padding : '7px 16px' , background : ` ${ SC . crit } 18 ` , color : SC . crit , border : ` 1px solid ${ SC . crit } 44 ` , borderRadius : 7 , cursor : 'pointer' , fontSize : 13 } } > Agent entfernen < / button >
< / div >
< / div >
< / div >
) ;
} ;
// ─── Unifi Config Modal (top-level so React doesn't remount on every render) ──
const UnifiConfigModal = ( { unifiConfig , onSave , onClose } ) => {
const [ form , setForm ] = useState ( {
controller _url : '' , username : '' , password : '' , site : 'default' , poll _interval _min : 5 ,
... ( unifiConfig || { } ) , enabled : ! ! unifiConfig ? . enabled ,
} ) ;
const [ saving , setSaving ] = useState ( false ) ;
const setF = ( k , v ) => setForm ( f => ( { ... f , [ k ] : v } ) ) ;
const inp = { width : '100%' , padding : '8px 10px' , borderRadius : 6 , border : '1px solid var(--border-color)' , background : 'var(--bg-primary)' , color : 'var(--text-primary)' , fontSize : 13 , boxSizing : 'border-box' } ;
const lbl = { fontSize : 11 , color : 'var(--text-muted)' , fontWeight : 700 , marginBottom : 4 , display : 'block' , textTransform : 'uppercase' , letterSpacing : '.4px' } ;
const save = async ( ) => {
setSaving ( true ) ;
try { const c = await unifiService . saveConfig ( form ) ; onSave ( c ) ; onClose ( ) ; }
catch ( e ) { alert ( e . message || 'Fehler' ) ; } finally { setSaving ( false ) ; }
} ;
return (
< div style = { { position : 'fixed' , inset : 0 , background : 'rgba(0,0,0,.6)' , zIndex : 1000 , display : 'flex' , alignItems : 'center' , justifyContent : 'center' } } onClick = { e => e . target === e . currentTarget && onClose ( ) } >
< div style = { { background : 'var(--bg-primary)' , borderRadius : 12 , width : '100%' , maxWidth : 480 , border : '1px solid var(--border-color)' } } >
< div style = { { padding : '18px 20px' , borderBottom : '1px solid var(--border-color)' , display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' } } >
< h2 style = { { fontSize : 16 , fontWeight : 700 } } > 📶 Unifi Controller < / h2 >
< button onClick = { onClose } style = { { background : 'none' , border : 'none' , color : 'var(--text-muted)' , fontSize : 20 , cursor : 'pointer' } } > × < / button >
< / div >
< div style = { { padding : 20 , display : 'grid' , gap : 12 } } >
< div > < label style = { lbl } > Controller URL < / label > < input style = { inp } value = { form . controller _url } onChange = { e => setF ( 'controller_url' , e . target . value ) } placeholder = "https://192.168.0.1:8443" / > < / div >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 } } >
< div > < label style = { lbl } > Benutzername < / label > < input style = { inp } value = { form . username } onChange = { e => setF ( 'username' , e . target . value ) } / > < / div >
< div > < label style = { lbl } > Passwort < / label > < input style = { inp } type = "password" value = { form . password } onChange = { e => setF ( 'password' , e . target . value ) } placeholder = "leer = unverändert" / > < / div >
< / div >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 } } >
< div > < label style = { lbl } > Site < / label > < input style = { inp } value = { form . site } onChange = { e => setF ( 'site' , e . target . value ) } placeholder = "default" / > < / div >
< div > < label style = { lbl } > Poll - Intervall ( Min ) < / label > < input style = { inp } type = "number" min = "1" max = "60" value = { form . poll _interval _min } onChange = { e => setF ( 'poll_interval_min' , parseInt ( e . target . value ) ) } / > < / div >
< / div >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 10 } } >
< input type = "checkbox" id = "unifiEnabled" checked = { ! ! form . enabled } onChange = { e => setF ( 'enabled' , e . target . checked ) } / >
< label htmlFor = "unifiEnabled" style = { { fontSize : 13 , cursor : 'pointer' } } > Unifi - Polling aktiviert < / label >
< / div >
< / div >
< div style = { { padding : '14px 20px' , borderTop : '1px solid var(--border-color)' , display : 'flex' , justifyContent : 'flex-end' , gap : 10 } } >
< button onClick = { onClose } style = { { padding : '8px 18px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-primary)' , cursor : 'pointer' , fontSize : 13 } } > Abbrechen < / button >
< button onClick = { save } disabled = { saving } style = { { padding : '8px 18px' , borderRadius : 7 , border : 'none' , background : 'var(--accent)' , color : '#fff' , cursor : 'pointer' , fontSize : 13 , fontWeight : 600 } } > { saving ? '...' : 'Speichern' } < / button >
< / div >
< / div >
< / div >
) ;
} ;
// ─── Main Page ────────────────────────────────────────────────────────────────
const MonitoringPage = ( ) => {
const [ devices , setDevices ] = useState ( [ ] ) ;
const [ agents , setAgents ] = useState ( [ ] ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ timeSeriesData , setTimeSeriesData ] = useState ( [ ] ) ;
const [ recentChecks , setRecentChecks ] = useState ( [ ] ) ;
const [ selectedDevice , setSelectedDevice ] = useState ( null ) ;
const [ selectedAgent , setSelectedAgent ] = useState ( null ) ;
const [ showDeviceForm , setShowDeviceForm ] = useState ( false ) ;
const [ editDevice , setEditDevice ] = useState ( null ) ;
const [ showDiscover , setShowDiscover ] = useState ( false ) ;
const [ showDeploy , setShowDeploy ] = useState ( false ) ;
const [ countdown , setCountdown ] = useState ( REFRESH _INTERVAL ) ;
const [ activeTab , setActiveTab ] = useState ( 'overview' ) ;
const [ netSearch , setNetSearch ] = useState ( '' ) ;
const [ netFilter , setNetFilter ] = useState ( 'all' ) ;
const [ agentSearch , setAgentSearch ] = useState ( '' ) ;
const [ agentFilter , setAgentFilter ] = useState ( 'all' ) ;
const [ expandedAgents , setExpandedAgents ] = useState ( new Set ( ) ) ;
const [ expandedHosts , setExpandedHosts ] = useState ( new Set ( ) ) ;
2026-06-04 17:00:41 +02:00
const [ agentSort , setAgentSort ] = useState ( 'cpu' ) ;
2026-06-01 20:49:07 +02:00
const [ hostFilter , setHostFilter ] = useState ( 'all' ) ;
const [ hostSearch , setHostSearch ] = useState ( '' ) ;
const [ proxmoxData , setProxmoxData ] = useState ( null ) ;
const [ acknowledged , setAcknowledged ] = useState ( new Set ( ) ) ;
const [ showAcked , setShowAcked ] = useState ( false ) ;
const [ uptimeStats , setUptimeStats ] = useState ( { } ) ;
const [ collapsedGroups , setCollapsedGroups ] = useState ( new Set ( ) ) ;
const [ unifiDevices , setUnifiDevices ] = useState ( [ ] ) ;
const [ unifiConfig , setUnifiConfig ] = useState ( null ) ;
const [ showUnifiCfg , setShowUnifiCfg ] = useState ( false ) ;
const [ extAlerts , setExtAlerts ] = useState ( [ ] ) ;
const [ expandedAlerts , setExpandedAlerts ] = useState ( { } ) ;
const [ hideAcknowledged , setHideAcknowledged ] = useState ( true ) ;
const esRef = useRef ( null ) , countRef = useRef ( REFRESH _INTERVAL ) , agentsRef = useRef ( [ ] ) , devicesRef = useRef ( [ ] ) ;
agentsRef . current = agents ; devicesRef . current = devices ;
const pushSnapshot = useCallback ( ( devs , agts ) => {
setTimeSeriesData ( prev => [ ... prev . slice ( - ( MAX _TS _POINTS - 1 ) ) , { time : new Date ( ) , devDown : devs . filter ( d => d . last _status === 'down' ) . length , devWarn : 0 , agentOffline : agts . filter ( a => a . status === 'offline' ) . length , agentWarn : agts . filter ( a => warnings ( a ) . some ( w => w . type === 'warn' ) ) . length } ] ) ;
} , [ ] ) ;
const loadRecentChecks = useCallback ( async ( devs ) => {
if ( ! devs ? . length ) return ;
const results = [ ] ;
await Promise . allSettled ( devs . slice ( 0 , 8 ) . map ( async d => {
try { const data = await networkMonitorService . getChecks ( d . id , 1 ) ; ( data ? . checks || [ ] ) . slice ( - 3 ) . forEach ( c => results . push ( { deviceName : d . name , status : c . status , checked _at : c . checked _at , rtt _ms : c . rtt _ms } ) ) ; } catch { }
} ) ) ;
results . sort ( ( a , b ) => new Date ( b . checked _at ) - new Date ( a . checked _at ) ) ;
setRecentChecks ( results . slice ( 0 , 12 ) ) ;
} , [ ] ) ;
useEffect ( ( ) => {
let pollTimer = null , sseConnected = false ;
const loadViaApi = ( ) => networkMonitorService . getAll ( ) . then ( d => { setDevices ( d ) ; setLoading ( false ) ; pushSnapshot ( d , agentsRef . current ) ; } ) . catch ( ( ) => setLoading ( false ) ) ;
const startPolling = ( ) => { if ( pollTimer ) return ; loadViaApi ( ) ; pollTimer = setInterval ( loadViaApi , 15000 ) ; } ;
let es ;
try {
es = networkMonitorService . createSSE ( ) ; esRef . current = es ;
const to = setTimeout ( ( ) => { if ( ! sseConnected ) { es . close ( ) ; startPolling ( ) ; } } , 4000 ) ;
es . addEventListener ( 'initial_state' , e => { sseConnected = true ; clearTimeout ( to ) ; const d = JSON . parse ( e . data ) ; setDevices ( d ) ; setLoading ( false ) ; pushSnapshot ( d , agentsRef . current ) ; } ) ;
es . addEventListener ( 'device_update' , e => { const u = JSON . parse ( e . data ) ; setDevices ( prev => { const ex = prev . find ( d => d . id === u . id ) ; const n = ex ? prev . map ( d => d . id === u . id ? u : d ) : [ ... prev , u ] ; pushSnapshot ( n , agentsRef . current ) ; return n ; } ) ; setSelectedDevice ( prev => prev ? . id === u . id ? u : prev ) ; } ) ;
es . addEventListener ( 'device_deleted' , e => { const { id } = JSON . parse ( e . data ) ; setDevices ( prev => prev . filter ( d => d . id !== id ) ) ; } ) ;
es . onerror = ( ) => { clearTimeout ( to ) ; if ( ! sseConnected ) { es . close ( ) ; startPolling ( ) ; } } ;
} catch { startPolling ( ) ; }
return ( ) => { es ? . close ( ) ; if ( pollTimer ) clearInterval ( pollTimer ) ; } ;
} , [ pushSnapshot ] ) ;
const loadAgents = useCallback ( async ( silent = false ) => {
try { const d = await monitoringService . getAll ( ) ; setAgents ( d ) ; countRef . current = REFRESH _INTERVAL ; setCountdown ( REFRESH _INTERVAL ) ; pushSnapshot ( devicesRef . current , d ) ; if ( ! silent ) setLoading ( false ) ; }
catch { if ( ! silent ) { toast . error ( 'Fehler beim Laden' ) ; setLoading ( false ) ; } }
} , [ pushSnapshot ] ) ;
useEffect ( ( ) => {
loadAgents ( ) ;
const r = setInterval ( ( ) => loadAgents ( true ) , REFRESH _INTERVAL * 1000 ) ;
const t = setInterval ( ( ) => { countRef . current = Math . max ( 0 , countRef . current - 1 ) ; setCountdown ( countRef . current ) ; } , 1000 ) ;
return ( ) => { clearInterval ( r ) ; clearInterval ( t ) ; } ;
} , [ loadAgents ] ) ;
useEffect ( ( ) => { if ( devices . length > 0 ) loadRecentChecks ( devices ) ; } , [ devices , loadRecentChecks ] ) ;
useEffect ( ( ) => { if ( devices . length > 0 ) networkMonitorService . getUptimeStats ( ) . then ( setUptimeStats ) . catch ( ( ) => { } ) ; } , [ devices . length ] ) ;
const loadUnifi = useCallback ( async ( ) => {
try {
const [ devs , cfg ] = await Promise . all ( [ unifiService . getDevices ( ) , unifiService . getConfig ( ) ] ) ;
setUnifiDevices ( devs ) ;
setUnifiConfig ( cfg ) ;
} catch { }
} , [ ] ) ;
useEffect ( ( ) => { loadUnifi ( ) ; } , [ loadUnifi ] ) ;
const loadExtAlerts = useCallback ( async ( ) => {
try { const d = await externalAlertService . getAll ( ) ; setExtAlerts ( d ) ; } catch { }
} , [ ] ) ;
useEffect ( ( ) => { loadExtAlerts ( ) ; const r = setInterval ( loadExtAlerts , 60000 ) ; return ( ) => clearInterval ( r ) ; } , [ loadExtAlerts ] ) ;
const loadProxmox = useCallback ( async ( ) => {
try {
const { default : api } = await import ( '../services/api' ) ;
const r = await api . get ( '/proxmox/overview' ) ;
setProxmoxData ( r . data ) ;
} catch { }
} , [ ] ) ;
useEffect ( ( ) => { loadProxmox ( ) ; const r = setInterval ( loadProxmox , 30000 ) ; return ( ) => clearInterval ( r ) ; } , [ loadProxmox ] ) ;
const handleExtAcknowledge = async ( id ) => {
try { await externalAlertService . acknowledge ( id ) ; loadExtAlerts ( ) ; toast . success ( 'Quittiert' ) ; } catch { toast . error ( 'Fehler' ) ; }
} ;
const handleExtCreateTicket = async ( id ) => {
try { await externalAlertService . createTicket ( id ) ; loadExtAlerts ( ) ; toast . success ( 'Ticket erstellt' ) ; } catch { toast . error ( 'Fehler beim Erstellen' ) ; }
} ;
const handleExtRemove = async ( id ) => {
if ( ! window . confirm ( 'Alert wirklich löschen?' ) ) return ;
try { await externalAlertService . remove ( id ) ; loadExtAlerts ( ) ; toast . success ( 'Gelöscht' ) ; } catch { toast . error ( 'Fehler' ) ; }
} ;
const handleAgentDelete = async ( id , hostname ) => {
if ( ! window . confirm ( ` Agent " ${ hostname } " wirklich entfernen? ` ) ) return ;
try { await monitoringService . delete ( id ) ; toast . success ( 'Entfernt' ) ; setSelectedAgent ( null ) ; loadAgents ( true ) ; } catch { toast . error ( 'Fehler' ) ; }
} ;
const handleNetDelete = async ( d ) => {
if ( ! window . confirm ( ` ${ d . name } wirklich löschen? ` ) ) return ;
try { await networkMonitorService . delete ( d . id ) ; setSelectedDevice ( null ) ; toast . success ( 'Gelöscht' ) ; } catch { toast . error ( 'Fehler' ) ; }
} ;
const handleNetSave = ( d ) => setDevices ( prev => { const ex = prev . find ( x => x . id === d . id ) ; return ex ? prev . map ( x => x . id === d . id ? d : x ) : [ ... prev , d ] ; } ) ;
const toggleAgent = id => setExpandedAgents ( prev => { const n = new Set ( prev ) ; n . has ( id ) ? n . delete ( id ) : n . add ( id ) ; return n ; } ) ;
const toggleGroup = g => setCollapsedGroups ( prev => { const n = new Set ( prev ) ; n . has ( g ) ? n . delete ( g ) : n . add ( g ) ; return n ; } ) ;
// Derived
const problems = [
... devices . filter ( d => d . last _status !== 'up' ) . map ( d => ( { id : ` dev- ${ d . id } ` , severity : d . last _status === 'down' ? 0 : 2 , statusColor : netColor ( d . last _status ) , statusLabel : netLabel ( d . last _status ) , name : d . name , service : TYPE _META [ d . type ] ? . label || d . type , typeIcon : TYPE _META [ d . type ] ? . icon || '🖥' , lastCheck : d . last _checked , isDevice : true , raw : d } ) ) ,
... agents . filter ( a => a . status !== 'online' || warnings ( a ) . length > 0 ) . map ( a => { const w = warnings ( a ) ; const hasErr = w . some ( x => x . type === 'error' ) ; const isOffline = a . status === 'offline' ; return { id : ` agt- ${ a . id } ` , severity : isOffline ? 3 : hasErr ? 1 : 2 , statusColor : isOffline ? SC . unknown : hasErr ? SC . crit : SC . warn , statusLabel : isOffline ? 'OFFLINE' : hasErr ? 'CRIT' : 'WARN' , name : a . hostname , service : 'Windows Agent' , typeIcon : '🖥' , lastCheck : a . last _checkin , isDevice : false , raw : a } ; } ) ,
] . sort ( ( a , b ) => a . severity - b . severity ) ;
const INACTIVE _DAYS = 7 ;
const isLongInactive = ( p ) => {
if ( ! p . lastCheck ) return false ;
const days = ( Date . now ( ) - new Date ( p . lastCheck ) . getTime ( ) ) / 86400000 ;
return days >= INACTIVE _DAYS ;
} ;
const openProblems = problems . filter ( p => ! acknowledged . has ( p . id ) ) ;
const activeProblems = openProblems . filter ( p => ! isLongInactive ( p ) ) ;
const inactiveProblems = openProblems . filter ( p => isLongInactive ( p ) ) ;
const lastUpdated = new Date ( ) . toLocaleTimeString ( 'de-DE' , { hour : '2-digit' , minute : '2-digit' , second : '2-digit' } ) ;
if ( loading ) return < LoadingSpinner / > ;
// ── Row background helper (Checkmk-style) ──
const rowBg = ( state ) => state === 'crit' || state === 'down' ? ` ${ SC . crit } 0a ` : state === 'warn' ? ` ${ SC . warn } 08 ` : 'transparent' ;
const rowBorder = ( state ) => state === 'crit' || state === 'down' ? SC . crit : state === 'warn' ? SC . warn : 'transparent' ;
// ── View toolbar helper ──
const pill = ( active , onClick , label ) => (
< button onClick = { onClick } style = { { padding : '4px 12px' , borderRadius : 20 , border : ` 1px solid ${ active ? 'var(--accent)' : 'var(--border-color)' } ` , background : active ? 'var(--accent)' : 'none' , color : active ? '#fff' : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' , fontWeight : active ? 600 : 400 , transition : 'all .15s' } } > { label } < / button >
) ;
const card = ( ex = { } ) => ( { background : 'var(--bg-secondary)' , borderRadius : 10 , border : '1px solid var(--border-color)' , ... ex } ) ;
// ─── Tab: Übersicht ───────────────────────────────────────────────────────
const renderOverview = ( ) => {
const toggleHost = ( id ) => setExpandedHosts ( prev => { const n = new Set ( prev ) ; n . has ( id ) ? n . delete ( id ) : n . add ( id ) ; return n ; } ) ;
const pxState = proxmoxData ? . node ? ( ( ) => {
const cpu = ( proxmoxData . node . cpu || 0 ) * 100 ;
const ram = proxmoxData . node . memory ? . total > 0 ? ( proxmoxData . node . memory . used / proxmoxData . node . memory . total ) * 100 : 0 ;
const load = proxmoxData . node . loadavg ? . [ 0 ] || 0 ;
const cpus = proxmoxData . node . cpuinfo ? . cpus || 1 ;
if ( cpu >= 90 || ram >= 90 || load > cpus * 0.9 ) return 'crit' ;
if ( cpu >= 70 || ram >= 80 || load > cpus * 0.7 ) return 'warn' ;
return 'ok' ;
} ) ( ) : 'unknown' ;
const allHosts = [
... ( proxmoxData ? [ { id : 'px-hve01' , type : 'proxmox' , name : proxmoxData . node _name || 'hve-01' , ip : '192.168.0.184' , icon : '🖥️' , subLabel : 'Proxmox VE Node' , state : pxState , services : proxmoxServices ( proxmoxData ) , lastCheck : proxmoxData . fetched _at } ] : [ ] ) ,
... devices . map ( d => {
const ut = uptimeStats [ d . id ] ;
const state = d . last _status === 'up' ? 'ok' : d . last _status === 'down' ? 'crit' : 'unknown' ;
return { id : ` net- ${ d . id } ` , type : 'network' , name : d . name , ip : d . host , icon : TYPE _META [ d . type ] ? . icon || '🌐' , subLabel : TYPE _META [ d . type ] ? . label || 'Netzwerkgerät' , state , services : netDeviceServices ( d , ut ) , lastCheck : d . last _checked , raw : d } ;
} ) ,
... agents . map ( a => {
const w = warnings ( a ) ;
const hasErr = w . some ( x => x . type === 'error' ) ;
const state = a . status === 'offline' ? 'crit' : hasErr ? 'crit' : w . length ? 'warn' : 'ok' ;
return { id : ` ag- ${ a . id } ` , type : 'agent' , name : a . hostname , ip : a . ip _address , icon : '💻' , subLabel : 'Windows Agent' , state , services : agentServices ( a ) , lastCheck : a . last _checkin , raw : a } ;
} ) ,
] . sort ( ( a , b ) => { const o = { crit : 0 , warn : 1 , unknown : 2 , ok : 3 } ; return ( o [ a . state ] ? ? 3 ) - ( o [ b . state ] ? ? 3 ) || a . name . localeCompare ( b . name ) ; } ) ;
const filtered = allHosts
. filter ( h => hostFilter === 'all' || h . state !== 'ok' )
. filter ( h => ! hostSearch || h . name . toLowerCase ( ) . includes ( hostSearch . toLowerCase ( ) ) || ( h . ip || '' ) . includes ( hostSearch ) ) ;
const stateLabel = { ok : 'OK' , warn : 'WARN' , crit : 'CRIT' , unknown : '?' } ;
const stateColor = { ok : SC . ok , warn : SC . warn , crit : SC . crit , unknown : SC . unknown } ;
return (
< >
{ /* Filter + Search */ }
< div style = { { display : 'flex' , gap : 8 , marginBottom : 14 , alignItems : 'center' , flexWrap : 'wrap' } } >
{ [ [ 'all' , ` Alle ( ${ allHosts . length } ) ` ] , [ 'problems' , ` Probleme ( ${ allHosts . filter ( h => h . state !== 'ok' ) . length } ) ` ] ] . map ( ( [ v , l ] ) => (
< button key = { v } onClick = { ( ) => setHostFilter ( v ) } style = { { padding : '4px 14px' , borderRadius : 20 , border : ` 1px solid ${ hostFilter === v ? 'var(--accent)' : 'var(--border-color)' } ` , background : hostFilter === v ? 'var(--accent)' : 'none' , color : hostFilter === v ? '#fff' : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' , fontWeight : hostFilter === v ? 700 : 400 } } > { l } < / button >
) ) }
< button onClick = { ( ) => { setExpandedHosts ( new Set ( filtered . map ( h => h . id ) ) ) ; } } style = { { padding : '4px 12px' , borderRadius : 20 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' } } > Alle aufklappen < / button >
< button onClick = { ( ) => setExpandedHosts ( new Set ( ) ) } style = { { padding : '4px 12px' , borderRadius : 20 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' } } > Alle zuklappen < / button >
< input value = { hostSearch } onChange = { e => setHostSearch ( e . target . value ) } placeholder = "🔍 Host suchen..." style = { { marginLeft : 'auto' , padding : '5px 12px' , borderRadius : 8 , border : '1px solid var(--border-color)' , background : 'var(--bg-secondary)' , color : 'var(--text-primary)' , fontSize : 13 , minWidth : 200 } } / >
< / div >
{ /* CheckMK Host Table */ }
< div style = { { border : '1px solid var(--border-color)' , borderRadius : 10 , overflow : 'hidden' } } >
{ /* Header */ }
< div style = { { display : 'grid' , gridTemplateColumns : '32px 1fr 90px 60px 60px 60px 130px' , background : 'var(--bg-secondary)' , borderBottom : '2px solid var(--border-color)' , padding : '8px 14px' , fontSize : 10 , color : 'var(--text-muted)' , fontWeight : 800 , textTransform : 'uppercase' , letterSpacing : .5 } } >
< div / >
< div > Host / IP < / div >
< div > Status < / div >
< div style = { { color : SC . ok } } > OK < / div >
< div style = { { color : SC . warn } } > WARN < / div >
< div style = { { color : SC . crit } } > CRIT < / div >
< div > Letzter Check < / div >
< / div >
{ filtered . map ( host => {
const expanded = expandedHosts . has ( host . id ) ;
const okC = host . services . filter ( s => s . status === 'ok' ) . length ;
const warnC = host . services . filter ( s => s . status === 'warn' ) . length ;
const critC = host . services . filter ( s => s . status === 'crit' ) . length ;
const hColor = stateColor [ host . state ] || SC . unknown ;
return (
< div key = { host . id } >
{ /* Host row */ }
< div onClick = { ( ) => toggleHost ( host . id ) } style = { { display : 'grid' , gridTemplateColumns : '32px 1fr 90px 60px 60px 60px 130px' , padding : '10px 14px' , borderBottom : '1px solid var(--border-color)' , cursor : 'pointer' , background : host . state === 'crit' ? ` ${ SC . crit } 12 ` : host . state === 'warn' ? ` ${ SC . warn } 09 ` : 'transparent' , borderLeft : ` 3px solid ${ hColor } ` , transition : 'background .1s' } }
onMouseEnter = { e => e . currentTarget . style . filter = 'brightness(1.08)' }
onMouseLeave = { e => e . currentTarget . style . filter = '' } >
< div style = { { color : 'var(--text-muted)' , fontSize : 11 , paddingTop : 2 } } > { expanded ? '▼' : '▶' } < / div >
< div >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 7 } } >
< span style = { { fontSize : 14 } } > { host . icon } < / span >
< span style = { { fontWeight : 700 , fontSize : 13 , color : 'var(--text-primary)' } } > { host . name } < / span >
{ host . ip && < span style = { { fontSize : 11 , color : 'var(--text-muted)' , fontFamily : 'monospace' } } > { host . ip } < / span > }
< / div >
< div style = { { fontSize : 10 , color : 'var(--text-muted)' , marginTop : 1 } } > { host . subLabel } < / div >
< / div >
< div > < span style = { { padding : '2px 8px' , borderRadius : 3 , fontSize : 11 , fontWeight : 800 , background : ` ${ hColor } 22 ` , color : hColor , border : ` 1px solid ${ hColor } 55 ` , fontFamily : 'monospace' } } > { stateLabel [ host . state ] || '?' } < / span > < / div >
< div style = { { fontWeight : 700 , fontSize : 13 , color : SC . ok } } > { okC } < / div >
< div style = { { fontWeight : warnC > 0 ? 700 : 400 , fontSize : 13 , color : warnC > 0 ? SC . warn : 'var(--text-muted)' } } > { warnC } < / div >
< div style = { { fontWeight : critC > 0 ? 700 : 400 , fontSize : 13 , color : critC > 0 ? SC . crit : 'var(--text-muted)' } } > { critC } < / div >
< div style = { { fontSize : 11 , color : 'var(--text-muted)' , fontFamily : 'monospace' } } > { timeAgo ( host . lastCheck ) } < / div >
< / div >
{ /* Service rows */ }
{ expanded && host . services . map ( ( svc , i ) => {
const sc = stateColor [ svc . status ] || SC . unknown ;
return (
< div key = { i } style = { { display : 'grid' , gridTemplateColumns : '32px 220px 80px 1fr 160px' , gap : 8 , padding : '6px 14px 6px 44px' , borderBottom : '1px solid rgba(255,255,255,0.04)' , background : svc . status === 'crit' ? ` ${ SC . crit } 09 ` : svc . status === 'warn' ? ` ${ SC . warn } 06 ` : i % 2 === 0 ? 'rgba(255,255,255,0.02)' : 'transparent' , borderLeft : ` 3px solid ${ i % 2 === 0 ? 'transparent' : 'transparent' } ` } } >
< div / >
< div style = { { fontSize : 12 , color : 'var(--text-secondary)' , fontWeight : 500 , display : 'flex' , alignItems : 'center' } } > { svc . name } < / div >
< div style = { { display : 'flex' , alignItems : 'center' } } >
< span style = { { padding : '1px 7px' , borderRadius : 3 , fontSize : 10 , fontWeight : 800 , background : ` ${ sc } 22 ` , color : sc , border : ` 1px solid ${ sc } 44 ` , fontFamily : 'monospace' } } > { ( svc . status || 'ok' ) . toUpperCase ( ) } < / span >
< / div >
< div style = { { fontSize : 11 , color : 'var(--text-muted)' , display : 'flex' , alignItems : 'center' , gap : 8 , overflow : 'hidden' } } >
< span style = { { fontWeight : 700 , color : svc . status === 'crit' ? SC . crit : svc . status === 'warn' ? SC . warn : 'var(--text-secondary)' , whiteSpace : 'nowrap' } } > { svc . value } < / span >
{ svc . detail && < span style = { { overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } > { svc . detail } < / span > }
< / div >
< div style = { { display : 'flex' , alignItems : 'center' } } >
{ svc . perf && < PerfBar { ...svc.perf } / > }
< / div >
< / div >
) ;
} ) }
< / div >
) ;
} ) }
{ filtered . length === 0 && (
< div style = { { padding : 48 , textAlign : 'center' , color : SC . ok , fontSize : 15 } } >
✅ { hostFilter === 'problems' ? 'Alle Services in Ordnung — keine Probleme' : 'Keine Hosts gefunden' }
< / div >
) }
< / div >
< / >
) ;
} ;
// ─── Tab: Netzwerk ────────────────────────────────────────────────────────
const renderNetwork = ( ) => {
const filtered = devices . filter ( d => {
if ( netSearch && ! d . name . toLowerCase ( ) . includes ( netSearch . toLowerCase ( ) ) && ! d . host . includes ( netSearch ) ) return false ;
if ( netFilter === 'up' && d . last _status !== 'up' ) return false ;
if ( netFilter === 'down' && d . last _status !== 'down' ) return false ;
if ( netFilter === 'unknown' && d . last _status === 'up' ) return false ;
return true ;
} ) ;
const grouped = { } ;
filtered . forEach ( d => { const g = d . type || 'host' ; if ( ! grouped [ g ] ) grouped [ g ] = [ ] ; grouped [ g ] . push ( d ) ; } ) ;
return (
< >
{ /* Toolbar */ }
< div style = { { display : 'flex' , gap : 10 , marginBottom : 16 , flexWrap : 'wrap' , alignItems : 'center' } } >
< input value = { netSearch } onChange = { e => setNetSearch ( e . target . value ) } placeholder = "Suchen..." style = { { padding : '7px 12px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'var(--bg-secondary)' , color : 'var(--text-primary)' , fontSize : 13 , width : 200 } } / >
< div style = { { display : 'flex' , gap : 5 } } >
{ pill ( netFilter === 'all' , ( ) => setNetFilter ( 'all' ) , 'Alle' ) }
{ pill ( netFilter === 'up' , ( ) => setNetFilter ( 'up' ) , 'UP' ) }
{ pill ( netFilter === 'down' , ( ) => setNetFilter ( 'down' ) , 'DOWN' ) }
{ pill ( netFilter === 'unknown' , ( ) => setNetFilter ( 'unknown' ) , 'UNBEKANNT' ) }
< / div >
< span style = { { fontSize : 12 , color : 'var(--text-muted)' } } > { filtered . length } Einträge < / span >
< div style = { { marginLeft : 'auto' , display : 'flex' , gap : 8 } } >
< button onClick = { ( ) => setShowDiscover ( true ) } style = { { padding : '7px 14px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'var(--bg-secondary)' , color : 'var(--text-primary)' , cursor : 'pointer' , fontSize : 13 , fontWeight : 600 } } > 📡 Discovery < / button >
< button onClick = { ( ) => { setEditDevice ( null ) ; setShowDeviceForm ( true ) ; } } style = { { padding : '7px 14px' , borderRadius : 7 , border : 'none' , background : 'var(--accent)' , color : '#fff' , cursor : 'pointer' , fontSize : 13 , fontWeight : 600 } } > + Gerät < / button >
< button onClick = { ( ) => setShowDeploy ( v => ! v ) } style = { { padding : '7px 12px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : showDeploy ? 'var(--bg-secondary)' : 'none' , color : 'var(--text-muted)' , cursor : 'pointer' , fontSize : 13 } } > Agent { showDeploy ? '▴' : '▾' } < / button >
< button onClick = { ( ) => setShowUnifiCfg ( true ) } title = "Unifi Controller konfigurieren" style = { { padding : '7px 12px' , borderRadius : 7 , border : ` 1px solid ${ unifiConfig ? . enabled ? SC . ok + '66' : 'var(--border-color)' } ` , background : 'none' , color : unifiConfig ? . enabled ? SC . ok : 'var(--text-muted)' , cursor : 'pointer' , fontSize : 13 } } > 📶 Unifi < / button >
< / div >
< / div >
{ /* Deploy panel */ }
{ showDeploy && (
< div style = { card ( { marginBottom : 16 , padding : 16 } ) } >
< div style = { { fontSize : 12 , fontWeight : 700 , borderLeft : '3px solid var(--accent)' , paddingLeft : 8 , marginBottom : 12 } } > Agent bereitstellen < / div >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 10 , marginBottom : 10 } } >
{ [ { href : '/downloads/IT-Nexus-Agent-Setup-v1.0.0.exe' , icon : '⬇' , title : 'Installer (.exe)' , sub : 'Manuelle Installation' } , { href : '/downloads/IT-Nexus-Agent-Setup-v1.0.0.intunewin' , icon : '📦' , title : 'Intune (.intunewin)' , sub : 'Microsoft Intune' } ] . map ( ( { href , icon , title , sub } ) => (
< a key = { href } href = { href } download style = { { textDecoration : 'none' , color : 'inherit' } } >
< div style = { { background : 'var(--bg-primary)' , border : '1px solid var(--border-color)' , borderRadius : 8 , padding : '12px 14px' , display : 'flex' , alignItems : 'center' , gap : 12 , transition : 'border-color .15s' } } onMouseEnter = { e => e . currentTarget . style . borderColor = 'var(--accent)' } onMouseLeave = { e => e . currentTarget . style . borderColor = 'var(--border-color)' } >
< span style = { { fontSize : 22 } } > { icon } < / span >
< div > < div style = { { fontWeight : 700 , fontSize : 13 } } > { title } < / div > < div style = { { color : 'var(--text-muted)' , fontSize : 11 } } > { sub } < / div > < / div >
< / div >
< / a >
) ) }
< / div >
< div style = { { background : 'var(--bg-primary)' , borderRadius : 7 , padding : '10px 12px' , fontSize : 11 } } >
< div style = { { display : 'grid' , gridTemplateColumns : '130px 1fr' , gap : '4px 10px' } } >
{ [ [ 'Install' , 'IT-Nexus-Agent-Setup-v1.0.0.exe /VERYSILENT /NORESTART' ] , [ 'Uninstall' , '{app}\\unins000.exe /VERYSILENT' ] , [ 'Detect' , 'C:\\ProgramData\\IT Nexus Agent\\it-nexus-agent.ps1' ] ] . map ( ( [ k , v ] ) => (
< React.Fragment key = { k } > < span style = { { color : 'var(--text-muted)' } } > { k } < / span > < code style = { { background : 'rgba(0,0,0,.3)' , padding : '1px 7px' , borderRadius : 3 , fontFamily : 'monospace' } } > { v } < / code > < / React.Fragment >
) ) }
< / div >
< / div >
< / div >
) }
{ /* Grouped device tables – Checkmk-style */ }
{ Object . keys ( grouped ) . length === 0
? < div style = { { textAlign : 'center' , padding : 40 , color : 'var(--text-muted)' } } > Keine Geräte gefunden < / div >
: Object . entries ( grouped ) . map ( ( [ type , devList ] ) => {
const meta = TYPE _META [ type ] || TYPE _META . host ;
const collapsed = collapsedGroups . has ( type ) ;
const downCount = devList . filter ( d => d . last _status === 'down' ) . length ;
return (
< div key = { type } style = { card ( { marginBottom : 10 , padding : 0 , overflow : 'hidden' } ) } >
{ /* Group header */ }
< div onClick = { ( ) => toggleGroup ( type ) } style = { { display : 'flex' , alignItems : 'center' , gap : 8 , padding : '10px 16px' , cursor : 'pointer' , borderBottom : collapsed ? 'none' : '1px solid var(--border-color)' , background : 'var(--bg-primary)' , userSelect : 'none' } } >
< span style = { { fontSize : 15 } } > { meta . icon } < / span >
< span style = { { fontWeight : 700 , fontSize : 13 } } > { meta . label } < / span >
< span style = { { fontSize : 11 , color : 'var(--text-muted)' , background : 'var(--bg-secondary)' , padding : '1px 7px' , borderRadius : 10 , border : '1px solid var(--border-color)' } } > { devList . length } < / span >
{ downCount > 0 && < StateSquare state = "crit" text = { ` ${ downCount } DOWN ` } / > }
< span style = { { marginLeft : 'auto' , color : 'var(--text-muted)' , fontSize : 12 } } > { collapsed ? '▸' : '▾' } < / span >
< / div >
{ ! collapsed && (
< table style = { { width : '100%' , borderCollapse : 'collapse' , fontSize : 12 } } >
< thead >
< tr style = { { borderBottom : '1px solid var(--border-color)' , background : 'rgba(255,255,255,0.02)' } } >
{ [ 'Status' , 'Host' , 'IP-Adresse' , 'Standort' , '24h Uptime' , 'RTT' , 'Letzter Check' , 'Aktionen' ] . map ( h => (
< th key = { h } style = { { padding : '6px 12px' , textAlign : 'left' , fontSize : 10 , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : .4 , whiteSpace : 'nowrap' } } > { h } < / th >
) ) }
< / tr >
< / thead >
< tbody >
{ devList . map ( d => {
const state = d . last _status === 'up' ? 'ok' : d . last _status === 'down' ? 'crit' : 'unknown' ;
const ut = uptimeStats [ d . id ] ;
return (
< tr key = { d . id } onClick = { ( ) => setSelectedDevice ( d ) } style = { { borderBottom : '1px solid var(--border-color)' , background : rowBg ( state ) , borderLeft : ` 3px solid ${ rowBorder ( state ) } ` , cursor : 'pointer' , transition : 'filter .1s' } }
onMouseEnter = { e => e . currentTarget . style . filter = 'brightness(1.1)' }
onMouseLeave = { e => e . currentTarget . style . filter = '' } >
< td style = { { padding : '8px 12px' , whiteSpace : 'nowrap' } } > < StateSquare state = { state } text = { netLabel ( d . last _status ) } / > < / td >
< td style = { { padding : '8px 12px' , fontWeight : 600 } } > { d . name } < / td >
< td style = { { padding : '8px 12px' , fontFamily : 'monospace' , color : 'var(--text-muted)' , fontSize : 11 } } > { d . host } < / td >
< td style = { { padding : '8px 12px' , color : 'var(--text-muted)' } } > { d . location || '– ' } < / td >
< td style = { { padding : '8px 12px' , minWidth : 100 } } >
{ ut ? . total > 0 ? < PerfBar val = { ut . total - ut . up } max = { ut . total } warn = { 10 } crit = { 20 } labelVal = { ` ${ ut . pct } % ` } / > : < span style = { { color : 'var(--text-muted)' } } > – < / span > }
< / td >
< td style = { { padding : '8px 12px' , fontFamily : 'monospace' , color : 'var(--text-muted)' , whiteSpace : 'nowrap' } } > { d . last _rtt _ms != null ? ` ${ Math . round ( d . last _rtt _ms ) } ms ` : '– ' } < / td >
< td style = { { padding : '8px 12px' , color : 'var(--text-muted)' , whiteSpace : 'nowrap' } } > { timeAgo ( d . last _checked ) } < / td >
< td style = { { padding : '8px 12px' } } onClick = { e => e . stopPropagation ( ) } >
< div style = { { display : 'flex' , gap : 5 } } >
< button onClick = { ( ) => networkMonitorService . checkNow ( d . id ) . then ( ( ) => toast . success ( 'OK' ) ) . catch ( ( ) => { } ) } style = { { padding : '2px 8px' , borderRadius : 4 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , cursor : 'pointer' , fontSize : 11 } } > ▶ < / button >
< button onClick = { ( ) => { setEditDevice ( d ) ; setShowDeviceForm ( true ) ; } } style = { { padding : '2px 8px' , borderRadius : 4 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , cursor : 'pointer' , fontSize : 11 } } > ✎ < / button >
< button onClick = { ( ) => handleNetDelete ( d ) } style = { { padding : '2px 8px' , borderRadius : 4 , border : ` 1px solid ${ SC . crit } 44 ` , background : 'none' , color : SC . crit , cursor : 'pointer' , fontSize : 11 } } > ✕ < / button >
< / div >
< / td >
< / tr >
) ;
} ) }
< / tbody >
< / table >
) }
< / div >
) ;
} )
}
{ /* ── Unifi-Geräte ─────────────────────────────────────────── */ }
{ unifiConfig ? . enabled && unifiDevices . length > 0 && ( ( ) => {
const upStr = ( s ) => { if ( ! s ) return '– ' ; const h = Math . floor ( s / 3600 ) ; return h < 24 ? ` ${ h } h ` : ` ${ Math . floor ( h / 24 ) } d ` ; } ;
const groups = [
{ key : 'uap' , label : 'Unifi Access Points' , icon : '📶' , items : unifiDevices . filter ( d => d . type === 'uap' ) } ,
{ key : 'usw' , label : 'Unifi Switches' , icon : '🔀' , items : unifiDevices . filter ( d => d . type === 'usw' ) } ,
{ key : 'other' , label : 'Unifi Sonstige' , icon : '🔌' , items : unifiDevices . filter ( d => d . type !== 'uap' && d . type !== 'usw' ) } ,
] . filter ( g => g . items . length > 0 ) ;
return groups . map ( ( { key , label , icon , items } ) => {
const collapsed = collapsedGroups . has ( 'unifi_' + key ) ;
const downCount = items . filter ( d => d . state !== 1 ) . length ;
const hasClients = items . some ( d => d . num _sta > 0 ) ;
return (
< div key = { key } style = { card ( { marginBottom : 10 , padding : 0 , overflow : 'hidden' } ) } >
< div onClick = { ( ) => toggleGroup ( 'unifi_' + key ) } style = { { display : 'flex' , alignItems : 'center' , gap : 8 , padding : '10px 16px' , cursor : 'pointer' , borderBottom : collapsed ? 'none' : '1px solid var(--border-color)' , background : 'var(--bg-primary)' , userSelect : 'none' } } >
< span style = { { fontSize : 15 } } > { icon } < / span >
< span style = { { fontWeight : 700 , fontSize : 13 } } > { label } < / span >
< span style = { { fontSize : 11 , color : 'var(--text-muted)' , background : 'var(--bg-secondary)' , padding : '1px 7px' , borderRadius : 10 , border : '1px solid var(--border-color)' } } > { items . length } < / span >
{ downCount > 0 && < StateSquare state = "crit" text = { ` ${ downCount } OFFLINE ` } / > }
< span style = { { marginLeft : 'auto' , fontSize : 10 , color : 'var(--text-muted)' , fontStyle : 'italic' } } > via Unifi Controller < / span >
< span style = { { color : 'var(--text-muted)' , fontSize : 12 , marginLeft : 6 } } > { collapsed ? '▸' : '▾' } < / span >
< / div >
{ ! collapsed && (
< table style = { { width : '100%' , borderCollapse : 'collapse' , fontSize : 12 } } >
< thead >
< tr style = { { borderBottom : '1px solid var(--border-color)' , background : 'rgba(255,255,255,0.02)' } } >
{ [ 'Status' , 'Name' , 'IP' , 'Modell' , 'Uptime' , hasClients && 'Clients' , 'CPU' , 'RAM' ] . filter ( Boolean ) . map ( h => (
< th key = { h } style = { { padding : '6px 12px' , textAlign : 'left' , fontSize : 10 , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : .4 , whiteSpace : 'nowrap' } } > { h } < / th >
) ) }
< / tr >
< / thead >
< tbody >
{ items . map ( d => (
< tr key = { d . unifi _id } style = { { borderBottom : '1px solid var(--border-color)' , background : d . state === 1 ? 'transparent' : ` ${ SC . crit } 0a ` , borderLeft : ` 3px solid ${ d . state === 1 ? 'transparent' : SC . crit } ` } } >
< td style = { { padding : '8px 12px' } } > < StateSquare state = { d . state === 1 ? 'ok' : 'crit' } text = { d . state === 1 ? 'ONLINE' : 'OFFLINE' } / > < / td >
< td style = { { padding : '8px 12px' , fontWeight : 600 } } > { d . name || d . mac } < / td >
< td style = { { padding : '8px 12px' , fontFamily : 'monospace' , color : 'var(--text-muted)' , fontSize : 11 } } > { d . ip || '– ' } < / td >
< td style = { { padding : '8px 12px' , color : 'var(--text-muted)' } } > { d . model || '– ' } < / td >
< td style = { { padding : '8px 12px' , fontFamily : 'monospace' , fontSize : 11 } } > { upStr ( d . uptime ) } < / td >
{ hasClients && < td style = { { padding : '8px 12px' , textAlign : 'center' } } >
{ d . num _sta > 0 ? < span style = { { background : ` ${ SC . ok } 22 ` , color : SC . ok , border : ` 1px solid ${ SC . ok } 44 ` , borderRadius : 10 , padding : '1px 8px' , fontSize : 11 , fontWeight : 700 , fontFamily : 'monospace' } } > { d . num _sta } < / span > : < span style = { { color : 'var(--text-muted)' } } > – < / span > }
< / td > }
< td style = { { padding : '8px 12px' , minWidth : 120 } } > { d . cpu _pct != null ? < PerfBar val = { d . cpu _pct } max = { 100 } warn = { 70 } crit = { 90 } unit = "%" / > : < span style = { { color : 'var(--text-muted)' } } > – < / span > } < / td >
< td style = { { padding : '8px 12px' , minWidth : 120 } } > { d . ram _pct != null ? < PerfBar val = { d . ram _pct } max = { 100 } warn = { 75 } crit = { 90 } unit = "%" / > : < span style = { { color : 'var(--text-muted)' } } > – < / span > } < / td >
< / tr >
) ) }
< / tbody >
< / table >
) }
< / div >
) ;
} ) ;
} ) ( ) }
< / >
) ;
} ;
// ─── Tab: Agents ──────────────────────────────────────────────────────────
const renderAgents = ( ) => {
2026-06-04 17:00:41 +02:00
const isAgentOffline = ( a ) => {
if ( ! a . last _checkin ) return true ;
return Date . now ( ) - new Date ( a . last _checkin ) . getTime ( ) > 5 * 60 * 1000 ;
} ;
2026-06-01 20:49:07 +02:00
const filtered = agents . filter ( a => {
if ( agentSearch && ! a . hostname . toLowerCase ( ) . includes ( agentSearch . toLowerCase ( ) ) && ! ( a . ip _address || '' ) . includes ( agentSearch ) ) return false ;
if ( agentFilter === 'online' && a . status !== 'online' ) return false ;
if ( agentFilter === 'offline' && a . status !== 'offline' ) return false ;
if ( agentFilter === 'warn' && warnings ( a ) . length === 0 ) return false ;
return true ;
} ) ;
2026-06-04 17:00:41 +02:00
const sorted = [ ... filtered ] . sort ( ( a , b ) => {
const aOffline = isAgentOffline ( a ) ;
const bOffline = isAgentOffline ( b ) ;
// Offline immer ans Ende
if ( aOffline !== bOffline ) return aOffline ? 1 : - 1 ;
if ( agentSort === 'cpu' ) {
const av = a . cpu _usage _percent ? ? ( a . ram _total _gb > 0 ? ( a . ram _used _gb / a . ram _total _gb ) * 100 : 0 ) ;
const bv = b . cpu _usage _percent ? ? ( b . ram _total _gb > 0 ? ( b . ram _used _gb / b . ram _total _gb ) * 100 : 0 ) ;
return bv - av ;
}
if ( agentSort === 'ram' ) {
const av = a . ram _total _gb > 0 ? ( a . ram _used _gb / a . ram _total _gb ) * 100 : 0 ;
const bv = b . ram _total _gb > 0 ? ( b . ram _used _gb / b . ram _total _gb ) * 100 : 0 ;
return bv - av ;
}
if ( agentSort === 'name' ) return ( a . hostname || '' ) . localeCompare ( b . hostname || '' ) ;
if ( agentSort === 'seen' ) return new Date ( b . last _checkin || 0 ) - new Date ( a . last _checkin || 0 ) ;
return 0 ;
} ) ;
2026-06-01 20:49:07 +02:00
return (
< >
< div style = { { display : 'flex' , gap : 10 , marginBottom : 16 , flexWrap : 'wrap' , alignItems : 'center' } } >
< input value = { agentSearch } onChange = { e => setAgentSearch ( e . target . value ) } placeholder = "Hostname / IP..." style = { { padding : '7px 12px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'var(--bg-secondary)' , color : 'var(--text-primary)' , fontSize : 13 , width : 220 } } / >
< div style = { { display : 'flex' , gap : 5 } } >
{ pill ( agentFilter === 'all' , ( ) => setAgentFilter ( 'all' ) , 'Alle' ) }
{ pill ( agentFilter === 'online' , ( ) => setAgentFilter ( 'online' ) , 'ONLINE' ) }
{ pill ( agentFilter === 'offline' , ( ) => setAgentFilter ( 'offline' ) , 'OFFLINE' ) }
{ pill ( agentFilter === 'warn' , ( ) => setAgentFilter ( 'warn' ) , 'WARNUNG' ) }
< / div >
2026-06-04 17:00:41 +02:00
< select value = { agentSort } onChange = { e => setAgentSort ( e . target . value ) } style = { { padding : '5px 10px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'var(--bg-secondary)' , color : 'var(--text-primary)' , fontSize : 12 , cursor : 'pointer' } } >
< option value = "cpu" > Sortierung : CPU - Auslastung < / option >
< option value = "ram" > Sortierung : RAM - Auslastung < / option >
< option value = "name" > Sortierung : Name < / option >
< option value = "seen" > Sortierung : Zuletzt gesehen < / option >
< / select >
< span style = { { fontSize : 12 , color : 'var(--text-muted)' , marginLeft : 'auto' } } > { sorted . length } Einträge < / span >
2026-06-01 20:49:07 +02:00
< / div >
2026-06-04 17:00:41 +02:00
{ sorted . length === 0
2026-06-01 20:49:07 +02:00
? < div style = { { textAlign : 'center' , padding : 40 , color : 'var(--text-muted)' } } > Keine Agents gefunden < / div >
: (
/* Checkmk-style service table */
< div style = { card ( { padding : 0 , overflow : 'hidden' } ) } >
< table style = { { width : '100%' , borderCollapse : 'collapse' , fontSize : 12 } } >
< thead >
< tr style = { { borderBottom : '1px solid var(--border-color)' , background : 'var(--bg-primary)' } } >
< th style = { { width : 28 } } / >
{ [ 'Host' , 'Status' , 'CPU' , 'RAM' , 'Disk' , 'Uptime' , 'Letzter Checkin' , '' ] . map ( h => (
< th key = { h } style = { { padding : '7px 12px' , textAlign : 'left' , fontSize : 10 , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : .4 } } > { h } < / th >
) ) }
< / tr >
< / thead >
< tbody >
2026-06-04 17:00:41 +02:00
{ sorted . map ( a => {
2026-06-01 20:49:07 +02:00
const w = warnings ( a ) , hasErr = w . some ( x => x . type === 'error' ) ;
const state = a . status === 'offline' ? 'crit' : hasErr ? 'crit' : w . length ? 'warn' : 'ok' ;
const isExp = expandedAgents . has ( a . id ) ;
const services = agentServices ( a ) ;
return (
< React.Fragment key = { a . id } >
{ /* Agent row */ }
< tr style = { { borderBottom : isExp ? 'none' : '1px solid var(--border-color)' , background : rowBg ( state ) , borderLeft : ` 3px solid ${ rowBorder ( state ) } ` , cursor : 'pointer' } }
onClick = { ( ) => toggleAgent ( a . id ) } >
< td style = { { padding : '8px 6px' , textAlign : 'center' , color : 'var(--text-muted)' , fontSize : 11 } } > { isExp ? '▼' : '▶' } < / td >
< td style = { { padding : '8px 12px' , fontWeight : 700 , whiteSpace : 'nowrap' , fontFamily : 'monospace' } } >
< Link to = { ` /monitoring/device/ ${ a . id } ` } onClick = { e => e . stopPropagation ( ) } style = { { color : 'inherit' , textDecoration : 'none' , borderBottom : '1px dotted var(--text-muted)' } } title = "Geräte-Details öffnen" >
{ a . hostname }
< / Link >
< / td >
< td style = { { padding : '8px 12px' } } > < StateSquare state = { state } text = { state . toUpperCase ( ) } / > < / td >
< td style = { { padding : '8px 12px' , minWidth : 120 } } >
{ a . cpu _usage _percent != null
? < PerfBar val = { a . cpu _usage _percent } max = { 100 } warn = { 80 } crit = { 90 } / >
: < span style = { { color : 'var(--text-muted)' } } > – < / span > }
< / td >
< td style = { { padding : '8px 12px' , minWidth : 120 } } >
{ a . ram _total _gb > 0
? < PerfBar val = { a . ram _used _gb } max = { a . ram _total _gb } warn = { 85 } crit = { 95 } / >
: < span style = { { color : 'var(--text-muted)' } } > – < / span > }
< / td >
< td style = { { padding : '8px 12px' , minWidth : 120 } } >
{ a . disk _total _gb > 0
? < PerfBar val = { a . disk _total _gb - a . disk _free _gb } max = { a . disk _total _gb } warn = { 75 } crit = { 90 } labelVal = { ` ${ a . disk _free _gb ? . toFixed ( 0 ) } GB ` } / >
: < span style = { { color : 'var(--text-muted)' } } > – < / span > }
< / td >
< td style = { { padding : '8px 12px' , color : 'var(--text-muted)' , fontFamily : 'monospace' , fontSize : 11 } } > { uptimeStr ( a . uptime _hours ) } < / td >
< td style = { { padding : '8px 12px' , color : 'var(--text-muted)' , fontSize : 11 , whiteSpace : 'nowrap' } } > { timeAgo ( a . last _checkin ) } < / td >
< td style = { { padding : '8px 12px' } } onClick = { e => e . stopPropagation ( ) } >
< button onClick = { ( ) => setSelectedAgent ( a ) } style = { { padding : '2px 9px' , borderRadius : 4 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , cursor : 'pointer' , fontSize : 11 } } > Details < / button >
< / td >
< / tr >
{ /* Expanded: service checks (Checkmk-style sub-table) */ }
{ isExp && (
< tr style = { { borderBottom : '1px solid var(--border-color)' } } >
< td / >
< td colSpan = { 8 } style = { { padding : '0 0 0 16px' , background : 'rgba(255,255,255,0.02)' } } >
< table style = { { width : '100%' , borderCollapse : 'collapse' , fontSize : 12 , marginBottom : 4 } } >
< thead >
< tr style = { { borderBottom : '1px solid var(--border-color)' } } >
{ [ 'Status' , 'Service' , 'Perf-O-Meter' , 'Wert' , 'Details' ] . map ( h => (
< th key = { h } style = { { padding : '5px 10px' , textAlign : 'left' , fontSize : 10 , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : .4 } } > { h } < / th >
) ) }
< / tr >
< / thead >
< tbody >
{ services . map ( ( svc , si ) => (
< tr key = { si } style = { { borderBottom : '1px solid var(--border-color)' , background : rowBg ( svc . status ) } } >
< td style = { { padding : '6px 10px' , borderLeft : ` 3px solid ${ rowBorder ( svc . status ) } ` } } >
< StateSquare state = { svc . status } / >
< / td >
< td style = { { padding : '6px 10px' , fontWeight : 600 } } > { svc . name } < / td >
< td style = { { padding : '6px 10px' , minWidth : 160 } } >
{ svc . perf
? < PerfBar val = { svc . perf . val } max = { svc . perf . max } warn = { svc . perf . warn } crit = { svc . perf . crit } unit = { svc . perf . unit } labelVal = { svc . perf . labelVal } / >
: < span style = { { color : 'var(--text-muted)' , fontSize : 11 } } > – < / span > }
< / td >
< td style = { { padding : '6px 10px' , fontFamily : 'monospace' , fontSize : 11 , color : SC [ svc . status ] , fontWeight : 700 } } > { svc . value } < / td >
< td style = { { padding : '6px 10px' , color : 'var(--text-muted)' , fontSize : 11 } } > { svc . detail } < / td >
< / tr >
) ) }
< / tbody >
< / table >
< div style = { { display : 'flex' , gap : 8 , padding : '8px 0 10px' } } >
< button onClick = { ( ) => setSelectedAgent ( a ) } style = { { padding : '4px 12px' , borderRadius : 5 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , cursor : 'pointer' , fontSize : 11 } } > Software & amp ; Details < / button >
< button onClick = { ( ) => handleAgentDelete ( a . id , a . hostname ) } style = { { padding : '4px 12px' , borderRadius : 5 , border : ` 1px solid ${ SC . crit } 44 ` , background : 'none' , color : SC . crit , cursor : 'pointer' , fontSize : 11 } } > Entfernen < / button >
< / div >
< / td >
< / tr >
) }
< / React.Fragment >
) ;
} ) }
< / tbody >
< / table >
< / div >
)
}
< / >
) ;
} ;
// ─── Tab: Probleme ────────────────────────────────────────────────────────
const renderProblems = ( ) => {
const visible = showAcked ? problems : activeProblems ;
const renderTable = ( rows ) => (
< div style = { card ( { padding : 0 , overflow : 'hidden' } ) } >
< table style = { { width : '100%' , borderCollapse : 'collapse' , fontSize : 12 } } >
< thead >
< tr style = { { borderBottom : '1px solid var(--border-color)' , background : 'var(--bg-primary)' } } >
{ [ 'Schwere' , 'Status' , 'Host' , 'Service' , 'Seit' , 'Bestätigen' ] . map ( h => (
< th key = { h } style = { { padding : '8px 14px' , textAlign : 'left' , fontSize : 10 , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : .4 } } > { h } < / th >
) ) }
< / tr >
< / thead >
< tbody >
{ rows . map ( p => {
const isAck = acknowledged . has ( p . id ) ;
const state = p . severity <= 1 ? 'crit' : 'warn' ;
return (
< tr key = { p . id } style = { { borderBottom : '1px solid var(--border-color)' , background : isAck ? 'transparent' : rowBg ( state ) , borderLeft : ` 3px solid ${ isAck ? 'transparent' : rowBorder ( state ) } ` , opacity : isAck ? 0.5 : 1 } } >
< td style = { { padding : '9px 14px' } } >
< StateSquare state = { state } text = { state . toUpperCase ( ) } size = "lg" / >
< / td >
< td style = { { padding : '9px 14px' } } >
< StateSquare state = { p . severity <= 1 ? 'crit' : 'warn' } text = { p . statusLabel } / >
{ isAck && < span style = { { marginLeft : 6 , fontSize : 10 , fontWeight : 700 , color : SC . unknown , background : ` ${ SC . unknown } 20 ` , padding : '1px 6px' , borderRadius : 3 , border : ` 1px solid ${ SC . unknown } 44 ` } } > ACK < / span > }
< / td >
< td style = { { padding : '9px 14px' } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 7 } } >
< span style = { { fontSize : 15 } } > { p . typeIcon } < / span >
< div >
< div style = { { fontWeight : 700 , fontFamily : 'monospace' } } > { p . name } < / div >
{ p . raw ? . host && < div style = { { fontSize : 10 , color : 'var(--text-muted)' } } > { p . raw . host } < / div > }
< / div >
< / div >
< / td >
< td style = { { padding : '9px 14px' , color : 'var(--text-muted)' } } > { p . service } < / td >
< td style = { { padding : '9px 14px' , fontFamily : 'monospace' , color : 'var(--text-muted)' , whiteSpace : 'nowrap' , fontSize : 11 } } > { duration ( p . lastCheck ) } < / td >
< td style = { { padding : '9px 14px' } } >
{ ! isAck
? < button onClick = { ( ) => setAcknowledged ( prev => new Set ( [ ... prev , p . id ] ) ) } style = { { padding : '3px 12px' , borderRadius : 5 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , cursor : 'pointer' , fontSize : 11 , fontWeight : 600 } } > ✓ Bestätigen < / button >
: < button onClick = { ( ) => p . isDevice ? setSelectedDevice ( p . raw ) : setSelectedAgent ( p . raw ) } style = { { padding : '3px 12px' , borderRadius : 5 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , cursor : 'pointer' , fontSize : 11 } } > Details < / button >
}
< / td >
< / tr >
) ;
} ) }
< / tbody >
< / table >
< / div >
) ;
return (
< >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 16 , marginBottom : 14 } } >
< label style = { { display : 'flex' , alignItems : 'center' , gap : 7 , fontSize : 13 , cursor : 'pointer' , color : 'var(--text-muted)' } } >
< input type = "checkbox" checked = { showAcked } onChange = { e => setShowAcked ( e . target . checked ) } style = { { accentColor : 'var(--accent)' , width : 14 , height : 14 } } / >
Bestätigte anzeigen
< / label >
< span style = { { fontSize : 12 , color : 'var(--text-muted)' } } > { activeProblems . length } aktiv · { inactiveProblems . length } langfristig offline · { acknowledged . size } bestätigt < / span >
< / div >
{ visible . length === 0 && inactiveProblems . length === 0
? (
< div style = { { textAlign : 'center' , padding : '60px 20px' , background : 'var(--bg-secondary)' , borderRadius : 10 , border : ` 1px solid ${ SC . ok } 30 ` } } >
< div style = { { fontSize : 40 , marginBottom : 10 } } > ✓ < / div >
< div style = { { fontSize : 16 , fontWeight : 700 , color : SC . ok } } > Keine offenen Probleme < / div >
< div style = { { fontSize : 13 , color : 'var(--text-muted)' , marginTop : 6 } } > Alle { devices . length + agents . length } Hosts sind in Ordnung < / div >
< / div >
)
: (
< >
{ visible . length > 0 && renderTable ( visible ) }
{ inactiveProblems . length > 0 && (
< div style = { { marginTop : 16 } } >
< button onClick = { ( ) => setCollapsedGroups ( prev => { const n = new Set ( prev ) ; n . has ( 'inactive' ) ? n . delete ( 'inactive' ) : n . add ( 'inactive' ) ; return n ; } ) } style = { { display : 'flex' , alignItems : 'center' , gap : 8 , width : '100%' , padding : '8px 14px' , background : 'var(--bg-secondary)' , border : '1px solid var(--border-color)' , borderRadius : 8 , cursor : 'pointer' , fontSize : 12 , color : 'var(--text-muted)' , textAlign : 'left' } } >
< span style = { { fontSize : 10 } } > { collapsedGroups . has ( 'inactive' ) ? '▶' : '▼' } < / span >
< span style = { { fontWeight : 600 } } > Langfristig offline ( & gt ; { INACTIVE _DAYS } Tage ) < / span >
< span style = { { fontSize : 10 , padding : '1px 6px' , borderRadius : 8 , background : ` ${ SC . unknown } 22 ` , color : SC . unknown , border : ` 1px solid ${ SC . unknown } 44 ` } } > { inactiveProblems . length } < / span >
< span style = { { marginLeft : 'auto' , fontSize : 11 } } > Geräte die längere Zeit nicht gesehen wurden < / span >
< / button >
{ ! collapsedGroups . has ( 'inactive' ) && (
< div style = { { marginTop : 6 , opacity : 0.6 } } >
{ renderTable ( inactiveProblems ) }
< / div >
) }
< / div >
) }
< / >
)
}
< / >
) ;
} ;
const extUnacked = extAlerts . filter ( a => ! a . acknowledged ) . length ;
const netgoAlerts = extAlerts . filter ( a => a . source === 'netgo' ) ;
const netgoUnacked = netgoAlerts . filter ( a => ! a . acknowledged ) . length ;
const awAlerts = extAlerts . filter ( a => a . source === 'arcticwolf' ) ;
const awUnacked = awAlerts . filter ( a => ! a . acknowledged ) . length ;
const mdoAlerts = extAlerts . filter ( a => a . source === 'mdo' ) ;
const mdoUnacked = mdoAlerts . filter ( a => ! a . acknowledged ) . length ;
const toggleAlertExpand = ( id ) => setExpandedAlerts ( p => ( { ... p , [ id ] : ! p [ id ] } ) ) ;
const renderExtAlerts = ( ) => {
const sevColor = ( s ) => s === 'CRIT' ? SC . crit : s === 'WARN' ? SC . warn : s === 'OK' ? SC . ok : SC . unknown ;
const renderAlertCard = ( alert ) => {
const color = sevColor ( alert . severity ) ;
const isAW = alert . source === 'arcticwolf' ;
const expanded = expandedAlerts [ alert . id ] ;
let aw = null ;
if ( isAW && alert . raw _body ) {
try { aw = JSON . parse ( alert . raw _body ) ; } catch { /* ignore */ }
}
const actions = (
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 6 , flexShrink : 0 } } >
{ ! alert . ticket _id && (
< button onClick = { ( ) => handleExtCreateTicket ( alert . id ) } style = { { padding : '5px 12px' , borderRadius : 7 , border : ` 1px solid var(--accent) ` , background : 'var(--accent)' , color : '#fff' , fontSize : 12 , cursor : 'pointer' , fontWeight : 600 , whiteSpace : 'nowrap' } } > 🎫 Ticket < / button >
) }
{ ! alert . acknowledged && (
< button onClick = { ( ) => handleExtAcknowledge ( alert . id ) } style = { { padding : '5px 12px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' , whiteSpace : 'nowrap' } } > ✓ Quittieren < / button >
) }
< button onClick = { ( ) => handleExtRemove ( alert . id ) } style = { { padding : '5px 12px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' } } > 🗑 < / button >
< / div >
) ;
// ── Arctic Wolf Detailkarte ──────────────────────────────────────
if ( isAW ) {
const labelStyle = { fontSize : 11 , color : 'var(--text-muted)' , minWidth : 130 , flexShrink : 0 } ;
const valStyle = { fontSize : 12 , color : 'var(--text-primary)' , wordBreak : 'break-word' } ;
const row = ( label , val ) => val ? (
< div style = { { display : 'flex' , gap : 8 , padding : '4px 0' , borderBottom : '1px solid var(--border-color)' } } >
< span style = { labelStyle } > { label } < / span >
< span style = { valStyle } > { val } < / span >
< / div >
) : null ;
const section = ( title , text ) => text ? (
< div style = { { marginTop : 10 } } >
< div style = { { fontSize : 11 , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : 1 , marginBottom : 4 } } > { title } < / div >
< div style = { { fontSize : 12 , color : 'var(--text-secondary)' , lineHeight : 1.6 } } > { text } < / div >
< / div >
) : null ;
return (
< div key = { alert . id } style = { { background : 'var(--bg-secondary)' , border : ` 1px solid ${ alert . acknowledged ? 'var(--border-color)' : color + '44' } ` , borderLeft : ` 4px solid ${ color } ` , borderRadius : 10 , padding : '14px 16px' , opacity : alert . acknowledged ? 0.65 : 1 } } >
{ /* Header */ }
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'flex-start' , gap : 12 } } >
< div style = { { flex : 1 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , flexWrap : 'wrap' , marginBottom : 8 } } >
< span style = { { fontSize : 10 , fontWeight : 700 , padding : '2px 8px' , borderRadius : 6 , background : color + '22' , color , border : ` 1px solid ${ color } 44 ` , letterSpacing : 1 } } >
{ aw ? . severity _label || alert . severity }
< / span >
< span style = { { fontSize : 11 , fontWeight : 600 , color : 'var(--text-muted)' , background : 'var(--bg-tertiary)' , padding : '2px 8px' , borderRadius : 6 } } > ARCTIC WOLF < / span >
{ alert . acknowledged && < span style = { { fontSize : 10 , padding : '2px 7px' , borderRadius : 6 , background : 'var(--bg-tertiary)' , color : 'var(--text-muted)' , border : '1px solid var(--border-color)' } } > ✓ Quittiert < / span > }
{ alert . ticket _id && < span style = { { fontSize : 10 , padding : '2px 7px' , borderRadius : 6 , background : SC . ok + '22' , color : SC . ok , border : ` 1px solid ${ SC . ok } 44 ` } } > 🎫 Ticket # { alert . ticket _id } < / span > }
< / div >
< div style = { { fontSize : 15 , fontWeight : 700 , marginBottom : 4 } } > { alert . message || aw ? . incident _name } < / div >
< div style = { { fontSize : 12 , color : 'var(--text-muted)' } } >
🖥 { alert . device } { aw ? . ip ? ` · ${ aw . ip } ` : '' }
{ alert . state _time && < span style = { { marginLeft : 12 } } > 🕐 { alert . state _time } < / span > }
< span style = { { marginLeft : 12 } } > Eingang : { timeAgo ( alert . created _at ) } < / span >
< / div >
< / div >
{ actions }
< / div >
{ /* Schnell-Info-Grid */ }
{ aw && (
< div style = { { marginTop : 12 , padding : '10px 12px' , background : 'var(--bg-primary)' , borderRadius : 8 } } >
{ row ( 'Event-Typ' , aw . event _type ) }
{ row ( 'Application' , aw . application ) }
{ row ( 'User' , aw . user ) }
{ row ( 'Process' , aw . process ) }
< / div >
) }
{ /* Aufklapp-Button */ }
{ aw && ( aw . what _is _it || aw . why _matters || aw . next _steps || aw . recommend ) && (
< button onClick = { ( ) => toggleAlertExpand ( alert . id ) } style = { { marginTop : 10 , padding : '4px 12px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' } } >
{ expanded ? '▲ Weniger' : '▼ Mehr Details' }
< / button >
) }
{ /* Erweiterte Sektionen */ }
{ expanded && aw && (
< div style = { { marginTop : 12 , padding : '12px' , background : 'var(--bg-primary)' , borderRadius : 8 , display : 'flex' , flexDirection : 'column' , gap : 2 } } >
{ section ( 'Was ist passiert?' , aw . what _is _it ) }
{ section ( 'Warum ist das relevant?' , aw . why _matters ) }
{ section ( 'Wie wurde es erkannt?' , aw . how _detected ) }
{ section ( 'Nächste Schritte' , aw . next _steps ) }
{ section ( 'Empfehlungen Arctic Wolf' , aw . recommend ) }
< / div >
) }
< / div >
) ;
}
// ── Standard-Karte (Netgo & andere) ────────────────────────────
return (
< div key = { alert . id } style = { { background : 'var(--bg-secondary)' , border : ` 1px solid ${ alert . acknowledged ? 'var(--border-color)' : color + '44' } ` , borderLeft : ` 4px solid ${ color } ` , borderRadius : 10 , padding : '12px 16px' , opacity : alert . acknowledged ? 0.6 : 1 } } >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'flex-start' , gap : 12 } } >
< div style = { { flex : 1 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , marginBottom : 6 } } >
< span style = { { fontSize : 10 , fontWeight : 700 , padding : '2px 7px' , borderRadius : 6 , background : color + '22' , color , border : ` 1px solid ${ color } 44 ` , letterSpacing : 1 } } > { alert . severity || 'UNKNOWN' } < / span >
{ alert . acknowledged && < span style = { { fontSize : 10 , padding : '2px 7px' , borderRadius : 6 , background : 'var(--bg-tertiary)' , color : 'var(--text-muted)' , border : '1px solid var(--border-color)' } } > ✓ Quittiert < / span > }
{ alert . ticket _id && < span style = { { fontSize : 10 , padding : '2px 7px' , borderRadius : 6 , background : SC . ok + '22' , color : SC . ok , border : ` 1px solid ${ SC . ok } 44 ` } } > 🎫 Ticket # { alert . ticket _id } < / span > }
< span style = { { fontSize : 11 , color : 'var(--text-muted)' , marginLeft : 4 } } > { alert . source ? . toUpperCase ( ) } < / span >
< / div >
{ alert . device && < div style = { { fontSize : 14 , fontWeight : 600 , marginBottom : 2 } } > 🖥 { alert . device } { alert . service ? ` · ${ alert . service } ` : '' } < / div > }
{ alert . state _transition && < div style = { { fontSize : 12 , color : 'var(--text-muted)' , marginBottom : 2 } } > Status : { alert . state _transition } < / div > }
{ alert . message && < div style = { { fontSize : 12 , color : 'var(--text-secondary)' , marginTop : 4 , wordBreak : 'break-word' } } > { alert . message } < / div > }
< div style = { { display : 'flex' , gap : 14 , marginTop : 6 , fontSize : 11 , color : 'var(--text-muted)' } } >
{ alert . customer && < span > Kunde : { alert . customer } < / span > }
{ alert . state _time && < span > 🕐 { alert . state _time } < / span > }
< span > Eingang : { timeAgo ( alert . created _at ) } < / span >
< / div >
< / div >
{ actions }
< / div >
< / div >
) ;
} ;
const otherAlerts = extAlerts . filter ( a => a . source !== 'netgo' && a . source !== 'arcticwolf' && a . source !== 'mdo' ) ;
const otherUnacked = otherAlerts . filter ( a => ! a . acknowledged ) . length ;
const hiddenCount = otherAlerts . filter ( a => a . acknowledged ) . length ;
const visibleAlerts = hideAcknowledged ? otherAlerts . filter ( a => ! a . acknowledged ) : otherAlerts ;
return (
< div >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 14 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 12 } } >
< span style = { { fontSize : 13 , color : 'var(--text-muted)' } } > { otherAlerts . length } Alert { otherAlerts . length !== 1 ? 's' : '' } · { otherUnacked } nicht quittiert < / span >
{ hiddenCount > 0 && (
< button onClick = { ( ) => setHideAcknowledged ( h => ! h ) } style = { { padding : '3px 10px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : hideAcknowledged ? 'var(--accent)' : 'none' , color : hideAcknowledged ? '#fff' : 'var(--text-muted)' , fontSize : 11 , cursor : 'pointer' } } >
{ hideAcknowledged ? ` ✓ Quittierte ausgeblendet ( ${ hiddenCount } ) ` : ` Quittierte ausblenden ( ${ hiddenCount } ) ` }
< / button >
) }
< / div >
< button onClick = { loadExtAlerts } style = { { padding : '5px 14px' , borderRadius : 8 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' } } > ↻ Aktualisieren < / button >
< / div >
{ visibleAlerts . length === 0 && (
< div style = { { textAlign : 'center' , padding : 60 , color : 'var(--text-muted)' , fontSize : 14 } } > ✅ Keine sonstigen Alerts vorhanden < / div >
) }
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 10 } } >
{ visibleAlerts . map ( renderAlertCard ) }
< / div >
< / div >
) ;
} ;
const renderNetGoAlerts = ( ) => {
const sevColor = ( s ) => s === 'CRIT' ? SC . crit : s === 'WARN' ? SC . warn : s === 'OK' ? SC . ok : SC . unknown ;
const parseAi = ( alert ) => {
if ( ! alert . ai _analysis ) return null ;
try { return typeof alert . ai _analysis === 'string' ? JSON . parse ( alert . ai _analysis ) : alert . ai _analysis ; } catch { return null ; }
} ;
const parseTransition = ( st ) => {
if ( ! st ) return { from : '' , to : '' } ;
const m = st . match ( /^(.+?)\s+(?:to|→)\s+(.+)$/i ) ;
return m ? { from : m [ 1 ] . trim ( ) , to : m [ 2 ] . trim ( ) } : { from : '' , to : st } ;
} ;
const hiddenCount = netgoAlerts . filter ( a => a . acknowledged ) . length ;
const visible = hideAcknowledged ? netgoAlerts . filter ( a => ! a . acknowledged ) : netgoAlerts ;
// Kanban-Spalten
const columns = [
{ key : 'CRIT' , label : 'Kritisch' , icon : '🔴' , color : SC . crit , alerts : visible . filter ( a => ( parseAi ( a ) ? . severity || a . severity ) === 'CRIT' ) } ,
{ key : 'WARN' , label : 'Warnung' , icon : '🟡' , color : SC . warn , alerts : visible . filter ( a => ( parseAi ( a ) ? . severity || a . severity ) === 'WARN' ) } ,
{ key : 'OK' , label : 'Wiederhergestellt' , icon : '🟢' , color : SC . ok , alerts : visible . filter ( a => ( parseAi ( a ) ? . severity || a . severity ) === 'OK' ) } ,
{ key : 'UNKNOWN' , label : 'Unbekannt' , icon : '⚪' , color : SC . unknown , alerts : visible . filter ( a => ! [ 'CRIT' , 'WARN' , 'OK' ] . includes ( parseAi ( a ) ? . severity || a . severity ) ) } ,
] . filter ( col => col . alerts . length > 0 ) ;
const KanbanCard = ( { alert } ) => {
const ai = parseAi ( alert ) ;
const sev = ai ? . severity || alert . severity ;
const col = sevColor ( sev ) ;
const trans = parseTransition ( alert . state _transition ) ;
const stateFrom = ai ? . state _from || trans . from ;
const stateTo = ai ? . state _to || trans . to ;
const device = ai ? . device || ( alert . device ? . length < 40 ? alert . device : '– ' ) ;
const service = ai ? . service || ( alert . service ? . length < 80 ? alert . service : '' ) ;
return (
< div style = { {
background : 'var(--bg-secondary)' ,
border : ` 1px solid ${ col } 33 ` ,
borderTop : ` 3px solid ${ col } ` ,
borderRadius : 10 ,
overflow : 'hidden' ,
opacity : alert . acknowledged ? 0.55 : 1 ,
fontSize : 12 ,
} } >
{ /* Card-Header */ }
< div style = { { padding : '10px 12px 8px' , borderBottom : '1px solid var(--border-color)' } } >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'flex-start' , gap : 6 } } >
< div style = { { fontWeight : 700 , fontSize : 13 , color : 'var(--text-primary)' , lineHeight : 1.3 } } >
🖥 { device }
< / div >
< span style = { { fontSize : 10 , color : 'var(--text-muted)' , flexShrink : 0 , marginTop : 2 } } > { timeAgo ( alert . created _at ) } < / span >
< / div >
{ service && < div style = { { fontSize : 11 , color : 'var(--text-secondary)' , marginTop : 2 } } > { service } < / div > }
< / div >
{ /* State Transition */ }
{ ( stateFrom || stateTo ) && (
< div style = { { display : 'flex' , alignItems : 'center' , gap : 6 , padding : '6px 12px' , background : col + '08' } } >
{ stateFrom && < span style = { { fontSize : 11 , fontWeight : 600 , padding : '2px 7px' , borderRadius : 5 , background : ` ${ SC . crit } 22 ` , color : SC . crit } } > { stateFrom } < / span > }
{ stateFrom && stateTo && < span style = { { color : 'var(--text-muted)' } } > → < / span > }
{ stateTo && < span style = { { fontSize : 11 , fontWeight : 700 , padding : '2px 7px' , borderRadius : 5 , background : ` ${ col } 22 ` , color : col } } > { stateTo } < / span > }
< / div >
) }
{ /* KI-Block */ }
< div style = { { padding : '8px 12px' } } >
{ ai ? . metrics && (
< div style = { { fontSize : 11 , color : 'var(--text-muted)' , marginBottom : 6 , padding : '3px 7px' , background : 'var(--bg-tertiary)' , borderRadius : 5 , display : 'inline-block' } } >
📊 { ai . metrics }
< / div >
) }
{ ai ? . what ? (
< >
< div style = { { fontSize : 12 , color : 'var(--text-primary)' , lineHeight : 1.5 , marginBottom : 6 } } > { ai . what } < / div >
{ ai . recommendation && (
< div style = { { display : 'flex' , gap : 5 , padding : '5px 8px' , borderRadius : 6 , background : ai . action _needed ? ` ${ SC . warn } 12 ` : ` ${ SC . ok } 12 ` , border : ` 1px solid ${ ai . action _needed ? SC . warn : SC . ok } 30 ` } } >
< span style = { { flexShrink : 0 } } > { ai . action _needed ? '⚠️' : '✅' } < / span >
< span style = { { fontSize : 11 , color : 'var(--text-secondary)' , lineHeight : 1.4 } } > { ai . recommendation } < / span >
< / div >
) }
< / >
) : (
< div style = { { fontSize : 11 , color : 'var(--text-muted)' } } > ⏳ KI - Analyse ausstehend … < / div >
) }
< / div >
{ /* Footer */ }
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , padding : '6px 12px 8px' , borderTop : '1px solid var(--border-color)' } } >
< div style = { { display : 'flex' , gap : 6 } } >
{ ai && < span style = { { fontSize : 10 , padding : '1px 6px' , borderRadius : 4 , background : 'rgba(124,92,246,0.12)' , color : '#a78bfa' } } > 🤖 KI < / span > }
{ alert . acknowledged && < span style = { { fontSize : 10 , padding : '1px 6px' , borderRadius : 4 , background : 'var(--bg-tertiary)' , color : 'var(--text-muted)' } } > ✓ Quittiert < / span > }
< / div >
< div style = { { display : 'flex' , gap : 4 } } >
{ ! alert . acknowledged && (
< button onClick = { ( ) => handleExtAcknowledge ( alert . id ) } style = { { padding : '3px 9px' , borderRadius : 6 , border : ` 1px solid ${ col } 55 ` , background : col + '15' , color : col , fontSize : 11 , cursor : 'pointer' , fontWeight : 600 } } > ✓ < / button >
) }
< button onClick = { ( ) => handleExtRemove ( alert . id ) } style = { { padding : '3px 7px' , borderRadius : 6 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 11 , cursor : 'pointer' } } > 🗑 < / button >
< / div >
< / div >
< / div >
) ;
} ;
return (
< div >
{ /* Header */ }
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 16 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 12 } } >
< span style = { { fontSize : 13 , color : 'var(--text-muted)' } } >
{ netgoAlerts . length } Alerts · { netgoUnacked } nicht quittiert
< / span >
{ hiddenCount > 0 && (
< button onClick = { ( ) => setHideAcknowledged ( h => ! h ) } style = { { padding : '3px 10px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 11 , cursor : 'pointer' } } >
{ hideAcknowledged ? ` Quittierte einblenden ( ${ hiddenCount } ) ` : 'Quittierte ausblenden' }
< / button >
) }
< / div >
< button onClick = { loadExtAlerts } style = { { padding : '5px 14px' , borderRadius : 8 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' } } > ↻ Aktualisieren < / button >
< / div >
{ visible . length === 0 && (
< div style = { { textAlign : 'center' , padding : 60 , color : 'var(--text-muted)' , fontSize : 14 } } > ✅ Keine aktiven NetGo - Alerts < / div >
) }
{ /* Kanban Board */ }
< div style = { { display : 'grid' , gridTemplateColumns : ` repeat( ${ columns . length } , 1fr) ` , gap : 16 , alignItems : 'start' } } >
{ columns . map ( col => (
< div key = { col . key } >
{ /* Spalten-Header */ }
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , marginBottom : 10 , padding : '8px 12px' , borderRadius : 8 , background : col . color + '12' , border : ` 1px solid ${ col . color } 33 ` } } >
< span style = { { fontSize : 14 } } > { col . icon } < / span >
< span style = { { fontWeight : 700 , fontSize : 13 , color : col . color } } > { col . label } < / span >
< span style = { { marginLeft : 'auto' , fontSize : 11 , fontWeight : 700 , padding : '1px 7px' , borderRadius : 10 , background : col . color + '22' , color : col . color } } > { col . alerts . length } < / span >
< / div >
{ /* Cards */ }
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 10 } } >
{ col . alerts . map ( alert => < KanbanCard key = { alert . id } alert = { alert } / > ) }
< / div >
< / div >
) ) }
< / div >
< / div >
) ;
} ;
const renderArcticWolfAlerts = ( ) => {
const sevColor = ( s ) => s === 'CRIT' ? SC . crit : s === 'WARN' ? SC . warn : s === 'OK' ? SC . ok : SC . unknown ;
const hiddenCount = awAlerts . filter ( a => a . acknowledged ) . length ;
const visible = hideAcknowledged ? awAlerts . filter ( a => ! a . acknowledged ) : awAlerts ;
const renderAWCard = ( alert ) => {
const color = sevColor ( alert . severity ) ;
const expanded = expandedAlerts [ alert . id ] ;
let aw = null ;
if ( alert . raw _body ) { try { aw = JSON . parse ( alert . raw _body ) ; } catch { } }
const labelStyle = { fontSize : 11 , color : 'var(--text-muted)' , minWidth : 130 , flexShrink : 0 } ;
const valStyle = { fontSize : 12 , color : 'var(--text-primary)' , wordBreak : 'break-word' } ;
const row = ( label , val ) => val ? (
< div style = { { display : 'flex' , gap : 8 , padding : '4px 0' , borderBottom : '1px solid var(--border-color)' } } >
< span style = { labelStyle } > { label } < / span > < span style = { valStyle } > { val } < / span >
< / div >
) : null ;
const section = ( title , text ) => text ? (
< div style = { { marginTop : 10 } } >
< div style = { { fontSize : 11 , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : 1 , marginBottom : 4 } } > { title } < / div >
< div style = { { fontSize : 12 , color : 'var(--text-secondary)' , lineHeight : 1.6 } } > { text } < / div >
< / div >
) : null ;
return (
< div key = { alert . id } style = { { background : 'var(--bg-secondary)' , border : ` 1px solid ${ alert . acknowledged ? 'var(--border-color)' : color + '44' } ` , borderLeft : ` 4px solid ${ color } ` , borderRadius : 10 , padding : '14px 16px' , opacity : alert . acknowledged ? 0.65 : 1 } } >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'flex-start' , gap : 12 } } >
< div style = { { flex : 1 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , flexWrap : 'wrap' , marginBottom : 8 } } >
< span style = { { fontSize : 10 , fontWeight : 700 , padding : '2px 8px' , borderRadius : 6 , background : color + '22' , color , border : ` 1px solid ${ color } 44 ` , letterSpacing : 1 } } > { aw ? . severity _label || alert . severity } < / span >
{ alert . acknowledged && < span style = { { fontSize : 10 , padding : '2px 7px' , borderRadius : 6 , background : 'var(--bg-tertiary)' , color : 'var(--text-muted)' , border : '1px solid var(--border-color)' } } > ✓ Quittiert < / span > }
{ alert . ticket _id && < span style = { { fontSize : 10 , padding : '2px 7px' , borderRadius : 6 , background : SC . ok + '22' , color : SC . ok , border : ` 1px solid ${ SC . ok } 44 ` } } > 🎫 Ticket # { alert . ticket _id } < / span > }
< / div >
< div style = { { fontSize : 15 , fontWeight : 700 , marginBottom : 4 } } > { alert . message || aw ? . incident _name } < / div >
< div style = { { fontSize : 12 , color : 'var(--text-muted)' } } >
🖥 { alert . device } { aw ? . ip ? ` · ${ aw . ip } ` : '' }
{ alert . state _time && < span style = { { marginLeft : 12 } } > 🕐 { alert . state _time } < / span > }
< span style = { { marginLeft : 12 } } > Eingang : { timeAgo ( alert . created _at ) } < / span >
< / div >
< / div >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 6 , flexShrink : 0 } } >
{ ! alert . ticket _id && (
< button onClick = { ( ) => handleExtCreateTicket ( alert . id ) } style = { { padding : '5px 12px' , borderRadius : 7 , border : ` 1px solid var(--accent) ` , background : 'var(--accent)' , color : '#fff' , fontSize : 12 , cursor : 'pointer' , fontWeight : 600 , whiteSpace : 'nowrap' } } > 🎫 Ticket < / button >
) }
{ ! alert . acknowledged && (
< button onClick = { ( ) => handleExtAcknowledge ( alert . id ) } style = { { padding : '5px 12px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' , whiteSpace : 'nowrap' } } > ✓ Quittieren < / button >
) }
< button onClick = { ( ) => handleExtRemove ( alert . id ) } style = { { padding : '5px 12px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' } } > 🗑 < / button >
< / div >
< / div >
{ aw && (
< div style = { { marginTop : 12 , padding : '10px 12px' , background : 'var(--bg-primary)' , borderRadius : 8 } } >
{ row ( 'Event-Typ' , aw . event _type ) }
{ row ( 'Application' , aw . application ) }
{ row ( 'User' , aw . user ) }
{ row ( 'Process' , aw . process ) }
< / div >
) }
{ aw && ( aw . what _is _it || aw . why _matters || aw . next _steps || aw . recommend ) && (
< button onClick = { ( ) => toggleAlertExpand ( alert . id ) } style = { { marginTop : 10 , padding : '4px 12px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' } } >
{ expanded ? '▲ Weniger' : '▼ Mehr Details' }
< / button >
) }
{ expanded && aw && (
< div style = { { marginTop : 12 , padding : '12px' , background : 'var(--bg-primary)' , borderRadius : 8 , display : 'flex' , flexDirection : 'column' , gap : 2 } } >
{ section ( 'Was ist passiert?' , aw . what _is _it ) }
{ section ( 'Warum ist das relevant?' , aw . why _matters ) }
{ section ( 'Wie wurde es erkannt?' , aw . how _detected ) }
{ section ( 'Nächste Schritte' , aw . next _steps ) }
{ section ( 'Empfehlungen Arctic Wolf' , aw . recommend ) }
< / div >
) }
< / div >
) ;
} ;
return (
< div >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 14 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 12 } } >
< span style = { { fontSize : 13 , color : 'var(--text-muted)' } } > { awAlerts . length } Alert { awAlerts . length !== 1 ? 's' : '' } · { awUnacked } nicht quittiert < / span >
{ hiddenCount > 0 && (
< button onClick = { ( ) => setHideAcknowledged ( h => ! h ) } style = { { padding : '3px 10px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : hideAcknowledged ? 'var(--accent)' : 'none' , color : hideAcknowledged ? '#fff' : 'var(--text-muted)' , fontSize : 11 , cursor : 'pointer' } } >
{ hideAcknowledged ? ` ✓ Quittierte ausgeblendet ( ${ hiddenCount } ) ` : ` Quittierte ausblenden ( ${ hiddenCount } ) ` }
< / button >
) }
< / div >
< button onClick = { loadExtAlerts } style = { { padding : '5px 14px' , borderRadius : 8 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' } } > ↻ Aktualisieren < / button >
< / div >
{ visible . length === 0 && (
< div style = { { textAlign : 'center' , padding : 60 , color : 'var(--text-muted)' , fontSize : 14 } } > ✅ Keine Arctic Wolf Alerts vorhanden < / div >
) }
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 10 } } >
{ visible . map ( renderAWCard ) }
< / div >
< / div >
) ;
} ;
const renderMdoAlerts = ( ) => {
const sevColor = ( s ) => s === 'CRIT' ? SC . crit : s === 'WARN' ? SC . warn : s === 'OK' ? SC . ok : SC . unknown ;
const parseAi = ( alert ) => { try { return alert . ai _analysis ? ( typeof alert . ai _analysis === 'string' ? JSON . parse ( alert . ai _analysis ) : alert . ai _analysis ) : null ; } catch { return null ; } } ;
const parseRaw = ( alert ) => { try { return alert . raw _body ? ( typeof alert . raw _body === 'string' ? JSON . parse ( alert . raw _body ) : alert . raw _body ) : null ; } catch { return null ; } } ;
const hiddenCount = mdoAlerts . filter ( a => a . acknowledged ) . length ;
const visible = hideAcknowledged ? mdoAlerts . filter ( a => ! a . acknowledged ) : mdoAlerts ;
const labelStyle = { fontSize : 11 , color : 'var(--text-muted)' , minWidth : 140 , flexShrink : 0 } ;
const valStyle = { fontSize : 12 , color : 'var(--text-primary)' , wordBreak : 'break-word' } ;
const row = ( label , val ) => val ? (
< div style = { { display : 'flex' , gap : 8 , padding : '4px 0' , borderBottom : '1px solid var(--border-color)' } } >
< span style = { labelStyle } > { label } < / span > < span style = { valStyle } > { val } < / span >
< / div >
) : null ;
const renderMdoCard = ( alert ) => {
const color = sevColor ( alert . severity ) ;
const expanded = expandedAlerts [ alert . id ] ;
const ai = parseAi ( alert ) ;
const raw = parseRaw ( alert ) ;
return (
< div key = { alert . id } style = { { background : 'var(--bg-secondary)' , border : ` 1px solid ${ alert . acknowledged ? 'var(--border-color)' : color + '44' } ` , borderLeft : ` 4px solid ${ color } ` , borderRadius : 10 , padding : '14px 16px' , opacity : alert . acknowledged ? 0.65 : 1 } } >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'flex-start' , gap : 12 } } >
< div style = { { flex : 1 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , flexWrap : 'wrap' , marginBottom : 8 } } >
< span style = { { fontSize : 10 , fontWeight : 700 , padding : '2px 8px' , borderRadius : 6 , background : color + '22' , color , border : ` 1px solid ${ color } 44 ` , letterSpacing : 1 } } > { alert . severity } < / span >
< span style = { { fontSize : 11 , fontWeight : 600 , color : '#60a5fa' , background : 'rgba(96,165,250,0.12)' , padding : '2px 8px' , borderRadius : 6 , border : '1px solid rgba(96,165,250,0.3)' } } > MS DEFENDER < / span >
{ alert . acknowledged && < span style = { { fontSize : 10 , padding : '2px 7px' , borderRadius : 6 , background : 'var(--bg-tertiary)' , color : 'var(--text-muted)' , border : '1px solid var(--border-color)' } } > ✓ Quittiert < / span > }
{ alert . ticket _id && < span style = { { fontSize : 10 , padding : '2px 7px' , borderRadius : 6 , background : SC . ok + '22' , color : SC . ok , border : ` 1px solid ${ SC . ok } 44 ` } } > 🎫 Ticket # { alert . ticket _id } < / span > }
< / div >
< div style = { { fontSize : 15 , fontWeight : 700 , marginBottom : 4 } } > { alert . message } < / div >
< div style = { { fontSize : 12 , color : 'var(--text-muted)' } } >
👤 { alert . device || '(kein Benutzer)' }
{ alert . service && < span style = { { marginLeft : 12 } } > 📂 { alert . service } < / span > }
{ alert . state _time && < span style = { { marginLeft : 12 } } > 🕐 { new Date ( alert . state _time ) . toLocaleString ( 'de-DE' ) } < / span > }
< span style = { { marginLeft : 12 } } > Eingang : { timeAgo ( alert . created _at ) } < / span >
< / div >
< / div >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 6 , flexShrink : 0 } } >
{ ! alert . ticket _id && < button onClick = { ( ) => handleExtCreateTicket ( alert . id ) } style = { { padding : '5px 12px' , borderRadius : 7 , border : ` 1px solid var(--accent) ` , background : 'var(--accent)' , color : '#fff' , fontSize : 12 , cursor : 'pointer' , fontWeight : 600 , whiteSpace : 'nowrap' } } > 🎫 Ticket < / button > }
{ ! alert . acknowledged && < button onClick = { ( ) => handleExtAcknowledge ( alert . id ) } style = { { padding : '5px 12px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' , whiteSpace : 'nowrap' } } > ✓ Quittieren < / button > }
< button onClick = { ( ) => handleExtRemove ( alert . id ) } style = { { padding : '5px 12px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' } } > 🗑 < / button >
< / div >
< / div >
{ raw && (
< div style = { { marginTop : 12 , padding : '10px 12px' , background : 'var(--bg-primary)' , borderRadius : 8 } } >
{ row ( 'Kategorie' , raw . category ) }
{ row ( 'Bedrohungsfamilie' , raw . threatFamilyName ) }
{ row ( 'Angriffsvektor' , ai ? . attack _vector || ( raw . attackTechniques || [ ] ) [ 0 ] ) }
{ row ( 'Bedrohungstyp' , ai ? . threat _type ) }
{ row ( 'Status (Defender)' , alert . state _transition ) }
< / div >
) }
{ ( ai ? . what || ai ? . recommendation ) && (
< button onClick = { ( ) => toggleAlertExpand ( alert . id ) } style = { { marginTop : 10 , padding : '4px 12px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' } } >
{ expanded ? '▲ Weniger' : '▼ KI-Analyse anzeigen' }
< / button >
) }
{ ! ai && < div style = { { marginTop : 8 , fontSize : 11 , color : 'var(--text-muted)' } } > ⏳ KI - Analyse ausstehend … < / div > }
{ expanded && ai && (
< div style = { { marginTop : 12 , padding : '12px' , background : 'var(--bg-primary)' , borderRadius : 8 } } >
{ ai . what && (
< div style = { { marginBottom : 10 } } >
< div style = { { fontSize : 11 , fontWeight : 700 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : 1 , marginBottom : 4 } } > Was ist passiert ? < / div >
< div style = { { fontSize : 12 , color : 'var(--text-secondary)' , lineHeight : 1.6 } } > { ai . what } < / div >
< / div >
) }
{ ai . recommendation && (
< div style = { { display : 'flex' , gap : 5 , padding : '8px 10px' , borderRadius : 7 , background : ai . action _needed ? ` ${ SC . warn } 12 ` : ` ${ SC . ok } 12 ` , border : ` 1px solid ${ ai . action _needed ? SC . warn : SC . ok } 30 ` } } >
< span style = { { flexShrink : 0 } } > { ai . action _needed ? '⚠️' : '✅' } < / span >
< span style = { { fontSize : 12 , color : 'var(--text-secondary)' , lineHeight : 1.5 } } > { ai . recommendation } < / span >
< / div >
) }
< / div >
) }
< / div >
) ;
} ;
return (
< div >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 14 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 12 } } >
< span style = { { fontSize : 13 , color : 'var(--text-muted)' } } > { mdoAlerts . length } Alert { mdoAlerts . length !== 1 ? 's' : '' } · { mdoUnacked } nicht quittiert < / span >
{ hiddenCount > 0 && (
< button onClick = { ( ) => setHideAcknowledged ( h => ! h ) } style = { { padding : '3px 10px' , borderRadius : 7 , border : '1px solid var(--border-color)' , background : hideAcknowledged ? 'var(--accent)' : 'none' , color : hideAcknowledged ? '#fff' : 'var(--text-muted)' , fontSize : 11 , cursor : 'pointer' } } >
{ hideAcknowledged ? ` ✓ Quittierte ausgeblendet ( ${ hiddenCount } ) ` : ` Quittierte ausblenden ( ${ hiddenCount } ) ` }
< / button >
) }
< / div >
< button onClick = { loadExtAlerts } style = { { padding : '5px 14px' , borderRadius : 8 , border : '1px solid var(--border-color)' , background : 'none' , color : 'var(--text-muted)' , fontSize : 12 , cursor : 'pointer' } } > ↻ Aktualisieren < / button >
< / div >
{ visible . length === 0 && < div style = { { textAlign : 'center' , padding : 60 , color : 'var(--text-muted)' , fontSize : 14 } } > ✅ Keine aktiven Microsoft Defender Alerts < / div > }
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 10 } } > { visible . map ( renderMdoCard ) } < / div >
< / div >
) ;
} ;
const tabs = [
{ id : 'overview' , label : '🖥 Host-Übersicht' , badge : null } ,
{ id : 'network' , label : 'Netzwerk' , badge : devices . length + unifiDevices . length } ,
{ id : 'agents' , label : 'Agents' , badge : agents . length } ,
{ id : 'problems' , label : 'Probleme' , badge : activeProblems . length , red : activeProblems . length > 0 } ,
{ id : 'netgo' , label : 'NetGo' , badge : netgoUnacked , red : netgoUnacked > 0 } ,
{ id : 'arcticwolf' , label : 'Arctic Wolf' , badge : awUnacked , red : awUnacked > 0 } ,
{ id : 'mdo' , label : 'MS Defender' , badge : mdoUnacked , red : mdoUnacked > 0 } ,
{ id : 'ext-alerts' , label : 'Sonstige' , badge : extUnacked - netgoUnacked - awUnacked - mdoUnacked > 0 ? extUnacked - netgoUnacked - awUnacked - mdoUnacked : null , red : false } ,
] ;
return (
< div style = { { padding : 24 } } >
< style > { `
@ keyframes monPulse { 0 % , 100 % { opacity : 1 } 50 % { opacity : .3 } }
@ keyframes nmPulse { 0 % , 100 % { opacity : 1 ; transform : scale ( 1 ) } 50 % { opacity : .5 ; transform : scale ( 1.3 ) } }
: : - webkit - scrollbar { width : 6 px ; height : 6 px ; }
: : - webkit - scrollbar - track { background : transparent ; }
: : - webkit - scrollbar - thumb { background : rgba ( 255 , 255 , 255 , 0.12 ) ; border - radius : 10 px ; }
: : - webkit - scrollbar - thumb : hover { background : rgba ( 255 , 255 , 255 , 0.22 ) ; }
` }</style>
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 18 } } >
< h1 style = { { fontSize : 22 , fontWeight : 700 , margin : 0 } } > 📡 Monitoring < / h1 >
< / div >
{ /* Checkmk-style Tactical Overview */ }
< TacticalOverview devices = { devices } agents = { agents } problems = { problems } acknowledged = { acknowledged } countdown = { countdown } lastUpdated = { lastUpdated } onTabChange = { setActiveTab } extAlerts = { extAlerts } / >
{ /* Tab bar */ }
< div style = { { display : 'flex' , gap : 0 , marginBottom : 20 , borderBottom : '1px solid var(--border-color)' } } >
{ tabs . map ( ( { id , label , badge , red } ) => (
< button key = { id } onClick = { ( ) => setActiveTab ( id ) } style = { { padding : '10px 22px' , border : 'none' , background : 'none' , cursor : 'pointer' , fontSize : 13 , fontWeight : activeTab === id ? 700 : 400 , color : activeTab === id ? 'var(--accent)' : 'var(--text-muted)' , borderBottom : activeTab === id ? '2px solid var(--accent)' : '2px solid transparent' , marginBottom : - 1 , transition : 'all .15s' , display : 'flex' , alignItems : 'center' , gap : 7 } } >
{ label }
{ badge != null && badge > 0 && (
< span style = { { fontSize : 10 , fontWeight : 700 , padding : '1px 6px' , borderRadius : 10 , background : red ? ` ${ SC . crit } 22 ` : 'var(--bg-secondary)' , color : red ? SC . crit : 'var(--text-muted)' , border : ` 1px solid ${ red ? SC . crit + '44' : 'var(--border-color)' } ` } } > { badge } < / span >
) }
< / button >
) ) }
< / div >
{ activeTab === 'overview' && renderOverview ( ) }
{ activeTab === 'network' && renderNetwork ( ) }
{ activeTab === 'agents' && renderAgents ( ) }
{ activeTab === 'problems' && renderProblems ( ) }
{ activeTab === 'netgo' && renderNetGoAlerts ( ) }
{ activeTab === 'arcticwolf' && renderArcticWolfAlerts ( ) }
{ activeTab === 'mdo' && renderMdoAlerts ( ) }
{ activeTab === 'ext-alerts' && renderExtAlerts ( ) }
{ selectedDevice && < NetworkDetailModal device = { selectedDevice } onClose = { ( ) => setSelectedDevice ( null ) } onEdit = { d => { setEditDevice ( d ) ; setShowDeviceForm ( true ) ; } } onDelete = { d => { handleNetDelete ( d ) ; setSelectedDevice ( null ) ; } } onCheckNow = { networkMonitorService . checkNow } / > }
{ showDeviceForm && < DeviceFormModal device = { editDevice } onSave = { handleNetSave } onClose = { ( ) => { setShowDeviceForm ( false ) ; setEditDevice ( null ) ; } } / > }
{ showDiscover && < DiscoverModal onClose = { ( ) => setShowDiscover ( false ) } onAdd = { handleNetSave } / > }
{ selectedAgent && < AgentDetailModal agent = { selectedAgent } onClose = { ( ) => setSelectedAgent ( null ) } onDelete = { handleAgentDelete } / > }
{ showUnifiCfg && < UnifiConfigModal unifiConfig = { unifiConfig } onSave = { cfg => { setUnifiConfig ( cfg ) ; loadUnifi ( ) ; toast . success ( 'Gespeichert' ) ; } } onClose = { ( ) => setShowUnifiCfg ( false ) } / > }
< / div >
) ;
} ;
export default MonitoringPage ;