Commit graph

35 commits

Author SHA1 Message Date
Barely Removable
106bd16de8 Show who requested each show on Admin rows
All checks were successful
Deploy / deploy (push) Successful in 39s
Sonarr has no concept of a requester, so this cross-references Ombi's
TV request list by title to attribute the show to whoever's request
triggered the grab, falling back to "Requester unknown" for anything
added straight in Sonarr or when Ombi can't be reached.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPJxtE9tPURsaWDZ6rC4Dt
2026-09-05 09:55:02 -07:00
Barely Removable
6d093a25d7 Show download date and watch status on Admin rows
All checks were successful
Deploy / deploy (push) Successful in 33s
Surfaces the download-age cutoff data (already computed for the 6-month
filter) directly on each row, plus an explicit "Never watched" label, so
the never-watched call behind the delete button is visible rather than
just trusted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPJxtE9tPURsaWDZ6rC4Dt
2026-09-05 09:38:53 -07:00
Barely Removable
cad4cd0a84 Add Admin section to System: reclaim space from unwatched shows
All checks were successful
Deploy / deploy (push) Successful in 48s
Cross-references Sonarr's per-series disk usage against Tautulli's full
play history to surface the 10 largest shows nobody has ever watched,
excluding anything added in the last 6 months so new downloads aren't
flagged before they've had a chance to be watched. Delete removes the
series from Sonarr along with its episode files and folder, behind the
site's usual two-tap arm/confirm rather than a native dialog.

Also dedupes the never-wired /api/tautulli/never-watched endpoint onto
the same helper, and clears the Sonarr/Radarr list caches on delete so
a removed title doesn't linger in other cards until the TTL expires.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPJxtE9tPURsaWDZ6rC4Dt
2026-09-05 09:32:45 -07:00
Barely Removable
d38ab1b561 Add Approve/Deny verdicts to pending requests on P104
All checks were successful
Deploy / deploy (push) Successful in 55s
Every TV request now needs a decision, so P104 offers the two verdicts
inline on the rows actually waiting rather than sending the visitor to
Ombi's own UI.

Ombi is now the one service the app writes to. Three details the new
ombi_write() helper and /api/ombi/decide route have to get right:
approve is a POST but deny is a PUT; a TV decision acts on the child
request id, not the show, so get_ombi_summary() carries child["id"] for
TV rows and m["id"] for movies; and Ombi answers 200 with
{"result": false, ...} on a logical refusal, so the body is checked
rather than the status code. A successful decision clears _ombi_cache —
the 4-hour TTL would otherwise keep calling the row pending for the
rest of the afternoon.

The buttons reuse the existing two-tap arming and share its armed slot,
so arming one disarms the other: an approval starts a download and a
denial is somebody's answer, neither wants a stray tap. They reverse
into green and red rather than the shared red of .tt-btn.armed, so the
confirming press states which verdict it is about to send. After a
decision the row's status flips in place and the Pending figure and its
"N of M" both settle; at zero the page drops its warn weight, since
nothing waits on a human any more (discipline 5). Below 560px the
verdicts take a full line of their own rather than shrinking below the
tap target.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNc3gd3GRPudFwgPXNEm8A
2026-09-02 09:05:53 -07:00
Barely Removable
8115ed4855 Add active-viewer list to Watch with per-user 48h history drill-down
All checks were successful
Deploy / deploy (push) Successful in 28s
Lists anyone who's watched something in the last 10 days; clicking a
name shows their play history for the last 48 hours.

