diff --git a/src/pages/ServerpulsPage.jsx b/src/pages/ServerpulsPage.jsx
index edd5e31..8a76c51 100644
--- a/src/pages/ServerpulsPage.jsx
+++ b/src/pages/ServerpulsPage.jsx
@@ -1,10 +1,11 @@
import { useCallback, useEffect, useRef, useMemo, useState } from 'react'
import { Navigate } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
-import { PageHeader } from '../components/ui'
+import { PageHeader, Badge, Stat } from '../components/ui'
+import { badgeToneClass } from '../components/ui/tones'
import { serverpulsApi } from '../lib/serverpulsApi'
import {
- ageLabel, formatBytes, formatNumber, formatPct, formatSince, usedPct,
+ ageLabel, deltaDir, deltaSign, formatBytes, formatNumber, formatPct, formatSince, levelTone, usedPct,
} from '../lib/serverpulsFormat'
import Chart from '../components/serverpuls/Chart'
import { CHART } from '../components/serverpuls/chartColors'
@@ -81,20 +82,17 @@ function Tile({ id, label, model }) {
let deltaEl = null
if (model && model.delta !== null && model.delta !== undefined) {
const d = model.delta
- const dir = Math.abs(d) < 1e-9 ? 'flat' : d > 0 ? 'up' : 'down'
- const sign = dir === 'up' ? '▲ +' : dir === 'down' ? '▼ −' : ''
+ const dir = deltaDir(d)
deltaEl = (
- {sign}{model.fmtDelta(d)} ggü. gestern
+ {deltaSign(dir)}{model.fmtDelta(d)} ggü. gestern
)
}
return (
-
{label}
-
{model ? model.main : '–'}
-
{model?.sub || ' '}
-
+
+
{deltaEl}
)
@@ -102,40 +100,44 @@ function Tile({ id, label, model }) {
function Pills({ items, value, onChange, testid }) {
return (
-
- {items.map((it) => (
-
- ))}
+
+ {items.map((it) => {
+ const active = value === it.id
+ return (
+
+ )
+ })}
)
}
function Cleanup({ data }) {
- if (!data || !data.day) return
Noch keine Messung.
+ if (!data || !data.day) 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)}
+ - {s.name}{formatBytes(s.bytes)}
))}
)}
-
Stand {data.day} · nur Anzeige, hier wird nichts gelöscht.
-
+
Stand {data.day} · nur Anzeige, hier wird nichts gelöscht.
+ >
)
}
@@ -245,7 +247,6 @@ export default function ServerpulsPage() {
const level = overview?.level || 'ok'
const alerts = overview?.alerts || []
const age = ageLabel(overview?.lastSample ?? null, now)
- const dot = level === 'critical' ? '🔴' : level === 'medium' ? '🟠' : '🟢'
const headline = level === 'ok'
? 'Alles ok'
: level === 'critical'
@@ -256,14 +257,20 @@ export default function ServerpulsPage() {
- {error &&
{error}
}
+ {error &&
{error}
}
-
+
-
{dot}
+
+ {overview ? (LEVEL_TEXT[level] || level) : '–'}
+
{overview ? headline : (error ? 'Keine Daten' : 'Lädt …')}
{overview && (
-
+
Letzte Messung {age.text}{age.stale ? ' – Daten veraltet' : ''}
)}
@@ -271,46 +278,46 @@ export default function ServerpulsPage() {
{alerts.length > 0 && (
)}
-
+
{METRICS.map((x) => (
))}
-
-
Verlauf
-
+
Verlauf
+
{chartData && chartData.data[0].length > 0
?
- :
{chart ? 'Für diesen Zeitraum liegen keine Messwerte vor.' : 'Lädt …'}
}
+ :
{chart ? 'Für diesen Zeitraum liegen keine Messwerte vor.' : 'Lädt …'}
}
-
-
Projekte
- {appsData?.sizesDay && Größen von {appsData.sizesDay}}
-
+
+ Projekte
+ {appsData?.sizesDay && Größen von {appsData.sizesDay}}
+
{selected && (
-
-
{detail?.name || selected}
+
{detail?.name || selected}
+
{detailData ? (
@@ -319,13 +326,13 @@ export default function ServerpulsPage() {
CPU
formatPct(v)} height={200} />
- ) :
Lädt …
}
+ ) :
Lädt …
}
)}
- Aufräumbar
+ Aufräumbar
diff --git a/src/styles/serverpuls.css b/src/styles/serverpuls.css
index e662155..4cda93c 100644
--- a/src/styles/serverpuls.css
+++ b/src/styles/serverpuls.css
@@ -1,49 +1,30 @@
-/* Serverpuls (Admin) */
-.sp-page { display: flex; flex-direction: column; gap: var(--sp-4); }
-.sp-error { background: var(--red-bg); border: 1px solid var(--danger-ring); color: var(--red-fg); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); }
-.sp-muted { color: var(--text-muted); }
-.sp-small { font-size: 12px; }
+/* Serverpuls (P9): Kopf, Warnungen, Kacheln, Umschalter, Panels. Die Tabellenregeln weiter unten gehoeren Task 5. */
+.sp-page .msg { margin: 0 0 16px; }
-.sp-head { background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--sp-4); }
-.sp-level-medium { border-color: var(--orange-fg); }
-.sp-level-critical { border-color: var(--danger); }
-.sp-head-top { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
-.sp-headline { font-size: 18px; }
-.sp-age { margin-left: auto; font-size: 13px; color: var(--text-muted); }
-.sp-age-stale { color: var(--orange-fg); background: var(--orange-bg); padding: 2px 10px; border-radius: var(--r-pill); }
-.sp-alerts { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
-.sp-alert { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: baseline; padding: 8px 12px; border-radius: var(--r-sm); background: var(--surface-2); border-left: 3px solid var(--text-faint); }
-.sp-alert-critical { border-left-color: var(--danger); }
-.sp-alert-medium { border-left-color: var(--warn); }
-.sp-alert-level { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); min-width: 56px; }
-.sp-alert-title { font-weight: 600; }
-.sp-alert-value { color: var(--text-muted); }
+.sp-head-top { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
+.sp-headline { font-size: 16px; line-height: 24px; font-weight: 600; }
+.sp-age { margin-left: auto; font-size: 13px; color: var(--text-2); }
+.sp-age-stale { color: var(--orange-fg); font-weight: 500; }
+.sp-alerts { list-style: none; margin: 12px 0 0; padding: 0; }
+.sp-alert-main { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; min-width: 0; }
+.sp-alert-title { font-weight: 500; overflow-wrap: anywhere; }
+.sp-alert-value { color: var(--text-2); }
-.sp-tiles { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: var(--sp-3); }
-@media (max-width: 1100px) { .sp-tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
-@media (max-width: 700px) { .sp-tiles { grid-template-columns: 1fr; } .sp-age { margin-left: 0; } }
-.sp-tile { background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--sp-3) var(--sp-4); display: flex; flex-direction: column; gap: 2px; min-width: 0; }
-.sp-tile-label { font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted); }
-.sp-tile-value { font-size: 26px; font-weight: 800; }
-.sp-tile-sub { font-size: 12px; color: var(--text-muted); min-height: 16px; }
-.sp-tile-delta { font-size: 12px; margin-top: 2px; }
+.stats.sp-tiles { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 24px 32px; }
+.sp-tile { min-width: 0; }
+.sp-tile .sub { min-height: 16px; }
.sp-spark { display: block; max-width: 100%; margin-top: 6px; }
+.sp-tile-delta { font-size: 12px; line-height: 16px; margin-top: 4px; }
+.sp-delta-up { color: var(--red-fg); }
+.sp-delta-down { color: var(--green-fg); }
+.sp-delta-flat { color: var(--text-2); }
-.sp-delta-up { color: var(--danger); }
-.sp-delta-down { color: var(--ok); }
-.sp-delta-flat { color: var(--text-muted); }
-
-.sp-panel { background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--sp-4); min-width: 0; }
-.sp-panel-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; margin-bottom: var(--sp-3); }
-.sp-panel-head h3 { margin: 0; font-size: 16px; }
-.sp-controls { display: flex; gap: var(--sp-3); flex-wrap: wrap; }
-.sp-pills { display: flex; gap: 4px; flex-wrap: wrap; }
-.sp-pill { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-muted); border-radius: var(--r-pill); padding: 4px 12px; font: inherit; font-size: 13px; cursor: pointer; }
-.sp-pill:hover { color: var(--text); }
-.sp-pill.active { background: var(--accent-soft); border-color: var(--border-accent); color: var(--accent); font-weight: 600; }
+.sp-panel { min-width: 0; }
+.sp-controls { display: flex; flex-wrap: wrap; gap: 8px 16px; margin-bottom: 12px; }
+.sp-controls .seg { flex-wrap: wrap; }
.sp-chart { width: 100%; min-height: 200px; }
-.sp-chart .u-legend { color: var(--text-muted); font-size: 12px; }
-.sp-nodata { padding: var(--sp-6, 32px) 0; text-align: center; }
+.sp-chart .u-legend { font-size: 13px; color: var(--text-2); }
+.sp-nodata { padding: 40px 0; text-align: center; color: var(--text-2); }
.sp-search { width: 100%; max-width: 320px; margin-bottom: var(--sp-3); background: var(--surface-2); border: 1px solid var(--border); color: var(--text); border-radius: var(--r-sm); padding: 8px 12px; font: inherit; }
.sp-table-wrap { overflow-x: auto; }
@@ -57,12 +38,19 @@
.sp-empty { text-align: center; color: var(--text-muted); }
.sp-delta { font-variant-numeric: tabular-nums; }
-.sp-detail { margin-top: var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--border); }
-.sp-detail-charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr)); gap: var(--sp-4); }
-.sp-chart-title { font-size: 13px; color: var(--text-muted); margin-bottom: 4px; }
+.sp-detail { margin-top: 28px; }
+.sp-detail-title { margin: 0 0 8px; font-size: 15px; line-height: 22px; font-weight: 600; overflow-wrap: anywhere; }
+.sp-detail-charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr)); gap: 24px; }
+.sp-chart-title { font-size: 13px; color: var(--text-2); margin-bottom: 4px; }
-.sp-cleanup-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--sp-3); }
-.sp-cleanup-grid > div { display: flex; flex-direction: column; gap: 2px; }
-.sp-cleanup-grid strong { font-size: 20px; }
-.sp-cleanup-list { list-style: none; margin: var(--sp-3) 0; padding: 0; }
-.sp-cleanup-list li { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid var(--border); font-size: 14px; }
+.sp-cleanup-list { list-style: none; margin: 12px 0; padding: 0; }
+
+@media (max-width: 1100px) {
+ .stats.sp-tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+}
+
+@media (max-width: 860px) {
+ .stats.sp-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px 24px; }
+ .sp-age { margin-left: 0; }
+ .sp-page .sp-cleanup-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}