9 Commits

Author SHA1 Message Date
librelad
17fe4d6ed5 ux(system): drop the Volumes category from the Storage view
LibrePortal apps keep data in bind mounts, so Docker named-volume
accounting is always ~empty and just reads as a confusing "0 B". Now that
per-app on-disk usage covers the real "what's filling my disk" question,
remove volumes end to end: the donut slice, category card, "Largest
volumes" table and the System-page count, plus the backend's volume
summation and top_volumes payload. Reclaim copy no longer references
volumes (it reassures about app data instead).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 20:26:13 +01:00
librelad
8e6691b7d3 feat(system): surface the Docker storage breakdown on the System page
Promote a compact Storage summary (breakdown donut + per-category legend
+ reclaimable) onto the System index, replacing the thin Docker strip and
its easily-missed "Open breakdown" link; it links through to the full
breakdown page. Drop the Disk usage trend chart, which duplicated the
Disk gauge's root-mount %.

Extract the donut + segment builders onto SystemStoragePage so the index
summary and the full page share one renderer. This also fixes a donut
stacking bug: the SVG used the final cumulative fraction for every
slice's dashoffset instead of each slice's own running offset, so the
ring only partially filled. It now fills proportionally.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 16:47:17 +01:00
librelad
7178dddae7 ux(admin/system): make the Load gauge capacity-aware, not alarmist
The Load ring was driven by load1_percent = min(100, load1/cores*100)
and coloured by the generic gauge thresholds (red at >=90%). On a
low-core box that pinned it red whenever load merely approached the core
count — which is normal "fully used" territory, not a problem.

Drive the ring from raw load1 with max = cores*2 (so load == cores sits
mid-gauge) and colour by load-per-core: green below capacity, orange
around capacity (>=1.0x), red only once load clearly exceeds it (>=1.7x,
tasks genuinely queuing). cpu.cores rides the live SSE payload, so the
colour is correct on live ticks too.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 16:28:42 +01:00
librelad
9c6cef5a05 ux(admin/system): give the host info strip a "Host" section head
The OS/Kernel/Uptime/CPU/Swap strip was the only section on the System
page rendered without a .sys-section-head, so it had no title and butted
directly against the charts above it (no top spacing). Add a "Host" head
— matching the Docker / Per-app pattern — which supplies both the label
and the section's 26px top margin. "Host" rather than "System" since the
page H1 is already "System".

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 16:16:22 +01:00
librelad
57a565aac2 refactor(system): per-app deep-dive moves to the app's Services tab
The Admin → System area was growing a parallel per-container surface
(/admin/config/system/app/<name>) alongside the existing per-app Services
tab on the app page. Two pages onto the same thing is the kind of
duplication that rots fast — they drift, users have to remember which
one to use, and the next person adding a feature has to decide twice.

This commit consolidates onto the existing Services tab (which already
has compose-service awareness, docker socket access, restart actions via
the task system, and live log streaming) and decommissions the parallel
admin sub-page:

  - Delete system-app-page.js and its lazyLoad entry. The dispatch in
    admin-system.js for the 'app' view now redirects to the app page's
    Services tab so old bookmarks still resolve cleanly.

  - System index per-app rows navigate to /app/<name>/services (not
    /admin/config/system/app/<name>) and the row hint copy is updated
    to match.

  - Services tab gains the rich container detail the old admin page
    rendered, fed by /api/system/containers + /containers/:id +
    /containers/:id/stats:

      * Inline live chips in each service header: CPU% and memory
        (with limit + percent if a limit is set). Memory chip flips
        amber at 80% and red at 95% of the configured limit.
      * New "service-rich" panel inside the existing expandable
        details section (above the log block, so the existing Logs
        toggle reveals both):
          - Image + image-id + uptime + restart count
          - Memory / CPU / PIDs limits + restart policy
          - Healthcheck pill + last 3 probes (collapsible per-probe)
          - Networks table (name, IP, gateway, MAC)
          - Mounts table with type badges (volume/bind/tmpfs)
      * Live stats refresh every 5 s; existing status refresh stays
        on 10 s. Both gated on the Services tab being active.

  - Backups for the app already live on the existing /app/<name>/backups
    tab (loadAppBackups → BackupAppCard.render), so the navigational
    promise of "one place per-app" is already met — System index just
    needed to route there.

  - CSS: services.css picks up .service-live-chip (with warn/danger
    colour cues) and the full .service-rich block (grid, tables, mount
    badges, healthcheck pills).

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 22:51:53 +01:00
librelad
dbcab8614f feat(system): route-based sub-pages — metric / per-container / storage
Promotes the admin → System area from a single index page with a transient
overlay into a real router with four addressable sub-pages, plus a docker-
api-backed read surface to drive them.

URLs:
  /admin/config/system                   index (gauges + trends + per-app table)
  /admin/config/system/metric/<key>      single-metric deep-dive
  /admin/config/system/app/<name>        per-container app deep-dive
  /admin/config/system/storage           docker disk-usage breakdown

