diff --git a/CLAUDE.md b/CLAUDE.md index d3e8212..25ce703 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,17 +42,6 @@ the user's Unraid home server. Server-side project directory is still named `SCRIPT_NAME` subpath. Server-rendered config (timezone, script root) reaches JS through `
`, never string-interpolated into a script block. - - **No `style="..."` attributes for anything static.** A one-off tweak - (button height, a margin, a font size) belongs in the matching - `static/css/*.css` file, scoped with a structural selector (an - adjacent-sibling or `:last-child` rule tied to the surrounding markup) - or a small modifier class — not inline on the element. The only - legitimate inline `style=` left in `dashboard.html` are per-request - computed values Jinja has to interpolate (a meter's `width: {{ pct - }}%`, a colour chosen by threshold) and JS-toggled initial visibility - (`display:none` on something `dashboard.js` reveals) — those can't - live in a static stylesheet. Everything else should read as a class - name, not a value. - **`static/css/base.css`** — the single source of truth for the design tokens, shared by all three templates: surfaces, a 6-step `--fs-*` type scale, semantic colours, and `--tap: 44px` for minimum touch targets. @@ -67,28 +56,6 @@ 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 94925e4..88d82b6 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -131,9 +131,6 @@ main { } .row.cols-3 { grid-template-columns: repeat(3, 1fr); } .row.cols-2 { grid-template-columns: repeat(2, 1fr); } -/* A section with two stacked rows (Watch insights) needs the same gap - between them as the grid uses between columns. */ -.row + .row { margin-top: var(--gap); } /* ── Card ───────────────────────────────────────────── */ .card { @@ -196,13 +193,13 @@ main { border-top: 1px solid var(--border); } .stream:first-child { border-top: none; padding-top: 0; } -.stream-icon { font-size: var(--fs-lg); flex-shrink: 0; } +.stream-icon { font-size: var(--fs-base); flex-shrink: 0; } .stream-info { min-width: 0; flex: 1; } .stream-title { - font-size: var(--fs-base); font-weight: 600; + font-size: var(--fs-sm); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.stream-user { font-size: var(--fs-sm); color: var(--text-3); } +.stream-user { font-size: var(--fs-xs); color: var(--text-3); } .live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); flex-shrink: 0; @@ -245,10 +242,6 @@ dl.stats { margin: 0; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); } -/* Only the Downloads card stacks stats below the hero figure/sub block — - give that pairing breathing room without affecting stats blocks that - open a card (Sonarr/Radarr put dl.stats right after the search bar). */ -.card-body > [aria-live="polite"] + dl.stats { margin-top: 0.9rem; } dl.stats > div { background: var(--surface-2); border: 1px solid var(--border); @@ -539,68 +532,32 @@ dd.is-accent { color: var(--accent); } } .req-sub { font-size: var(--fs-xs); color: var(--text-3); } -/* ── 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; +/* ── 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; } -.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-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-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); } @@ -638,8 +595,6 @@ dd.is-accent { color: var(--accent); } } .empty { color: var(--text-3); font-size: var(--fs-sm); } -/* Storage card's usage note trails the meter with no room to spare below it. */ -.meter-legend + .empty { margin-bottom: 0; } /* ── Errors ─────────────────────────────────────────── */ .error { diff --git a/templates/dashboard.html b/templates/dashboard.html index c32dd32..d509b23 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -25,19 +25,6 @@ {% 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) %}| Disk | -Used | -Size | -Free | -Usage | -Temp | -
|---|---|---|---|---|---|
| {{ dk.name }} | -{{ dk.used }} | -{{ dk.total }} | -{{ dk.free }} | -
-
-
-
-
-
- {{ dpct }}%
- |
- {{ disk_state(dk) }} | -
| {{ dk.name }} | -— | -— | -— | -Parity · no filesystem | -{{ disk_state(dk) }} | -
Container disk usage ({{ unraid.mount }}). Set UNRAID_API_KEY for full array stats.