Fix #2: Auslastungsmonitor in Agent-Tab + Monitoring nach CPU/RAM sortiert
This commit is contained in:
@@ -241,7 +241,7 @@ const AssignModal = ({ asset, onClose, onAssigned }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
authFetch(`${API}/users`)
|
authFetch(`${API}/users`)
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => setUsers(Array.isArray(data) ? data : (data.users || [])))
|
.then(data => setUsers(data.data || data.users || (Array.isArray(data) ? data : [])))
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -449,7 +449,7 @@ const AssetsPage = () => {
|
|||||||
if (!detail) return;
|
if (!detail) return;
|
||||||
if (!window.confirm('Zuweisung wirklich aufheben?')) return;
|
if (!window.confirm('Zuweisung wirklich aufheben?')) return;
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`${API}/assets/${detail.id}/unassign`, { method: 'POST' });
|
const res = await authFetch(`${API}/assets/${detail.id}/unassign`, { method: 'POST', body: JSON.stringify({ new_status: 'verfuegbar' }) });
|
||||||
if (!res.ok) throw new Error(await res.text());
|
if (!res.ok) throw new Error(await res.text());
|
||||||
toast.success('Zuweisung aufgehoben');
|
toast.success('Zuweisung aufgehoben');
|
||||||
loadAssets();
|
loadAssets();
|
||||||
@@ -1024,6 +1024,72 @@ const AssetsPage = () => {
|
|||||||
) : null)
|
) : null)
|
||||||
) : <div style={{ color: '#64748b', fontSize: 13 }}>Kein Agent verbunden</div>}
|
) : <div style={{ color: '#64748b', fontSize: 13 }}>Kein Agent verbunden</div>}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Auslastung */}
|
||||||
|
<div className="ap-card">
|
||||||
|
<div className="ap-card-title">📊 Auslastung</div>
|
||||||
|
{agent ? (() => {
|
||||||
|
const cpuPct = agent.cpu_usage_percent ?? null;
|
||||||
|
const ramUsed = agent.ram_used_gb ?? null;
|
||||||
|
const ramTotal = agent.ram_total_gb ?? null;
|
||||||
|
const ramPct = ramTotal > 0 ? Math.round((ramUsed / ramTotal) * 100) : null;
|
||||||
|
const diskTotal = agent.disk_total_gb ?? null;
|
||||||
|
const diskFree = agent.disk_free_gb ?? null;
|
||||||
|
const diskUsed = diskTotal != null && diskFree != null ? diskTotal - diskFree : null;
|
||||||
|
const diskPct = diskTotal > 0 ? Math.round((diskUsed / diskTotal) * 100) : null;
|
||||||
|
const uptimeH = agent.uptime_hours ?? null;
|
||||||
|
const fmtUptime = (h) => {
|
||||||
|
if (h == null) return '—';
|
||||||
|
if (h < 24) return `${Math.floor(h)} Std.`;
|
||||||
|
return `${Math.floor(h / 24)} Tag(e)`;
|
||||||
|
};
|
||||||
|
const barColor = (p) => p > 80 ? '#ef4444' : p > 60 ? '#f59e0b' : '#34d399';
|
||||||
|
const Bar = ({ pct: p }) => (
|
||||||
|
<div style={{ background: 'rgba(255,255,255,0.08)', borderRadius: 4, height: 6, marginTop: 4 }}>
|
||||||
|
<div style={{ background: barColor(p), width: `${Math.min(100, p)}%`, height: 6, borderRadius: 4, transition: 'width 0.3s' }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{cpuPct != null && (
|
||||||
|
<div className="ap-info-row" style={{ flexDirection: 'column', alignItems: 'stretch', gap: 2 }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span className="ap-info-label">CPU-Auslastung</span>
|
||||||
|
<span className="ap-info-value" style={{ color: barColor(cpuPct) }}>{cpuPct.toFixed(1)} %</span>
|
||||||
|
</div>
|
||||||
|
<Bar pct={cpuPct} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ramUsed != null && ramTotal != null && (
|
||||||
|
<div className="ap-info-row" style={{ flexDirection: 'column', alignItems: 'stretch', gap: 2 }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span className="ap-info-label">RAM</span>
|
||||||
|
<span className="ap-info-value" style={{ color: barColor(ramPct) }}>{ramUsed.toFixed(1)} / {ramTotal.toFixed(0)} GB</span>
|
||||||
|
</div>
|
||||||
|
<Bar pct={ramPct} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{diskUsed != null && diskTotal != null && (
|
||||||
|
<div className="ap-info-row" style={{ flexDirection: 'column', alignItems: 'stretch', gap: 2 }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span className="ap-info-label">Disk</span>
|
||||||
|
<span className="ap-info-value" style={{ color: barColor(diskPct) }}>{diskUsed.toFixed(0)} / {diskTotal.toFixed(0)} GB</span>
|
||||||
|
</div>
|
||||||
|
<Bar pct={diskPct} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{uptimeH != null && (
|
||||||
|
<div className="ap-info-row">
|
||||||
|
<span className="ap-info-label">Laufzeit</span>
|
||||||
|
<span className="ap-info-value">{fmtUptime(uptimeH)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{cpuPct == null && ramUsed == null && diskUsed == null && uptimeH == null && (
|
||||||
|
<div style={{ color: '#64748b', fontSize: 13 }}>Keine Auslastungsdaten</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})() : <div style={{ color: '#64748b', fontSize: 13 }}>Kein Agent verbunden</div>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1031,14 +1097,27 @@ const AssetsPage = () => {
|
|||||||
{activeTab === 'dokumente' && (
|
{activeTab === 'dokumente' && (
|
||||||
<div className="ap-card">
|
<div className="ap-card">
|
||||||
<div className="ap-card-title">📁 Dokumente & Aktionen</div>
|
<div className="ap-card-title">📁 Dokumente & Aktionen</div>
|
||||||
<div
|
<div className="ap-doc-item" style={{cursor:'default', flexDirection:'column', alignItems:'flex-start', gap:10}}>
|
||||||
className="ap-doc-item"
|
<div style={{display:'flex', alignItems:'center', gap:10}}>
|
||||||
onClick={() => window.open(`${API}/assets/${detail.id}/label`, '_blank')}
|
|
||||||
>
|
|
||||||
<div className="ap-doc-icon">🏷️</div>
|
<div className="ap-doc-icon">🏷️</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="ap-doc-name">Asset-Label drucken</div>
|
<div className="ap-doc-name">Asset-Label drucken</div>
|
||||||
<div className="ap-doc-meta">QR-Code Label als PDF öffnen</div>
|
<div className="ap-doc-meta">Format wählen und als PDF öffnen</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{display:'flex', gap:8, flexWrap:'wrap', paddingLeft:4}}>
|
||||||
|
{[
|
||||||
|
{ key: 'small', label: 'Klein (50×25mm)' },
|
||||||
|
{ key: 'medium', label: 'Mittel (80×40mm)' },
|
||||||
|
{ key: 'large', label: 'Groß (A5)' },
|
||||||
|
{ key: 'ptouch12', label: 'P-Touch 700 (12mm)' },
|
||||||
|
].map(f => (
|
||||||
|
<button key={f.key}
|
||||||
|
className="ap-btn ap-btn-sm"
|
||||||
|
style={{fontSize:11, padding:'4px 10px', background:'rgba(13,148,136,0.12)', border:'1px solid rgba(13,148,136,0.3)', color:'#0d9488', borderRadius:6, cursor:'pointer'}}
|
||||||
|
onClick={() => openPdf(`${API}/assets/${detail.id}/label?size=${f.key}`)}
|
||||||
|
>{f.label}</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{detail.status === 'zugewiesen' && (
|
{detail.status === 'zugewiesen' && (
|
||||||
|
|||||||
@@ -748,6 +748,7 @@ const MonitoringPage = () => {
|
|||||||
const [agentFilter, setAgentFilter] = useState('all');
|
const [agentFilter, setAgentFilter] = useState('all');
|
||||||
const [expandedAgents, setExpandedAgents] = useState(new Set());
|
const [expandedAgents, setExpandedAgents] = useState(new Set());
|
||||||
const [expandedHosts, setExpandedHosts] = useState(new Set());
|
const [expandedHosts, setExpandedHosts] = useState(new Set());
|
||||||
|
const [agentSort, setAgentSort] = useState('cpu');
|
||||||
const [hostFilter, setHostFilter] = useState('all');
|
const [hostFilter, setHostFilter] = useState('all');
|
||||||
const [hostSearch, setHostSearch] = useState('');
|
const [hostSearch, setHostSearch] = useState('');
|
||||||
const [proxmoxData, setProxmoxData] = useState(null);
|
const [proxmoxData, setProxmoxData] = useState(null);
|
||||||
@@ -1186,6 +1187,11 @@ const MonitoringPage = () => {
|
|||||||
|
|
||||||
// ─── Tab: Agents ──────────────────────────────────────────────────────────
|
// ─── Tab: Agents ──────────────────────────────────────────────────────────
|
||||||
const renderAgents = () => {
|
const renderAgents = () => {
|
||||||
|
const isAgentOffline = (a) => {
|
||||||
|
if (!a.last_checkin) return true;
|
||||||
|
return Date.now() - new Date(a.last_checkin).getTime() > 5 * 60 * 1000;
|
||||||
|
};
|
||||||
|
|
||||||
const filtered = agents.filter(a => {
|
const filtered = agents.filter(a => {
|
||||||
if (agentSearch && !a.hostname.toLowerCase().includes(agentSearch.toLowerCase()) && !(a.ip_address || '').includes(agentSearch)) return false;
|
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 === 'online' && a.status !== 'online') return false;
|
||||||
@@ -1193,6 +1199,27 @@ const MonitoringPage = () => {
|
|||||||
if (agentFilter === 'warn' && warnings(a).length === 0) return false;
|
if (agentFilter === 'warn' && warnings(a).length === 0) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', gap: 10, marginBottom: 16, flexWrap: 'wrap', alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 10, marginBottom: 16, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
@@ -1203,10 +1230,16 @@ const MonitoringPage = () => {
|
|||||||
{pill(agentFilter === 'offline', () => setAgentFilter('offline'), 'OFFLINE')}
|
{pill(agentFilter === 'offline', () => setAgentFilter('offline'), 'OFFLINE')}
|
||||||
{pill(agentFilter === 'warn', () => setAgentFilter('warn'), 'WARNUNG')}
|
{pill(agentFilter === 'warn', () => setAgentFilter('warn'), 'WARNUNG')}
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontSize: 12, color: 'var(--text-muted)', marginLeft: 'auto' }}>{filtered.length} Einträge</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filtered.length === 0
|
{sorted.length === 0
|
||||||
? <div style={{ textAlign: 'center', padding: 40, color: 'var(--text-muted)' }}>Keine Agents gefunden</div>
|
? <div style={{ textAlign: 'center', padding: 40, color: 'var(--text-muted)' }}>Keine Agents gefunden</div>
|
||||||
: (
|
: (
|
||||||
/* Checkmk-style service table */
|
/* Checkmk-style service table */
|
||||||
@@ -1221,7 +1254,7 @@ const MonitoringPage = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filtered.map(a => {
|
{sorted.map(a => {
|
||||||
const w = warnings(a), hasErr = w.some(x => x.type === 'error');
|
const w = warnings(a), hasErr = w.some(x => x.type === 'error');
|
||||||
const state = a.status === 'offline' ? 'crit' : hasErr ? 'crit' : w.length ? 'warn' : 'ok';
|
const state = a.status === 'offline' ? 'crit' : hasErr ? 'crit' : w.length ? 'warn' : 'ok';
|
||||||
const isExp = expandedAgents.has(a.id);
|
const isExp = expandedAgents.has(a.id);
|
||||||
|
|||||||
Reference in New Issue
Block a user