Files
arr/web/index.html
T
2026-08-24 03:04:06 +01:00

521 lines
24 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<title>arr</title>
<script type="module" src="/src/main.ts"></script>
</head>
<body>
<!--
THESIS: arr is a signal chain — TMDB and Prowlarr feed in, Transmission is
driven out — and health is lamps on that chain, read in one glance. Refuses
the admin-dashboard rut: no sidebar, no stat cards, no hero metric.
OWN-WORLD: broadcast master-control panel. Near-black charcoal ground,
machined panel modules with inset hairline borders, engraved uppercase
micro-labels, monospace readouts, status lamps (signal green / amber /
fault red) with a soft glow, phosphor-cyan for the one interactive control.
STORY: the operator opens the board, reads the master lamp, and knows in a
second whether arr can do its job — and which upstream is down, by name.
FIRST VIEWPORT: a top rail (wordmark + master lamp + verdict + version),
then the chain: TMDB and PROWLARR modules on the left feeding the central
arr module, TRANSMISSION driven on the right, joined by bus hairlines with
direction arrows. On a phone the chain stacks vertically.
FORM: broadcast master-control panel, #2 of 7 on the ordered list; external
roll (shell RANDOM → 2, witness 57910; concept-seed.mjs degraded to empty
output in this environment).
FINISH: unreviewed and undocumented is unfinished; this build ends with the
finish review, the verdict, and DESIGN.md.
SEARCH (§9.2, issue #30): one command input on the rail — the board's
command line. Typing swaps the chain for the search deck: IN LIBRARY
first, ON TMDB below, engraved group labels, machined rows. Enter on a
TMDB row expands an inline add panel with root and policy pre-filled;
a pasted magnet or .torrent routes to a manual-intake panel. There is
no separate add page, ever. Esc returns to the board; "/" focuses the
box from anywhere.
BUCKETS (§9.3, issue #31): a library row opens the release deck — the
policy engine's verdicts, inverted from Radarr: ELIGIBLE rows lead with
fixed-width attribute chips (score res source hdr audio size seed) in
aligned mono columns; WAIVED and REJECTED collapse to counts. The
release name is secondary, truncated, full on expand. Every rejected
row names its killing rule; one click on a waived row writes the
override and grabs. No horizontal scroll at any viewport. Esc returns
to the search deck.
LIBRARY (§4.2, issue #32): the LIBRARY control on the rail opens the
library deck — SERIES then MOVIES, machined rows with the title left
and chips right. Status is derived and only displayed: series carry a
§4.2 status chip (channel-violet activity, neutral satisfaction, the
mono word always present), movies carry their state chip. There is no
monitored flag anywhere; intent is wanted, at the leaf. The default
list shows what needs attention — airing and incomplete series,
wanted-and-missing movies — and everything satisfied collapses behind
ONE toggle with its count. A file imported under a waiver reads
honestly ("english, no dub") as a dashed amber chip, never as a clean
match. A movie row opens the release deck and returns here on Esc.
QUEUES (§5.2 + §5.7, issue #33): the QUEUES control opens the attention
deck — the only two places the app asks for a human. NO PT SOURCE holds
kids titles with no qualifying pt release; parked for months by design;
one click writes allow_english_audio and the normal search proceeds.
NEEDS DECISION holds titles that hard-failed twice on different
releases; the row opens the release deck to decide by hand. Both
sections always render, even empty — the operator visits to confirm
nothing needs them. The rail control carries an amber entry count,
refreshed with the health poll; amber is attention, cyan stays the
interaction hue.
-->
<header class="rail">
<div class="rail-id">
<span class="lamp" id="master-lamp" data-state="probing" aria-hidden="true"></span>
<h1 class="wordmark">arr</h1>
</div>
<nav class="rail-nav" aria-label="views">
<button type="button" class="control" id="nav-library" aria-pressed="false">
library
</button>
<button type="button" class="control" id="nav-queues" aria-pressed="false">
queues<span class="nav-count readout" id="queues-count" hidden></span>
</button>
<button type="button" class="control" id="nav-settings" aria-pressed="false">
settings
</button>
</nav>
<div class="rail-search">
<input
id="search"
class="search-input readout"
type="text"
spellcheck="false"
autocomplete="off"
placeholder="search · title / tmdb id / tt… / magnet"
aria-label="unified search — titles, TMDB or IMDb ids, magnets and .torrent links"
/>
<span class="search-hint readout" id="search-hint" aria-hidden="true">/</span>
</div>
<p class="rail-verdict readout" id="master-verdict" role="status">probing chain…</p>
<div class="rail-meta">
<span class="readout dim" id="version" data-testid="version">v —</span>
</div>
</header>
<main class="deck" id="deck" hidden>
<p class="deck-status readout" id="deck-status" role="status" hidden></p>
<section class="deck-group" id="group-library" hidden aria-labelledby="label-library">
<header class="deck-head">
<h2 class="deck-label" id="label-library">in library</h2>
<span class="deck-count readout" id="count-library"></span>
</header>
<ul class="deck-rows" id="rows-library"></ul>
</section>
<section class="deck-group" id="group-tmdb" hidden aria-labelledby="label-tmdb">
<header class="deck-head">
<h2 class="deck-label" id="label-tmdb">on tmdb</h2>
<span class="deck-count readout" id="count-tmdb"></span>
</header>
<ul class="deck-rows" id="rows-tmdb"></ul>
</section>
<section class="deck-group" id="group-manual" hidden aria-labelledby="label-manual">
<header class="deck-head">
<h2 class="deck-label" id="label-manual">manual grab</h2>
</header>
<article class="module intake" id="manual-intake"></article>
</section>
</main>
<main class="deck" id="library" hidden aria-label="library">
<header class="deck-head library-bar">
<h2 class="deck-label">library</h2>
<span class="deck-count readout" id="library-summary"></span>
<div class="view-toggle">
<button type="button" class="view-btn" id="library-view-grid" aria-pressed="true">grid</button>
<button type="button" class="view-btn" id="library-view-list" aria-pressed="false">list</button>
</div>
<button
type="button"
class="bucket-toggle readout"
id="library-toggle"
aria-expanded="false"
aria-controls="library-series library-movies"
hidden
></button>
</header>
<p class="deck-status readout" id="library-status" role="status" hidden></p>
<section class="deck-group" id="library-series" hidden aria-labelledby="label-lib-series">
<header class="deck-head">
<h3 class="deck-label" id="label-lib-series">series</h3>
<span class="deck-count readout" id="count-lib-series"></span>
</header>
<ul class="deck-rows" id="rows-lib-series"></ul>
</section>
<section class="deck-group" id="library-movies" hidden aria-labelledby="label-lib-movies">
<header class="deck-head">
<h3 class="deck-label" id="label-lib-movies">movies</h3>
<span class="deck-count readout" id="count-lib-movies"></span>
</header>
<ul class="deck-rows" id="rows-lib-movies"></ul>
</section>
</main>
<main class="deck" id="queues" hidden aria-label="attention queues">
<header class="deck-head library-bar">
<h2 class="deck-label">attention</h2>
<span class="deck-count readout" id="queues-summary"></span>
</header>
<p class="deck-status readout" id="queues-status" role="status" hidden></p>
<section class="deck-group" id="queue-no-pt" hidden aria-labelledby="label-no-pt">
<header class="deck-head">
<h3 class="deck-label" id="label-no-pt">no pt source</h3>
<span class="deck-count readout" id="count-no-pt"></span>
</header>
<p class="queue-note readout dim">
kids titles with no qualifying pt release — parked here, sometimes for months, by
design. allow english writes a movie's override and its search proceeds; series wait
for the detail view.
</p>
<ul class="deck-rows" id="rows-no-pt"></ul>
</section>
<section class="deck-group" id="queue-decision" hidden aria-labelledby="label-decision">
<header class="deck-head">
<h3 class="deck-label" id="label-decision">needs decision</h3>
<span class="deck-count readout" id="count-decision"></span>
</header>
<p class="queue-note readout dim">
hard-failed twice on different releases — open the movie's page and decide by hand;
series entries are display-only for now.
</p>
<ul class="deck-rows" id="rows-decision"></ul>
</section>
</main>
<!--
MOVIE PAGE (§9.6, issue 149): a library row opens /movies/{id} — one
machined module carries what the film IS: backdrop hotlinked from TMDB
under a scrim that sinks it into the console charcoal, poster framed at
left, Rajdhani title with year, TMDB rating and vote count, runtime and
genres, the tagline quoted, overview below. TRAILER resolves from the
metadata response's own key (no second call); TMDB, IMDb and Rotten
Tomatoes sit beside it — RT is a search URL built here, never a resolved
page. LIBRARY controls follow on their own row: wanted, blocked, root,
then search indexers and remove. CAST is the top 10 billed, each photo
linking out to that person's TMDB page. ON DISK lists imported files
with ffprobe attribute tags and any §5.7 waiver, worded honestly. The
§9.3 deck closes the page unchanged — buckets, chips, score heat and
rejection rules untouched; /movies/{id}/releases lands here with the
deck section in view.
-->
<main class="deck" id="movie" hidden aria-label="movie detail">
<header class="releases-head">
<button type="button" class="control" id="movie-back">back</button>
<p class="deck-status readout" id="movie-status" role="status" hidden></p>
</header>
<section class="module movie-hero" id="movie-hero" aria-label="title metadata">
<div class="movie-body">
<img class="movie-poster" id="movie-poster" alt="" hidden />
<div class="movie-info">
<div class="movie-idline">
<h2 class="movie-title" id="movie-title"></h2>
<span class="movie-year readout dim" id="movie-year"></span>
</div>
<span class="row-chips movie-chips" id="movie-chips"></span>
<p class="movie-rating readout" id="movie-rating" hidden></p>
<p class="movie-meta readout dim" id="movie-meta" hidden></p>
<p class="movie-tagline" id="movie-tagline" hidden></p>
<p class="movie-overview" id="movie-overview" hidden></p>
<div class="movie-actions" id="movie-actions"></div>
</div>
</div>
</section>
<section class="deck-group" id="movie-controls" aria-label="library controls">
<header class="deck-head">
<h3 class="deck-label">library</h3>
</header>
<div class="movie-controls">
<button type="button" class="control control-quiet" id="movie-wanted" aria-pressed="false">
wanted
</button>
<button
type="button"
class="control control-quiet"
id="movie-blocked"
aria-pressed="false"
>
blocked
</button>
<label class="movie-root-field">
<span class="field-label readout dim" id="movie-root-label">root</span>
<select class="form-input movie-root" id="movie-root" aria-labelledby="movie-root-label">
</select>
</label>
<span class="movie-controls-space"></span>
<button type="button" class="control" id="movie-sweep">search indexers</button>
<button
type="button"
class="control control-quiet"
id="movie-remove"
aria-expanded="false"
aria-controls="remove-panel"
>
remove
</button>
</div>
<div class="remove-panel" id="remove-panel" hidden></div>
</section>
<section class="deck-group" id="movie-cast" hidden aria-labelledby="label-cast">
<header class="deck-head">
<h3 class="deck-label" id="label-cast">cast</h3>
</header>
<ul class="cast-grid" id="rows-cast"></ul>
</section>
<section class="deck-group" id="movie-files" hidden aria-labelledby="label-disk">
<header class="deck-head">
<h3 class="deck-label" id="label-disk">on disk</h3>
<span class="deck-count readout" id="count-disk"></span>
</header>
<ul class="deck-rows" id="rows-disk"></ul>
</section>
<section class="deck-group" id="movie-releases" aria-labelledby="label-releases">
<header class="deck-head">
<h3 class="deck-label" id="label-releases">releases</h3>
</header>
<p class="deck-status readout" id="releases-status" role="status" hidden></p>
<section class="deck-group" id="bucket-eligible" hidden aria-labelledby="label-eligible">
<header class="deck-head">
<h3 class="deck-label" id="label-eligible">eligible</h3>
<span class="deck-count readout" id="count-eligible"></span>
</header>
<div class="colhead" aria-hidden="true">
<span class="cw cw-score">score</span>
<span class="cw cw-res">res</span>
<span class="cw cw-src">source</span>
<span class="cw cw-hdr">hdr</span>
<span class="cw cw-aud">audio</span>
<span class="cw cw-size">size</span>
<span class="cw cw-seed">seed</span>
</div>
<ul class="deck-rows" id="rows-eligible"></ul>
</section>
<section class="deck-group" id="bucket-waived" hidden aria-labelledby="label-waived">
<header class="deck-head bucket-head">
<h3 class="deck-label" id="label-waived">waived</h3>
<button
type="button"
class="bucket-toggle readout"
id="toggle-waived"
aria-expanded="false"
aria-controls="wrap-waived"
></button>
</header>
<div id="wrap-waived" hidden>
<div class="colhead" aria-hidden="true">
<span class="cw cw-score">score</span>
<span class="cw cw-res">res</span>
<span class="cw cw-src">source</span>
<span class="cw cw-hdr">hdr</span>
<span class="cw cw-aud">audio</span>
<span class="cw cw-size">size</span>
<span class="cw cw-seed">seed</span>
</div>
<ul class="deck-rows" id="rows-waived"></ul>
</div>
</section>
<section class="deck-group" id="bucket-rejected" hidden aria-labelledby="label-rejected">
<header class="deck-head bucket-head">
<h3 class="deck-label" id="label-rejected">rejected</h3>
<button
type="button"
class="bucket-toggle readout"
id="toggle-rejected"
aria-expanded="false"
aria-controls="wrap-rejected"
></button>
</header>
<div id="wrap-rejected" hidden>
<div class="colhead" aria-hidden="true">
<span class="cw cw-score">score</span>
<span class="cw cw-res">res</span>
<span class="cw cw-src">source</span>
<span class="cw cw-hdr">hdr</span>
<span class="cw cw-aud">audio</span>
<span class="cw cw-size">size</span>
<span class="cw cw-seed">seed</span>
</div>
<ul class="deck-rows" id="rows-rejected"></ul>
</div>
</section>
</section>
</main>
<main class="deck" id="settings" hidden aria-label="settings">
<header class="deck-head library-bar">
<h2 class="deck-label">settings</h2>
<span class="deck-count readout" id="settings-summary"></span>
</header>
<p class="deck-status readout" id="settings-status" role="status" hidden></p>
<section class="deck-group" id="group-roots" hidden aria-labelledby="label-roots">
<header class="deck-head bucket-head">
<h3 class="deck-label" id="label-roots">roots</h3>
<button type="button" class="control control-quiet readout" id="root-add">add root</button>
</header>
<ul class="deck-rows" id="rows-roots"></ul>
</section>
<section class="deck-group" id="group-policies" hidden aria-labelledby="label-policies">
<header class="deck-head bucket-head">
<h3 class="deck-label" id="label-policies">policies</h3>
<button type="button" class="control control-quiet readout" id="policy-add">
add policy
</button>
</header>
<p class="queue-note readout dim">
a policy edit rescores search decks immediately — open any title's releases to check it.
</p>
<ul class="deck-rows" id="rows-policies"></ul>
</section>
</main>
<!--
SERIES DETAIL (§4.1 + §4.2 + §9.3, issue 129): a series row anywhere
opens /series/{id} — the title line carries the audience chip, the
derived status and the wanted/on-disk count (season 0 excluded, per
§4.2). Seasons are collapsible groups: engraved name, the tracked
toggle (the one way an operator wants a season — no bulk backfill,
§4.1), counts, search and deck. Episodes read as tag, title, air date,
state, then the imported file's probed attribute chips — ffprobe truth,
not name claims — with want/search/deck only where they can do
something: an unaired episode offers no search button at all.
-->
<main class="deck releases" id="series" hidden aria-label="series detail">
<header class="releases-head">
<button type="button" class="control" id="series-back">back</button>
<p class="deck-status readout" id="series-status" role="status" hidden></p>
</header>
<section class="module movie-hero" id="series-hero" aria-label="title metadata">
<div class="movie-body">
<img class="movie-poster" id="series-poster" alt="" hidden />
<div class="movie-info">
<div class="movie-idline">
<h2 class="movie-title" id="series-title"></h2>
<span class="movie-year readout dim" id="series-year"></span>
</div>
<!-- audience, episode counts and §4.2 status: operational state,
outranking the artwork, folded into this one header (issue 150) -->
<span class="row-chips movie-chips" id="series-chips"></span>
<p class="movie-rating readout" id="series-rating" hidden></p>
<p class="movie-meta readout dim" id="series-meta" hidden></p>
<p class="movie-tagline" id="series-tagline" hidden></p>
<p class="movie-overview" id="series-overview" hidden></p>
<div class="movie-actions" id="series-actions"></div>
</div>
</div>
</section>
<section class="deck-group" id="series-cast" hidden aria-labelledby="label-series-cast">
<header class="deck-head">
<h3 class="deck-label" id="label-series-cast">cast</h3>
</header>
<ul class="cast-grid" id="rows-series-cast"></ul>
</section>
<ul class="deck-rows seasons" id="rows-seasons"></ul>
</main>
<main class="deck releases" id="tv-releases" hidden aria-label="season and episode releases">
<header class="releases-head">
<button type="button" class="control" id="tv-releases-back">back</button>
<div class="releases-id">
<h2 class="releases-title" id="tv-releases-title"></h2>
<span class="releases-year readout dim" id="tv-releases-sub"></span>
</div>
<button type="button" class="control" id="tv-releases-sweep">search indexers</button>
</header>
<p class="deck-status readout" id="tv-releases-status" role="status" hidden></p>
<div id="tv-buckets"></div>
</main>
<main class="board">
<section class="chain" aria-label="signal chain">
<article class="module area-tmdb" data-check="tmdb">
<header class="module-head">
<span class="lamp" data-state="probing" aria-hidden="true"></span>
<h2 class="module-name">tmdb</h2>
<span class="module-status readout" data-role="status">probing</span>
</header>
<p class="module-role">metadata in</p>
<p class="module-detail readout" data-role="detail">titles, languages, release dates</p>
</article>
<div class="bus area-t1" aria-hidden="true">
<svg viewBox="0 0 40 8" preserveAspectRatio="none" class="bus-trace" data-trace="tmdb" aria-hidden="true">
<line x1="0" y1="4" x2="32" y2="4" />
<path d="M32 0.5 39 4 32 7.5 Z" class="bus-arrow" />
</svg>
</div>
<article class="module area-prowlarr" data-check="prowlarr">
<header class="module-head">
<span class="lamp" data-state="probing" aria-hidden="true"></span>
<h2 class="module-name">prowlarr</h2>
<span class="module-status readout" data-role="status">probing</span>
</header>
<p class="module-role">indexers in</p>
<p class="module-detail readout" data-role="detail">torznab search and rss</p>
</article>
<div class="bus area-t2" aria-hidden="true">
<svg viewBox="0 0 40 8" preserveAspectRatio="none" class="bus-trace" data-trace="prowlarr" aria-hidden="true">
<line x1="0" y1="4" x2="32" y2="4" />
<path d="M32 0.5 39 4 32 7.5 Z" class="bus-arrow" />
</svg>
</div>
<article class="module module-master area-master" data-check="master">
<header class="module-head">
<span class="lamp lamp-big" data-state="probing" aria-hidden="true"></span>
<h2 class="module-name">arr</h2>
<span class="module-status readout" data-role="status">probing</span>
</header>
<p class="module-role">reconcile core</p>
<p class="module-detail readout" data-role="detail">decide · find · fetch · import</p>
</article>
<div class="bus area-out" aria-hidden="true">
<svg viewBox="0 0 40 8" preserveAspectRatio="none" class="bus-trace" data-trace="master" aria-hidden="true">
<line x1="0" y1="4" x2="32" y2="4" />
<path d="M32 0.5 39 4 32 7.5 Z" class="bus-arrow" />
</svg>
</div>
<article class="module area-transmission" data-check="transmission">
<header class="module-head">
<span class="lamp" data-state="probing" aria-hidden="true"></span>
<h2 class="module-name">transmission</h2>
<span class="module-status readout" data-role="status">probing</span>
</header>
<p class="module-role">torrents out</p>
<p class="module-detail readout" data-role="detail">rpc · grab and seed</p>
</article>
</section>
</main>
</body>
</html>