librelad 6346d76a92 feat(system): binary ring history with 7-day retention + fullscreen detail UI
Replaces the JSON history file behind /api/system/history with a fixed-size
binary ring buffer on disk and adds a second, downsampled tier so the chart
can now span seven days, not just twenty-four hours.

Two on-disk rings under frontend/data/system/:
  metrics_ring_1m.bin  1440 pts @ 1 min  ( 24 h)
  metrics_ring_5m.bin  2016 pts @ 5 min  (  7 d)

Each point is 32 bytes (uint32 timestamp + 7 float32 metrics — cpu / mem /
swap / disk / load1 / net_rx / net_tx); files carry a 32-byte header with
magic, version, capacity, head, count, bucket seconds, and last bucket time
so they're self-describing and torn-write recoverable.

A persistent 1-minute ticker inside the backend (independent of whether
anyone's subscribed to /api/system/stream) composes points from /proc plus
the bash generator's latest snapshots and appends to the 1m ring; every
five minutes it averages the last five 1m points into the 5m ring. On
first run, the writer backfills the 1m ring from the legacy
metrics_history.json so first paint already has 24 h.

/api/system/history?range=N auto-selects the tier (≤1440 → 1m, else 5m),
keeps the existing { points, updated } shape, and additionally returns
`tier` for clients that care. Falls back to the legacy JSON on cold start.

Admin → System: 7d added to the range picker (now 1h / 6h / 24h / 7d),
swap + load1 promoted to their own trend cards, and every gauge / chart
card grows an Expand affordance that opens a fullscreen single-metric
deep-dive overlay:
  - Big themed chart with grid, gradient area, peak/min/now markers, and
    a live-pulsing "now" dot
  - Hover crosshair + tooltip scrubs the series with formatted time +
    value
  - now / peak / avg / min stat strip with deltas
  - Range picker (1h / 6h / 24h / 7d) re-fetches and re-themes per metric
  - 1 Hz live SSE feed updates the overlay's now-stat in real time
  - Escape / backdrop / close button all dismiss
  - Per-metric accent colour (cpu=accent, mem=info, disk/swap=warning,
    net_rx=success, net_tx=accent, load=accent) flows through gradient,
    border, dot, and stats card

Zero new dependencies — hand-rolled SVG and pointer events throughout.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 21:04:27 +01:00

621 lines
18 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Admin area — Overview board + shared admin-page chrome. Visually aligned
with the backup dashboard (tile/card style) and the config page header. */
.admin-page {
padding: 4px 2px 40px;
}
.admin-breadcrumb {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: rgba(var(--text-rgb), 0.45);
margin-bottom: 2px;
}
/* Overview cards */
.admin-card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
margin-top: 8px;
}
.admin-card {
display: flex;
flex-direction: column;
padding: 16px;
border: 1px solid rgba(var(--text-rgb), 0.10);
border-radius: 12px;
background: var(--card-bg);
}
.admin-card-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.admin-card-title {
font-size: 0.95rem;
font-weight: 700;
}
.admin-status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.admin-status-dot.ok { background: #36d399; }
.admin-status-dot.warn { background: #fbbd23; }
.admin-status-dot.none { background: rgba(var(--text-rgb), 0.25); }
.admin-card-body {
display: flex;
flex-direction: column;
gap: 6px;
flex: 1;
}
.admin-card-line {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
font-size: 0.85rem;
color: rgba(var(--text-rgb), 0.6);
}
.admin-card-line strong {
color: var(--text-primary);
font-weight: 600;
text-align: right;
word-break: break-word;
}
.admin-card-actions {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid rgba(var(--text-rgb), 0.08);
}
.admin-card-ok {
font-size: 0.82rem;
color: rgba(var(--text-rgb), 0.55);
}
/* ============================================================
Admin → System (in-depth statistics page)
============================================================ */
.sys-section-head {
display: flex;
align-items: center;
justify-content: space-between;
margin: 26px 0 12px;
}
.sys-section-head h2 {
font-size: 1.05rem;
font-weight: 700;
margin: 0;
}
.sys-chart-meta {
font-size: 0.78rem;
color: rgba(var(--text-rgb), 0.5);
}
/* Range selector (1h / 6h / 24h) */
.sys-range { display: inline-flex; gap: 4px; }
.sys-range-btn {
padding: 4px 12px;
font-size: 0.78rem;
font-weight: 600;
color: rgba(var(--text-rgb), 0.7);
background: rgba(var(--text-rgb), 0.06);
border: 1px solid rgba(var(--text-rgb), 0.12);
border-radius: 999px;
cursor: pointer;
transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.sys-range-btn:hover { background: rgba(var(--accent-rgb), 0.15); }
.sys-range-btn.active {
color: var(--text-primary);
background: rgba(var(--accent-rgb), 0.22);
border-color: rgba(var(--accent-rgb), 0.55);
}
/* Gauges row */
.sys-gauges {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
margin-top: 10px;
}
.lp-gauge {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 14px 12px 10px;
background: var(--card-bg);
border: 1px solid rgba(var(--text-rgb), 0.10);
border-radius: 12px;
}
.lp-gauge-svg { width: 116px; height: 116px; display: block; }
.lp-gauge-center {
position: absolute;
top: 72px;
left: 0; right: 0;
transform: translateY(-50%);
text-align: center;
pointer-events: none;
}
.lp-gauge-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-primary);
line-height: 1;
}
.lp-gauge-value span { font-size: 0.8rem; font-weight: 600; opacity: 0.6; margin-left: 1px; }
.lp-gauge-sub { font-size: 0.68rem; color: rgba(var(--text-rgb), 0.5); margin-top: 3px; }
.lp-gauge-label {
margin-top: 6px;
font-size: 0.82rem;
font-weight: 600;
color: rgba(var(--text-rgb), 0.75);
}
/* Trend chart cards */
.sys-charts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.sys-chart-card {
background: var(--card-bg);
border: 1px solid rgba(var(--text-rgb), 0.10);
border-radius: 12px;
padding: 14px 16px 10px;
transition: border-color .2s ease, box-shadow .2s ease;
}
.sys-chart-card:hover {
border-color: rgba(var(--accent-rgb), 0.28);
box-shadow: 0 6px 22px rgba(var(--accent-rgb), 0.10);
}
.sys-chart-card:hover .sys-expand { color: var(--accent); background: rgba(var(--accent-rgb), 0.18); }
.sys-chart-head {
display: flex;
align-items: baseline;
justify-content: space-between;
font-size: 0.88rem;
font-weight: 700;
margin-bottom: 8px;
}
.sys-chart-head .sys-chart-meta { font-weight: 500; }
.sys-chart-body { position: relative; }
.lp-chart { width: 100%; height: 92px; display: block; overflow: visible; }
.lp-chart-last {
position: absolute;
top: 0; right: 0;
font-size: 0.85rem;
font-weight: 700;
}
.lp-chart-empty {
height: 92px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.82rem;
color: rgba(var(--text-rgb), 0.4);
}
.sys-net-legend {
display: flex;
gap: 16px;
margin-top: 6px;
font-size: 0.8rem;
color: rgba(var(--text-rgb), 0.7);
}
.sys-net-legend .dot {
display: inline-block;
width: 8px; height: 8px;
border-radius: 50%;
margin-right: 5px;
}
.sys-net-legend .dot.ok { background: var(--status-success); }
.sys-net-legend .dot.accent { background: var(--accent); }
/* Info / docker stat strips */
.sys-strip {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.sys-stat {
display: flex;
flex-direction: column;
gap: 3px;
padding: 12px 14px;
background: var(--card-bg);
border: 1px solid rgba(var(--text-rgb), 0.10);
border-radius: 10px;
}
.sys-stat-label {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: rgba(var(--text-rgb), 0.45);
}
.sys-stat-value {
font-size: 0.92rem;
font-weight: 600;
color: var(--text-primary);
word-break: break-word;
}
/* Per-app table */
.sys-apps-wrap {
background: var(--card-bg);
border: 1px solid rgba(var(--text-rgb), 0.10);
border-radius: 12px;
overflow: hidden;
}
table.sys-apps {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
table.sys-apps th {
text-align: left;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: rgba(var(--text-rgb), 0.45);
padding: 12px 14px;
border-bottom: 1px solid rgba(var(--text-rgb), 0.10);
}
table.sys-apps td {
padding: 11px 14px;
border-bottom: 1px solid rgba(var(--text-rgb), 0.06);
vertical-align: middle;
color: var(--text-primary);
}
table.sys-apps tr:last-child td { border-bottom: none; }
table.sys-apps tr:hover td { background: rgba(var(--text-rgb), 0.03); }
.sys-app-name { font-weight: 600; display: flex; align-items: center; gap: 8px; }
.sys-app-sub { font-size: 0.72rem; font-weight: 500; color: rgba(var(--text-rgb), 0.45); }
.sys-cell-val { font-size: 0.78rem; color: rgba(var(--text-rgb), 0.65); margin-left: 2px; }
.sys-net-cell { font-size: 0.78rem; color: rgba(var(--text-rgb), 0.7); white-space: nowrap; }
.sys-spark-cell { width: 110px; }
.sys-apps-empty {
text-align: center;
color: rgba(var(--text-rgb), 0.45);
padding: 24px 14px !important;
}
/* Bars + sparklines (shared by LPCharts) */
.lp-bar {
display: inline-block;
width: 90px;
max-width: 40%;
height: 6px;
border-radius: 3px;
background: rgba(var(--text-rgb), 0.12);
overflow: hidden;
vertical-align: middle;
}
.lp-bar-fill { display: block; height: 100%; border-radius: 3px; transition: width .4s ease; }
.lp-spark { width: 100px; height: 24px; display: block; }
/* Gauge wrapper — each gauge tile is a button that opens the detail overlay.
The visual is identical to before (.lp-gauge); the wrapper adds the press
affordance + the corner expand icon. */
.sys-gauge-wrap {
all: unset;
position: relative;
display: block;
cursor: pointer;
border-radius: 12px;
transition: transform .15s ease, box-shadow .15s ease;
}
.sys-gauge-wrap:focus-visible { outline: 2px solid rgba(var(--accent-rgb), 0.6); outline-offset: 2px; }
.sys-gauge-wrap:hover { transform: translateY(-1px); }
.sys-gauge-wrap:hover .lp-gauge { box-shadow: 0 6px 24px rgba(var(--accent-rgb), 0.18); border-color: rgba(var(--accent-rgb), 0.35); }
.sys-gauge-wrap .lp-gauge { transition: box-shadow .2s ease, border-color .2s ease; height: 100%; }
.sys-gauge-expand {
position: absolute;
top: 8px; right: 10px;
width: 24px; height: 24px;
display: flex;
align-items: center; justify-content: center;
color: rgba(var(--text-rgb), 0.45);
border-radius: 6px;
background: rgba(var(--text-rgb), 0.04);
opacity: 0;
transition: opacity .15s ease, color .15s ease, background .15s ease;
pointer-events: none;
}
.sys-gauge-wrap:hover .sys-gauge-expand,
.sys-gauge-wrap:focus-visible .sys-gauge-expand { opacity: 1; }
.sys-gauge-wrap:hover .sys-gauge-expand { color: var(--accent); background: rgba(var(--accent-rgb), 0.12); }
/* Chart card head — right cluster keeps the meta + expand button aligned. */
.sys-chart-head { gap: 12px; }
.sys-chart-head-right { display: inline-flex; align-items: center; gap: 10px; }
.sys-expand {
all: unset;
display: inline-flex;
align-items: center; justify-content: center;
width: 24px; height: 24px;
color: rgba(var(--text-rgb), 0.5);
background: rgba(var(--text-rgb), 0.05);
border-radius: 6px;
cursor: pointer;
transition: color .15s ease, background .15s ease, transform .15s ease;
}
.sys-expand:hover { color: var(--accent); background: rgba(var(--accent-rgb), 0.14); transform: scale(1.06); }
.sys-expand:focus-visible { outline: 2px solid rgba(var(--accent-rgb), 0.6); outline-offset: 2px; }
/* =========================================================================
Admin → System — fullscreen single-metric deep-dive overlay (.sys-detail)
========================================================================= */
body.sys-detail-active { overflow: hidden; }
.sys-detail {
position: fixed; inset: 0;
z-index: 2000;
display: none;
pointer-events: none;
}
.sys-detail.open { display: block; pointer-events: auto; }
.sys-detail-backdrop {
position: absolute; inset: 0;
background:
radial-gradient(60% 50% at 50% 35%, rgba(var(--accent-rgb), 0.12) 0%, transparent 70%),
rgba(0, 0, 0, 0.62);
backdrop-filter: blur(14px) saturate(140%);
-webkit-backdrop-filter: blur(14px) saturate(140%);
animation: sys-detail-fade .22s ease;
}
@keyframes sys-detail-fade { from { opacity: 0; } to { opacity: 1; } }
.sys-detail-panel {
--metric-rgb: var(--accent-rgb);
position: absolute;
inset: 24px;
display: grid;
grid-template-rows: auto auto 1fr auto;
gap: 18px;
padding: 22px 26px 18px;
background:
radial-gradient(80% 60% at 80% 0%, rgba(var(--metric-rgb), 0.10) 0%, transparent 60%),
linear-gradient(160deg, rgba(255,255,255,0.05) 0%, rgba(0,0,0,0.18) 100%),
var(--surface-bg-solid, #0f1729);
border: 1px solid rgba(var(--metric-rgb), 0.32);
border-radius: 18px;
box-shadow:
0 24px 80px rgba(0, 0, 0, 0.55),
0 0 0 1px rgba(255, 255, 255, 0.04) inset,
0 -1px 0 rgba(var(--metric-rgb), 0.4) inset;
animation: sys-detail-rise .28s cubic-bezier(.2,.7,.2,1.0);
color: var(--text-primary);
}
@keyframes sys-detail-rise {
from { opacity: 0; transform: translateY(14px) scale(.985); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.sys-detail-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.sys-detail-eyebrow {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: rgba(var(--text-rgb), 0.45);
margin-bottom: 4px;
}
.sys-detail-name {
font-size: 1.7rem;
font-weight: 700;
margin: 0 0 4px;
background: linear-gradient(120deg, var(--text-primary) 0%, rgba(var(--metric-rgb), 1) 110%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
letter-spacing: -0.01em;
}
.sys-detail-sub {
margin: 0;
font-size: 0.88rem;
color: rgba(var(--text-rgb), 0.6);
}
.sys-detail-actions {
display: flex;
align-items: center;
gap: 14px;
}
.sys-detail-range { display: inline-flex; gap: 4px; padding: 4px; background: rgba(var(--text-rgb), 0.06); border-radius: 999px; }
.sys-detail-range-btn {
all: unset;
padding: 5px 14px;
font-size: 0.8rem;
font-weight: 600;
color: rgba(var(--text-rgb), 0.65);
border-radius: 999px;
cursor: pointer;
transition: background .15s ease, color .15s ease;
}
.sys-detail-range-btn:hover { color: var(--text-primary); }
.sys-detail-range-btn.active {
color: var(--text-on-accent, #0a1426);
background: rgba(var(--metric-rgb), 0.95);
box-shadow: 0 2px 12px rgba(var(--metric-rgb), 0.35);
}
.sys-detail-close {
all: unset;
width: 36px; height: 36px;
display: inline-flex;
align-items: center; justify-content: center;
border-radius: 10px;
color: rgba(var(--text-rgb), 0.7);
background: rgba(var(--text-rgb), 0.06);
cursor: pointer;
transition: background .15s ease, color .15s ease;
}
.sys-detail-close:hover { color: var(--text-primary); background: rgba(var(--text-rgb), 0.14); }
.sys-detail-close:focus-visible { outline: 2px solid rgba(var(--metric-rgb), 0.6); outline-offset: 2px; }
.sys-detail-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
}
.sys-detail-stat {
padding: 14px 18px 12px;
background: rgba(var(--text-rgb), 0.04);
border: 1px solid rgba(var(--text-rgb), 0.08);
border-radius: 14px;
display: flex;
flex-direction: column;
gap: 4px;
transition: border-color .2s ease;
}
.sys-detail-stat[data-stat="now"] {
background: linear-gradient(135deg, rgba(var(--metric-rgb), 0.16) 0%, rgba(var(--metric-rgb), 0.05) 100%);
border-color: rgba(var(--metric-rgb), 0.35);
}
.sys-detail-stat-k {
font-size: 0.66rem;
text-transform: uppercase;
letter-spacing: 0.14em;
color: rgba(var(--text-rgb), 0.5);
font-weight: 700;
}
.sys-detail-stat-v {
font-size: 1.55rem;
font-weight: 700;
color: var(--text-primary);
line-height: 1.05;
font-variant-numeric: tabular-nums;
}
.sys-detail-stat[data-stat="now"] .sys-detail-stat-v {
background: linear-gradient(120deg, var(--text-primary) 0%, rgba(var(--metric-rgb), 1) 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.sys-detail-stat-t {
font-size: 0.72rem;
color: rgba(var(--text-rgb), 0.45);
font-variant-numeric: tabular-nums;
}
.sys-detail-canvas {
position: relative;
min-height: 260px;
background:
radial-gradient(120% 80% at 50% 100%, rgba(var(--metric-rgb), 0.08) 0%, transparent 60%),
rgba(0, 0, 0, 0.22);
border: 1px solid rgba(var(--text-rgb), 0.06);
border-radius: 14px;
overflow: hidden;
}
.sys-detail-svg { width: 100%; height: 100%; display: block; }
.sys-detail-loading,
.sys-detail-empty {
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
color: rgba(var(--text-rgb), 0.45);
font-size: 0.92rem;
pointer-events: none;
}
.sys-detail-empty { color: rgba(var(--text-rgb), 0.55); }
.sys-detail-grid {
stroke: rgba(var(--text-rgb), 0.07);
stroke-width: 1;
stroke-dasharray: 2 4;
vector-effect: non-scaling-stroke;
}
.sys-detail-axis-line {
stroke: rgba(var(--text-rgb), 0.18);
stroke-width: 1;
vector-effect: non-scaling-stroke;
}
.sys-detail-axis {
fill: rgba(var(--text-rgb), 0.55);
font-size: 11px;
font-variant-numeric: tabular-nums;
}
.sys-detail-axis-y { text-anchor: end; }
.sys-detail-axis-x { text-anchor: middle; }
.sys-detail-peak { fill: var(--status-warning); filter: drop-shadow(0 0 6px rgba(var(--status-warning-rgb, 255 193 7), 0.6)); }
.sys-detail-min { fill: rgba(var(--text-rgb), 0.55); }
.sys-detail-now {
fill: rgb(var(--metric-rgb));
filter: drop-shadow(0 0 8px rgba(var(--metric-rgb), 0.7));
animation: sys-detail-pulse 2.2s ease-in-out infinite;
}
@keyframes sys-detail-pulse {
0%, 100% { r: 5; }
50% { r: 7; }
}
.sys-detail-cross {
stroke: rgba(var(--metric-rgb), 0.55);
stroke-width: 1;
stroke-dasharray: 3 3;
vector-effect: non-scaling-stroke;
pointer-events: none;
}
.sys-detail-cross-dot {
fill: rgb(var(--metric-rgb));
stroke: var(--surface-bg-solid, #0f1729);
stroke-width: 2;
pointer-events: none;
}
.sys-detail-tooltip {
position: absolute;
pointer-events: none;
padding: 8px 12px;
background: rgba(15, 23, 41, 0.92);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(var(--metric-rgb), 0.4);
border-radius: 10px;
color: var(--text-primary);
font-size: 0.85rem;
transition: transform 60ms linear;
will-change: transform;
box-shadow: 0 8px 28px rgba(0,0,0,0.4);
min-width: 110px;
}
.sys-detail-tip-v { font-weight: 700; font-size: 0.95rem; font-variant-numeric: tabular-nums; color: rgb(var(--metric-rgb)); }
.sys-detail-tip-t { font-size: 0.72rem; color: rgba(var(--text-rgb), 0.6); margin-top: 2px; font-variant-numeric: tabular-nums; }
.sys-detail-foot {
display: flex;
justify-content: space-between;
align-items: center;
color: rgba(var(--text-rgb), 0.45);
font-size: 0.75rem;
font-variant-numeric: tabular-nums;
}
.sys-detail-foot-hint kbd,
.sys-detail-foot-hint { letter-spacing: 0.02em; }
/* Narrower viewports: collapse the stat grid to 2×2 and shrink padding. */
@media (max-width: 900px) {
.sys-detail-panel { inset: 10px; padding: 16px 16px 12px; gap: 12px; }
.sys-detail-name { font-size: 1.3rem; }
.sys-detail-stats { grid-template-columns: 1fr 1fr; }
.sys-detail-stat-v { font-size: 1.25rem; }
.sys-detail-actions { flex-direction: column; align-items: flex-end; gap: 8px; }
}