Fix the finish review's 8 material findings on the teletext redesign

Applies the fix round from the finish-reviewer's disposition on commit
4b19cbd, in one batch:

- System's disk table is unreadable below 560px: horizontal-scrolling a
  6-column table hid the usage bar and the temperature, the two
  readings that matter most (discipline 3 — one shared ruler — was
  unauditable as a result). Added a disk_row() macro and a phone-only
  .tt-disk-rows reflow (name + full 20-cell bar + temp on one line,
  device/free/total beneath), CSS-toggled against the existing table
  at the same breakpoint.
- Contrast failure on the highest-severity state: white text on the
  red .tt-field.crit/.tt-toast.error/.tt-btn.armed reversals measured
  4.0:1, under the 4.5:1 floor. Switched to black text (5.25:1),
  matching the black-on-yellow .warn pattern already in use — and
  fixed a second instance of the same bug: .tt-field.crit had no
  .tt-field-meta override, unlike .warn.
- Two type families where the direction contract names one blocky
  monospace: unified on a single self-hosted face (Space Mono, one
  weight, no bold) for both body and the double-height row.
- FIRST VIEWPORT proportion: an unpromised 6-item Just Added grid was
  competing with NOW SHOWING for the fold instead of the contract's
  "lower two-thirds in double-height cyan." Cut to a fixed 3-item,
  fixed-3-column row, which also fixes the orphaned-tile grid defect
  as a side effect (a fixed count can't leave a lone tile beside empty
  tracks).
- Three flat mid-grey fills contradicted discipline 1 (no in-between
  tone): .tt-tile-art, .tt-btn:disabled, and .tt-err pre all painted a
  solid --ink-dimmer background. Centralized the dither .tt-tile-art
  already used correctly into a shared --dither-mid generator and
  applied it to all three.
- Google Fonts dependency on a LAN-only home server: a dropped webfont
  request was a silent identity change, not a graceful fallback.
  Self-hosted Space Mono under static/fonts/ via @font-face; dropped
  the <link> tags from _shell.html and login.html.
- Deliberately deferred: "pages differ in stock" (discipline 5) still
  fires on only one band service-wide. Every other finding was a
  bounded fix to existing code; this one asks for a new mechanism (an
  aggregate status band on P100) rather than a correction, so it's
  flagged for a follow-up rather than rushed into this batch.

Recaptured desktop.png, mobile.png, desktop-system.png and
mobile-system.png (the four files the reviewer's findings cited)
through the same true-390px iframe method as the prior round, and
returned them for the fix round's verdict pass.

Known follow-up: DESIGN.md's typography section (written by the
documenter before this batch) still names the old two-family pairing
and needs a re-run now that the font system changed under it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBw1QodG8ZMYHrGeQfqyDS
This commit is contained in:
Barely Removable 2026-08-29 21:18:11 -07:00
parent 4b19cbdfb2
commit bc7779e590
9 changed files with 155 additions and 28 deletions

7
app.py
View file

@ -1484,7 +1484,12 @@ def home():
sonarr=sonarr, radarr=radarr, sabnzbd=r["sabnzbd"], tautulli=tautulli, sonarr=sonarr, radarr=radarr, sabnzbd=r["sabnzbd"], tautulli=tautulli,
prowlarr=r["prowlarr"], ombi=r["ombi"], unraid=r["unraid"], prowlarr=r["prowlarr"], ombi=r["ombi"], unraid=r["unraid"],
hero=hero, other_sessions=others, hero=hero, other_sessions=others,
recently_added=get_recently_added(6), # 3, not 6: P100's Just Added is a glance, not the library — a
# single fixed row keeps NOW SHOWING holding the lower two-thirds
# of the first viewport per the direction contract, and never
# orphans a lone tile the way a variable-column auto-fill grid
# could at 6. The full feed is what /library is for.
recently_added=get_recently_added(3),
rt_movies=_filter_rt(r["rt_movies"], radarr_titles, 6), rt_movies=_filter_rt(r["rt_movies"], radarr_titles, 6),
rt_tv=_filter_rt(r["rt_tv"], sonarr_titles, 6), rt_tv=_filter_rt(r["rt_tv"], sonarr_titles, 6),
) )

