Redesign Media Centre as a teletext broadcast information service

Replaces the neutral dark card-based theme with "Page 100": teletext's
locked eight colours at full saturation on pure black, numbered pages
reachable by typing the number (or the r/g/y/c fastext keys), and no
borders, boxes, or radius anywhere — grouping is carried by full-bleed
colour fields and row weight instead.

Chosen via the impeccable skill's replacement-world flow: seven visual
candidates derived from the audience's own world, a mandatory roll
assigned Ceefax/US-cable-guide teletext, six catalog challengers weighed
against it, and the user locked the assigned direction on the decision
page (seed aab4510e).

- New templates for all nine pages (home/watch/library/discover/
  requests/system/activity/login/settings) behind a shared _shell.html
  carrying the direction contract and _macros.html for the row/bar/tile
  vocabulary; deletes the old single-page dashboard.html and its
  dedicated stylesheets/script.
- static/css/base.css + teletext.css: the locked palette, a two-height
  type scale (Chivo Mono / DotGothic16), and the five disciplines the
  system is held to (no in-between tone, no enclosure, one shared bar
  ruler, trend only where real history exists, pages weighted by state).
- static/js/service.js: header clock, page-number jump, fastext keys,
  two-tap arm/confirm add, guarded live polling — replaces dashboard.js.
- app.py: nav_items now carry page numbers; fastext defaults added to
  the page-chrome context processor; version bumped to 2.0.0.
- DESIGN.md + .impeccable/design.json regenerated from the built world
  by the documenter pass; PRODUCT.md and CLAUDE.md updated to describe
  the new system in place of the retired one.
- Fixed a live bug found along the way: settings.html called
  url_for('dashboard'), an endpoint removed earlier in the branch, which
  would have 500'd the Settings page.
- Fixed a One-Ruler violation the finish review's recapture request
  surfaced: --bar-cells disagreed with the bar() macro's hardcoded cell
  count below the 760px breakpoint, so a fill could land mid-cell on a
  phone. Removed the mobile override; the ruler is now 20 cells at every
  width, matching the macro unconditionally.

Passed two inspection rounds (desktop + true-390px mobile, via an
iframe harness since macOS Chrome clamps --window-size) and a finish
review; QA screenshots and interview logs are gitignored as scratch,
not shipped in the repo.

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:00:45 -07:00
parent 9a277b9031
commit 4b19cbdfb2
25 changed files with 3002 additions and 2343 deletions

151
templates/system.html Normal file
View file

