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

View file

@ -1,107 +1,80 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Settings — Media Centre</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.svg') }}">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
</head>
<body data-url-base="{{ request.script_root }}">
{% extends "_shell.html" %}
{% block title %}P900 Settings{% endblock %}
<header>
<svg width="30" height="30" viewBox="0 0 100 100" aria-hidden="true">
<rect width="100" height="100" rx="22" fill="var(--accent)"/>
<path d="M40 32 L72 50 L40 68 Z" fill="var(--bg)"/>
</svg>
<h1>Settings</h1>
<a href="{{ url_for('dashboard') }}" class="back-btn">← Dashboard</a>
</header>
<main>
<div class="grid">
<!-- General -->
<div class="card full">
<div class="card-header"><h2>General</h2></div>
<div class="card-body">
<div class="field">
<label for="timezone">Timezone</label>
<select id="timezone">
{% set tzs = [
'America/New_York','America/Chicago','America/Denver','America/Los_Angeles',
'America/Toronto','America/Vancouver','America/Phoenix','America/Halifax',
'Europe/London','Europe/Paris','Europe/Berlin','Europe/Amsterdam',
'Europe/Stockholm','Europe/Rome','Europe/Madrid','Europe/Zurich',
'Australia/Sydney','Australia/Melbourne','Australia/Brisbane','Australia/Perth',
'Pacific/Auckland','Asia/Tokyo','Asia/Singapore','Asia/Dubai','Asia/Kolkata'
] %}
{% for tz in tzs %}
<option value="{{ tz }}" {% if tz == timezone %}selected{% endif %}>{{ tz }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label for="arr_host">Host IP (ARR_HOST)</label>
<input type="text" id="arr_host" value="{{ arr_host }}" placeholder="e.g. 192.168.86.33">
<p class="hint">LAN address of the machine running Sonarr, Radarr, and the rest.</p>
</div>
<button class="save" onclick="saveGeneral()">Save general</button>
</div>
</div>
<!-- Change password -->
<div class="card full">
<div class="card-header"><h2>Change password</h2></div>
<div class="card-body">
<div class="field">
<label for="pw_current">Current password</label>
<input type="password" id="pw_current" autocomplete="current-password">
</div>
<div class="field">
<label for="pw_new">New password</label>
<input type="password" id="pw_new" placeholder="At least 6 characters" autocomplete="new-password">
</div>
<div class="field">
<label for="pw_confirm">Confirm new password</label>
<input type="password" id="pw_confirm" autocomplete="new-password">
<div class="error-inline" id="pw-error" role="alert"></div>
</div>
<button class="save" onclick="changePassword()">Update password</button>
</div>
</div>
<!-- Service API keys -->
{% for svc, label in [
('sonarr','Sonarr'), ('radarr','Radarr'), ('sabnzbd','SABnzbd'),
('tautulli','Tautulli'), ('prowlarr','Prowlarr'), ('ombi','Ombi')
] %}
<div class="card">
<div class="card-header"><h2>{{ label }}</h2></div>
<div class="card-body">
<div class="field">
<label for="{{ svc }}_api_key">API key</label>
<input type="password" id="{{ svc }}_api_key"
value="{{ {'sonarr': sonarr_api_key, 'radarr': radarr_api_key,
'sabnzbd': sabnzbd_api_key, 'tautulli': tautulli_api_key,
'prowlarr': prowlarr_api_key, 'ombi': ombi_api_key}[svc] }}"
autocomplete="off">
</div>
<button class="save" onclick="saveService('{{ svc }}')">Save</button>
</div>
</div>
{% endfor %}
{% block content %}
<h1 class="tt-dh bloom tt-masthead">SETTINGS</h1>
<p class="tt-empty">Keys saved here override the values in <code>.env</code>. Both need a container restart to take effect.</p>
{#- ── General ────────────────────────────────────────────────── -#}
<div class="tt-field"><span>General</span></div>
<div class="tt-form">
<div class="tt-field-row">
<label class="tt-label" for="timezone">Timezone</label>
<select class="tt-input tt-select" id="timezone">
{% set tzs = [
'America/New_York','America/Chicago','America/Denver','America/Los_Angeles',
'America/Toronto','America/Vancouver','America/Phoenix','America/Halifax',
'Europe/London','Europe/Paris','Europe/Berlin','Europe/Amsterdam',
'Europe/Stockholm','Europe/Rome','Europe/Madrid','Europe/Zurich',
'Australia/Sydney','Australia/Melbourne','Australia/Brisbane','Australia/Perth',
'Pacific/Auckland','Asia/Tokyo','Asia/Singapore','Asia/Dubai','Asia/Kolkata'
] %}
{% for tz in tzs %}
<option value="{{ tz }}" {% if tz == timezone %}selected{% endif %}>{{ tz }}</option>
{% endfor %}
</select>
</div>
</main>
<div class="tt-field-row">
<label class="tt-label" for="arr_host">Host IP (ARR_HOST)</label>
<input class="tt-input" type="text" id="arr_host" value="{{ arr_host }}" placeholder="192.168.86.33">
<p class="tt-hint">LAN address of the machine running Sonarr, Radarr and the rest.</p>
</div>
<button class="tt-btn" onclick="saveGeneral()">Save General</button>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
{#- ── Password ───────────────────────────────────────────────── -#}
<div class="tt-field"><span>Change Password</span></div>
<div class="tt-form">
<div class="tt-field-row">
<label class="tt-label" for="pw_current">Current password</label>
<input class="tt-input" type="password" id="pw_current" autocomplete="current-password">
</div>
<div class="tt-field-row">
<label class="tt-label" for="pw_new">New password</label>
<input class="tt-input" type="password" id="pw_new" placeholder="At least 6 characters" autocomplete="new-password">
</div>
<div class="tt-field-row">
<label class="tt-label" for="pw_confirm">Confirm new password</label>
<input class="tt-input" type="password" id="pw_confirm" autocomplete="new-password">
<div class="tt-inline-err" id="pw-error" role="alert"></div>
</div>
<button class="tt-btn" onclick="changePassword()">Update Password</button>
</div>
{#- ── Keys ───────────────────────────────────────────────────── -#}
<div class="tt-field"><span>Service Keys</span></div>
<div class="tt-keys">
{% for svc, label in [
('sonarr','Sonarr'), ('radarr','Radarr'), ('sabnzbd','SABnzbd'),
('tautulli','Tautulli'), ('prowlarr','Prowlarr'), ('ombi','Ombi')
] %}
<div class="tt-form">
<div class="tt-field-row">
<label class="tt-label" for="{{ svc }}_api_key">{{ label }} API key</label>
<input class="tt-input" type="password" id="{{ svc }}_api_key"
value="{{ {'sonarr': sonarr_api_key, 'radarr': radarr_api_key,
'sabnzbd': sabnzbd_api_key, 'tautulli': tautulli_api_key,
'prowlarr': prowlarr_api_key, 'ombi': ombi_api_key}[svc] }}"
autocomplete="off">
</div>
<button class="tt-btn tt-btn-quiet" onclick="saveService('{{ svc }}')">Save {{ label }}</button>
</div>
{% endfor %}
</div>
<div class="tt-toast" id="toast" role="status" aria-live="polite"></div>
{% endblock %}
{% block js %}
<script src="{{ url_for('static', filename='js/settings.js') }}"></script>
</body>
</html>
{% endblock %}