Bump to v2.1.0 per the just-added CLAUDE.md rule that every commit
should be matched with a version change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kn4ZaFd6RCnbMewMyMsfe
2026-08-31 07:56:18 -07:00
Barely Removable
5dd49c505c Give every fastext destination a distinct mark; add skip-link, fix aria-label
All checks were successful
Deploy / deploy (push) Successful in 23s
Colours: "if the intent is to have them all different colors than
lets have them all different colors." The locked eight only give five
safe as full text (blue fails contrast, black's invisible on black),
so the other three get their own treatments instead of sharing plain
ink: Watch gets blue as a swatch-only accent (the button cap's colour,
not the label), Activity gets an ordered-dither swatch (the other
legal way to mark something without a ninth colour), and Settings
alone keeps plain ink deliberately, as the one page with no live
status of its own.

Accessibility (items 1 and 3 from the design review, item 2 skipped
per request — the header's download figure stays live and unmuted):
- Added a skip-link, off-screen until focused, jumping past the
  8-item top nav straight to each page's #main-content.
- Renamed the nav's aria-label from "Fastext" (meaningless without
  the visual teletext context) to "Page navigation".
2026-08-30 09:24:52 -07:00
Barely Removable
59beb9490f Fix design-review findings: self-linking nav, colour reuse, badge clash
All checks were successful
Deploy / deploy (push) Successful in 24s
From a frontend-design skill review of all pages, after last session's
chrome changes (top nav, all-tabs nav, header download row):

- The top/closing fastext nav now mutes the entry matching the current
  page (.fastext-here) instead of rendering a link to the page you're
  already on.
- Colour in fastext is reserved for the four entries that still carry
  a real keyboard shortcut (r/g/y/c); the other four render in plain
  ink instead of reusing k-red/k-green/k-yellow/k-cyan for a
  destination they don't semantically mean (e.g. Requests no longer
  shows in the app's own off-air red).
- Discover's badge slot meant "critic score" on its RT tiles and
  "release year" on its new search tiles at the same time. Search
  results (Library and Discover) now fold year/held-state into the
  sub-line instead, leaving the badge exclusively for critic score.
- System's Transmission rows now carry a small ↗ glyph, since they
  silently changed from in-app navigation to opening each service's
  own UI in a new tab with no visual cue.
- SABnzbd polling drops from 5s to 20s on pages that only show the
  header's download figure (everywhere but System/Activity, which
  keep 5s for their own live figures) — a number idle 95% of the time
  doesn't need second-level freshness sitewide.
- DESIGN.md's fastext description, now-current per this session's nav
  changes.
2026-08-30 09:11:59 -07:00
Barely Removable
d672c3f02d Show SABnzbd's queue in the header on every page
All checks were successful
Deploy / deploy (push) Successful in 21s
Adds a live download figure next to the header clock, present on
every page (not just Activity/System). It's populated by the same
/api/sabnzbd poll those pages already use, extended to run
unconditionally and to fill in a new #head-dl span — never a
server-rendered fetch, so an unreachable SABnzbd can't add latency to
pages that don't otherwise touch it. Quiet ink at "0 IN QUEUE" when
idle, live cyan with a speed reading once something's moving.
2026-08-30 08:54:41 -07:00
Barely Removable
cef66e7361 Search Sonarr/Radarr directly on Discover, full top nav, live System links
All checks were successful
Deploy / deploy (push) Successful in 24s
- Discover's search now hits Radarr's/Sonarr's own lookup endpoints
  directly (the same ones /add-to-radarr and /add-to-sonarr already
  use), instead of Ombi — one fewer round trip, and a result always
  resolves the same way its own Add button will. The Ombi-based
  ombi_search/_tmdb_poster helpers are gone.
- The top nav on every page but the index now lists all eight
  destinations (FULL_FASTEXT) instead of the original four; the index
  keeps the original four-entry row as its own closing line
  (HOME_FASTEXT), since its body already lists every destination once.
- System page's Transmission rows are now links to each service's own
  reverse-proxied instance (service_urls, already injected into every
  template but unused until now).
2026-08-30 08:40:52 -07:00
Barely Removable
abaccaaacd Move fastext nav to page top, fix Settings header, add working search
All checks were successful
Deploy / deploy (push) Successful in 23s
- Fastext colour keys now lead every page but the index, since they're
  the only nav those pages carry; the index keeps them as its closing
  row since its own body is already the full nav.
- Settings page was missing page="settings" in its render_template
  call, so its header silently fell back to "P100 Index".
- Library's search form posted a q param the route never read; it now
  filters the cached Sonarr/Radarr lists and shows matching tiles.
- Discover gained a search bar that queries Ombi (already in front of
  TMDB/TVDB) and lets you add a result straight to Radarr/Sonarr,
  showing "Held" instead of a button for anything already in library.
- Fixed the Add button's success check, which read d.ok against a
  response that actually sets d.success — it reported FAILED even
  when the add worked.