The path resolves to category=`system` in adminCategoryFromPath, so the
existing SPA dispatch still drops you into AdminSystem; AdminSystem then
reads the rest of the path and mounts the right sub-renderer into
config-section. Each sub-page owns its own DOM + lifecycle and is disposed
when the orchestrator re-mounts on the next navigation. Browser back, page
reload, and shareable URLs all work — no modal, no overlay state, no
fragile open/close lifecycle. Esc on the metric page navigates back to the
index.

Backend (containers/libreportal/backend):
  - utils/docker.js — shared client for the bind-mounted Docker socket
    (extracted from service-routes.js' inline copy). dockerRequest,
    dockerStream, and a multiplex-log decoder for /containers/:id/logs.
  - routes/docker-info-routes.js mounted at /api/system, contributes:
      GET /containers              full list, plus grouped-by-app shape
      GET /containers/:id          inspect projection (limits, mounts,
                                   networks, ports, health, restart count)
      GET /containers/:id/stats    one-shot CPU% / memory / network /
                                   blkio / pids (derived from precpu/cpu
                                   deltas, like `docker stats`)
      GET /containers/:id/logs     last N lines, multiplex-decoded
      GET /storage                 `docker system df` rolled up per
                                   category, plus top-10 images +
                                   top-10 volumes by size

Frontend (containers/libreportal/frontend/js/components/admin):
  - admin-system.js — refactored into orchestrator + index view. _parsePath
    drives dispatch; sub-views are window.SystemMetricPage /
    SystemAppPage / SystemStoragePage classes mounted into config-section.
    The per-app table is now keyboard-focusable rows that navigate to the
    per-container page; the Docker strip grows a "Storage" tile that
    navigates to the storage page.
  - system-metric-page.js (renamed from system-detail.js, rewritten as an
    in-flow page renderer). Same chart visuals as the old overlay — grid,
    axis, area gradient, peak/min/now markers, hover crosshair + tooltip
    scrubbing, per-metric accent theming — but rendered into the page
    instead of a fixed-position panel. Range picker reflects to ?range=
    so refresh preserves the selection. 1 Hz SSE feed splices into the
    chart tail in real time.
  - system-app-page.js — for each container in the app stack: status,
    image, image-id, uptime; live stats card (cpu / mem with limit-pct /
    rx / tx / blkio r-w / pids, polled every 2s with warn+danger colour
    cues at 80% and 95% of memory limit); limits panel (memory, cpu,
    pids, restart policy, restart count, started-ago); healthcheck
    status + last 3 probes; networks table (name, IP, gateway, MAC);
    published ports; mounts table with type badges; collapsible log tail
    with refresh.
  - system-storage-page.js — donut chart (cumulative-arc, hand-rolled
    SVG) splits total in-use disk by images / volumes / containers /
    build cache; per-category cards with size + reclaimable; top-10
    images and top-10 volumes tables with "unused" / "orphan" badges.

CSS (containers/libreportal/frontend/css/admin.css):
  Overlay-specific rules (.sys-detail wrapper, backdrop, panel, close
  button, body lock) removed. Inner chart rules (stats grid, svg, grid,
  axes, peak/min/now, crosshair, tooltip, foot) retained and reused by
  the metric page. New blocks for .sys-metric-page, .sys-app-page (with
  stat warn/danger colour states, health pills, mount-type badges, log
  pre styling), .sys-storage-page (donut + legend + headline + per-
  category cards + orphan/unused badges), .sys-app-row (clickable
  rows with arrow + accent hover), .sys-stat-link (clickable Docker
  strip tile).

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 21:53:13 +01:00
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
librelad
9f7ad8f177 feat(system): live 1 Hz SSE stream behind admin gauges + dashboard tile
Adds /api/system/stream — a Server-Sent Events feed driven by a single
per-process ticker that reads /proc directly and splices in the latest
host-side metrics.json each second. Subscribers share the connection so
N open tabs cost one ticker, and the ticker pauses entirely when nobody
is listening.

Frontend gets a singleton LiveSystem EventSource manager with auto-
reconnect, Page-Visibility integration (closes on tab hide), and last-
sample replay for late subscribers. Admin -> System gauges and the
dashboard memory + disk tile now tick at 1 Hz; trend charts and the
per-app table keep their 30 s poll because the underlying files only
regenerate once a minute.

Also adds /api/system/history as a thin range-query wrapper over the
existing 24 h JSON ring buffer — the binary ring backend will slot in
behind it in the next phase without changing the response shape.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 20:17:58 +01:00
librelad
62f7a84126 feat(webui): Admin System page with gauges, trend charts & per-app stats
New 'System' admin page (sidebar Tools group) rendering the metrics the
collector now produces:
- live ring gauges for CPU, memory, disk and load
- SVG trend charts (CPU/mem/disk/network) with 1h/6h/24h range toggle
- host info + swap + docker summary strips
- per-app table: CPU/mem bars, network, status, CPU sparkline

Charts are hand-rolled SVG in charts.js (LPCharts) — no third-party libs or
CDN calls — themed entirely from the active theme's CSS variables. The
Overview System card now links here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 16:47:20 +01:00