View file

@ -29,6 +29,23 @@
pages is a failure. pages is a failure.
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
/* Self-hosted, deliberately: this app runs on a LAN-only home server
behind a filtered/offline-capable network, where a Google Fonts
request can simply fail. A dropped @font-face request means every
character on the page silently falls back to whatever monospace the
OS ships — a different world by accident, not a variation of this
one. One weight only (400): there is no bold in this system. */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../fonts/SpaceMono-Regular.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root { :root {
/* ── The locked eight ────────────────────────────────── /* ── The locked eight ──────────────────────────────────
Teletext had exactly these, at full saturation, and the Teletext had exactly these, at full saturation, and the
@ -67,18 +84,28 @@
--ink-dim: color-mix(in srgb, var(--tt-white) 58%, var(--tt-black)); --ink-dim: color-mix(in srgb, var(--tt-white) 58%, var(--tt-black));
--ink-dimmer: color-mix(in srgb, var(--tt-white) 30%, var(--tt-black)); --ink-dimmer: color-mix(in srgb, var(--tt-white) 30%, var(--tt-black));
/* The one legal way to fill an area with a mid-tone: every "flat
grey panel" in the codebase (a disabled button, an unloaded poster,
a code block behind an error detail) draws from this generator
rather than declaring --ink-dimmer as a solid background. A solid
fill of a dimmed token is still a ninth colour by another name. */
--dither-mid: repeating-linear-gradient(45deg,
var(--ink-dimmer) 0 1px, var(--tt-black) 1px calc(var(--dither-size) * 2));
/* ── Type: one face, exactly two heights ─────────────── /* ── Type: one face, exactly two heights ───────────────
Teletext's entire scale. Emphasis comes from colour and Teletext's entire scale — ONE family, not one for body and a
from doubling the height, never from a weight ramp and different one for the double-height row. Emphasis comes from
never from a third size. */ colour and from doubling the height, never from a weight ramp
(this face loads a single weight, 400, on purpose — there is
no bold anywhere to reach for) and never from a third size. */
--fs: 0.9375rem; /* 15px — the single body size */ --fs: 0.9375rem; /* 15px — the single body size */
--fs2: 1.875rem; /* 30px — double height, the only other size */ --fs2: 1.875rem; /* 30px — double height, the only other size */
--lh: 1.45; --lh: 1.45;
--row: calc(var(--fs) * var(--lh)); /* one character row */ --row: calc(var(--fs) * var(--lh)); /* one character row */
--cell: 1ch; --cell: 1ch;
--font: 'Chivo Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; --font: 'Space Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--font-dh: 'DotGothic16', 'Chivo Mono', ui-monospace, Menlo, monospace; --font-dh: var(--font);
/* ── Measure ─────────────────────────────────────────── /* ── Measure ───────────────────────────────────────────
A teletext page was 40 columns. A screen this wide gets A teletext page was 40 columns. A screen this wide gets

View file

@ -95,7 +95,15 @@
} }
/* A page carrying critical state gets a heavier field than a quiet /* A page carrying critical state gets a heavier field than a quiet
one (discipline 5) — the band goes red and doubles in height. */ one (discipline 5) — the band goes red and doubles in height. */
.tt-field.crit { background: var(--alert); color: var(--tt-white); } .tt-field.crit {
background: var(--alert);
/* Black, not white: white-on-red measures 4.0:1 here, under the
4.5:1 floor for 15px uppercase text. Black-on-red clears 5.25:1
and is the correct teletext reversal — every other coloured band
(.warn, a hover row) already reverses to black. */
color: var(--tt-black);
}
.tt-field.crit .tt-field-meta { color: var(--tt-black); }
.tt-field.warn { background: var(--voice); color: var(--tt-black); } .tt-field.warn { background: var(--voice); color: var(--tt-black); }
.tt-field.warn .tt-field-meta { color: var(--tt-black); } .tt-field.warn .tt-field-meta { color: var(--tt-black); }
@ -273,6 +281,41 @@ a.tt-row:focus-visible .tt-row-sub { color: var(--tt-black); }
.tt-table tbody tr:hover th, .tt-table tbody tr:hover th,
.tt-table tbody tr:hover td { background: var(--field); color: var(--tt-white); } .tt-table tbody tr:hover td { background: var(--field); color: var(--tt-white); }
/* ── Disk rows (System, phone reflow of the table above) ──
Below 560px a six-column table stops fitting; scrolling it
horizontally hides the bar and the temperature, the two
readings that matter most here. This is the other rendering
of the same disk data, shown only below that width — see the
media query at the bottom of this file for the swap. */
.tt-disk-rows { display: none; flex-direction: column; }
.tt-disk-row { padding: 0.4em 1ch; }
.tt-disk-row-top {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.4em 1ch;
}
.tt-disk-name {
flex: 0 0 auto;
min-width: 5ch;
color: var(--ink);
letter-spacing: 0.16em;
text-transform: uppercase;
}
.tt-disk-pct {
flex: 0 0 auto;
color: var(--live);
font-variant-numeric: tabular-nums;
}
.tt-disk-row-sub {
margin-top: 0.15em;
color: var(--ink-dim);
}
.tt-disk-row:hover { background: var(--field); }
.tt-disk-row:hover .tt-disk-name,
.tt-disk-row:hover .tt-disk-pct,
.tt-disk-row:hover .tt-disk-row-sub { color: var(--tt-white); }
/* ── Tiles (library / discover) ───────────────────────── /* ── Tiles (library / discover) ─────────────────────────
Artwork is the one thing in this world that is not made of Artwork is the one thing in this world that is not made of
characters, so it is treated as an inserted transmission: a characters, so it is treated as an inserted transmission: a
@ -284,21 +327,28 @@ a.tt-row:focus-visible .tt-row-sub { color: var(--tt-black); }
grid-template-columns: repeat(auto-fill, minmax(min(12.5ch, 100%), 1fr)); grid-template-columns: repeat(auto-fill, minmax(min(12.5ch, 100%), 1fr));
padding: 0 1ch; padding: 0 1ch;
} }
/* P100's Just Added: a fixed row, not a viewport-dependent auto-fill.
Paired with a fixed item count (see home()), this guarantees a full
row every time rather than leaving a lone tile beside empty tracks —
and it keeps this block compact, so NOW SHOWING above it can hold the
lower two-thirds of the first viewport the direction contract names. */
.tt-tiles-row {
grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 560px) {
.tt-tiles-row { grid-template-columns: repeat(1, 1fr); }
}
.tt-tile { display: flex; flex-direction: column; min-width: 0; } .tt-tile { display: flex; flex-direction: column; min-width: 0; }
.tt-tile-art { .tt-tile-art {
display: block; display: block;
width: 100%; width: 100%;
aspect-ratio: 2 / 3; aspect-ratio: 2 / 3;
object-fit: cover; object-fit: cover;
background: var(--ink-dimmer); /* Covers the box while a real poster is still loading, and IS the
} fallback for a poster that never arrives — dithered, per
/* No artwork came back. A dithered field, not a fake poster. */ discipline 1, never a flat mid-grey panel. */
.tt-tile-art-empty { background-color: var(--tt-black);
background-image: repeating-linear-gradient( background-image: var(--dither-mid);
45deg,
var(--ink-dimmer) 0 1px,
var(--tt-black) 1px calc(var(--dither-size) * 2)
);
} }
.tt-tile-t { .tt-tile-t {
margin-top: 0.4em; margin-top: 0.4em;
@ -330,8 +380,13 @@ a.tt-row:focus-visible .tt-row-sub { color: var(--tt-black); }
cursor: pointer; cursor: pointer;
} }
.tt-btn:hover:not(:disabled) { background: var(--live); } .tt-btn:hover:not(:disabled) { background: var(--live); }
.tt-btn:disabled { background: var(--ink-dimmer); color: var(--ink-dim); cursor: default; } .tt-btn:disabled {
.tt-btn.armed { background: var(--alert); color: var(--tt-white); } background-color: var(--tt-black);
background-image: var(--dither-mid);
color: var(--ink-dim);
cursor: default;
}
.tt-btn.armed { background: var(--alert); color: var(--tt-black); }
.tt-btn.done { background: transparent; color: var(--ok); cursor: default; } .tt-btn.done { background: transparent; color: var(--ok); cursor: default; }
.tt-btn-quiet { .tt-btn-quiet {
background: transparent; background: transparent;
@ -398,7 +453,8 @@ a.tt-row:focus-visible .tt-row-sub { color: var(--tt-black); }
.tt-err pre { .tt-err pre {
margin: 0.3em 0 0; margin: 0.3em 0 0;
padding: 0.5em 1ch; padding: 0.5em 1ch;
background: var(--ink-dimmer); background-color: var(--tt-black);
background-image: var(--dither-mid);
color: var(--ink); color: var(--ink);
font-family: inherit; font-family: inherit;
font-size: inherit; font-size: inherit;
@ -539,6 +595,11 @@ a.tt-row:focus-visible .tt-row-sub { color: var(--tt-black); }
.tt-row-val { margin-left: auto; white-space: normal; } .tt-row-val { margin-left: auto; white-space: normal; }
.tt-head-name { letter-spacing: 0.12em; } .tt-head-name { letter-spacing: 0.12em; }
.tt-head-clock { margin-left: 0; flex-basis: 100%; } .tt-head-clock { margin-left: 0; flex-basis: 100%; }
/* The System disk table swaps for its row reflow here — see the
"Disk rows" block above. */
.tt-table-scroll { display: none; }
.tt-disk-rows { display: flex; }
} }
/* ── Search ───────────────────────────────────────────── /* ── Search ─────────────────────────────────────────────
@ -615,7 +676,10 @@ a.tt-row:focus-visible .tt-row-sub { color: var(--tt-black); }
pointer-events: none; pointer-events: none;
} }
.tt-toast.show { opacity: 1; transform: translateY(0); } .tt-toast.show { opacity: 1; transform: translateY(0); }
.tt-toast.error { background: var(--alert); color: var(--tt-white); } .tt-toast.error {
background: var(--alert);
color: var(--tt-black); /* same contrast fix as .tt-field.crit */
}
/* ── Sign-in ──────────────────────────────────────────── /* ── Sign-in ────────────────────────────────────────────
The one page with no navigation, so it centres its single The one page with no navigation, so it centres its single

Binary file not shown.

View file

@ -95,6 +95,26 @@
{% endmacro %} {% endmacro %}
{#- A disk/cache row for the phone reflow of the System table (below
560px, where a six-column table stops fitting and its horizontal
scroll would hide the bar and the temperature — the two readings
that matter most). Name, the one shared bar, and the temperature
sit on one line; device/size/free sit beneath as a sub-line. The
desktop <table> stays the table at wider widths; this is not a
replacement for it, only the reflow. -#}
{% macro disk_row(dk) %}
<div class="tt-disk-row">
<div class="tt-disk-row-top">
<span class="tt-disk-name">{{ dk.name }}</span>
{{ bar(dk.used_pct | default(0)) }}
<span class="tt-disk-pct">{{ dk.used_pct | default(0) | round | int }}%</span>
{{ disk_state(dk) }}
</div>
<div class="tt-disk-row-sub">{{ dk.device | default('—') }} &middot; {{ dk.free | default('—') }} free of {{ dk.total | default('—') }}</div>
</div>
{% endmacro %}
{#- A list row: number, name, body, value. The index rows and every {#- A list row: number, name, body, value. The index rows and every
listing in the service share this one shape. -#} listing in the service share this one shape. -#}
{% macro row(name, title, sub='', value='', tone='') %} {% macro row(name, title, sub='', value='', tone='') %}

View file

@ -7,9 +7,10 @@
<title>{% block title %}P{{ here[1] }} Media Centre{% endblock %}</title> <title>{% block title %}P{{ here[1] }} Media Centre{% endblock %}</title>
<meta name="theme-color" content="#000000"> <meta name="theme-color" content="#000000">
<link rel="icon" href="{{ url_for('static', filename='favicon.svg') }}"> <link rel="icon" href="{{ url_for('static', filename='favicon.svg') }}">
<link rel="preconnect" href="https://fonts.googleapis.com"> {#- Space Mono is self-hosted (see base.css's @font-face) — this app
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> runs on a LAN-only home server where a Google Fonts request can
<link href="https://fonts.googleapis.com/css2?family=Chivo+Mono:wght@400;600&family=DotGothic16&display=swap" rel="stylesheet"> simply fail, and a dropped webfont request is a silent identity
change, not a graceful fallback. -#}
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/teletext.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/teletext.css') }}">
{% block css %}{% endblock %} {% block css %}{% endblock %}

View file

@ -135,7 +135,7 @@
{#- ── Just added ──────────────────────────────────────────────── -#} {#- ── Just added ──────────────────────────────────────────────── -#}
{% if recently_added %} {% if recently_added %}
<div class="tt-field"><span>Just Added</span></div> <div class="tt-field"><span>Just Added</span></div>
<div class="tt-tiles"> <div class="tt-tiles tt-tiles-row">
{% for it in recently_added %} {% for it in recently_added %}
{{ tile(it.title, it.sub, it.poster, it.ago) }} {{ tile(it.title, it.sub, it.poster, it.ago) }}
{% endfor %} {% endfor %}

View file

@ -6,9 +6,7 @@
<title>Media Centre</title> <title>Media Centre</title>
<meta name="theme-color" content="#000000"> <meta name="theme-color" content="#000000">
<link rel="icon" href="{{ url_for('static', filename='favicon.svg') }}"> <link rel="icon" href="{{ url_for('static', filename='favicon.svg') }}">
<link rel="preconnect" href="https://fonts.googleapis.com"> {#- Space Mono is self-hosted — see base.css's @font-face. -#}
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Chivo+Mono:wght@400;600&family=DotGothic16&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/teletext.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/teletext.css') }}">
</head> </head>

View file

@ -1,5 +1,5 @@
{% extends "_shell.html" %} {% extends "_shell.html" %}
{% from "_macros.html" import service_error, bar, disk_state, trend %} {% from "_macros.html" import service_error, bar, disk_state, disk_row, trend %}
{% block title %}P105 System{% endblock %} {% block title %}P105 System{% endblock %}
{% block content %} {% block content %}
@ -58,6 +58,10 @@
</div> </div>
{% if unraid.disks %} {% if unraid.disks %}
{#- Desktop: a real table — columns line up, a new metric is a new
column. Hidden below 560px, where six columns stop fitting and
a horizontal scroll would hide the bar and the temperature,
the two readings that matter most on this page. -#}
<div class="tt-table-scroll"> <div class="tt-table-scroll">
<table class="tt-table"> <table class="tt-table">
<thead> <thead>
@ -94,6 +98,14 @@
</tbody> </tbody>
</table> </table>
</div> </div>
{#- Phone (≤560px): the .tt-row-derived reflow — name, the shared
20-cell bar and the temperature on one line, device/size/free
beneath. Hidden above 560px in favour of the table. -#}
<div class="tt-disk-rows">
{% for dk in unraid.disks %}{{ disk_row(dk) }}{% endfor %}
{% for ck in unraid.caches | default([]) %}{{ disk_row(ck) }}{% endfor %}
</div>
{% endif %} {% endif %}
{% endif %} {% endif %}