2026-08-30 08:26:47 -07:00
Barely Removable
08491c0c9b Fix System page crash, retire "held" copy, and slim CLAUDE.md
All checks were successful
Deploy / deploy (push) Successful in 27s
- system.html crashed with a 500 whenever SABnzbd history loaded: the
  trend() macro compared the formatted "avg_speed" string (e.g. "900
  KB/s") against a number. Added a real numeric avg_speed_kbps field
  and pointed the macro at that instead.
- Replaced "held"/"not held" wording with "in library"/"not in
  library" on Home and Discover.
- Moved CLAUDE.md's Deployment/reverse-proxy and account-management
  sections into two on-demand skills (deploy-arr-summary,
  manage-accounts) so they only load when actually relevant, with a
  one-line pointer left in their place.
- Updated PRODUCT.md's restructure note now that meridian-marquee has
  merged to main.
- Dropped two stale/broken entries from settings.local.json's
  permission allowlist (a rule from a different machine/path, and one
  with a corrupted token).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBw1QodG8ZMYHrGeQfqyDS
2026-08-30 08:05:49 -07:00
Barely Removable
bc7779e590 Fix the finish review's 8 material findings on the teletext redesign
Applies the fix round from the finish-reviewer's disposition on commit
4b19cbd, in one batch:

- System's disk table is unreadable below 560px: horizontal-scrolling a
  6-column table hid the usage bar and the temperature, the two
  readings that matter most (discipline 3 — one shared ruler — was
  unauditable as a result). Added a disk_row() macro and a phone-only
  .tt-disk-rows reflow (name + full 20-cell bar + temp on one line,
  device/free/total beneath), CSS-toggled against the existing table
  at the same breakpoint.
- Contrast failure on the highest-severity state: white text on the
  red .tt-field.crit/.tt-toast.error/.tt-btn.armed reversals measured
  4.0:1, under the 4.5:1 floor. Switched to black text (5.25:1),
  matching the black-on-yellow .warn pattern already in use — and
  fixed a second instance of the same bug: .tt-field.crit had no
  .tt-field-meta override, unlike .warn.
- Two type families where the direction contract names one blocky
  monospace: unified on a single self-hosted face (Space Mono, one
  weight, no bold) for both body and the double-height row.
- FIRST VIEWPORT proportion: an unpromised 6-item Just Added grid was
  competing with NOW SHOWING for the fold instead of the contract's
  "lower two-thirds in double-height cyan." Cut to a fixed 3-item,
  fixed-3-column row, which also fixes the orphaned-tile grid defect
  as a side effect (a fixed count can't leave a lone tile beside empty
  tracks).
- Three flat mid-grey fills contradicted discipline 1 (no in-between
  tone): .tt-tile-art, .tt-btn:disabled, and .tt-err pre all painted a
  solid --ink-dimmer background. Centralized the dither .tt-tile-art
  already used correctly into a shared --dither-mid generator and
  applied it to all three.
- Google Fonts dependency on a LAN-only home server: a dropped webfont
  request was a silent identity change, not a graceful fallback.
  Self-hosted Space Mono under static/fonts/ via @font-face; dropped
  the <link> tags from _shell.html and login.html.
- Deliberately deferred: "pages differ in stock" (discipline 5) still
  fires on only one band service-wide. Every other finding was a
  bounded fix to existing code; this one asks for a new mechanism (an
  aggregate status band on P100) rather than a correction, so it's
  flagged for a follow-up rather than rushed into this batch.

Recaptured desktop.png, mobile.png, desktop-system.png and
mobile-system.png (the four files the reviewer's findings cited)
through the same true-390px iframe method as the prior round, and
returned them for the fix round's verdict pass.

Known follow-up: DESIGN.md's typography section (written by the
documenter before this batch) still names the old two-family pairing
and needs a re-run now that the font system changed under it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBw1QodG8ZMYHrGeQfqyDS
2026-08-29 21:18:11 -07:00
Barely Removable
4b19cbdfb2 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
2026-08-29 21:00:45 -07:00
Barely Removable
395638aaf2 Surface parity temp in the Storage header, enlarge the settings cog
Parity went back into the GraphQL query, but for its temperature only —
it still gets no table row, since it has no filesystem to measure. The
temp rides in the card header next to the array summary, which was the
one genuinely useful thing the old parity row carried: a parity drive
cooking away is worth seeing. Multiple parity drives each get a badge,
and .meta-parity keeps the label and badge from wrapping apart.

The settings cog was a speck. The button was always a full 44px --tap
target, but the glyph inside it was set at --fs-sm (13px), so the
affordance read as far smaller than the hit area actually was. Now
--fs-xl (24px) with line-height 1 — roughly 55% of the button, which is
the usual proportion for an icon button. .icon-btn has exactly one user
(the cog), so nothing else shifts.

Verified at 1400px and 375px: no page overflow, the cog still measures
44x44, and the parity badge stays visible and intact on both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 13:19:17 -07:00
Barely Removable
5ef2502d4e Report disk sizes in the right unit and drop parity from the card
Two fixes to the Storage card.

Unraid's GraphQL reports fsSize/fsFree in KB, not bytes — the schema is
explicit about it ("(KB) Total Size of the FS"). We were passing those
straight to fmt_bytes as if they were bytes, so every figure came out
1024x too small and picked the unit below the right one: a 13 TB disk
rendered as "13.0 GB" and a 64.8 TB array as "64.8 GB". _fs_bytes() now
does the conversion at the point of reading, so everything downstream
still deals in bytes. The shutil fallback already returns real bytes and
deliberately doesn't go through it.

Parity is gone from the table and is no longer requested at all. It
mirrors the array rather than holding a filesystem — the schema notes
fsSize is "Not present on Parity type drive" — so it could only ever
render as a name, three em-dashes and a temperature, which earned little
for a whole row. Worth knowing: its temperature is no longer surfaced
anywhere, so a hot parity drive won't show up here.

Verified against the raw KB shape the API actually returns: an array of
3.7/11.2/13.0 TB disks now totals 27.9 TB rather than 27.9 GB, the sub-TB
cache still correctly reads in GB, and the local fallback still reports
this machine's real 228.3 GB rather than a 1024x-inflated figure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 13:15:42 -07:00
Barely Removable
8c6992035f Add per-user logins alongside the shared password
The dashboard had one shared password and no concept of a user. This
adds real accounts: a username field on the login page, per-user
credentials, and the signed-in user recorded on the session, which is
the groundwork for per-user permissions later.

Credentials are salted pbkdf2 hashes in settings.json's `users` map.
Three deliberate choices there:

- settings.json rather than an env var, because a pbkdf2 hash contains
  '$' and docker compose interpolates that when it reads .env. It's also
  already the bind-mounted file that survives `up -d --build`.
- pbkdf2 rather than Werkzeug's default scrypt, which needs a hashlib
  built against OpenSSL — the macOS system Python has no hashlib.scrypt
  and would raise at hash time.
- load_users() re-reads the file per check, so adding a user or changing
  a password is live in every gunicorn worker with no restart. The old
  _runtime_password was per-worker and only ever half-applied.

Multi-user activates only once an account exists. Until then the
original LOGIN_PASSWORD path is used unchanged and the username field is
hidden, so deploying this can't lock the existing install out.
manage_users.py prompts for passwords via getpass — never echoed, never
a CLI argument — and only ever writes hashes; it refuses to remove the
last account, which would silently drop the dashboard back to the shared
password.

Two fixes carried along: login no longer requires the LOGIN_PASSWORD env
var specifically, so a settings.json-only password works instead of
locking you out; and the legacy comparison now uses hmac.compare_digest,
which the old docstring claimed but '==' didn't provide.

Also gitignores settings.json and data/ — they hold API keys and now
password hashes, were excluded from the deploy rsync but not from git.

Verified: legacy fallback still signs in with no accounts configured; a
9-case credential matrix (right/wrong user x right/wrong password, plus
the retired shared password); per-user change-password scoped to the
signed-in account and live without restart; and the add/passwd/remove
guards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 13:03:21 -07:00
Barely Removable
2a4236efea Drop a redundant inline margin from the Storage fallback note
All checks were successful
Deploy / deploy (push) Successful in 24s
The container-disk-usage note carried style="margin-bottom:0", which
326ab6c had already covered with `.meter-legend + .empty`. The <p> is
that rule's adjacent sibling in the rendered output (the Jinja {% if %}
around it emits no element), so the inline copy was doing nothing and
violated CLAUDE.md's no-static-inline-style rule for no benefit.

