Fix design-review findings: self-linking nav, colour reuse, badge clash
All checks were successful
Deploy / deploy (push) Successful in 24s

From a frontend-design skill review of all pages, after last session's
chrome changes (top nav, all-tabs nav, header download row):

- The top/closing fastext nav now mutes the entry matching the current
  page (.fastext-here) instead of rendering a link to the page you're
  already on.
- Colour in fastext is reserved for the four entries that still carry
  a real keyboard shortcut (r/g/y/c); the other four render in plain
  ink instead of reusing k-red/k-green/k-yellow/k-cyan for a
  destination they don't semantically mean (e.g. Requests no longer
  shows in the app's own off-air red).
- Discover's badge slot meant "critic score" on its RT tiles and
  "release year" on its new search tiles at the same time. Search
  results (Library and Discover) now fold year/held-state into the
  sub-line instead, leaving the badge exclusively for critic score.
- System's Transmission rows now carry a small ↗ glyph, since they
  silently changed from in-app navigation to opening each service's
  own UI in a new tab with no visual cue.
- SABnzbd polling drops from 5s to 20s on pages that only show the
  header's download figure (everywhere but System/Activity, which
  keep 5s for their own live figures) — a number idle 95% of the time
  doesn't need second-level freshness sitewide.
- DESIGN.md's fastext description, now-current per this session's nav
  changes.
This commit is contained in:
Barely Removable 2026-08-30 09:11:59 -07:00
parent d672c3f02d
commit 59beb9490f
9 changed files with 45 additions and 15 deletions

View file

@ -107,7 +107,12 @@ the user's Unraid home server. Server-side project directory is still named
with `HOME_FASTEXT`, the original four-entry row, as its own closing
line, since its body already lists every destination once. Only
those original four (Index/Library/Discover/System) carry an `r`/
`g`/`y`/`c` keyboard shortcut; the rest are click-only.
`g`/`y`/`c` keyboard shortcut and a colour swatch — colour is
semantic everywhere else in this app, so the other four entries
render in plain ink rather than reusing one of the four colours for
a destination it doesn't mean. Whichever entry matches the current
page renders as a `.fastext-here` span instead of a link — a page
linking to itself did nothing, so it isn't offered as a click.
- **Nothing is clipped, hidden, or sticky.** All seven page numbers and
every fastext entry stay visible at every width, reflowing to another
row rather than scrolling, truncating, or folding into a menu. No

View file

@ -140,7 +140,7 @@ components:
Media Centre is a broadcast information service, not a dashboard. It refuses the bordered-card grid that every self-hosted *arr dashboard ships — including this one, until this redesign. The world is teletext: Ceefax and the American cable guide channel, fused. Eight colours at full saturation on pure black, one blocky monospace at exactly two heights, a character-cell grid, and grouping carried entirely by colour fields and row weight. The visitor types or clicks a page number, reads the machine's state off colour alone, and leaves knowing whether anything needs them.
The density is high and unapologetic. A teletext page was 40 columns; this one gets 92 (`--page-cols: 92ch`) because the screen allows it, but the character grid is the substrate everywhere — padding is measured in `ch`, bars are measured in cells, and nothing floats free of the row rhythm (`--row`, one character row). There is no chrome. The header is a row of the page, not furniture bolted above it, so it scrolls away with everything else. The fastext colour keys sit at the bottom of the page where they always sat, not fixed to the viewport.
The density is high and unapologetic. A teletext page was 40 columns; this one gets 92 (`--page-cols: 92ch`) because the screen allows it, but the character grid is the substrate everywhere — padding is measured in `ch`, bars are measured in cells, and nothing floats free of the row rhythm (`--row`, one character row). There is no chrome. The header is a row of the page, not furniture bolted above it, so it scrolls away with everything else. The fastext keys sit at the top of every page but the index — which closes with them instead, since its own body already is the full nav — never fixed to the viewport.
The confirmed anti-reference is the world this replaced and every dashboard that shares its reflexes: the rounded, bordered, drop-shadowed card grid with a neutral dark-grey palette and a soft accent. Nothing here is enclosed. There is not one `border-radius` and not one `box-shadow` in the codebase — the single `text-shadow` is a phosphor bloom, and it is spent deliberately. Emphasis is bought with colour and with doubled height, never with a weight ramp, never with a third type size, never with a container.

15
app.py
View file

@ -1304,17 +1304,20 @@ def service_urls():
# Every destination, for the top of every page but the index. Order
# matches nav_items; the four letter keys keep the destinations they
# always had, cycling colour for the rest since only four exist.
# matches nav_items. Colour is semantic everywhere else in this app
# (red = off-air, magenta = needs a decision, ...), so only the four
# that still carry a real teletext keyboard shortcut (r/g/y/c) get a
# colour swatch; the rest render in plain ink rather than borrowing
# one of the four colours for a destination it doesn't mean.
FULL_FASTEXT = [
("k-red", "r", "100", "Index", "/"),
("k-cyan", "", "101", "Watch", "/watch"),
("", "", "101", "Watch", "/watch"),
("k-green", "g", "102", "Library", "/library"),
("k-yellow", "y", "103", "Discover", "/discover"),
("k-red", "", "104", "Requests", "/requests"),
("", "", "104", "Requests", "/requests"),
("k-cyan", "c", "105", "System", "/system"),
("k-green", "", "106", "Activity", "/activity"),
("k-yellow", "", "900", "Settings", "/settings"),
("", "", "106", "Activity", "/activity"),
("", "", "900", "Settings", "/settings"),
]
# The index's own closing row: just the four letter keys, since the

