+ {sign}{model.fmtDelta(d)} ggü. gestern
+
+ )
+ }
+ return (
+ Noch keine Messung.
+ const stopped = Array.isArray(data.stopped) ? data.stopped : []
+ return (
+
+
+
Ungenutzte Images{formatBytes(data.imagesBytes)}
+
Build-Cache{formatBytes(data.buildCacheBytes)}
+
Gestoppte Container{formatBytes(data.stoppedBytes)}
+
+ {stopped.length > 0 && (
+
+ {stopped.map((s) => (
+ - {s.name}{formatBytes(s.bytes)}
+ ))}
+
+ )}
+
Stand {data.day} · nur Anzeige, hier wird nichts gelöscht.
+
+ )
+}
+
+export default function ServerpulsPage() {
+ const { isAdmin, loading } = useAuth()
+ const [overview, setOverview] = useState(null)
+ const [appsData, setAppsData] = useState(null)
+ const [cleanup, setCleanup] = useState(null)
+ const [chart, setChart] = useState(null)
+ const [detail, setDetail] = useState(null)
+ const [metric, setMetric] = useState('cpu')
+ const [range, setRange] = useState('24h')
+ const [selected, setSelected] = useState(null)
+ const [detailRange, setDetailRange] = useState('30d')
+ const [error, setError] = useState('')
+ const [now, setNow] = useState(Date.now())
+ const [tick, setTick] = useState(0)
+
+ // Uebersicht, Projekte, Aufraeumbar: beim Laden und jede Minute (nur sichtbar)
+ const loadMain = useCallback(async () => {
+ try {
+ const [o, a, c] = await Promise.all([
+ serverpulsApi.overview(), serverpulsApi.apps(), serverpulsApi.cleanup(),
+ ])
+ setOverview(o)
+ setAppsData(a)
+ setCleanup(c)
+ setError('')
+ } catch (e) {
+ setError(e.message || 'Serverpuls ist nicht erreichbar.')
+ }
+ setNow(Date.now())
+ setTick((t) => t + 1)
+ }, [])
+
+ useEffect(() => {
+ if (!isAdmin) return undefined
+ loadMain()
+ const iv = setInterval(() => {
+ if (document.visibilityState === 'visible') loadMain()
+ }, 60000)
+ const onVis = () => { if (document.visibilityState === 'visible') loadMain() }
+ document.addEventListener('visibilitychange', onVis)
+ return () => { clearInterval(iv); document.removeEventListener('visibilitychange', onVis) }
+ }, [isAdmin, loadMain])
+
+ // grosses Diagramm
+ useEffect(() => {
+ if (!isAdmin) return undefined
+ let dead = false
+ serverpulsApi.host(metric, range)
+ .then((d) => { if (!dead) { setChart({ ...d, metric, range }); } })
+ .catch((e) => { if (!dead) { setChart(null); setError(e.message || 'Diagramm konnte nicht geladen werden.') } })
+ return () => { dead = true }
+ }, [isAdmin, metric, range, tick])
+
+ // Detaildiagramm der App
+ useEffect(() => {
+ if (!isAdmin || !selected) { setDetail(null); return undefined }
+ let dead = false
+ serverpulsApi.app(selected, detailRange)
+ .then((d) => { if (!dead) setDetail(d) })
+ .catch((e) => { if (!dead) { setDetail(null); setError(e.message || 'Projektdaten konnten nicht geladen werden.') } })
+ return () => { dead = true }
+ }, [isAdmin, selected, detailRange, tick])
+
+ const m = METRICS.find((x) => x.id === metric)
+
+ const chartData = useMemo(() => {
+ if (!chart) return null
+ const pts = displayPoints(chart.metric, chart.points || [], chart.total)
+ const xs = pts.map((p) => Math.round(p.ts / 1000))
+ const avg = pts.map((p) => p.v)
+ const withMax = chart.res !== 'raw' && ['cpu', 'load', 'zombies'].includes(chart.metric)
+ const data = [xs, avg]
+ const series = [{ label: m.label, color: '#10b981', fill: 'rgba(16,185,129,0.10)' }]
+ if (withMax) {
+ data.push(pts.map((p) => p.max ?? null))
+ series.push({ label: `${m.label} (Spitze)`, color: '#f59e0b', dash: [4, 4] })
+ }
+ return { data, series }
+ }, [chart, m.label])
+
+ const detailData = useMemo(() => {
+ if (!detail) return null
+ const days = (detail.size || []).map((s) => Math.round(new Date(`${s.day}T12:00:00Z`).getTime() / 1000))
+ return {
+ size: { data: [days, (detail.size || []).map((s) => s.bytes)], series: [{ label: 'Größe', color: '#3b82f6', fill: 'rgba(59,130,246,0.10)' }] },
+ mem: { data: [(detail.mem || []).map((p) => Math.round(p.ts / 1000)), (detail.mem || []).map((p) => p.v)], series: [{ label: 'RAM', color: '#10b981' }] },
+ cpu: { data: [(detail.cpu || []).map((p) => Math.round(p.ts / 1000)), (detail.cpu || []).map((p) => p.v)], series: [{ label: 'CPU', color: '#f59e0b' }] },
+ }
+ }, [detail])
+
+ if (loading) return null
+ if (!isAdmin) return