Verified the computed margin-bottom is still 0 with the attribute gone.
The eight remaining inline styles are the kinds the rule allows —
Jinja-computed meter widths and threshold colours, and display:none on
elements dashboard.js reveals.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 08:11:27 -07:00
Barely Removable
3c931873b1 Keep the status pills on exactly one row at every width
All checks were successful
Deploy / deploy (push) Successful in 21s
The strip wrapped to two or three rows on narrow screens. It's now
nowrap with flex: 1 1 0 pills, so all six share the row equally and
shrink together instead of spilling onto a second line.

Six pills can't hold full detail in 375px, so detail degrades inside
the pill rather than the strip breaking: the stat drops at 1024px (the
dot already carries up/down), then below 560px the name swaps to a
short label — Son, Rad, Prow, SAB, Taut, Ombi. Without that swap the
names ellipsised to ~23px, about two characters, which identifies
nothing. The full name stays in the DOM at font-size: 0 so screen
readers still announce it, and it's on the pill's title attribute.

The dot and the pill itself never go, so no service is ever hidden —
which was the point of the old wrapping rule.

Verified one row with nothing clipped and no page overflow at 320, 375,
430, 560, 620, 700, 820, 1024, 1100 and 1400px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 08:06:31 -07:00
Barely Removable
377524c037 Move the service pills out of the header into a Status section
All checks were successful
Deploy / deploy (push) Successful in 22s
The pills occupied a second full-width row of the header at every width,
which is a lot of permanent chrome for something you glance at. They now
open <main> as their own "Status" section, directly above Right now —
where you'd look for them, and consistent with every other band on the
page carrying a heading.

