2026-06-01 20:49:07 +02:00
import React , { useState , useEffect , useRef } from 'react' ;
import { useParams , useNavigate , Link } from 'react-router-dom' ;
import monitoringService from '../services/monitoringService' ;
import assetService from '../services/assetService' ;
import api from '../services/api' ;
import { toast } from 'react-toastify' ;
2026-06-10 13:31:11 +02:00
import { useAuth } from '../context/AuthContext' ;
2026-06-01 20:49:07 +02:00
// ─── CSS Variables injected inline (design from Device Detail.html) ─────────────
const C = {
teal : '#14b8a8' ,
teal2 : '#2dd2c2' ,
tealDim : 'rgba(20,184,168,0.14)' ,
success : '#22c55e' ,
warning : '#f59e0b' ,
danger : '#ef4444' ,
info : '#3b82f6' ,
purple : '#7c3aed' ,
} ;
// ─── 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 uptimeStr = ( h ) => {
if ( ! h ) return '– ' ;
const d = Math . floor ( h / 24 ) , rh = Math . floor ( h % 24 ) , rm = Math . floor ( ( h * 60 ) % 60 ) ;
if ( d > 0 ) return ` ${ d } d ${ rh } h ${ rm } min ` ;
return ` ${ rh } h ${ rm } min ` ;
} ;
const pct = ( used , total ) => ( total > 0 ? Math . min ( 100 , Math . round ( ( used / total ) * 100 ) ) : 0 ) ;
const barColor = ( p , warn = 70 , crit = 90 ) => {
if ( p >= crit ) return C . danger ;
if ( p >= warn ) return C . warning ;
return C . teal ;
} ;
const colorFor = ( name ) => {
let h = 0 ;
for ( const c of name ) h = ( h * 31 + c . charCodeAt ( 0 ) ) >>> 0 ;
const hues = [ 200 , 220 , 260 , 280 , 170 , 150 , 35 , 20 , 0 , 320 ] ;
return ` hsl( ${ hues [ h % hues . length ] } , 60%, 50%) ` ;
} ;
const copyToClipboard = ( text ) => {
navigator . clipboard . writeText ( text ) . then ( ( ) => toast . success ( 'Kopiert!' , { autoClose : 1200 } ) ) ;
} ;
// ─── Sub-components ─────────────────────────────────────────────────────────────
const MetricTile = ( { label , icon , value , unit , of : ofVal , footnote , pctVal , warnAt = 70 , critAt = 90 } ) => {
const p = pctVal ? ? 0 ;
const col = barColor ( p , warnAt , critAt ) ;
return (
< div style = { {
background : 'var(--bg-secondary)' , border : '1px solid var(--border-color)' ,
borderRadius : 18 , padding : '22px 24px' , display : 'flex' , flexDirection : 'column' , gap : 14 ,
boxShadow : '0 1px 2px rgba(0,0,0,0.35)' ,
} } >
< div style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 9 , fontSize : 11.5 , fontWeight : 600 , textTransform : 'uppercase' , letterSpacing : '0.10em' , color : 'var(--text-muted)' } } >
< span style = { { width : 26 , height : 26 , borderRadius : 7 , background : 'var(--bg-tertiary)' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , fontSize : 13 } } > { icon } < / span >
{ label }
< / div >
< span style = { { fontSize : 13.5 , fontWeight : 600 , color : col , fontVariantNumeric : 'tabular-nums' } } > { p } % < / span >
< / div >
< div style = { { display : 'flex' , alignItems : 'baseline' , gap : 8 , fontVariantNumeric : 'tabular-nums' } } >
< span style = { { fontSize : 36 , fontWeight : 600 , letterSpacing : '-0.03em' , lineHeight : 1 , color : 'var(--text-primary)' } } > { value } < / span >
{ unit && < span style = { { fontSize : 14 , color : 'var(--text-muted)' , fontWeight : 500 } } > { unit } < / span > }
{ ofVal && < span style = { { fontSize : 14 , color : 'var(--text-muted)' } } > { ofVal } < / span > }
< / div >
< div style = { { height : 6 , borderRadius : 99 , background : 'var(--bg-tertiary)' , overflow : 'hidden' } } >
< div style = { { height : '100%' , width : ` ${ p } % ` , borderRadius : 99 , background : col , transition : 'width .4s ease' } } / >
< / div >
{ footnote && (
< div style = { { fontSize : 12 , color : 'var(--text-muted)' , display : 'flex' , alignItems : 'center' , gap : 6 } } >
{ footnote }
< / div >
) }
< / div >
) ;
} ;
const SecRow = ( { ok , warn : isWarn , name , desc , state } ) => {
const bg = isWarn ? 'rgba(245,158,11,0.16)' : ok ? 'rgba(34,197,94,0.14)' : 'rgba(239,68,68,0.14)' ;
const color = isWarn ? C . warning : ok ? C . success : C . danger ;
return (
< div style = { { display : 'flex' , alignItems : 'center' , gap : 14 , padding : '12px 22px' , borderTop : '1px solid var(--border-color)' } } >
< div style = { { width : 28 , height : 28 , borderRadius : 8 , background : bg , color , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , flexShrink : 0 , fontSize : 14 } } >
{ ok && ! isWarn ? '✓' : isWarn ? '!' : '✗' }
< / div >
< div style = { { flex : 1 , minWidth : 0 } } >
< div style = { { fontSize : 13.5 , fontWeight : 600 , color : 'var(--text-primary)' } } > { name } < / div >
< div style = { { fontSize : 12 , color : 'var(--text-muted)' , marginTop : 1 } } > { desc } < / div >
< / div >
< span style = { {
fontSize : 11.5 , fontWeight : 600 , color ,
padding : '3px 9px' , borderRadius : 999 ,
background : bg ,
} } > { state } < / span >
< / div >
) ;
} ;
const DlRow = ( { label , value , mono , copyVal } ) => (
< div style = { { display : 'grid' , gridTemplateColumns : '150px 1fr' , gap : 16 , padding : '11px 22px' , alignItems : 'center' , borderTop : '1px solid var(--border-color)' } } >
< div style = { { fontSize : 12.5 , color : 'var(--text-muted)' , fontWeight : 500 } } > { label } < / div >
< div style = { { fontSize : 13.5 , color : 'var(--text-primary)' , overflow : 'hidden' , textOverflow : 'ellipsis' , fontFamily : mono ? 'ui-monospace, monospace' : 'inherit' , fontSize : mono ? 12.5 : 13.5 } } >
{ value }
{ copyVal && (
< button onClick = { ( ) => copyToClipboard ( copyVal ) } title = "Kopieren" style = { { marginLeft : 6 , background : 'none' , border : 'none' , cursor : 'pointer' , color : 'var(--text-muted)' , padding : 0 , fontSize : 11 , opacity : 0.6 } } > ⎘ < / button >
) }
< / div >
< / div >
) ;
const CardHead = ( { icon , title , meta } ) => (
< div style = { { display : 'flex' , alignItems : 'center' , gap : 12 , padding : '18px 22px' , borderBottom : '1px solid var(--border-color)' } } >
< div style = { { width : 30 , height : 30 , borderRadius : 8 , background : 'var(--bg-tertiary)' , color : 'var(--text-muted)' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , flexShrink : 0 , fontSize : 15 } } >
{ icon }
< / div >
< h2 style = { { margin : 0 , fontSize : 14 , fontWeight : 600 , letterSpacing : '-0.005em' , color : 'var(--text-primary)' } } > { title } < / h2 >
{ meta && < span style = { { marginLeft : 'auto' , fontSize : 12 , color : 'var(--text-muted)' } } > { meta } < / span > }
< / div >
) ;
const Btn = ( { onClick , children , variant = 'default' , disabled } ) => {
const styles = {
default : { background : 'var(--bg-secondary)' , color : 'var(--text-primary)' , border : '1px solid var(--border-color)' } ,
primary : { background : C . teal , color : '#fff' , border : ` 1px solid ${ C . teal } ` } ,
danger : { background : 'var(--bg-secondary)' , color : C . danger , border : ` 1px solid rgba(239,68,68,0.30) ` } ,
} ;
return (
< button onClick = { onClick } disabled = { disabled } style = { {
... styles [ variant ] ,
borderRadius : 10 , padding : '9px 14px' , fontSize : 13 , fontWeight : 500 ,
cursor : disabled ? 'not-allowed' : 'pointer' , display : 'inline-flex' , alignItems : 'center' , gap : 7 ,
whiteSpace : 'nowrap' , transition : 'background .14s' , opacity : disabled ? 0.5 : 1 ,
fontFamily : 'inherit' ,
} } >
{ children }
< / button >
) ;
} ;
2026-06-10 13:31:11 +02:00
// ─── Remote Shell (WebSocket Live Terminal) ───────────────────────────────────
const API = process . env . REACT _APP _API _URL || '/api' ;
const authFetch = ( url , opts = { } ) => {
const token = localStorage . getItem ( 'token' ) ;
return fetch ( url , { ... opts , headers : { ... ( opts . headers || { } ) , Authorization : ` Bearer ${ token } ` , 'Content-Type' : 'application/json' } } ) ;
} ;
// Einfacher ANSI-Code-Stripper für die Anzeige ohne xterm.js
function stripAnsi ( str ) {
// eslint-disable-next-line no-control-regex
return str . replace ( /\x1b\[[0-9;]*[mGKHF]/g , '' ) . replace ( /\x1b\[[0-9;]*[A-Z]/g , '' ) ;
}
function RemoteShell ( { agentId , agentHostname } ) {
const [ input , setInput ] = useState ( '' ) ;
const [ output , setOutput ] = useState ( '' ) ;
const [ status , setStatus ] = useState ( 'disconnected' ) ; // disconnected | connecting | connected
const outputRef = useRef ( null ) ;
const wsRef = useRef ( null ) ;
const inputRef = useRef ( null ) ;
const getWsUrl = ( ) => {
const token = localStorage . getItem ( 'token' ) ;
const proto = window . location . protocol === 'https:' ? 'wss' : 'ws' ;
const host = window . location . host ;
return ` ${ proto } :// ${ host } /ws?type=shell&agentId= ${ agentId } &token= ${ token } ` ;
} ;
const connect = ( ) => {
if ( wsRef . current && wsRef . current . readyState <= 1 ) return ;
setStatus ( 'connecting' ) ;
setOutput ( '' ) ;
const ws = new WebSocket ( getWsUrl ( ) ) ;
wsRef . current = ws ;
ws . onopen = ( ) => setStatus ( 'connected' ) ;
ws . onmessage = ( e ) => {
setOutput ( prev => prev + stripAnsi ( e . data ) ) ;
setTimeout ( ( ) => {
if ( outputRef . current ) outputRef . current . scrollTop = outputRef . current . scrollHeight ;
} , 10 ) ;
} ;
ws . onclose = ( ) => {
setStatus ( 'disconnected' ) ;
setOutput ( prev => prev + '\r\n[Verbindung getrennt]\r\n' ) ;
} ;
ws . onerror = ( ) => {
setStatus ( 'disconnected' ) ;
} ;
} ;
const disconnect = ( ) => {
wsRef . current ? . close ( ) ;
wsRef . current = null ;
} ;
// Beim Unmount trennen
useEffect ( ( ) => { return ( ) => disconnect ( ) ; } , [ ] ) ;
const send = ( ) => {
if ( ! input . trim ( ) || ! wsRef . current || wsRef . current . readyState !== 1 ) return ;
wsRef . current . send ( input + '\n' ) ;
setInput ( '' ) ;
} ;
const onKey = ( e ) => {
if ( e . key === 'Enter' ) { e . preventDefault ( ) ; send ( ) ; }
if ( e . key === 'c' && e . ctrlKey ) {
wsRef . current ? . send ( '\x03' ) ; // Ctrl+C
e . preventDefault ( ) ;
}
} ;
const statusColor = status === 'connected' ? '#34d399' : status === 'connecting' ? '#f59e0b' : '#6b7280' ;
const statusLabel = status === 'connected' ? 'Verbunden' : status === 'connecting' ? 'Verbinde…' : 'Getrennt' ;
return (
< div style = { { marginTop : 24 , borderRadius : 16 , border : '1px solid var(--border-color)' , overflow : 'hidden' , background : 'var(--bg-secondary)' } } >
< div style = { { padding : '14px 20px' , borderBottom : '1px solid var(--border-color)' , display : 'flex' , alignItems : 'center' , gap : 10 } } >
< span style = { { fontSize : 15 } } > 🖥 ️ < / span >
< span style = { { fontWeight : 600 , fontSize : 14 , color : 'var(--text-primary)' } } > Remote Shell < / span >
< span style = { { fontSize : 12 , color : 'var(--text-muted)' , marginLeft : 4 } } > { agentHostname } · PowerShell ( SYSTEM ) < / span >
< span style = { { marginLeft : 8 , display : 'flex' , alignItems : 'center' , gap : 5 , fontSize : 12 } } >
< span style = { { width : 8 , height : 8 , borderRadius : '50%' , background : statusColor , display : 'inline-block' } } / >
< span style = { { color : statusColor } } > { statusLabel } < / span >
< / span >
< div style = { { marginLeft : 'auto' , display : 'flex' , gap : 8 } } >
{ status === 'disconnected' ? (
< button onClick = { connect } style = { { background : '#238636' , border : 'none' , borderRadius : 8 , color : '#fff' , padding : '5px 14px' , fontSize : 12 , fontWeight : 600 , cursor : 'pointer' } } >
Verbinden
< / button >
) : (
< button onClick = { disconnect } style = { { background : '#21262d' , border : '1px solid #30363d' , borderRadius : 8 , color : '#cdd9e5' , padding : '5px 14px' , fontSize : 12 , cursor : 'pointer' } } >
Trennen
< / button >
) }
< button onClick = { ( ) => setOutput ( '' ) } style = { { background : '#21262d' , border : '1px solid #30363d' , borderRadius : 8 , color : '#8b949e' , padding : '5px 10px' , fontSize : 12 , cursor : 'pointer' } } >
Leeren
< / button >
< / div >
< / div >
< div
ref = { outputRef }
onClick = { ( ) => inputRef . current ? . focus ( ) }
style = { { fontFamily : 'ui-monospace, Cascadia Code, Consolas, monospace' , fontSize : 12.5 , lineHeight : 1.6 , padding : '16px 20px' , minHeight : 240 , maxHeight : 480 , overflowY : 'auto' , background : '#0d1117' , color : '#e6edf3' , cursor : 'text' , whiteSpace : 'pre-wrap' , wordBreak : 'break-all' } }
>
{ output || < span style = { { color : '#8b949e' } } > { status === 'disconnected' ? 'Auf "Verbinden" klicken um eine Shell-Sitzung zu starten.' : 'Warte auf Agent…' } < / span > }
< / div >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , padding : '10px 16px' , borderTop : '1px solid #21262d' , background : '#161b22' } } >
< span style = { { color : statusColor , fontFamily : 'monospace' , fontSize : 13 , whiteSpace : 'nowrap' } } > PS & gt ; < / span >
< input
ref = { inputRef }
value = { input }
onChange = { e => setInput ( e . target . value ) }
onKeyDown = { onKey }
disabled = { status !== 'connected' }
placeholder = { status === 'connected' ? 'Befehl eingeben… (Enter = Senden, Ctrl+C = Abbrechen)' : 'Nicht verbunden' }
style = { { flex : 1 , background : 'transparent' , border : 'none' , outline : 'none' , color : '#e6edf3' , fontFamily : 'ui-monospace, monospace' , fontSize : 13 , opacity : status !== 'connected' ? 0.4 : 1 } }
/ >
< button
onClick = { send }
disabled = { status !== 'connected' || ! input . trim ( ) }
style = { { background : C . teal , border : 'none' , borderRadius : 8 , color : '#fff' , padding : '6px 14px' , fontSize : 13 , fontWeight : 600 , cursor : status !== 'connected' ? 'not-allowed' : 'pointer' , opacity : status !== 'connected' ? 0.4 : 1 } }
>
Senden
< / button >
< / div >
< / div >
) ;
}
2026-06-10 14:13:18 +02:00
// ─── Remote Desktop (JPEG-over-WebSocket) ────────────────────────────────────────
2026-06-10 13:31:11 +02:00
function RemoteDesktop ( { agentId , agentHostname } ) {
2026-06-10 14:13:18 +02:00
const [ status , setStatus ] = useState ( 'idle' ) ; // idle | connecting | connected | error
2026-06-10 13:31:11 +02:00
const [ error , setError ] = useState ( '' ) ;
2026-06-10 14:13:18 +02:00
const [ fps , setFps ] = useState ( 0 ) ;
const [ resolution , setResolution ] = useState ( '' ) ;
const imgRef = useRef ( null ) ;
2026-06-10 13:31:11 +02:00
const wsRef = useRef ( null ) ;
2026-06-10 14:13:18 +02:00
const fpsCounterRef = useRef ( { count : 0 , last : Date . now ( ) } ) ;
2026-06-10 13:31:11 +02:00
const getWsUrl = ( ) => {
const token = localStorage . getItem ( 'token' ) ;
const proto = window . location . protocol === 'https:' ? 'wss' : 'ws' ;
2026-06-10 14:13:18 +02:00
return ` ${ proto } :// ${ window . location . host } /ws?type=rdp&agentId= ${ agentId } &token= ${ token } ` ;
2026-06-10 13:31:11 +02:00
} ;
2026-06-10 14:13:18 +02:00
const connect = ( ) => {
setStatus ( 'connecting' ) ;
2026-06-10 13:31:11 +02:00
setError ( '' ) ;
const ws = new WebSocket ( getWsUrl ( ) ) ;
wsRef . current = ws ;
2026-06-10 14:13:18 +02:00
ws . onopen = ( ) => {
ws . send ( JSON . stringify ( { type : 'rdp_start' } ) ) ;
setStatus ( 'connected' ) ;
2026-06-10 13:31:11 +02:00
} ;
2026-06-10 14:13:18 +02:00
ws . onmessage = ( e ) => {
2026-06-10 13:31:11 +02:00
try {
const msg = JSON . parse ( e . data ) ;
2026-06-10 14:13:18 +02:00
if ( msg . type === 'rdp_frame' && imgRef . current ) {
imgRef . current . src = 'data:image/jpeg;base64,' + msg . data ;
if ( msg . w && msg . h ) setResolution ( ` ${ msg . w } × ${ msg . h } ` ) ;
// FPS-Counter
const now = Date . now ( ) ;
fpsCounterRef . current . count ++ ;
if ( now - fpsCounterRef . current . last >= 1000 ) {
setFps ( fpsCounterRef . current . count ) ;
fpsCounterRef . current = { count : 0 , last : now } ;
}
} else if ( msg . type === 'rdp_disconnected' ) {
setStatus ( 'idle' ) ;
setError ( 'Agent hat die Verbindung getrennt' ) ;
2026-06-10 13:31:11 +02:00
}
2026-06-10 14:13:18 +02:00
} catch { /* ignorieren */ }
2026-06-10 13:31:11 +02:00
} ;
ws . onerror = ( ) => { setStatus ( 'error' ) ; setError ( 'WebSocket-Fehler' ) ; } ;
2026-06-10 14:13:18 +02:00
ws . onclose = ( ) => { setStatus ( 'idle' ) ; } ;
2026-06-10 13:31:11 +02:00
} ;
const disconnect = ( ) => {
2026-06-10 14:13:18 +02:00
if ( wsRef . current ? . readyState === WebSocket . OPEN ) {
wsRef . current . send ( JSON . stringify ( { type : 'rdp_stop' } ) ) ;
wsRef . current . close ( ) ;
}
2026-06-10 13:31:11 +02:00
wsRef . current = null ;
2026-06-10 14:13:18 +02:00
if ( imgRef . current ) imgRef . current . src = '' ;
2026-06-10 13:31:11 +02:00
setStatus ( 'idle' ) ;
2026-06-10 14:13:18 +02:00
setFps ( 0 ) ;
setResolution ( '' ) ;
2026-06-10 13:31:11 +02:00
} ;
2026-06-10 14:13:18 +02:00
useEffect ( ( ) => ( ) => { wsRef . current ? . close ( ) ; } , [ ] ) ;
2026-06-10 13:31:11 +02:00
2026-06-10 14:13:18 +02:00
const statusColor = status === 'connected' ? '#34d399' : status === 'connecting' ? '#f59e0b' : status === 'error' ? '#ef4444' : '#6b7280' ;
const statusLabel = { idle : 'Getrennt' , connecting : 'Verbinde…' , connected : 'Verbunden' , error : 'Fehler' } [ status ] ;
2026-06-10 13:31:11 +02:00
return (
< div style = { { marginTop : 24 , borderRadius : 16 , border : '1px solid var(--border-color)' , overflow : 'hidden' , background : 'var(--bg-secondary)' } } >
2026-06-10 14:13:18 +02:00
< div style = { { padding : '14px 20px' , borderBottom : '1px solid var(--border-color)' , display : 'flex' , alignItems : 'center' , gap : 10 , flexWrap : 'wrap' } } >
2026-06-10 13:31:11 +02:00
< span style = { { fontSize : 15 } } > 🖥 ️ < / span >
< span style = { { fontWeight : 600 , fontSize : 14 , color : 'var(--text-primary)' } } > Remote Desktop < / span >
2026-06-10 14:13:18 +02:00
< span style = { { fontSize : 12 , color : 'var(--text-muted)' , marginLeft : 4 } } > { agentHostname } < / span >
< span style = { { display : 'flex' , alignItems : 'center' , gap : 5 , fontSize : 12 } } >
2026-06-10 13:31:11 +02:00
< span style = { { width : 8 , height : 8 , borderRadius : '50%' , background : statusColor , display : 'inline-block' } } / >
< span style = { { color : statusColor } } > { statusLabel } < / span >
< / span >
2026-06-10 14:13:18 +02:00
{ status === 'connected' && resolution && (
< span style = { { fontSize : 11 , color : 'var(--text-muted)' , background : 'var(--bg-tertiary)' , borderRadius : 6 , padding : '2px 8px' } } >
{ resolution } · { fps } fps
< / span >
) }
2026-06-10 13:31:11 +02:00
< div style = { { marginLeft : 'auto' , display : 'flex' , gap : 8 } } >
{ status === 'idle' || status === 'error' ? (
< button onClick = { connect } style = { { background : '#238636' , border : 'none' , borderRadius : 8 , color : '#fff' , padding : '5px 14px' , fontSize : 12 , fontWeight : 600 , cursor : 'pointer' } } >
Verbinden
< / button >
) : (
< button onClick = { disconnect } style = { { background : '#21262d' , border : '1px solid #30363d' , borderRadius : 8 , color : '#cdd9e5' , padding : '5px 14px' , fontSize : 12 , cursor : 'pointer' } } >
Trennen
< / button >
) }
< / div >
< / div >
{ status === 'idle' || status === 'error' ? (
< div style = { { display : 'flex' , flexDirection : 'column' , alignItems : 'center' , justifyContent : 'center' , minHeight : 300 , gap : 16 , color : 'var(--text-muted)' } } >
< span style = { { fontSize : 48 } } > 🖥 ️ < / span >
< div style = { { textAlign : 'center' } } >
< div style = { { fontSize : 15 , fontWeight : 600 , color : 'var(--text-primary)' , marginBottom : 6 } } > Remote Desktop ( Beta ) < / div >
2026-06-10 14:13:18 +02:00
< div style = { { fontSize : 13 } } > Bildschirmübertragung via WebSocket — Agent muss v2 .2 .0 + haben < / div >
2026-06-10 13:31:11 +02:00
{ error && < div style = { { marginTop : 8 , color : '#ef4444' , fontSize : 12 } } > { error } < / div > }
< / div >
< / div >
) : (
2026-06-10 14:13:18 +02:00
< div style = { { background : '#000' , lineHeight : 0 , position : 'relative' } } >
< img
ref = { imgRef }
alt = "Remote Desktop"
style = { { width : '100%' , display : 'block' , maxHeight : 640 , objectFit : 'contain' } }
2026-06-10 13:31:11 +02:00
/ >
2026-06-10 14:13:18 +02:00
{ status === 'connecting' && (
2026-06-10 13:31:11 +02:00
< div style = { { position : 'absolute' , inset : 0 , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , background : 'rgba(0,0,0,0.7)' , color : '#fff' , fontSize : 14 } } >
Verbinde …
< / div >
) }
< / div >
) }
< / div >
) ;
}
2026-06-01 20:49:07 +02:00
// ─── Main Page ───────────────────────────────────────────────────────────────────
export default function AgentDetailPage ( ) {
const { id , hostname } = useParams ( ) ;
const navigate = useNavigate ( ) ;
2026-06-10 13:31:11 +02:00
const { isSuperAdmin , isAdmin } = useAuth ( ) ;
2026-06-01 20:49:07 +02:00
const [ agent , setAgent ] = useState ( null ) ;
const [ asset , setAsset ] = useState ( null ) ;
const [ assignedUser , setAssignedUser ] = useState ( null ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ swQuery , setSwQuery ] = useState ( '' ) ;
const [ sendingAnn , setSendingAnn ] = useState ( false ) ;
const [ annText , setAnnText ] = useState ( '' ) ;
const [ showAnnModal , setShowAnnModal ] = useState ( false ) ;
const [ patchHistory , setPatchHistory ] = useState ( [ ] ) ;
2026-06-10 13:31:11 +02:00
const [ shellTab , setShellTab ] = useState ( 'shell' ) ; // 'shell' | 'rdp'
2026-06-01 20:49:07 +02:00
const [ countdown , setCountdown ] = useState ( 60 ) ;
const agentRef = useRef ( null ) ;
const refreshTimer = useRef ( null ) ;
const countdownTimer = useRef ( null ) ;
useEffect ( ( ) => {
loadAgent ( ) ;
return ( ) => {
clearTimeout ( refreshTimer . current ) ;
clearInterval ( countdownTimer . current ) ;
} ;
} , [ id , hostname ] ) ;
const loadAgent = async ( silent = false ) => {
try {
if ( ! silent ) setLoading ( true ) ;
let data ;
if ( hostname ) {
const all = await monitoringService . getAll ( ) ;
data = all . find ( a => a . hostname ? . toLowerCase ( ) === decodeURIComponent ( hostname ) . toLowerCase ( ) ) ;
if ( ! data ) throw new Error ( 'not found' ) ;
} else {
data = await monitoringService . getById ( id ) ;
}
setAgent ( data ) ;
agentRef . current = data ;
// Countdown neu starten basierend auf last_checkin
clearInterval ( countdownTimer . current ) ;
const startCountdown = ( ) => {
const secSince = data . last _checkin
? Math . floor ( ( Date . now ( ) - new Date ( data . last _checkin ) . getTime ( ) ) / 1000 )
: 60 ;
let remaining = Math . max ( 0 , 60 - ( secSince % 60 ) ) ;
setCountdown ( remaining ) ;
countdownTimer . current = setInterval ( ( ) => {
setCountdown ( p => {
if ( p <= 1 ) { loadAgent ( true ) ; return 60 ; }
return p - 1 ;
} ) ;
} , 1000 ) ;
} ;
startCountdown ( ) ;
// Patch History laden
try {
const ph = await api . get ( ` /patch/commands?agent_id= ${ data . id } ` ) ;
setPatchHistory ( ph . data || [ ] ) ;
} catch { }
// Asset + User nur beim ersten Laden
if ( ! silent ) {
try {
const assets = await assetService . getAll ( ) ;
const match = assets . find ( a => a . name ? . toLowerCase ( ) === data . hostname ? . toLowerCase ( ) ) ;
if ( match ) {
setAsset ( match ) ;
if ( match . assigned _to _user _id ) {
const userRes = await api . get ( ` /users/ ${ match . assigned _to _user _id } ` ) ;
setAssignedUser ( userRes . data . data ) ;
}
}
} catch { }
}
} catch {
if ( ! silent ) {
toast . error ( 'Agent nicht gefunden' ) ;
navigate ( '/monitoring' ) ;
}
} finally {
if ( ! silent ) setLoading ( false ) ;
}
} ;
const handleSendCommand = async ( command ) => {
try {
await api . post ( '/patch/commands/trigger' , { agent _id : agent . id , command } ) ;
toast . success ( 'Befehl gesendet' ) ;
} catch {
toast . error ( 'Fehler beim Senden' ) ;
}
} ;
const handleSendAnnouncement = async ( ) => {
if ( ! annText . trim ( ) ) return ;
setSendingAnn ( true ) ;
try {
await api . post ( '/announcements' , {
title : 'Nachricht vom IT-Team' ,
message : annText ,
type : 'info' ,
target _type : 'specific' ,
target _agent _ids : [ agent . id ] ,
} ) ;
toast . success ( 'Ankündigung gesendet' ) ;
setShowAnnModal ( false ) ;
setAnnText ( '' ) ;
} catch {
toast . error ( 'Fehler beim Senden' ) ;
} finally {
setSendingAnn ( false ) ;
}
} ;
if ( loading ) return (
< div style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'center' , height : '60vh' , color : 'var(--text-muted)' } } >
Lade Gerätedaten …
< / div >
) ;
if ( ! agent ) return null ;
const sw = agent . installed _software || [ ] ;
const filteredSw = sw . filter ( s => {
if ( ! swQuery ) return true ;
const q = swQuery . toLowerCase ( ) ;
const nm = typeof s === 'string' ? s : ( s . name || s . Name || s . DisplayName || '' ) ;
const pub = typeof s === 'string' ? '' : ( s . publisher || s . Publisher || '' ) ;
return nm . toLowerCase ( ) . includes ( q ) || pub . toLowerCase ( ) . includes ( q ) ;
} ) ;
const cpuPct = agent . cpu _usage _percent ? ? 0 ;
const ramPct = pct ( agent . ram _used _gb , agent . ram _total _gb ) ;
const diskUsed = ( agent . disk _total _gb ? ? 0 ) - ( agent . disk _free _gb ? ? 0 ) ;
const diskPct = pct ( diskUsed , agent . disk _total _gb ) ;
const bitlockerOk = agent . bitlocker _status === 'on' || agent . bitlocker _status === 'encrypted' ;
const defenderOk = agent . defender _enabled === 1 ;
const sigAge = agent . defender _signatures _age >= 0 ? agent . defender _signatures _age : null ;
const defenderWarn = defenderOk && sigAge !== null && sigAge > 7 ;
const tpmOk = agent . tpm _present === 1 ;
const tpmVersion = agent . tpm _version ? agent . tpm _version . split ( ',' ) [ 0 ] . trim ( ) : null ;
const secureBootOk = agent . secure _boot === 1 ;
const win11Ok = agent . win11 _ready === 1 ;
const isWorkgroup = ! agent . domain || agent . domain === 'WORKGROUP' ;
const allSecOk = bitlockerOk && defenderOk && ! defenderWarn && tpmOk && secureBootOk ;
const userInitials = assignedUser
? ( ( assignedUser . first _name ? . [ 0 ] || '' ) + ( assignedUser . last _name ? . [ 0 ] || '' ) ) . toUpperCase ( ) || assignedUser . username ? . [ 0 ] ? . toUpperCase ( )
: null ;
return (
< div style = { { maxWidth : 1320 , margin : '0 auto' , padding : '24px 28px 80px' } } >
{ /* Back link */ }
< Link to = { hostname ? '/assets' : '/monitoring' } style = { { display : 'inline-flex' , alignItems : 'center' , gap : 6 , color : 'var(--text-muted)' , fontSize : 13 , textDecoration : 'none' , marginBottom : 20 } } >
← Zurück zu { hostname ? 'Assets' : 'Monitoring' }
< / Link >
{ /* Offline Banner */ }
{ agent . status === 'offline' && (
< div style = { { background : 'rgba(239,68,68,0.12)' , border : '1px solid rgba(239,68,68,0.35)' , borderRadius : 12 , padding : '12px 18px' , marginBottom : 20 , display : 'flex' , alignItems : 'center' , gap : 10 , color : C . danger , fontSize : 13.5 , fontWeight : 500 } } >
⚠ ️ Dieses Gerät ist offline — Daten vom letzten Check - in ( { timeAgo ( agent . last _checkin ) } )
< / div >
) }
{ /* ── HERO ─────────────────────────────────────────────────────────── */ }
< div style = { { display : 'grid' , gridTemplateColumns : '1fr auto' , gap : 24 , alignItems : 'flex-end' , marginBottom : 28 , paddingBottom : 28 , borderBottom : '1px solid var(--border-color)' } } >
< div >
< div style = { { display : 'inline-flex' , alignItems : 'center' , gap : 8 , fontSize : 11 , fontWeight : 600 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '0.10em' , marginBottom : 6 } } >
< span style = { { color : C . purple } } > Workstation · Windows < / span >
{ agent . domain && < span > · { agent . domain } < / span > }
< / div >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 14 , flexWrap : 'wrap' } } >
< div style = { { width : 48 , height : 48 , borderRadius : 12 , background : 'rgba(124,58,237,0.14)' , color : C . purple , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , fontSize : 22 , flexShrink : 0 } } >
🖥
< / div >
< h1 style = { { margin : 0 , fontSize : 32 , fontWeight : 600 , letterSpacing : '-0.025em' , lineHeight : 1.1 , color : 'var(--text-primary)' } } >
{ agent . hostname }
< / h1 >
< span style = { {
display : 'inline-flex' , alignItems : 'center' , gap : 6 ,
padding : '4px 11px' , borderRadius : 999 , fontSize : 12 , fontWeight : 600 ,
background : agent . status === 'online' ? 'rgba(34,197,94,0.12)' : 'rgba(239,68,68,0.14)' ,
color : agent . status === 'online' ? C . success : C . danger ,
} } >
< span style = { {
width : 7 , height : 7 , borderRadius : '50%' , background : 'currentColor' ,
... ( agent . status === 'online' ? { animation : 'agPulse 1.6s ease-out infinite' } : { } ) ,
} } / >
{ agent . status === 'online' ? 'Online' : 'Offline' }
< / span >
< / div >
< div style = { { fontSize : 13.5 , color : 'var(--text-muted)' , marginTop : 8 , display : 'flex' , alignItems : 'center' , gap : 8 , flexWrap : 'wrap' } } >
< span > Letzter Check - in < strong style = { { color : 'var(--text-secondary)' } } > { timeAgo ( agent . last _checkin ) } < / strong > < / span >
< span > · < / span >
< span > Nächster in < strong style = { { color : countdown <= 10 ? C . warning : 'var(--text-secondary)' , fontFamily : 'ui-monospace, monospace' } } > { countdown } s < / strong > < / span >
< span > · < / span >
< span > Agent < strong style = { { color : 'var(--text-secondary)' } } > v { agent . agent _version || '– ' } < / strong > < / span >
{ agent . ip _address && < > < span > · < / span > < span style = { { fontFamily : 'ui-monospace, monospace' , fontSize : 12.5 } } > { agent . ip _address } < / span > < / > }
< / div >
< / div >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , flexWrap : 'wrap' } } >
< Btn onClick = { ( ) => setShowAnnModal ( true ) } > 📢 Ankündigung senden < / Btn >
< Btn onClick = { ( ) => handleSendCommand ( 'check_updates' ) } > 🔄 Updates prüfen < / Btn >
{ agent . ip _address && (
< Btn onClick = { ( ) => window . open ( ` rdp:// ${ agent . ip _address } ` , '_blank' ) || ( window . location . href = ` ms-rd:openLocalSubnetRDP?computer= ${ agent . ip _address } ` ) } >
🖥 RDP
< / Btn >
) }
< Btn variant = "danger" onClick = { ( ) => {
if ( window . confirm ( ` Reboot für ${ agent . hostname } anfordern? ` ) ) handleSendCommand ( 'reboot' ) ;
} } > ⏻ Reboot anfordern < / Btn >
< / div >
< / div >
{ /* ── METRIC TILES ─────────────────────────────────────────────────── */ }
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(3, 1fr)' , gap : 16 , marginBottom : 22 } } >
< MetricTile
label = "CPU" icon = "⚙"
value = { cpuPct . toFixed ( 0 ) } unit = "%"
pctVal = { cpuPct } warnAt = { 80 } critAt = { 90 }
footnote = { < span style = { { fontFamily : 'ui-monospace, monospace' , fontSize : 11.5 , color : 'var(--text-secondary)' } } > { agent . cpu _model ? . replace ( /\(R\)|\(TM\)/g , '' ) || '– ' } { agent . cpu _cores ? ` · ${ agent . cpu _cores } Kerne ` : '' } < / span > }
/ >
< MetricTile
label = "RAM" icon = "💾"
value = { agent . ram _used _gb ? . toFixed ( 1 ) ? ? '– ' } ofVal = { ` / ${ agent . ram _total _gb ? . toFixed ( 0 ) ? ? '– ' } GB ` }
pctVal = { ramPct } warnAt = { 85 } critAt = { 95 }
footnote = { < span > { ramPct } % belegt · { ( ( agent . ram _total _gb ? ? 0 ) - ( agent . ram _used _gb ? ? 0 ) ) . toFixed ( 1 ) } GB frei < / span > }
/ >
< MetricTile
label = "Festplatte C:" icon = "🗄"
value = { agent . disk _free _gb ? . toFixed ( 0 ) ? ? '– ' } ofVal = { ` GB frei von ${ agent . disk _total _gb ? . toFixed ( 0 ) ? ? '– ' } GB ` }
pctVal = { diskPct } warnAt = { 75 } critAt = { 90 }
footnote = { < span > { diskPct } % belegt < / span > }
/ >
< / div >
{ /* ── TWO COLUMNS: SYSTEM + SECURITY ───────────────────────────────── */ }
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 16 , marginBottom : 22 } } >
{ /* System Info */ }
< div style = { { background : 'var(--bg-secondary)' , border : '1px solid var(--border-color)' , borderRadius : 18 , overflow : 'hidden' , boxShadow : '0 1px 2px rgba(0,0,0,0.35)' } } >
< CardHead icon = "🖥" title = "Systeminformationen" meta = "Hardware & OS" / >
< div >
< DlRow label = "Betriebssystem" value = { agent . os _name || '– ' } / >
< DlRow label = "IP-Adresse" value = { agent . ip _address || '– ' } mono copyVal = { agent . ip _address } / >
< DlRow label = "MAC-Adresse" value = { agent . mac _address || '– ' } mono copyVal = { agent . mac _address } / >
< DlRow label = "Domain" value = { agent . domain ? < > { agent . domain } { ! isWorkgroup && < span style = { { color : 'var(--text-muted)' , fontSize : 12 } } > ( Active Directory ) < / span > } < / > : '– ' } / >
< DlRow label = "Letzter Benutzer" value = { agent . last _user ? < span style = { { fontFamily : 'ui-monospace, monospace' , fontSize : 12.5 } } > { agent . last _user } < / span > : '– ' } / >
< DlRow label = "Seriennummer" value = { agent . hardware _serial || '– ' } mono copyVal = { agent . hardware _serial } / >
< DlRow label = "Uptime" value = { < strong style = { { fontVariantNumeric : 'tabular-nums' } } > { uptimeStr ( agent . uptime _hours ) } < / strong > } / >
< / div >
< / div >
{ /* Security */ }
< div style = { { background : 'var(--bg-secondary)' , border : '1px solid var(--border-color)' , borderRadius : 18 , overflow : 'hidden' , boxShadow : '0 1px 2px rgba(0,0,0,0.35)' } } >
< CardHead icon = "🛡" title = "Sicherheit & Compliance" meta = { allSecOk ? 'Alle Checks bestanden' : 'Handlungsbedarf' } / >
< div >
< SecRow
ok = { bitlockerOk } warn = { false }
name = "BitLocker"
desc = { bitlockerOk ? 'Systemlaufwerk C: verschlüsselt' : 'Laufwerk C: nicht verschlüsselt' }
state = { bitlockerOk ? 'Verschlüsselt' : agent . bitlocker _status === 'unknown' ? 'Unbekannt' : 'Nicht verschlüsselt' }
/ >
< SecRow
ok = { defenderOk } warn = { defenderWarn }
name = "Windows Defender"
desc = { defenderOk
? ` Echtzeitschutz aktiv · Signaturen ${ sigAge !== null ? ` ${ sigAge } Tag ${ sigAge !== 1 ? 'e' : '' } alt ` : 'aktuell' } `
: 'Echtzeitschutz inaktiv' }
state = { defenderOk ? ( defenderWarn ? 'Veraltet' : 'Aktiv' ) : 'Inaktiv' }
/ >
< SecRow
ok = { tpmOk } warn = { false }
name = "TPM"
desc = { tpmOk ? ` TPM ${ tpmVersion || '2.0' } vorhanden ` : 'Kein TPM gefunden' }
state = { tpmOk ? 'Vorhanden' : 'Nicht gefunden' }
/ >
< SecRow
ok = { secureBootOk } warn = { false }
name = "Secure Boot"
desc = { secureBootOk ? 'UEFI-Firmware konfiguriert' : 'Secure Boot deaktiviert' }
state = { secureBootOk ? 'Aktiviert' : 'Deaktiviert' }
/ >
< SecRow
ok = { win11Ok } warn = { false }
name = "Windows 11 Kompatibilität"
desc = { win11Ok ? 'Alle Hardware-Anforderungen erfüllt' : 'Nicht kompatibel' }
state = { win11Ok ? 'Bereit' : 'Nicht bereit' }
/ >
< / div >
< / div >
< / div >
{ /* ── BENUTZER CARD ────────────────────────────────────────────────── */ }
< div style = { { display : 'grid' , gridTemplateColumns : asset ? '1fr 1fr' : '1fr' , gap : 16 , marginBottom : 22 } } >
{ /* Zugewiesener Benutzer */ }
< div style = { { background : 'var(--bg-secondary)' , border : '1px solid var(--border-color)' , borderRadius : 18 , overflow : 'hidden' , boxShadow : '0 1px 2px rgba(0,0,0,0.35)' } } >
< CardHead icon = "👤" title = "Zugewiesener Benutzer" meta = { assignedUser ? 'Aus Asset-Daten' : '' } / >
{ assignedUser ? (
< div style = { { padding : '20px 22px' , display : 'flex' , alignItems : 'center' , gap : 16 } } >
< div style = { {
width : 52 , height : 52 , borderRadius : '50%' , flexShrink : 0 ,
background : ` linear-gradient(135deg, ${ C . teal } , #0a7a78) ` ,
color : '#fff' , fontWeight : 700 , fontSize : 18 ,
display : 'flex' , alignItems : 'center' , justifyContent : 'center' ,
} } >
{ userInitials }
< / div >
< div style = { { flex : 1 , minWidth : 0 } } >
< div style = { { fontSize : 16 , fontWeight : 600 , color : 'var(--text-primary)' } } >
{ assignedUser . first _name } { assignedUser . last _name }
{ ( ! assignedUser . first _name && ! assignedUser . last _name ) && assignedUser . username }
< / div >
< div style = { { fontSize : 12.5 , color : 'var(--text-muted)' , marginTop : 2 } } > @ { assignedUser . username } < / div >
{ assignedUser . email && < div style = { { fontSize : 12.5 , color : 'var(--text-muted)' } } > { assignedUser . email } < / div > }
{ assignedUser . department && < div style = { { fontSize : 12 , color : 'var(--text-muted)' , marginTop : 4 } } > { assignedUser . department } < / div > }
< / div >
< Link to = { ` /users ` } style = { { fontSize : 12.5 , color : C . teal , textDecoration : 'none' , fontWeight : 500 } } > Profil → < / Link >
< / div >
) : (
< div style = { { padding : '20px 22px' , display : 'flex' , alignItems : 'center' , gap : 12 } } >
< div style = { { width : 42 , height : 42 , borderRadius : '50%' , background : 'var(--bg-tertiary)' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , fontSize : 20 } } > ? < / div >
< div >
< div style = { { fontSize : 13.5 , color : 'var(--text-secondary)' , fontWeight : 500 } } > Nicht zugewiesen < / div >
{ agent . last _user && < div style = { { fontSize : 12 , color : 'var(--text-muted)' , marginTop : 2 } } > Zuletzt angemeldet : < span style = { { fontFamily : 'monospace' } } > { agent . last _user } < / span > < / div > }
< / div >
< / div >
) }
< / div >
{ /* Asset-Verknüpfung */ }
{ asset && (
< div style = { { background : 'var(--bg-secondary)' , border : '1px solid var(--border-color)' , borderRadius : 18 , overflow : 'hidden' , boxShadow : '0 1px 2px rgba(0,0,0,0.35)' } } >
< CardHead icon = "🏷" title = "Verknüpftes Asset" meta = "IT Nexus Asset-Verwaltung" / >
< div style = { { padding : '16px 22px' } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 12 , marginBottom : 12 } } >
< div style = { { fontSize : 16 , fontWeight : 600 , color : 'var(--text-primary)' } } > { asset . name } < / div >
< span style = { { fontSize : 11.5 , fontWeight : 600 , padding : '2px 8px' , borderRadius : 6 , background : 'var(--bg-tertiary)' , color : 'var(--text-muted)' } } > { asset . type || 'Notebook' } < / span >
< / div >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : '6px 16px' , fontSize : 12.5 , color : 'var(--text-muted)' } } >
{ asset . manufacturer && < div > Hersteller : < strong style = { { color : 'var(--text-secondary)' } } > { asset . manufacturer } < / strong > < / div > }
{ asset . model && < div > Modell : < strong style = { { color : 'var(--text-secondary)' } } > { asset . model } < / strong > < / div > }
{ asset . serial _number && < div > S / N : < span style = { { fontFamily : 'monospace' , color : 'var(--text-secondary)' } } > { asset . serial _number } < / span > < / div > }
{ asset . status && < div > Status : < strong style = { { color : 'var(--text-secondary)' } } > { asset . status } < / strong > < / div > }
< / div >
< Link to = "/assets" style = { { display : 'inline-block' , marginTop : 12 , fontSize : 12.5 , color : C . teal , textDecoration : 'none' , fontWeight : 500 } } >
Asset öffnen →
< / Link >
< / div >
< / div >
) }
< / div >
{ /* ── UPDATES BANNER ───────────────────────────────────────────────── */ }
{ ( agent . windows _updates _pending ? ? 0 ) > 0 && (
< div style = { {
background : ` linear-gradient(135deg, rgba(245,158,11,0.08), transparent 60%), var(--bg-secondary) ` ,
border : '1px solid rgba(245,158,11,0.28)' ,
borderRadius : 18 , padding : '22px 24px' , marginBottom : 22 ,
display : 'grid' , gridTemplateColumns : 'auto 1fr auto' , gap : 18 , alignItems : 'center' ,
boxShadow : '0 1px 2px rgba(0,0,0,0.35)' ,
} } >
< div style = { { width : 44 , height : 44 , borderRadius : 12 , background : 'rgba(245,158,11,0.16)' , color : C . warning , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , fontSize : 22 , flexShrink : 0 } } >
🔄
< / div >
< div >
< div style = { { fontSize : 15 , fontWeight : 600 , letterSpacing : '-0.01em' , display : 'flex' , alignItems : 'center' , gap : 10 , color : 'var(--text-primary)' } } >
Ausstehende Updates
< span style = { { background : C . warning , color : '#fff' , padding : '2px 9px' , borderRadius : 999 , fontSize : 11 , fontWeight : 700 } } > { agent . windows _updates _pending } < / span >
< / div >
< div style = { { fontSize : 12.5 , color : 'var(--text-muted)' , marginTop : 4 } } > Updates können über Patch Management installiert werden < / div >
< / div >
< div style = { { display : 'flex' , gap : 8 } } >
< Btn onClick = { ( ) => handleSendCommand ( 'install_updates' ) } variant = "primary" > ⬇ Updates installieren < / Btn >
< / div >
< / div >
) }
{ /* ── PATCH HISTORY ────────────────────────────────────────────────── */ }
{ patchHistory . length > 0 && (
< div style = { { background : 'var(--bg-secondary)' , border : '1px solid var(--border-color)' , borderRadius : 18 , overflow : 'hidden' , boxShadow : '0 1px 2px rgba(0,0,0,0.35)' , marginBottom : 22 } } >
< CardHead icon = "📋" title = "Patch-Historie" meta = { ` ${ patchHistory . length } Einträge ` } / >
< div >
{ patchHistory . slice ( 0 , 10 ) . map ( ( cmd , i ) => {
const statusColor = cmd . status === 'done' ? C . success : cmd . status === 'error' ? C . danger : cmd . status === 'running' ? C . warning : 'var(--text-muted)' ;
const statusLabel = { done : 'Erledigt' , error : 'Fehler' , running : 'Läuft' , pending : 'Ausstehend' , sent : 'Gesendet' } [ cmd . status ] || cmd . status ;
const cmdLabel = { install _updates : 'Updates installieren' , check _updates : 'Updates prüfen' , reboot : 'Neustart' , upgrade _win11 : 'Win11 Upgrade' , update _agent : 'Agent Update' } [ cmd . command ] || cmd . command ;
return (
< div key = { cmd . id } style = { { display : 'grid' , gridTemplateColumns : '1fr auto auto' , gap : 16 , padding : '11px 22px' , alignItems : 'center' , borderTop : i === 0 ? 'none' : '1px solid var(--border-color)' } } >
< div >
< div style = { { fontSize : 13.5 , fontWeight : 600 , color : 'var(--text-primary)' } } > { cmdLabel } < / div >
{ cmd . result && < div style = { { fontSize : 12 , color : 'var(--text-muted)' , marginTop : 2 } } > { cmd . result } < / div > }
< / div >
< div style = { { fontSize : 12 , color : 'var(--text-muted)' , textAlign : 'right' , whiteSpace : 'nowrap' } } >
{ cmd . triggered _by _username && < div > von { cmd . triggered _by _username } < / div > }
< div > { new Date ( cmd . created _at ) . toLocaleString ( 'de-DE' , { day : '2-digit' , month : '2-digit' , year : '2-digit' , hour : '2-digit' , minute : '2-digit' } ) } < / div >
< / div >
< span style = { { fontSize : 11.5 , fontWeight : 600 , padding : '3px 10px' , borderRadius : 999 , background : ` ${ statusColor } 18 ` , color : statusColor , whiteSpace : 'nowrap' } } >
{ statusLabel }
< / span >
< / div >
) ;
} ) }
< / div >
< / div >
) }
{ /* ── INSTALLED SOFTWARE ───────────────────────────────────────────── */ }
< div style = { { background : 'var(--bg-secondary)' , border : '1px solid var(--border-color)' , borderRadius : 18 , overflow : 'hidden' , boxShadow : '0 1px 2px rgba(0,0,0,0.35)' } } >
< CardHead icon = "📦" title = "Installierte Software" meta = { ` ${ sw . length } Programme ` } / >
< div style = { { padding : '14px 22px' , borderBottom : '1px solid var(--border-color)' , background : 'var(--bg-tertiary)' , display : 'flex' , alignItems : 'center' , gap : 10 } } >
< div style = { { position : 'relative' , flex : 1 , maxWidth : 380 } } >
< span style = { { position : 'absolute' , left : 12 , top : '50%' , transform : 'translateY(-50%)' , color : 'var(--text-muted)' , pointerEvents : 'none' } } > 🔍 < / span >
< input
value = { swQuery }
onChange = { e => setSwQuery ( e . target . value ) }
placeholder = "Nach Name oder Hersteller suchen…"
style = { {
width : '100%' , boxSizing : 'border-box' ,
background : 'var(--bg-secondary)' , color : 'var(--text-primary)' ,
border : '1px solid var(--border-color)' , borderRadius : 10 ,
padding : '8px 14px 8px 36px' , fontSize : 13 , outline : 'none' ,
fontFamily : 'inherit' ,
} }
/ >
< / div >
< span style = { { marginLeft : 'auto' , fontSize : 12.5 , color : 'var(--text-muted)' } } >
{ swQuery ? ` ${ filteredSw . length } Treffer ` : ` ${ sw . length } Programme ` }
< / span >
< / div >
{ sw . length === 0 ? (
< div style = { { padding : '40px 20px' , textAlign : 'center' , color : 'var(--text-muted)' } } >
Keine Software - Daten verfügbar — Check - in abwarten
< / div >
) : filteredSw . length === 0 ? (
< div style = { { padding : '40px 20px' , textAlign : 'center' , color : 'var(--text-muted)' } } > Keine Programme gefunden . < / div >
) : (
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' } } >
{ filteredSw . map ( ( s , i ) => {
const nm = typeof s === 'string' ? s : ( s . name || s . Name || s . DisplayName || '?' ) ;
const ver = typeof s === 'string' ? '' : ( s . version || s . Version || s . DisplayVersion || '' ) ;
const pub = typeof s === 'string' ? '' : ( s . publisher || s . Publisher || '' ) ;
const col = colorFor ( nm ) ;
return (
< div key = { i } style = { { display : 'flex' , alignItems : 'center' , gap : 12 , padding : '11px 22px' , borderBottom : '1px solid var(--border-color)' , minWidth : 0 } } >
< div style = { { width : 30 , height : 30 , borderRadius : 7 , background : ` ${ col } 22 ` , color : col , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , flexShrink : 0 , fontWeight : 700 , fontSize : 13 } } >
{ nm [ 0 ] }
< / div >
< div style = { { flex : 1 , minWidth : 0 } } >
< div style = { { fontSize : 13 , fontWeight : 600 , color : 'var(--text-primary)' , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } > { nm } < / div >
< div style = { { fontFamily : 'ui-monospace, monospace' , fontSize : 11 , color : 'var(--text-muted)' , marginTop : 2 } } > { ver } { pub ? ` · ${ pub } ` : '' } < / div >
< / div >
< / div >
) ;
} ) }
< / div >
) }
< / div >
2026-06-10 13:31:11 +02:00
{ /* ── REMOTE TOOLS ─────────────────────────────────────────────────── */ }
{ ( isSuperAdmin || isAdmin ) && (
< div style = { { marginTop : 24 } } >
< div style = { { display : 'flex' , gap : 4 , marginBottom : 0 , borderBottom : '1px solid var(--border-color)' } } >
{ [
{ id : 'shell' , label : '⌨️ Remote Shell' } ,
{ id : 'rdp' , label : '🖥️ Remote Desktop' , badge : 'Beta' } ,
] . map ( t => (
< button key = { t . id } onClick = { ( ) => setShellTab ( t . id ) } style = { {
background : shellTab === t . id ? 'var(--bg-secondary)' : 'transparent' ,
border : '1px solid var(--border-color)' ,
borderBottom : shellTab === t . id ? '1px solid var(--bg-secondary)' : '1px solid var(--border-color)' ,
borderRadius : '10px 10px 0 0' , color : shellTab === t . id ? 'var(--text-primary)' : 'var(--text-muted)' ,
padding : '8px 18px' , fontSize : 13 , fontWeight : 600 , cursor : 'pointer' ,
display : 'flex' , alignItems : 'center' , gap : 6 , marginBottom : - 1 ,
} } >
{ t . label }
{ t . badge && < span style = { { fontSize : 10 , background : C . tealDim , color : C . teal , borderRadius : 4 , padding : '1px 5px' , fontWeight : 700 } } > { t . badge } < / span > }
< / button >
) ) }
< / div >
{ shellTab === 'shell' && < RemoteShell agentId = { agent . id } agentHostname = { agent . hostname } / > }
{ shellTab === 'rdp' && < RemoteDesktop agentId = { agent . id } agentHostname = { agent . hostname } / > }
< / div >
) }
{ ! isSuperAdmin && ! isAdmin && (
< RemoteShell agentId = { agent . id } agentHostname = { agent . hostname } / >
) }
2026-06-01 20:49:07 +02:00
{ /* ── ANKÜNDIGUNG MODAL ────────────────────────────────────────────── */ }
{ showAnnModal && (
< 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 && setShowAnnModal ( false ) } >
< div style = { { background : 'var(--bg-primary)' , borderRadius : 16 , width : '100%' , maxWidth : 500 , border : '1px solid var(--border-color)' , overflow : 'hidden' } } >
< div style = { { padding : '16px 20px' , borderBottom : '1px solid var(--border-color)' , display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' } } >
< h2 style = { { margin : 0 , fontSize : 15 , fontWeight : 600 , color : 'var(--text-primary)' } } > 📢 Ankündigung senden an { agent . hostname } < / h2 >
< button onClick = { ( ) => setShowAnnModal ( false ) } style = { { background : 'none' , border : 'none' , color : 'var(--text-muted)' , fontSize : 22 , cursor : 'pointer' , lineHeight : 1 } } > × < / button >
< / div >
< div style = { { padding : 20 , display : 'flex' , flexDirection : 'column' , gap : 14 } } >
< div style = { { background : 'rgba(20,184,168,0.10)' , border : '1px solid rgba(20,184,168,0.3)' , borderRadius : 8 , padding : '8px 12px' , fontSize : 12.5 , color : '#2dd2c2' } } >
🎯 Wird < strong > nur < / strong > an < strong > { agent ? . hostname } < / strong > gesendet ( Agent - ID : { agent ? . id } )
< / div >
< textarea
value = { annText }
onChange = { e => setAnnText ( e . target . value ) }
placeholder = "Nachricht eingeben…"
rows = { 4 }
style = { { width : '100%' , boxSizing : 'border-box' , background : 'var(--bg-secondary)' , color : 'var(--text-primary)' , border : '1px solid var(--border-color)' , borderRadius : 10 , padding : '10px 14px' , fontSize : 13 , fontFamily : 'inherit' , outline : 'none' , resize : 'vertical' } }
/ >
< div style = { { display : 'flex' , gap : 8 , justifyContent : 'flex-end' } } >
< Btn onClick = { ( ) => setShowAnnModal ( false ) } > Abbrechen < / Btn >
< Btn variant = "primary" onClick = { handleSendAnnouncement } disabled = { sendingAnn || ! annText . trim ( ) } >
{ sendingAnn ? 'Sende…' : '📢 Senden' }
< / Btn >
< / div >
< / div >
< / div >
< / div >
) }
< style > { `
@ keyframes agPulse {
0 % { box - shadow : 0 0 0 0 rgba ( 34 , 197 , 94 , 0.5 ) ; }
70 % { box - shadow : 0 0 0 6 px rgba ( 34 , 197 , 94 , 0 ) ; }
100 % { box - shadow : 0 0 0 0 rgba ( 34 , 197 , 94 , 0 ) ; }
}
` }</style>
< / div >
) ;
}