From 08c71ba68dbe0aca09126dd6d94b96d19666b8a3 Mon Sep 17 00:00:00 2001 From: Barely Removable Date: Thu, 27 Aug 2026 06:52:34 -0700 Subject: [PATCH] Give Storage its own row and rebuild the disk list as a table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ten disks stacked at --row-h each turned one third of the "Right now" strip into a ~950px tower. The row is a grid, so Now playing and Downloads stretched to match and held roughly 1500px of empty card between them. The information needed width, not a shorter list. "Right now" drops to two columns and Storage moves to a full-width row beneath it, where the same ten disks are a six-column table — disk, used, size, free, usage bar, temp — at ~270px total. Nothing is hidden and nothing scrolls except the table itself, which gets its own overflow container below 720px rather than wrapping the sizes into mush. Also: the array total moves to a bar above the table and the summary into the card header, so the hero figure isn't repeating what every row already says. The header summary is a sentence rather than a badge, so it needs .card-meta-wrap to opt out of the global nowrap — without it that one string pushed the whole page sideways on a phone. Local-fallback (no per-disk data) and error states keep the previous hero-figure treatment. CLAUDE.md now carries the dead-space rule this came from. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 22 ++++++ static/css/dashboard.css | 84 ++++++++++++++++------- templates/dashboard.html | 144 +++++++++++++++++++++++++-------------- 3 files changed, 176 insertions(+), 74 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c6dbb59..d3e8212 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,6 +67,28 @@ the user's Unraid home server. Server-side project directory is still named tiers in `app.py`: a "Right now" strip (live streams, download queue, array usage) sits above the fold, with library, discovery, requests and watch insights below it. + - **Cards in a row must earn their height — never leave dead space.** A + `.row` is a CSS grid, so every card stretches to the tallest one in that + row. A card whose content outgrows its neighbours doesn't just get tall, + it hollows out everything beside it. This has bitten once already: the + per-disk Storage list stacked ten disks at `--row-h` each, turning one + third of the "Right now" strip into a ~950px tower and leaving ~1500px of + empty card across Now playing and Downloads. + - Before adding a repeating list to a card in a multi-column row, check + what the row's other cards hold at their **quietest** — one stream, an + idle queue, zero pending requests — and keep the tallest card within + roughly 1.5× of that. + - When the content genuinely won't fit, the fix is **more width**: give + the card its own full-width row (`grid-template-columns: 1fr`) and lay + its list out in columns inside, or use more columns within the existing + card. Ten items down one narrow column is three tidy rows of tiles once + the card spans the page. + - The fix is **never** an inner scroll area, a truncated list, or content + hidden behind a toggle the user has to find — see the "nothing clipped + or hidden" rule the header pills already follow. + - Where a short card still can't be avoided, centre its content + vertically rather than reaching for `align-items: start`; equal-height + cards are a settled preference. - Degraded services render through the `service_error()` macro, which pairs `friendly_error` (human cause) with the raw exception behind a disclosure, passed through `redact_secrets` — SABnzbd and Tautulli put diff --git a/static/css/dashboard.css b/static/css/dashboard.css index 06fd4da..94925e4 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -539,32 +539,68 @@ dd.is-accent { color: var(--accent); } } .req-sub { font-size: var(--fs-xs); color: var(--text-3); } -/* ── Storage — per-disk breakdown ──────────────────────── */ -/* Same row anatomy as indexer/request rows: name/sub on the left, a status - badge on the right. Data + cache disks get a usage meter; parity disks - (no filesystem) are rendered compact — name and temp only. */ -.disk-list { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.85rem; } -.disk-item { - display: flex; - align-items: center; - gap: 0.7rem; - min-height: var(--row-h); - padding: 0.6rem 0.7rem; - background: var(--surface-2); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - min-width: 0; +/* ── Storage — per-disk table ──────────────────────────── */ +/* Storage owns a full-width row of its own, so the per-disk breakdown is a + real table rather than a stack of cards: columns line up, digits are + tabular, and a new metric is a new column instead of a redesign. Stacking + these as rows in a third-of-a-page column is what hollowed out the two + cards beside it — see the dead-space rule in CLAUDE.md. */ +/* Storage's header meta is a sentence, not a badge. .card-meta is nowrap so + short status words never break; this one has to wrap or it pushes the whole + page sideways on a phone. */ +.card-meta.card-meta-wrap { white-space: normal; text-align: right; } +.array-meter.meter { margin-top: 0; margin-bottom: 1.1rem; } +.table-scroll { overflow-x: auto; } +.disk-table { + width: 100%; + border-collapse: collapse; + font-variant-numeric: tabular-nums; } -.disk-info { min-width: 0; flex: 1; } -.disk-name { - font-size: var(--fs-sm); font-weight: 600; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +.disk-table th, .disk-table td { padding: 0.45rem 0.6rem; text-align: left; } +.disk-table th:first-child, .disk-table td:first-child { padding-left: 0; } +.disk-table th:last-child, .disk-table td:last-child { padding-right: 0; } +.disk-table thead th { + font-size: var(--fs-xs); + font-weight: 500; + color: var(--text-3); + white-space: nowrap; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); +} +.disk-table tbody th { + font-size: var(--fs-sm); + font-weight: 600; + color: var(--text); + white-space: nowrap; +} +.disk-table tbody td { + font-size: var(--fs-sm); + color: var(--text-2); + border-bottom: 1px solid var(--border); +} +.disk-table tbody th { border-bottom: 1px solid var(--border); } +.disk-table tbody tr:last-child th, +.disk-table tbody tr:last-child td { border-bottom: none; } +.disk-table .num { text-align: right; white-space: nowrap; } +/* The bar and its percentage share one cell, so the bar flexes with the + table while the number stays a fixed, right-aligned column of digits. */ +.disk-table .usage { width: 32%; min-width: 140px; } +.usage-cell { display: flex; align-items: center; gap: 0.6rem; } +.usage-cell .meter { margin-top: 0; flex: 1; height: 5px; } +.usage-pct { + font-size: var(--fs-xs); + color: var(--text-3); + width: 2.6em; + text-align: right; + flex-shrink: 0; +} +.disk-null { color: var(--text-3); } +.disk-table tr.is-parity th { color: var(--text-2); } +@media (max-width: 720px) { + /* Below this the six columns stop fitting; the table scrolls in its own + container rather than squeezing the sizes into unreadable wraps. */ + .table-scroll .disk-table { min-width: 540px; } } -.disk-sub { font-size: var(--fs-xs); color: var(--text-3); } -.disk-meter.meter { margin-top: 0.4rem; height: 4px; } -.disk-list-compact { margin-top: 0.5rem; } -.disk-item-compact { justify-content: space-between; padding: 0.5rem 0.7rem; min-height: auto; } -.disk-item-compact .disk-name { flex: 1; } .badge.temp-ok { background: var(--ok-dim); color: var(--ok); } .badge.temp-warn { background: var(--warn-dim); color: var(--warn); } .badge.temp-danger { background: var(--danger-dim); color: var(--danger); } diff --git a/templates/dashboard.html b/templates/dashboard.html index 4e952be..c32dd32 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -25,6 +25,19 @@ {% endmacro %} +{#- Disk temperature / spin state, as one badge. A spun-down disk reports no + usable temperature, so Standby replaces the reading rather than showing + a stale or zero degree value. -#} +{% macro disk_state(dk) %} + {%- if dk.spinning == false -%} + Standby + {%- elif dk.temp is not none -%} + {{ dk.temp }}°C + {%- else -%} + — + {%- endif -%} +{% endmacro %} + {#- One service status pill in the header strip. -#} {% macro svc_pill(key, label, url, up, stat) %}

Right now

-
+ {# Two cards here, not three. Storage carries a per-disk table that no + third-of-a-page column can hold without towering over these two and + hollowing them out — it gets its own full-width row below. #} +
@@ -141,18 +157,89 @@
- +
+ + {# Storage spans the page so its per-disk table has room to be a table. + Ten disks down a narrow column is a tower; across the full width it + is ten short rows. #} +

Storage

- {% if unraid.status == 'ok' and unraid.array_state %} - {{ unraid.array_state | capitalize }} + {% if unraid.status == 'ok' %} + + {{ unraid.array_used_pct }}% used · {{ unraid.array_free }} free of {{ unraid.array_total }} + {%- if unraid.disk_count %} · {{ unraid.disk_count }} disk{{ 's' if unraid.disk_count != 1 }}{% endif %} + {%- if unraid.array_state %} · {{ unraid.array_state | capitalize }}{% endif %} + {% endif %}
{% if unraid.status == 'ok' %} + + {% if unraid.disks or unraid.caches or unraid.parities %} + {# Array total leads, then the disks that make it up. #} {% set pct = unraid.array_used_pct %} -
+
+
+
+ + + + + + + + + + + + + {% for dk in unraid.disks + unraid.caches %} + {% set dpct = dk.used_pct %} + + + + + + + + + {% endfor %} + {% for dk in unraid.parities %} + {# Parity mirrors the array rather than holding a filesystem, + so it has no size or usage of its own to report. #} + + + + + + + + + {% endfor %} + +
DiskUsedSizeFreeUsageTemp
{{ dk.name }}{{ dk.used }}{{ dk.total }}{{ dk.free }} +
+
+
+
+ {{ dpct }}% +
+
{{ disk_state(dk) }}
{{ dk.name }}———Parity · no filesystem{{ disk_state(dk) }}
+
+ + {% else %} + {# Fallback: container-visible disk usage, no per-disk detail. #} + {% set pct = unraid.array_used_pct %} +
{{ pct }}%
@@ -163,52 +250,10 @@ role="progressbar" aria-valuenow="{{ pct }}" aria-valuemin="0" aria-valuemax="100" aria-label="Array usage">
-
- {{ unraid.array_used }} used - {% if unraid.disk_count %}{{ unraid.disk_count }} disk{{ 's' if unraid.disk_count != 1 }}{% endif %} -
+
{{ unraid.array_used }} used
{% if unraid.source == "local" %} -

Container disk usage ({{ unraid.mount }}). Set UNRAID_API_KEY for full array stats.

+

Container disk usage ({{ unraid.mount }}). Set UNRAID_API_KEY for full array stats.

{% endif %} - - {% if unraid.disks or unraid.caches %} -
- {% for dk in unraid.disks + unraid.caches %} - {% set dpct = dk.used_pct %} -
-
-
{{ dk.name }}
-
{{ dk.used }} of {{ dk.total }}
-
-
-
-
- {% if dk.spinning == false %} - Standby - {% elif dk.temp is not none %} - {{ dk.temp }}°C - {% endif %} -
- {% endfor %} -
- {% endif %} - - {% if unraid.parities %} -
- {% for dk in unraid.parities %} -
-
{{ dk.name }}
- {% if dk.spinning == false %} - Standby - {% elif dk.temp is not none %} - {{ dk.temp }}°C - {% endif %} -
- {% endfor %} -
{% endif %} {% else %} @@ -216,7 +261,6 @@ {% endif %}
-