webui: overhaul dashboard cards

Split the old monolithic Hardware card into dedicated Board, WiFi, and
Sensors cards, matching the structure of `show system` / `show hardware`
in the CLI:

- System Information: add Current Time row, rename Firmware → Boot Partition
- Board: model, manufacturer, Base MAC only (no sensors)
- WiFi (new, conditional): all radios with supported bands, standards
  (11n/11ac/11ax), and max AP count; card spans 2 grid columns so the
  table never needs horizontal scroll
- Sensors (new, conditional, collapsible): all hardware sensors grouped
  by parent component (SFP sub-sensors indent under sfp1/sfp2 headings);
  JS "Show more" button injected only when content overflows 300 px
- Disk Usage: replaced wide multi-column table with a per-mount widget
  (path + % on one line, health bar, "X free of Y" below); health colour
  follows the same warn/crit thresholds as the memory bar
- Fix humanKiB to output IEC units (MiB / GiB) with a space, matching
  the memory display style
- Fix info-table th wrapping (width:1%; white-space:nowrap)
- Fix JS collapsible init to run on DOMContentLoaded, not immediately in
  <head>, so scrollHeight check has a real DOM to inspect
- CSS scroll-shadow on data-table-wrap: pure-CSS right-edge shadow
  indicates overflow without needing JS or a scroll listener
- Equal-height grid rows via align-items:stretch; info-grid-span-2 for
  cards with wide tables

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-06-14 22:07:27 +02:00
parent 12482a5ab5
commit b2bf034a91
4 changed files with 285 additions and 90 deletions
+89 -4
View File
@@ -405,9 +405,21 @@ details[open].nav-group-top > summary.nav-group-summary-top::after {
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
margin-bottom: 1.5rem;
align-items: stretch;
}
/* Cards inside a grid don't need their own bottom margin — the grid gap handles spacing */
.info-grid > .info-card { margin-bottom: 0; }
/* Full-width card spanning all grid columns */
.info-grid-full { grid-column: 1 / -1; }
/* Two-column card for wide tables — only kicks in once the grid has ≥2 columns */
@media (min-width: 680px) {
.info-grid-span-2 { grid-column: span 2; }
}
.info-card {
@@ -449,6 +461,7 @@ details[open].nav-group-top > summary.nav-group-summary-top::after {
padding: 1.25rem;
}
/* ==========================================================================
Tables
========================================================================== */
@@ -466,6 +479,60 @@ details[open].nav-group-top > summary.nav-group-summary-top::after {
border-bottom: 1px solid var(--border);
}
.info-table th {
width: 1%;
white-space: nowrap;
color: var(--fg-muted);
font-weight: 500;
text-align: left;
}
/* Disk usage widget (one entry per mount point) */
.disk-item {
padding: 0.7rem 1.25rem;
border-bottom: 1px solid var(--border);
}
.disk-item:last-child { border-bottom: none; }
.disk-item-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.4rem;
}
.disk-mount {
font-family: var(--font-mono);
font-size: 0.85rem;
font-weight: 500;
color: var(--fg);
}
.disk-pct { font-size: 0.8rem; color: var(--fg-muted); }
.disk-stats {
font-size: 0.78rem;
color: var(--fg-muted);
margin-top: 0.3rem;
font-variant-numeric: tabular-nums;
}
.sensor-group-hdr th {
padding: 0.35rem 1.25rem 0.2rem;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--fg-muted);
background: var(--bg);
border-bottom: none;
}
.sensor-child th {
padding-left: 2rem;
}
.data-table th.num,
.data-table td.num {
text-align: right;
font-variant-numeric: tabular-nums;
}
.info-table tr:last-child th, .info-table tr:last-child td,
.disk-table tbody tr:last-child td,
.counters-table tbody tr:last-child td {
@@ -504,6 +571,22 @@ details[open].nav-group-top > summary.nav-group-summary-top::after {
overflow-x: auto;
border: 1px solid var(--border);
border-radius: var(--radius);
/* Scroll-shadow: a right-edge shadow appears automatically when the table
overflows, fading away once scrolled to the end. Pure CSS, no JS needed.
The `local` gradients act as white masks pinned to the scroll content;
the `scroll` gradients are the actual shadows fixed to the viewport edge. */
background:
linear-gradient(to right, var(--surface), var(--surface)) left / 24px 100% no-repeat local,
linear-gradient(to left, var(--surface), var(--surface)) right / 24px 100% no-repeat local,
linear-gradient(to right, rgba(0,0,0,0.07), transparent) left / 8px 100% no-repeat scroll,
linear-gradient(to left, rgba(0,0,0,0.07), transparent) right / 8px 100% no-repeat scroll;
background-color: var(--surface);
}
/* Remove the nested border when a table lives inside a card — the card already provides the frame */
.info-card .data-table-wrap {
border: none;
border-radius: 0;
}
.data-table {
@@ -531,9 +614,11 @@ details[open].nav-group-top > summary.nav-group-summary-top::after {
background-color: var(--zebra-stripe, rgba(0,0,0,0.025));
}
.data-table tbody tr:hover {
background-color: rgba(0,0,0,0.05);
.data-table tbody tr:hover { background-color: var(--slate-100); }
@media (prefers-color-scheme: dark) {
.data-table tbody tr:hover { background-color: rgba(255,255,255,0.06); }
}
.dark .data-table tbody tr:hover { background-color: rgba(255,255,255,0.06); }
/* ==========================================================================
Forms & Inputs
-25
View File
@@ -432,31 +432,6 @@
});
})();
// Card collapsible — "Show more / Show less" toggle injected when content overflows
(function() {
function initCollapsibles() {
document.querySelectorAll('.card-collapsible:not([data-ci])').forEach(function(card) {
card.setAttribute('data-ci', '1');
var body = card.querySelector('.card-collapsible-body');
if (!body || body.scrollHeight <= body.clientHeight + 4) return;
var btn = document.createElement('button');
btn.className = 'card-expand-btn';
btn.type = 'button';
btn.textContent = 'Show more \u25be';
btn.style.display = 'block';
card.appendChild(btn);
btn.addEventListener('click', function() {
var expanded = card.classList.toggle('is-expanded');
btn.textContent = expanded ? 'Show less \u25b4' : 'Show more \u25be';
});
});
}
document.addEventListener('DOMContentLoaded', initCollapsibles);
document.addEventListener('htmx:afterSettle', initCollapsibles);
})();
// Sidebar toggle (mobile)
(function() {
var MOBILE_BP = 1024;