@ -0,0 +1,151 @@
{% extends "_shell.html" %}
{% from "_macros.html" import service_error, bar, disk_state, trend %}
{% block title %}P105 System{% endblock %}
{% block content %}
<h1 class="tt-dh bloom tt-masthead">SYSTEM</h1>
{#- ── Service transmission status ───────────────────────────────
Six services, one row each, colour carrying up or down. This is
what the old header pills were, given room to say something. -#}
<div class="tt-field"><span>Transmission</span></div>
<div class="tt-rows">
{% for name, svc in [('Sonarr', sonarr), ('Radarr', radarr), ('SABnzbd', sabnzbd),
('Tautulli', tautulli), ('Prowlarr', prowlarr), ('Ombi', ombi)] %}
<div class="tt-row">
<span class="tt-row-name">{{ name }}</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' -}}
</span>
</div>
{% endfor %}
</div>
{#- ── Array ───────────────────────────────────────────────────── -#}
{% if unraid.status != 'ok' %}
<div class="tt-field crit"><span>Array</span><span class="tt-field-meta">unreachable</span></div>
{{ service_error('Unraid', unraid.error) }}
{% else %}
{%- set apct = unraid.array_used_pct | default(0) | float -%}
<div class="tt-field {% if apct >= 90 %}crit{% elif apct >= 75 %}warn{% endif %}">
<span>Array</span>
<span class="tt-field-meta">
{{ unraid.array_state | default('') }}
{%- if unraid.parities %} &middot; parity
{%- for p in unraid.parities %} {{ p.temp if p.temp is not none else '—' }}&deg;C{% endfor %}
{%- endif %}
</span>
</div>
<div class="tt-figs">
<div class="tt-fig">
<div class="tt-dh tt-fig-v {{ 'alert' if apct >= 90 else ('warn' if apct >= 75 else 'ok') }}">{{ apct | round | int }}%</div>
<div class="tt-fig-l">Used</div>
</div>
<div class="tt-fig">
<div class="tt-dh tt-fig-v">{{ unraid.array_free | default('—') }}</div>
<div class="tt-fig-l">Free</div>
</div>
<div class="tt-fig">
<div class="tt-dh tt-fig-v">{{ unraid.array_total | default('—') }}</div>
<div class="tt-fig-l">Total</div>
</div>
<div class="tt-fig">
<div class="tt-dh tt-fig-v">{{ unraid.disk_count | default(0) }}</div>
<div class="tt-fig-l">Disks</div>
</div>
</div>
{% if unraid.disks %}
<div class="tt-table-scroll">
<table class="tt-table">
<thead>
<tr>
<th scope="col">Disk</th>
<th scope="col">Device</th>
<th scope="col" class="num">Size</th>
<th scope="col" class="num">Free</th>
<th scope="col">Usage</th>
<th scope="col" class="num">Temp</th>
</tr>
</thead>
<tbody>
{% for dk in unraid.disks %}
<tr>
<th scope="row">{{ dk.name }}</th>
<td>{{ dk.device | default('—') }}</td>
<td class="num">{{ dk.total | default('—') }}</td>
<td class="num">{{ dk.free | default('—') }}</td>
<td>{{ bar(dk.used_pct | default(0)) }} <span class="val">{{ dk.used_pct | default(0) | round | int }}%</span></td>
<td class="num">{{ disk_state(dk) }}</td>
</tr>
{% endfor %}
{% for ck in unraid.caches | default([]) %}
<tr>
<th scope="row">{{ ck.name }}</th>
<td>{{ ck.device | default('—') }}</td>
<td class="num">{{ ck.total | default('—') }}</td>
<td class="num">{{ ck.free | default('—') }}</td>
<td>{{ bar(ck.used_pct | default(0)) }} <span class="val">{{ ck.used_pct | default(0) | round | int }}%</span></td>
<td class="num">{{ disk_state(ck) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endif %}
{#- ── Downloads ─────────────────────────────────────────────────
The one place the service holds real history, so the one place a
trend arrow is honest: current speed against the 7-day average. -#}
{% if sabnzbd.status == 'ok' %}
<div class="tt-field"><span>Downloads</span><span class="tt-field-meta">{{ sabnzbd.sab_status | default('') }}</span></div>
<div class="tt-figs">
<div class="tt-fig">
<div class="tt-dh tt-fig-v"><span id="sab-queue">{{ sabnzbd.queue_count }}</span></div>
<div class="tt-fig-l">In queue</div>
</div>
<div class="tt-fig">
<div class="tt-dh tt-fig-v"><span id="sab-speed">{{ sabnzbd.speed | default('0') }}</span> {{ trend(sabnzbd.speed_kbps, sabnzbd.avg_speed) }}</div>
<div class="tt-fig-l">Speed vs 7-day avg</div>
</div>
<div class="tt-fig">
<div class="tt-dh tt-fig-v"><span id="sab-remaining">{{ sabnzbd.sizeleft | default('—') }}</span></div>
<div class="tt-fig-l">Remaining</div>
</div>
<div class="tt-fig">
<div class="tt-dh tt-fig-v">{{ sabnzbd.week_size | default('—') }}</div>
<div class="tt-fig-l">This week</div>
</div>
</div>
{% else %}
<div class="tt-field crit"><span>Downloads</span><span class="tt-field-meta">off air</span></div>
{{ service_error('SABnzbd', sabnzbd.error) }}
{% endif %}
{#- ── Indexers ────────────────────────────────────────────────── -#}
{% if prowlarr.status == 'ok' %}
<div class="tt-field">
<span>Indexers</span>
<span class="tt-field-meta">{{ prowlarr.up_count }} of {{ prowlarr.total }} up</span>
</div>
<div class="tt-rows">
{% for idx in prowlarr.indexers %}
<div class="tt-row">
<span class="tt-row-name">{{ idx.protocol | default('') }}</span>
<span class="tt-row-body">
<span class="tt-row-title">{{ idx.name }}</span>
{% if prowlarr.has_stats %}<span class="tt-row-sub">{{ idx.grabs | default(0) }} grabs &middot; {{ idx.queries | default(0) }} queries</span>{% endif %}
</span>
{% if prowlarr.has_stats %}{{ bar(idx.grab_share | default(0), 'ok' if idx.up else 'alert') }}{% endif %}
<span class="tt-row-val {{ 'ok' if idx.up else 'alert' }}">{{ 'UP' if idx.up else 'DOWN' }}</span>
</div>
{% endfor %}
</div>
{% else %}
<div class="tt-field crit"><span>Indexers</span><span class="tt-field-meta">off air</span></div>
{{ service_error('Prowlarr', prowlarr.error) }}
{% endif %}
{% endblock %}