View file

@ -167,6 +167,10 @@ a.tt-row:focus-visible .tt-row-sub { color: var(--tt-black); }
text-transform: uppercase;
min-width: 11ch;
}
/* Marks a row that leaves the app (System's Transmission links to each
service's own UI) — everywhere else a .tt-row stays inside Media
Centre, so leaving needs its own quiet signal. */
.tt-row-ext { color: var(--ink-dim); }
.tt-row-body { flex: 1; min-width: 0; }
.tt-row-title {
overflow-wrap: anywhere;
@ -494,7 +498,7 @@ a.tt-row:focus-visible .tt-row-sub { color: var(--tt-black); }
.fastext-top {
margin: var(--row) auto calc(var(--row) * 1.5);
}
.fastext a {
.fastext a, .fastext-here {
display: flex;
align-items: center;
min-width: 0;
@ -510,18 +514,25 @@ a.tt-row:focus-visible .tt-row-sub { color: var(--tt-black); }
}
/* The key's colour is carried by a solid cell at its head — the
physical coloured button on the remote. */
.fastext a > span {
.fastext a > span, .fastext-here > span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.fastext a::before {
.fastext a::before, .fastext-here::before {
content: "";
flex: 0 0 auto;
width: 1.2ch;
height: 1em;
background: currentColor;
}
/* The page you're already on: present for orientation, same shape as
its neighbours, but not a link — clicking your own destination did
nothing useful, so now it isn't offered as a click at all. */
.fastext-here {
color: var(--ink-dim);
cursor: default;
}
.fastext .k-red { color: var(--alert); }
.fastext .k-green { color: var(--ok); }
.fastext .k-yellow { color: var(--voice); }

View file

@ -199,8 +199,11 @@
.catch(function () { /* as above */ });
}
/* Second-accurate only where the page shows the full figures
(System, Activity); everywhere else it's a header glance at a
number that's idle most of the time, so it can poll slower. */
pollSab();
setInterval(pollSab, 5000);
setInterval(pollSab, document.getElementById('sab-queue') ? 5000 : 20000);
if (document.getElementById('tt-streams')) {
pollTautulli();
setInterval(pollTautulli, 15000);

View file

@ -58,9 +58,13 @@ provenance.
{% set fastext_nav %}
<nav class="fastext{{ '' if page == 'home' else ' fastext-top' }}" aria-label="Fastext">
{% for cls, key, num, label, href in fastext %}
{% if num == here[1] %}
<span class="fastext-here" aria-current="page"><span>{{ num }} {{ label }}</span></span>
{% else %}
<a class="{{ cls }}" href="{{ request.script_root }}{{ href }}" data-key="{{ key }}">
<span>{{ num }} {{ label }}</span>
</a>
{% endif %}
{% endfor %}
</nav>
{% endset %}

View file

@ -27,8 +27,12 @@
{{ service_error('Sonarr/Radarr', search.error) }}
{% elif search.results %}
<div class="tt-tiles">
{#- No badge here: the badge slot means "critic score" below,
from the RT feed. A search result has no score, so its
year and held-state live in the sub-line instead of
overloading that slot with a second meaning. -#}
{% for it in search.results %}
{{ tile(it.title, it.kind ~ (' · Held' if it.held else ''), it.poster, it.year, ('' if it.held else '+ Add'), it.target) }}
{{ tile(it.title, it.kind ~ (' · ' ~ it.year if it.year else '') ~ (' · Held' if it.held else ''), it.poster, '', ('' if it.held else '+ Add'), it.target) }}
{% endfor %}
</div>
{% else %}

View file

@ -22,7 +22,7 @@
{% if search_results %}
<div class="tt-tiles">
{% for it in search_results %}
{{ tile(it.title, it.kind, it.poster, it.year) }}
{{ tile(it.title, it.kind ~ (' · ' ~ it.year if it.year else ''), it.poster) }}
{% endfor %}
</div>
{% else %}

View file

@ -14,7 +14,7 @@
{% for name, key, svc in [('Sonarr', 'sonarr', sonarr), ('Radarr', 'radarr', radarr), ('SABnzbd', 'sabnzbd', sabnzbd),
('Tautulli', 'tautulli', tautulli), ('Prowlarr', 'prowlarr', prowlarr), ('Ombi', 'ombi', ombi)] %}
<a class="tt-row" href="{{ service_urls[key] }}" target="_blank" rel="noopener">
<span class="tt-row-name">{{ name }}</span>
<span class="tt-row-name">{{ name }} <span class="tt-row-ext" aria-hidden="true">&#8599;</span></span>
<span class="tt-row-body"></span>
<span class="tt-row-val {{ 'ok' if svc.status == 'ok' else 'alert' }}">
{{- 'ON AIR' if svc.status == 'ok' else 'OFF AIR' -}}