Files
arr/web/index.html
T
Miguel Palhas 7c30928907 feat(web): series, season and episode removal
Series detail exposes DELETE /api/series/{id} through the movie
removePanel, generalised over endpoints and file rollup instead of
copied. Season headers and on-disk episode rows get the settings
armed-delete control wired to #174's file endpoints, worded for what
they do: files go, wanted clears, the row stays listed.

Closes #175

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 17:09:46 +01:00

538 lines
25 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 + 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.
SHELL (§9.7, issue 173): the library is the homepage — "/" renders the
library deck, and the wordmark on the rail is a link home. The signal
chain above is no longer a route: it lives as the first section of
/settings, its lamps still driven by the health poll, and the master
lamp stays on the rail so the one-glance read survives the move. Where
earlier notes here say "returns to the board", read "the library".
-->
<header class="rail">
<div class="rail-inner">
<div class="rail-id">
<span class="lamp" id="master-lamp" data-state="probing" aria-hidden="true"></span>
<a class="wordmark" id="wordmark" href="/">arr</a>
</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>
<div class="rail-meta">
<span class="readout dim" id="version" data-testid="version">v —</span>
</div>
</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. 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-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-chain" aria-labelledby="label-chain">
<header class="deck-head">
<h3 class="deck-label" id="label-chain">signal chain</h3>
</header>
<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>
</section>
<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>
<!-- removal is the one library control a series carries (issue 175):
the same arming panel a movie uses — evidence first, then the
decision — wired to DELETE /api/series/{id} -->
<section class="deck-group" id="series-library" aria-label="library controls">
<header class="deck-head">
<h3 class="deck-label">library</h3>
</header>
<div class="movie-controls">
<span class="movie-controls-space"></span>
<button
type="button"
class="control control-quiet"
id="series-remove"
aria-expanded="false"
aria-controls="series-remove-panel"
>
remove
</button>
</div>
<div class="remove-panel" id="series-remove-panel" hidden></div>
</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">re-search</button>
</header>
<p class="deck-status readout" id="tv-releases-status" role="status" hidden></p>
<div id="tv-buckets"></div>
</main>
</body>
</html>