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

66
templates/library.html Normal file
View file

@ -0,0 +1,66 @@
{% extends "_shell.html" %}
{% from "_macros.html" import service_error, tile %}
{% block title %}P102 Library{% endblock %}
{% block content %}
<h1 class="tt-dh bloom tt-masthead">LIBRARY</h1>
{#- Search sits at the top because arriving here with a title in mind is
the common case. It posts to the same lookup the old page used. -#}
<form class="tt-search" action="{{ request.script_root }}/library" method="get" role="search">
<label class="visually-hidden" for="q">Search the library</label>
<input class="tt-input" type="search" id="q" name="q" value="{{ request.args.get('q', '') }}"
placeholder="TITLE" autocomplete="off" spellcheck="false">
<button class="tt-btn" type="submit">Search</button>
</form>
<div class="tt-field">
<span>Holdings</span>
<span class="tt-field-meta">
{%- if sonarr.status == 'ok' %}{{ sonarr.total_series }} shows{% endif -%}
{%- if sonarr.status == 'ok' and radarr.status == 'ok' %} &middot; {% endif -%}
{%- if radarr.status == 'ok' %}{{ radarr.total_movies }} films{% endif -%}
</span>
</div>
<div class="tt-figs">
{% if sonarr.status == 'ok' %}
<div class="tt-fig">
<div class="tt-dh tt-fig-v">{{ sonarr.total_series }}</div>
<div class="tt-fig-l">Shows</div>
</div>
<div class="tt-fig">
<div class="tt-dh tt-fig-v {{ 'alert' if sonarr.missing_episodes else 'ok' }}">{{ sonarr.missing_episodes }}</div>
<div class="tt-fig-l">Missing eps</div>
</div>
{% endif %}
{% if radarr.status == 'ok' %}
<div class="tt-fig">
<div class="tt-dh tt-fig-v">{{ radarr.total_movies }}</div>
<div class="tt-fig-l">Films</div>
</div>
<div class="tt-fig">
<div class="tt-dh tt-fig-v {{ 'alert' if radarr.missing_movies else 'ok' }}">{{ radarr.missing_movies }}</div>
<div class="tt-fig-l">Missing films</div>
</div>
{% endif %}
</div>
{% if sonarr.status != 'ok' %}
<div class="tt-field crit"><span>Sonarr</span><span class="tt-field-meta">off air</span></div>
{{ service_error('Sonarr', sonarr.error) }}
{% endif %}
{% if radarr.status != 'ok' %}
<div class="tt-field crit"><span>Radarr</span><span class="tt-field-meta">off air</span></div>
{{ service_error('Radarr', radarr.error) }}
{% endif %}
{% if recently_added %}
<div class="tt-field"><span>Newest In</span></div>
<div class="tt-tiles">
{% for it in recently_added %}
{{ tile(it.title, it.sub, it.poster, it.ago) }}
{% endfor %}
</div>
{% endif %}
{% endblock %}