The header drops to a single row (brand left, clock and settings right),
so it no longer needs flex-wrap or the order: 1/2/3 shuffle that kept
the pill row beneath the brand. Pills go to var(--tap) height now
they're content rather than header furniture, and left-align with every
other section instead of centring.

Behaviour is unchanged: the pills keep their svc-<key> ids, so the
polling in dashboard.js needed no edit, and they still wrap rather than
scroll so no service is ever hidden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 07:59:26 -07:00
Barely Removable
08c71ba68d Give Storage its own row and rebuild the disk list as a table
All checks were successful
Deploy / deploy (push) Successful in 23s
Ten disks stacked at --row-h each turned one third of the "Right now"
strip into a ~950px tower. The row is a grid, so Now playing and
Downloads stretched to match and held roughly 1500px of empty card
between them.

The information needed width, not a shorter list. "Right now" drops to
two columns and Storage moves to a full-width row beneath it, where the
same ten disks are a six-column table — disk, used, size, free, usage
bar, temp — at ~270px total. Nothing is hidden and nothing scrolls
except the table itself, which gets its own overflow container below
720px rather than wrapping the sizes into mush.

Also: the array total moves to a bar above the table and the summary
into the card header, so the hero figure isn't repeating what every row
already says. The header summary is a sentence rather than a badge, so
it needs .card-meta-wrap to opt out of the global nowrap — without it
that one string pushed the whole page sideways on a phone.

Local-fallback (no per-disk data) and error states keep the previous
hero-figure treatment. CLAUDE.md now carries the dead-space rule this
came from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 06:52:34 -07:00
Barely Removable
326ab6cee5 Move remaining static inline styles into dashboard.css, bump Now Playing text size
Three inline style="..." attributes were pure styling with no per-request
data behind them (a hero-block spacing tweak, a trailing note's margin, a
gap between two stacked rows) — moved each into dashboard.css as a scoped
structural selector so future adjustments happen in one place instead of
being split between the template and the stylesheet.

Also bumped .stream-title/.stream-user/.stream-icon in the Now Playing
card one step up the existing --fs-* scale (13px->15px, 12px->13px,
15px->18px) rather than introducing a one-off size.

