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

144
templates/home.html Normal file
View file

@ -0,0 +1,144 @@
{% extends "_shell.html" %}
{% from "_macros.html" import service_error, bar, tile %}
{% block title %}P100 Media Centre{% endblock %}
{% block content %}
{#- The masthead. Double height, the service's own colour, and the one
place on the page bloom is allowed. -#}
<h1 class="tt-dh bloom tt-masthead">MEDIA CENTRE</h1>
{#- ── The index IS the status page ──────────────────────────────
Every destination row carries its own live figure in its own
colour, so choosing where to go and reading how things are are
the same act. This is the whole thesis in one block. -#}
<div class="tt-rows">
<a class="tt-row" href="{{ request.script_root }}/watch">
<span class="tt-row-num">101</span>
<span class="tt-row-name">Watch</span>
<span class="tt-row-body"><span class="tt-row-sub">Who is streaming right now</span></span>
{% if tautulli.status == 'ok' %}
<span class="tt-row-val {{ 'ok' if tautulli.active_streams else 'off' }}" id="tt-streams-row">
<span id="tt-streams">{{ tautulli.active_streams }}</span> STREAMING</span>
{% else %}
<span class="tt-row-val alert">OFF AIR</span>
{% endif %}
</a>
<a class="tt-row" href="{{ request.script_root }}/library">
<span class="tt-row-num">102</span>
<span class="tt-row-name">Library</span>
<span class="tt-row-body"><span class="tt-row-sub">Everything already held</span></span>
{% if sonarr.status == 'ok' or radarr.status == 'ok' %}
<span class="tt-row-val">{{ sonarr.total_series | default(0) }} SHOWS &nbsp;{{ radarr.total_movies | default(0) }} FILMS</span>
{% else %}
<span class="tt-row-val alert">OFF AIR</span>
{% endif %}
</a>
<a class="tt-row" href="{{ request.script_root }}/discover">
<span class="tt-row-num">103</span>
<span class="tt-row-name">Discover</span>
<span class="tt-row-body"><span class="tt-row-sub">Popular, and not yet held</span></span>
<span class="tt-row-val">{{ (rt_movies | length) + (rt_tv | length) }} SUGGESTED</span>
</a>
<a class="tt-row" href="{{ request.script_root }}/requests">
<span class="tt-row-num">104</span>
<span class="tt-row-name">Requests</span>
<span class="tt-row-body"><span class="tt-row-sub">Waiting on a decision</span></span>
{% if ombi.status == 'ok' %}
<span class="tt-row-val {{ 'pending' if ombi.total_pending else 'off' }}">{{ ombi.total_pending | default(0) }} PENDING</span>
{% else %}
<span class="tt-row-val alert">OFF AIR</span>
{% endif %}
</a>
<a class="tt-row" href="{{ request.script_root }}/system">
<span class="tt-row-num">105</span>
<span class="tt-row-name">System</span>
<span class="tt-row-body"><span class="tt-row-sub">Array, disks and indexers</span></span>
{% if unraid.status == 'ok' %}
{%- set pct = unraid.array_used_pct | default(0) | float -%}
<span class="tt-row-val {{ 'alert' if pct >= 90 else ('warn' if pct >= 75 else 'ok') }}">{{ pct | round | int }}% FULL</span>
{% else %}
<span class="tt-row-val alert">OFF AIR</span>
{% endif %}
</a>
<a class="tt-row" href="{{ request.script_root }}/activity">
<span class="tt-row-num">106</span>
<span class="tt-row-name">Activity</span>
<span class="tt-row-body"><span class="tt-row-sub">What the stack has been doing</span></span>
{% if sabnzbd.status == 'ok' %}
<span class="tt-row-val {{ '' if sabnzbd.queue_count else 'off' }}"><span id="sab-queue">{{ sabnzbd.queue_count }}</span> IN QUEUE</span>
{% else %}
<span class="tt-row-val alert">OFF AIR</span>
{% endif %}
</a>
<a class="tt-row" href="{{ request.script_root }}/settings">
<span class="tt-row-num">900</span>
<span class="tt-row-name">Settings</span>
<span class="tt-row-body"><span class="tt-row-sub">Keys, timezone and password</span></span>
</a>
</div>
<div class="tt-sep" aria-hidden="true"></div>
{#- ── Now showing ───────────────────────────────────────────────
The lower two-thirds. The furthest-along stream is set at double
height in the live colour, with its progress as whole mosaic
cells on the service's one ruler. -#}
<div class="tt-field">
<span>Now Showing</span>
{% if tautulli.status == 'ok' %}
<span class="tt-field-meta">{{ tautulli.active_streams }} active</span>
{% endif %}
</div>
{% if tautulli.status != 'ok' %}
{{ service_error('Tautulli', tautulli.error) }}
{% elif hero %}
<div class="tt-now">
<div class="tt-dh bloom tt-now-title">{{ hero.title }}</div>
<div class="tt-now-meta">
{{ hero.user or hero.username }}
{%- if hero.player %} &middot; {{ hero.player }}{% endif %}
{%- if hero.quality %} &middot; {{ hero.quality }}{% endif %}
{%- if hero.state %} &middot; {{ hero.state | upper }}{% endif %}
</div>
<div class="tt-now-line">
{{ bar(hero.progress | default(0), 'ok') }}
<span class="tt-now-pct">{{ hero.progress | default(0) | round | int }}%</span>
{% if hero.remaining %}<span class="tt-row-sub">{{ hero.remaining }} LEFT</span>{% endif %}
</div>
</div>
{% if other_sessions %}
<div class="tt-rows tt-rows-after">
{% for s in other_sessions %}
<div class="tt-row">
<span class="tt-row-body">
<span class="tt-row-title">{{ s.title }}</span>
<span class="tt-row-sub">{{ s.user or s.username }}{% if s.player %} &middot; {{ s.player }}{% endif %}</span>
</span>
{{ bar(s.progress | default(0), 'ok') }}
<span class="tt-row-val">{{ s.progress | default(0) | round | int }}%</span>
</div>
{% endfor %}
</div>
{% endif %}
{% else %}
<p class="tt-empty">Nothing streaming. The service is quiet.</p>
{% endif %}
{#- ── Just added ──────────────────────────────────────────────── -#}
{% if recently_added %}
<div class="tt-field"><span>Just Added</span></div>
<div class="tt-tiles">
{% for it in recently_added %}
{{ tile(it.title, it.sub, it.poster, it.ago) }}
{% endfor %}
</div>
{% endif %}
{% endblock %}