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';
import { useAuth } from '../context/AuthContext';
import RemoteDesktopPanel from '../components/common/RemoteDesktopPanel';
// ─── 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 (
{value}
{unit && {unit}}
{ofVal && {ofVal}}
{footnote && (
{footnote}
)}
);
};
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 (
{ok && !isWarn ? '✓' : isWarn ? '!' : '✗'}
{state}
);
};
const DlRow = ({ label, value, mono, copyVal }) => (
{label}
{value}
{copyVal && (
)}
);
const CardHead = ({ icon, title, meta }) => (
{icon}
{title}
{meta &&
{meta}}
);
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 (
);
};
// ─── 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 proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
const host = window.location.host;
return `${proto}://${host}/ws?type=shell&agentId=${agentId}`;
};
const connect = () => {
if (wsRef.current && wsRef.current.readyState <= 1) return;
setStatus('connecting');
setOutput('');
const ws = new WebSocket(getWsUrl());
wsRef.current = ws;
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'auth', token: localStorage.getItem('token') }));
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 (
🖥️
Remote Shell
{agentHostname} · PowerShell (SYSTEM)
{statusLabel}
{status === 'disconnected' ? (
) : (
)}
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 || {status === 'disconnected' ? 'Auf "Verbinden" klicken um eine Shell-Sitzung zu starten.' : 'Warte auf Agent…'}}
PS>
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 }}
/>
);
}
// ─── Remote Desktop (via RemoteDesktopPanel) ─────────────────────────────────────
function RemoteDesktop({ agentId, agentHostname }) {
return (
);
}
// ─── Main Page ───────────────────────────────────────────────────────────────────
export default function AgentDetailPage() {
const { id, hostname } = useParams();
const navigate = useNavigate();
const { isSuperAdmin, isAdmin } = useAuth();
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([]);
const [shellTab, setShellTab] = useState('shell'); // 'shell' | 'rdp'
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 (
Lade Gerätedaten…
);
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 (
{/* Back link */}
← Zurück zu {hostname ? 'Assets' : 'Monitoring'}
{/* Offline Banner */}
{agent.status === 'offline' && (
⚠️ Dieses Gerät ist offline — Daten vom letzten Check-in ({timeAgo(agent.last_checkin)})
)}
{/* ── HERO ─────────────────────────────────────────────────────────── */}
Workstation · Windows
{agent.domain && · {agent.domain}}
🖥
{agent.hostname}
{agent.status === 'online' ? 'Online' : 'Offline'}
Letzter Check-in {timeAgo(agent.last_checkin)}
·
Nächster in {countdown}s
·
Agent v{agent.agent_version || '–'}
{agent.ip_address && <>·{agent.ip_address}>}
setShowAnnModal(true)}>📢 Ankündigung senden
handleSendCommand('check_updates')}>🔄 Updates prüfen
{agent.ip_address && (
window.open(`rdp://${agent.ip_address}`, '_blank') || (window.location.href = `ms-rd:openLocalSubnetRDP?computer=${agent.ip_address}`)}>
🖥 RDP
)}
{
if (window.confirm(`Reboot für ${agent.hostname} anfordern?`)) handleSendCommand('reboot');
}}>⏻ Reboot anfordern
{/* ── METRIC TILES ─────────────────────────────────────────────────── */}
{agent.cpu_model?.replace(/\(R\)|\(TM\)/g, '') || '–'}{agent.cpu_cores ? ` · ${agent.cpu_cores} Kerne` : ''}}
/>
{ramPct}% belegt · {((agent.ram_total_gb ?? 0) - (agent.ram_used_gb ?? 0)).toFixed(1)} GB frei}
/>
{diskPct}% belegt}
/>
{/* ── TWO COLUMNS: SYSTEM + SECURITY ───────────────────────────────── */}
{/* System Info */}
{agent.domain}{!isWorkgroup && (Active Directory)}> : '–'} />
{agent.last_user} : '–'} />
{uptimeStr(agent.uptime_hours)}} />
{/* Security */}
{/* ── BENUTZER CARD ────────────────────────────────────────────────── */}
{/* Zugewiesener Benutzer */}
{assignedUser ? (
{userInitials}
{assignedUser.first_name} {assignedUser.last_name}
{(!assignedUser.first_name && !assignedUser.last_name) && assignedUser.username}
@{assignedUser.username}
{assignedUser.email &&
{assignedUser.email}
}
{assignedUser.department &&
{assignedUser.department}
}
Profil →
) : (
?
Nicht zugewiesen
{agent.last_user &&
Zuletzt angemeldet: {agent.last_user}
}
)}
{/* Asset-Verknüpfung */}
{asset && (
{asset.name}
{asset.type || 'Notebook'}
{asset.manufacturer &&
Hersteller: {asset.manufacturer}
}
{asset.model &&
Modell: {asset.model}
}
{asset.serial_number &&
S/N: {asset.serial_number}
}
{asset.status &&
Status: {asset.status}
}
Asset öffnen →
)}
{/* ── UPDATES BANNER ───────────────────────────────────────────────── */}
{(agent.windows_updates_pending ?? 0) > 0 && (
🔄
Ausstehende Updates
{agent.windows_updates_pending}
Updates können über Patch Management installiert werden
handleSendCommand('install_updates')} variant="primary">⬇ Updates installieren
)}
{/* ── PATCH HISTORY ────────────────────────────────────────────────── */}
{patchHistory.length > 0 && (
{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 (
{cmdLabel}
{cmd.result &&
{cmd.result}
}
{cmd.triggered_by_username &&
von {cmd.triggered_by_username}
}
{new Date(cmd.created_at).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })}
{statusLabel}
);
})}
)}
{/* ── INSTALLED SOFTWARE ───────────────────────────────────────────── */}
🔍
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',
}}
/>
{swQuery ? `${filteredSw.length} Treffer` : `${sw.length} Programme`}
{sw.length === 0 ? (
Keine Software-Daten verfügbar — Check-in abwarten
) : filteredSw.length === 0 ? (
Keine Programme gefunden.
) : (
{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 (
{nm[0]}
{nm}
{ver}{pub ? ` · ${pub}` : ''}
);
})}
)}
{/* ── REMOTE TOOLS ─────────────────────────────────────────────────── */}
{(isSuperAdmin() || isAdmin()) && (
{[
{ id: 'shell', label: '⌨️ Remote Shell' },
{ id: 'rdp', label: '🖥️ Remote Desktop', badge: 'Beta' },
].map(t => (
))}
{shellTab === 'shell' &&
}
{shellTab === 'rdp' &&
}
)}
{/* ── ANKÜNDIGUNG MODAL ────────────────────────────────────────────── */}
{showAnnModal && (
e.target === e.currentTarget && setShowAnnModal(false)}>
📢 Ankündigung senden an {agent.hostname}
🎯 Wird nur an {agent?.hostname} gesendet (Agent-ID: {agent?.id})
)}
);
}