Documented the "styles belong in CSS, not inline" rule in CLAUDE.md,
with the actual exception: per-request computed values (a meter's width,
a threshold colour) and JS-toggled initial visibility still can't live
in a static stylesheet and are fine to stay inline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 06:27:43 -07:00
Barely Removable
5045316fa5 Match Ombi's Show more button to the request pill height
The button used the default 38px .show-more height, shorter than the
68px (--row-h) request pills above it, so it read as a smaller
tacked-on control rather than one more row in the list. Scoped to a
new .show-more-row modifier rather than changing .show-more globally —
the Discover cards' Show more buttons sit under much taller RT items
and are correctly compact at 38px.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 06:19:41 -07:00
Barely Removable
bd292fbe32 Rebuild request rows to match indexer rows
The Requests and Indexers cards sit side by side but read as two
unrelated lists: requests led with a type badge, indexers with a status
dot, and the rows drifted out of alignment once the share bar landed.

Give a request row the same anatomy as an indexer row — status dot,
title over a dimmed detail line, badge on the right — folding the type
into the detail line and mapping request status onto the existing
dot colours (a new .warn light covers pending). Both row types now
share a --row-h floor so the two cards line up row for row; the share
bar's top margin is trimmed so an indexer row reaches that height
without overshooting it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 23:02:36 -07:00
Barely Removable
909a0b568c Show per-indexer grab share and rebuild the Indexers card as a list
All checks were successful
Deploy / deploy (push) Successful in 26s
The Indexers card sat beside Requests as a stretched, vertically centred
grid of pills — mostly dead space once the Ombi list was expanded, and it
said nothing about which indexer was actually doing the work.

- Fetch Prowlarr's indexerstats over a rolling 30-day window and merge
  per-indexer queries/grabs into the summary. Cached 10 min on success and
  never on failure, so a stats outage can't blank the column after it
  recovers or take the up/down list down with it.
- Rank indexers busiest-first by grab share (query share as fallback);
  queries mostly count enabled indexers, grabs count who delivered.
- One indexer per line, matched to the Ombi request rows: same padding,
  gap, radius and two-line body, plus a share bar scaled to the busiest
  indexer and an Online/Offline badge the 30s poll keeps current.
- Let that row size cards to their own content instead of stretching.

Bump APP_VERSION to 1.2.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 22:28:39 -07:00
Barely Removable
18c7719cd7 Extract CSS/JS to static files, wrap header pills, add version footer
All checks were successful
Deploy / deploy (push) Successful in 23s
The palette lived in a :root block duplicated across all three
templates, so every reshade meant three edits — and drifted twice in
practice (the favicon hardcoded its own hex; stale danger-border rgba
literals survived two palette changes). Move tokens to a single
static/css/base.css, derive every dim/border tint from the four source
hues with color-mix, and point the favicon at one static/favicon.svg.

Also splits the 1558-line dashboard.html into template + static assets:
662 lines of CSS and 396 of JS were re-sent uninlined on every load
with no browser caching, which costs real time on the remote/cellular
access this app is built for. Templates drop to 501/42/107 lines.
Server-rendered config (timezone, script_root) now arrives via
<body data-*>; wiring URL_BASE to request.script_root also fixes /api/*
fetches for a future subpath mount, which the hardcoded "" would break.

Header service pills now wrap instead of scrolling — they were clipped
at narrow widths, and a hidden pill is a service whose status you
cannot see. Verified 0 clipped and no page overflow at 320/375/700/1280.

Footer gains a version and date so the running build is identifiable
without trusting the Unraid Docker tab's cached container IDs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 14:27:52 -07:00
Barely Removable
faf25a811a Make Watch insights cards match in height
All checks were successful
Deploy / deploy (push) Successful in 22s
Most played rows carry a two-line title+username layout while Top
viewers/TV/movies rows are single-line, so the two card rows ended up
visibly different heights even though each pair matched within its own
row. Give .top-item a fixed min-height so every row — and therefore
every card, all four hold exactly 3 items — lines up the same.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 14:04:47 -07:00
Barely Removable
0d63339490 Keep Requests/Indexers cards always equal height
All checks were successful
Deploy / deploy (push) Successful in 23s
Revert to the default grid stretch for this row so Ombi and Prowlarr
always match height exactly, and center the indexer grid vertically
within the taller Prowlarr card so any leftover space splits evenly
top/bottom instead of collecting as dead space at the bottom.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 14:03:26 -07:00
Barely Removable
ecf3209abd Enlarge Prowlarr indexer pills
All checks were successful
Deploy / deploy (push) Successful in 21s
Bigger padding, font size, and status dot, plus a wider minimum column,
so the indexer grid reads less cramped next to the Ombi request list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 14:01:17 -07:00
Barely Removable
2e58d9e26f Cap Ombi requests to 5 with a Show more toggle
Some checks failed
Deploy / deploy (push) Has been cancelled
An unbounded request list made the Ombi card grow far taller than the
Prowlarr indexer grid beside it, leaving the row looking lopsided even
after the earlier align-items fix. Reuse the same show-more pattern
already used for the Rotten Tomatoes lists instead of an internal
scrollbar, so both cards land at a comparable height.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 14:00:54 -07:00
Barely Removable
bf10f438d2 Rework dashboard color palette and fix Prowlarr card layout
All checks were successful
Deploy / deploy (push) Successful in 25s
Switch from the warm graphite/orange theme to a cooler charcoal/indigo
palette with a red (not pink/purple) danger color, and restyle error
banners to sit on the normal card surface with an accent stripe instead
of washing the whole box in color. Also fix the Requests/Indexers row
stretching the Prowlarr card to match Ombi's longer request list,
leaving a large blank area under the indexer grid.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 13:26:56 -07:00
Barely Removable
018bc58bbf Switch palette to graphite & orange
All checks were successful
Deploy / deploy (push) Successful in 27s
Replaces the slate/indigo palette with a warmer graphite base and an
orange accent (#e07a35), per user's pick from a side-by-side comparison
of five directions. Same token structure, same contrast bar (all pairs
still >= 4.5:1) — only the hex values changed, across dashboard, login
and settings plus the three embedded favicons.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 08:20:40 -07:00
Barely Removable
339b9f3310 Redesign dashboard: single neutral theme, urgency-ordered layout
Replaces the five novelty themes with one professional dark theme built
on a shared token set (surfaces, 6-step type scale, semantic colours,
44px tap target) used across dashboard, login and settings.

Layout now matches the cache tiers in app.py: a "Right now" strip with
live streams, the download queue and array usage sits above the fold,
with library, discovery, requests and watch insights below. Previously
the page opened on rarely-actionable disk stats and buried active
streams under twenty recommendation rows.

Also from the design review:
- Discover lists show 5 with a Show more toggle instead of 10 each
- RT descriptions render inline and clamped, not hover-only tooltips
  that touch devices could never reach
- Adding a title is now two-step (tap to arm, tap to confirm) since it
  queues a real download
- role=alert on errors, aria-live on polled regions, semantic dl/dt/dd
  stats, labelled search inputs
- every text/surface pair verified at >= 5:1 contrast

Drops the dead delete-modal JS, which threw a TypeError on every load,
and the stale root-level template copies that Flask never rendered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 08:12:41 -07:00
Barely Removable
951914dd69 Redact API keys from error technical-details disclosure
All checks were successful
Deploy / deploy (push) Successful in 27s
SABnzbd and Tautulli embed their API key in the request URL, so it
was showing up verbatim in the raw exception text under "Technical
details" on the dashboard — visible to anyone with the study-room
password. Added a redact_secrets filter that strips apikey/token/
password query params before the raw string is rendered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 05:11:28 -07:00
Barely Removable
2e1f8efa33 Fix unreachable mobile Settings link and raw error tracebacks
All checks were successful
Deploy / deploy (push) Successful in 2m6s
Settings lived inside .header-right, which is hidden below 600px,
leaving no way to reach it on mobile. It's now a standalone 44x44px
button next to the title.

Dashboard error cards showed raw Python/urllib3 exception strings.
Added a friendly_error() filter that classifies the failure (timeout,
connection refused, DNS, auth, 4xx/5xx, TLS) into plain-English copy,
with the raw string still available behind a details disclosure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 23:59:07 -07:00
c1e30bc8f0 Initial commit: Arr Summary source from server 2026-08-25 22:59:31 -07:00