Commit Graph
1312 Commits
Author SHA1 Message Date
peter e395fb816d release: 0.62.2 (bug/download-worker-redos-hotfix) 2026-08-12 18:50:02 +02:00
peter 4af919ac4d chore: bump VERSION 0.62.2 + release notes (download ReDoS + Reddit thumbnail hotfix) 2026-08-12 18:49:42 +02:00
peter fea6f55d4f refactor(downloads): address /code-review on the download hotfix
- edit.py: extract _run_ffmpeg_best_effort (one run+timeout+swallow policy shared by
  thumbnail_to_jpg / ensure_poster / ensure_storyboard) + a build_thumbnail_jpg_cmd
  builder, matching the module's other ffmpeg commands.
- thumbnail_to_jpg: copy an already-JPEG thumbnail (Facebook, and Reddit's jpeg-under-
  a-.png-name) instead of re-encoding it; only webp/png-proper are decoded via ffmpeg.
- errors.py: drop the redundant "account authentication" classify marker
  ("authentication is required" already covers the known Reddit message).
- formats.py: document that the opt-in EmbedThumbnail is unaffected by dropping the
  convertor (it converts unsupported thumbs to PNG, not the crashing mjpeg2jpeg path).
- test_titles.py: guard the ReDoS regression with a thread + timeout (deterministic
  "must not hang") instead of a wall-clock assertion.
2026-08-12 18:47:01 +02:00
peter 37cca723fc fix(downloads): stop a ReDoS title and a fatal thumbnail convert from wedging/killing downloads
Three prod-observed download bugs, root-caused from a frozen prod queue with the
worker pinned at 100% CPU:

- ReDoS in normalize_title: _TRAILING_HASHTAGS had two overlapping unbounded
  quantifiers around the required letter, so a Facebook title with a long hashtag
  cluster that is NOT at the end ("... #a #b #c | Page") backtracked catastrophically.
  It runs inside the yt-dlp progress hook holding the GIL, so one such download froze
  the whole worker (and could stall feed enrichment too - sync/videos.py calls it).
  Rewrite with a zero-width "has a letter" lookahead + a possessive tag body: one
  unambiguous split, nothing to backtrack. 40-tag stress case: ~0.1ms.

- Reddit download killed by a cosmetic thumbnail convert: yt-dlp's
  FFmpegThumbnailsConvertor runs "-bsf:v mjpeg2jpeg" on a JPEG-served-under-a-.png-name
  (Reddit) and fails; being a postprocessor, it failed the WHOLE download even though
  the media was fine. Drop it and produce the <base>.jpg poster/og:image ourselves via
  editmod.thumbnail_to_jpg (plain ffmpeg decode->mjpeg, best-effort -> ensure_poster
  cuts a frame on failure). The FB Messenger / MS Teams share preview keeps working
  (posters verified as real JPEGs).

- classify: map "authentication is required" (Reddit's auth wall) to login_required
  instead of unknown, so the UI explains it rather than offering a pointless retry.

Tests: ReDoS-timing + numeric-marker regression, the Reddit classify case, and
thumbnail_to_jpg (real JPEG out / None on bad input, never raises).
2026-08-12 18:27:35 +02:00
peter 6883fedc1b release: 0.62.1 (r12-plex-layering) 2026-08-12 02:01:04 +02:00
peter d9a12144dc chore: bump VERSION 0.62.1 + release notes (R12 S4a PlexPlayer refactor) 2026-08-12 01:58:48 +02:00
peter 4c3ae00492 refactor(plex): address /code-review on the S4a decomposition
Two behaviour-equivalent cleanups from the review:
- Drop the redundant per-render `audioRef.current = audioOrd` mirror in
  PlexPlayer: the only two setAudioOrd callers (usePlexTracks.changeAudio
  + the detail effect) already set audioRef synchronously, so the mirror
  never carried a value they hadn't written.
- Narrow the time-tracking effect deps from [id, patchPrefs] to [id];
  patchPrefs was a vestigial, unused dependency (eslint confirms [id] is
  the complete set).

The third finding (usePlayerVolume duplicating playerVolume.ts) is the
tracked R12 volume-consolidation sprint, not a review fix.
2026-08-12 01:52:33 +02:00
peter f86313eaa9 refactor(plex): extract PlexPlayer track selection into usePlexTracks
S4a-4a of the PlexPlayer decomposition. Move audio/subtitle track
selection + subtitle rendering into usePlexTracks: the selected-ordinal
state, change/cycle audio+subtitle, the debounced sub-offset, the audio
A/V-sync offset, and the two subtitle effects (force-showing the mounted
track + positioning cues at the user's subPos). The shared media refs
(video/hls/audio/aoff/abs/multiAudio) stay component-owned and are passed
in, since loadSession + the detail effect write audioRef too; absRef is
hoisted above the hook call for that. No behaviour change; verified by
self-E2E (subtitle on/off via 'S' mounts/removes the <track>, mode
'showing', VTT 804 cues all positioned to line=subPos, subLang persists;
playback uninterrupted). Audio switch is single-track here so cycleAudio
correctly no-ops.
2026-08-12 01:21:20 +02:00
peter 521d2aeaa8 refactor(plex): extract PlexPlayer auto-skip into useAutoSkip
S4a-3b of the PlexPlayer decomposition. Move the intro/credits auto-skip
machinery (activeMarker detection, the skipProgress countdown, doSkip,
cancelAutoSkip + the running-interval and reset-on-id effects) into
useAutoSkip. The hook returns skipProgressRef / cancelAutoSkipRef — the
forward-declaration bridge the earlier keyboard/back handlers read to
cancel a running countdown; those two stable refs are now listed in the
backIntent/keyboard dep arrays (they were provably-stable local refs
before, params now). No behaviour change; verified by self-E2E (credits
marker detected -> countdown fill renders + advances while playing ->
auto-skip fires -> item marked Watched + player closes).
2026-08-12 00:46:54 +02:00
peter 68f4834695 refactor(plex): extract PlexPlayer progress + volume hooks
S4a-3a of the PlexPlayer decomposition. Move two byte-equivalent
behaviours into lib/plexPlayerHooks.ts: usePlexProgress (the 10s
interval flush + pause/pagehide beacon + reopen cache-patch checkpoint;
absRef stays in the component since seek/skip/ended mutate it too) and
usePlayerVolume (HTML5 <video> 0..1 volume + independent mute, persisted
+ re-applied per session). usePlayerVolume is the seam the later R12
volume-consolidation sprint will rework to share playerVolume.ts's
tested pure core with the YouTube player. No behaviour change; verified
by self-E2E (volume-bar sets + persists video.volume, 'm' toggles mute
keeping the level, pause fires plexProgress 200).
2026-08-12 00:32:44 +02:00
peter c7a69596ad refactor(plex): extract PlexPlayer leaf hooks (clock/fullscreen/chrome)
S4a-2 of the PlexPlayer decomposition. Move three self-contained,
byte-equivalent behaviours into lib/plexPlayerHooks.ts: useWallClock
(15s top-bar clock), useFullscreen (wrapper fs toggle + reactive flag),
and useControlsVisibility (auto-hiding control bar + wake). The ref
mirrors the hooks read (barAutoHide/menuOpen/video-paused) stay in the
component and are passed in, so the timer sees fresh state. No behaviour
change; verified by self-E2E (real HLS playback: clock renders, controls
auto-hide after 3s while playing, wake restores + stays while paused).
2026-08-12 00:15:41 +02:00
peter a0e9912886 refactor(plex): extract PlexPlayer pure helpers + control primitives
S4a-1 of the PlexPlayer decomposition. Move the module-level pure
helpers (prefs types/defaults, track-language resolution, subtitle
shadow CSS, wall-clock formatting, HLS_SUB_LAG, fmt) to
lib/plexPlayerPrefs.ts and the presentational building blocks
(Ctrl/Slider/Toggle/PanelHead) to plexPlayerControls.tsx. No hooks or
behaviour touched; PlexPlayer.tsx 1889->1661.
2026-08-12 00:06:21 +02:00
peter dd2118a547 refactor: address /code-review — rename uploader_url() to build_uploader_url()
The R12 S3 underscore-publish left worker.uploader_url() sharing a name with the
`uploader_url` model attribute and the yt-dlp `"uploader_url"` info-dict key. The
function is now build_uploader_url() — the attribute, dict key, log line and
docstrings keep the plain `uploader_url` name. Rename touches only the def, its
two call sites, and the backfill import; no behaviour change (ruff clean,
pytest 193).
2026-08-11 23:50:49 +02:00
peter c74f69babc refactor(plex): dedupe show/season heroes into DetailHero + WatchedToggle
R12 S4b. PlexShowView and PlexSeasonView were copy-paste siblings — the same
glass hero frame, play/resume poster button, customize-menu slot, and the
mark-watched action button, duplicated verbatim. Two shared components now own
that structure:

- DetailHero: the hero shell (frame + poster button + customize slot + info
  column via children); the season page passes a smaller poster size.
- WatchedToggle: the mark-whole-show/season watched action (icon + label flip);
  the differing labels are props.

Single source of truth for the hero — a future hero change touches one place
instead of two. Gate green (tsc / eslint 0 errors / prettier / knip / vitest 79)
+ self-E2E: the show detail and season detail pages both render their hero,
metadata, actions and watched-toggle identically to before (screenshot-verified;
the stale [hmr] PlexBrowse errors in the console buffer are from an intermediate
edit — the fresh Vite server logs clean).
2026-08-11 23:38:57 +02:00
peter 69d34f7fa3 refactor(channels): move cells + column defs into channelColumns.tsx
R12 S4c. Channels.tsx shed its ~150-line column block and 3 cell components
into the co-located channelColumns.tsx:

- SyncBadge / SyncCell / ActionsCell move verbatim (prop-driven, self-contained).
- the column array becomes a buildChannelColumns(deps) factory next to
  subsColumn — the manager owns the state/mutations and passes them in; the
  inline patch.mutate wrappers become onHide/onDeep callbacks so the factory
  doesn't couple to the mutation type.

Channels.tsx 905 -> 628 lines; no behaviour change. Gate green (tsc / eslint 0
errors / prettier / knip / vitest 79) + self-E2E: the manager renders every
column and cell (priority stepper, actions, sync badges, tags) with no console
errors.
2026-08-11 23:27:16 +02:00
peter 51463b226e refactor: R12 S3 — invert scheduler->routes, publish cross-module privates
Layering cleanup, no behaviour change (ruff clean backend+scripts, pytest 193):

- Invert the scheduler->routes dependency: reset_demo_state (+ its demo-seed
  helpers) move from routes/admin.py to a new service module app/demo.py.
  Both routes/admin.py and scheduler.py now import it from app.demo, so the
  scheduler no longer reaches into the routes layer — its lazy import (the
  routes<->scheduler cycle workaround) is gone, hoisted to a top-level import.

- Publish the private symbols that were imported across module boundaries (the
  underscore convention was lying): feed._serialize -> serialize,
  feed._to_tsquery_str -> to_tsquery_str, videos._insert_stubs -> insert_stubs,
  videos._stub_from_playlist_item -> stub_from_playlist_item,
  worker._uploader_url -> uploader_url, plex._shared._item_or_404 ->
  item_or_404. Scoped renames — the 5 other route modules keep their own truly
  private _serialize helpers.

- _release_asset moves from routes/downloads.py to app/downloads/service.py as
  the public release_asset (asset bookkeeping is a service concern); the routes
  call it via service.release_asset.
2026-08-11 22:58:10 +02:00
peter ac6383e92c refactor(auth): address /code-review on the S2 split
Cleanup only, no behaviour change (ruff clean + pytest 193):
- keep the whole OAuth scope vocabulary together in app/auth.py: the bundles
  (BASE_SCOPES/READ_SCOPES/WRITE_SCOPES/_YOUTUBE_SCOPES) move back next to the
  atoms (READ_SCOPE/WRITE_SCOPE) as core; routes/auth.py imports the bundles it
  needs instead of rebuilding them from the atoms.
- compact the const groups the split generator spread out (the six rate
  limiters, the TTLs, the demo/scope constants) back into blocks.
2026-08-11 22:43:33 +02:00
peter 1b07d54663 refactor(auth): split app/auth.py — endpoints to app/routes/auth.py
R12 S2. app/auth.py (1154 lines) mixed the core auth surface with 18 HTTP
endpoints on one /auth router. Split by dependency graph:

- app/auth.py (223 lines) keeps ONLY the core surface other modules import
  (current_user, resolved_user_id, admin_user, require_*, has_*_scope,
  purge_user, count_admins, get_or_create_demo_user, google_enabled,
  is_allowed, operator_contact, optional_current_user) plus everything it
  transitively depends on.
- app/routes/auth.py (883 lines, new) gets the router + all 18 endpoints +
  their endpoint-only helpers, importing the core surface from app.auth.
- main.py includes auth_routes.router (was app.auth.router).
- test_oauth_redirect imports _oauth_redirect_uri from its new home.

The STAY/MOVE partition is driven by an AST scan of the COMPLETE external
surface (every `from app.auth import` incl. multiline + `from app import
auth; auth.X`), so no still-referenced symbol was moved. Behaviour-equivalent:
every symbol relocated verbatim (get_source_segment equality), route set
proven identical, no undefined names (symtable sweep).

Gate: ruff clean (backend + scripts), pytest 193 passed.
2026-08-11 22:01:14 +02:00
peter f484de1008 refactor(plex): address /code-review on the R12 S1 split
Cleanup only, no behaviour change (ruff clean + pytest 193):
- imports hoisted above `router =` and grouped/sorted (isort) across the split
  files; ruff-format pass restores the 2-blank-line PEP8 spacing the generator
  collapsed to 1.
- the shared on-disk cache root moves to a neutral app/plex/cache.py, so
  subtitles.py no longer imports it from imgcache.py (drops the cross-import).
- the former plex.py module docstring's access-model note (read endpoints open
  to any authed user; admin endpoints test/sync) is restored in the package
  __init__.py.
2026-08-11 21:46:43 +02:00
peter 99befa6e38 refactor(plex): split routes/plex.py into an admin/browse/playback package
R12 S1 step B. The 2011-line routes/plex.py becomes an app/routes/plex/
package:

- admin.py     — connection test, catalog sync, owner watch-link config (5)
- browse.py    — libraries, facets, collections, playlists, show/item, people (23)
- playback.py  — image/subtitle proxies, HLS streaming, watch-state writes (10)
- _shared.py   — the two genuinely cross-module helpers (_item_or_404, log)
- __init__.py  — assembles `router` from the three sub-routers; the app wiring
                 (plex_routes.router in main.py) is unchanged.

Mechanically generated and proven behaviour-equivalent: every function/class
source relocated verbatim (get_source_segment equality) and the (method, path)
route set is identical to before. Endpoints keep the /api/plex prefix on each
sub-router. No SQL change (db.query untouched; 2.0 select() is the next step).

Gate: ruff clean, pytest 193 passed (pure-logic + DB lane).
2026-08-11 21:30:01 +02:00
peter 4106bceb08 refactor(plex): extract img/subtitle/rollup helpers into app/plex services
R12 S1 step A. Move the pure, leaf-level helpers out of the 2227-line
routes/plex.py into the app/plex service package with public names:

- app/plex/imgcache.py — on-disk poster/art cache + proxy (was _img_cache_*,
  _image_key, _img_response, _IMG_CACHE/_IMG_SIZES). Owns the shared cache dir.
- app/plex/subtitles.py — WebVTT convert/time-shift + subtitle cache (was
  _srt_to_vtt, _shift_vtt, _ffmpeg_to_vtt, _sub_cache_*, _BITMAP_SUBS).
- app/plex/rollup.py — show watch-state aggregation (was _rollup,
  _show_status, _show_agg_subq, _show_state_map).

Behaviour-equivalent move: bodies verbatim, only the public API renamed and
call sites re-qualified. No endpoint or SQL change (db.query kept as-is; the
2.0 select() conversion is a later step). plex.py 2227 -> 2011 lines.

Gate: ruff clean, pytest 193 passed (pure-logic + DB lane).
2026-08-11 21:16:44 +02:00
peter 2671ab4cc8 release: 0.62.0 (r11-feedback-symmetry) 2026-08-11 20:41:43 +02:00
peter c4fde626a4 chore(release): 0.62.0 — R11 feedback symmetry
VERSION bump + user-facing release notes for R11 (Plex error toasts,
download/edit ready notifications, saved-view rollbacks + retries, access-request
denial emails).
2026-08-11 20:40:46 +02:00
peter 359bac149b refactor: address /code-review on R11 feedback symmetry
- admin._decide: add an idempotency guard (409 unless status was 'pending') so a
  repeat/stale approve OR deny can't re-run its side effects — now that deny emails,
  this stops duplicate denial mails and a rejection mail to an already-approved user.
- SavedViewsWidget: extract runViewMutation(fn, failMsg) (returns success) — dedups
  the five try/catch+refresh blocks AND lets create/rename close their input only on
  success, so a failed create keeps the user's typed name.
- worker._notify_download_done: take user_id/title captured BEFORE the caller's commit
  (no post-commit refresh SELECT); add a  so the inbox labels an edit job
  'Edit ready' instead of 'Download ready' (new inbox.editDone i18n, HU+EN).

Gate green (ruff/tsc/eslint-0/prettier/knip/vitest 79/pytest 193); smoke-tested the
saved-views + notifications surfaces render clean on a fresh dev server.
2026-08-11 20:25:18 +02:00
peter e2fe6fa286 feat(ux): R11 feedback symmetry — Plex/download/rollback/deny notices
Give equal-weight actions equal-weight feedback (R11).

S1 Plex error toasts: the watched-toggles (card / season mark-all / episode /
info-panel / show mark-all) used to fail silently on 403/404/429; each now surfaces
the server reason via a shared notifyActionError() + resyncs the optimistic flip.
The playback auto-mark-watched stays intentionally silent (a mid-play toast is
noise; the next sync corrects it). New generic lib/actionErrors.ts (Plex + saved
views reuse it; notifyYouTubeActionError delegates its non-403 case to it).

S2 download-done inbox notification: worker.py emits a best-effort type=download_done
notice to the job owner on real completion (regular + edit jobs, not instant
cache-hits), rendered/localized in the inbox with an Open-Downloads link.

S3 optimistic rollbacks: SavedViewsWidget create/rename/toggle-default/remove/reorder
and the inbox revert-state no longer swallow failures — they toast + refetch to
resync (the reorder's optimistic order reverts to server truth).

S4 access-request deny: deny_invite now emails the requester (send_access_denied),
mirroring approve — no more silent limbo.
2026-08-11 05:05:50 +02:00
peter 354b3dd10c release: 0.61.1 (r10-btn-primary) 2026-08-11 04:24:22 +02:00
peter b5cf355ae4 chore(release): 0.61.1 — .btn-{tone} action-button recipe family
VERSION bump + chores release note for the R10 .btn-primary/.btn-danger/.btn-warn
recipe unification (refactor; a few primary buttons gained the standard hover).
2026-08-11 04:23:26 +02:00
peter fc186e1230 refactor(ui): address /code-review on .btn-primary — tone family + doc fixes
- Generalize the recipe into a .btn-{tone} family: add .btn-danger / .btn-warn
  siblings sharing one hover-dim, and migrate the two hand-rolled tone-action
  buttons (ConfirmProvider danger confirm, Banner warning action) onto them, so
  primary/danger/warn action paints can't drift.
- Reword the recipe comment: it owns paint + one standard hover-dim (not
  'PAINT-ONLY' — that mislabel obscured the hover behaviour).
- Drop the now-redundant hover:opacity-90 from Banner's action-button base (each
  tone's recipe supplies the hover).

The 3 previously hover-less primary buttons keep the standard hover-dim gained in
the prior commit (intentional consistency; now accurately documented). Gate green
(tsc/eslint-0/prettier/knip/vitest 79); self-E2E confirmed btn-danger/btn-warn
compute to their tone/tone-fg tokens.
2026-08-11 04:20:10 +02:00
peter 76f8de258c refactor(ui): R10 — .btn-primary recipe for the primary/action buttons
Add a paint-only .btn-primary recipe (solid accent bg + accent-fg text +
hover:opacity dim) in index.css @layer components, alongside the selection-paint
recipes (.pill/.chip/.is-current). Migrate ~38 primary/action buttons across 24
files off the hand-rolled 'bg-accent text-accent-fg hover:opacity-90', keeping
each site's geometry (padding/radius/font/gap/disabled/shadow).

Deliberately NOT migrated: accent BADGES/labels (spans), the chat message bubble,
selection toggles (tableHeader — R8 popover territory), and the round play FAB
(hover:scale, not the opacity-dim primary idiom). Three previously hover-less
primary buttons (SettingsPanel/SetupWizard/DraftSaveBar) gain the standard hover
dim for consistency.

Gate green (tsc/eslint-0/prettier/knip/vitest 79); self-E2E confirmed a migrated
button computes to accent/accent-fg and both recipe rules load.
2026-08-11 04:04:06 +02:00
peter 87da6155e1 release: 0.61.0 (fixpack-cards-nav-plex) 2026-08-11 03:37:57 +02:00
peter 3b41b68d22 chore(release): 0.61.0 — cards/nav/plex fixpack
VERSION bump + user-facing release notes for the 7-item UX batch (channel-page
priority stepper, cross-season Plex binge, unified card hover, slim icon nav,
scoped filter peek, full-viewport Plex backdrop, player end-of-queue exit).
2026-08-11 03:36:42 +02:00
peter 02754f479f refactor(ux): address /code-review high findings on the fixpack
- Extract the optimistic + debounced-flush feed-priority flow into a shared
  useChannelPriority hook (mirrors useChannelTagActions); the manager and the
  channel-detail page now share it instead of two drifting copies.
- NavSidebar: drop the always-true `slim` const and its dead expanded-rail
  branches; refresh the now-stale pinned/collapsible comments.
- Remove the orphaned `navCollapsed` localStorage key (no readers/writers left
  after the always-slim rail).
- Channel page: drop the wrapper native `title` that duplicated PriorityStepper's
  own Tooltip.

The keyboard-focus nav-label finding needed no change: the popover platform
already wires useFocus (visibleOnly), so a real Tab press shows the flyout label
(verified in-browser).
2026-08-11 03:29:08 +02:00
peter 71bbf35f5a fix(ux): dock filter tab flush to the nav + reliable full-viewport plex backdrop
Follow-ups on the 7-item batch:
- The slim nav slot was still w-20 while its card is w-14, leaving a ~24px dead
  gap the filter tab/bar couldn't dock against. Slot is now w-14 (exactly the
  card width), so the filter's flat-left tab sits flush on the rail's right edge
  (measured gap 0), both collapsed and expanded.
- The Plex art backdrop was invisible: a position:fixed layer at z-index -1 is
  painted BEHIND the body's opaque background here, so nothing showed. Paint the
  art on the app root (#root) instead — it's exactly the viewport and a real
  ancestor of the transparent nav/header/content, so the art fills the whole page
  edge-to-edge and the glass chrome floats over it (verified in-browser).
2026-08-11 02:53:55 +02:00
peter 9d7a2a75d8 feat(ux): card unification, slim icon nav, filter/plex fixes (7-item batch)
1A ChannelPage "Full history" button no longer wraps (whitespace-nowrap).
1B Extract the manager's press-and-hold priority stepper into a shared
   PriorityStepper and add it to the channel detail header (subscribed, non-demo);
   backend channel_detail now returns the per-subscription `priority`.
2  Unify YouTube + Plex media cards onto one look/behaviour: drop the whole-card
   hover LIFT (VideoCard) — which also fixes the clipped-top bug — and instead the
   inner thumbnail/poster ZOOMS on hover (both); give the flat Plex posters the same
   glass-card 3D depth + accent hover border as the feed cards (shadow-md is a
   transparent no-op here, so it's glass-card that provides the depth); clamp Plex
   card titles to a fixed 2-line height so 1- vs 2-line titles align.
3  Nav rail is now always a slim icon-only column, flush to the left edge; each icon
   reveals a floating name label on hover (Tooltip gains `always` + `openDelay`, and
   a right side). Removed the pin/unpin toggle, the expand-on-hover mode and the
   navCollapsed pref plumbing.
4  Scope the side-panel peek-open HOVER to the tab handle + the card (not the whole
   full-height slot next to the rail), so approaching the nav no longer pops the
   filter open; focus stays container-wide. useHoverFocusWithin gains a hover-out
   grace period + split hover/focus handler groups.
5  Plex art backdrop fills the whole viewport (a fixed inset-0 layer behind the
   transparent app) instead of only the inset <main> box.
6  Plex season binge continues across seasons: the season view's play queue now
   spans the whole show (allKeys), so a season's last episode rolls into the next
   season's first (seasonKeys stays for "add season to playlist").
7  When a Plex video/episode ends with nothing queued next, the player returns to
   the previous page (onEnded → onClose) instead of stranding a black screen.
2026-08-11 02:20:52 +02:00
peter e5d5735b9a release: 0.60.10 (r10-selected-paint) 2026-08-11 00:50:24 +02:00
peter a9b53bbc9d chore(release): 0.60.10 — VERSION bump + release note (R10 S4 selection-paint unification) 2026-08-11 00:48:26 +02:00
peter cb168c800a fix(ui): R10 S4 review r3 — preserve glass-card edge on pills, ease transform
Two low-severity follow-ups from the third /code-review round:
- .pill's resting border-color is now scoped `.pill:not(.glass-card)`, so a
  glass-card-resting pill (PlexSidebar scope/facet, PlexPlaylistView layout)
  keeps glass-card's own border incl. its lighter top-edge specular highlight,
  instead of having it flattened to a uniform var(--border).
- add `transform` to the recipe transition so the `active:translate-y-px` press
  nudge on the Sidebar tag/date pills eases again (it lost the utility transition).

Verified plain pills keep their resting border (Feed + YOUR TAGS) and active
pills still paint solid accent.
2026-08-11 00:45:14 +02:00
peter 90b33fc325 refactor(ui): R10 S4 — complete the selection-paint sweep (all pill idioms)
The first S4 pass migrated a subset, which left same-idiom selection pills
hand-rolled and made adjacent controls inconsistent (a /code-review finding).
This completes the sweep so every selected-among-options control shares the
recipe:

- plain pills → .pill (+ aria-pressed): Channels status filters + view tabs,
  Sidebar filter-tag chips + date presets + custom-dates, Stats tabs + ranges.
- glass-card-resting pills → .pill with the resting `glass-card glass-hover` kept
  in the non-pressed branch (solid accent when pressed): PlexSidebar library
  scope + the facet Chip (retired the round-1 "deferred" note — the recipe
  already expresses this via the resting branch, no new variant needed).
- custom-dates aria-pressed is coerced with !! so a date string can't leak into
  the attribute and miss the [aria-pressed="true"] paint.
- drop the dead `border` on ChannelTags' chip-solid (accent border on accent bg).

Out of scope (a separate concern, NOT selection-among-options): primary/action
buttons that share the accent fill — Banner action, ConfirmProvider primary,
PlexBrowse primary, PlayerModal watched-toggle — these want a future .btn-primary
recipe. Left by-design: tableHeader filter options (PopoverItem + keepSelectedPaint,
owned by the popover platform).
2026-08-11 00:27:04 +02:00
peter 7ff1ec1db0 fix(ui): R10 S4 review — guard .is-current against aria-current="false" + shadow transition
Follow-ups from the /code-review pass:
- .is-current[aria-current] now excludes aria-current="false" — the recipe is a
  shared primitive and a consumer using the common `x ? "true" : "false"` idiom
  would otherwise paint a non-current row as current.
- add box-shadow to the .pill/.is-current transition so the Tabs/SettingsPanel
  on-state shadow fades in instead of popping (it lost the utility `transition`).
- document that .chip-solid is intentionally paint-only (composes on a site's own
  geometry) so a truncating single-element chip isn't forced into inline-flex.

The 3 intentional visual changes (hover-border 55%, tag-filter transparent
resting, RemovableChip tint+border) were UAT-accepted as-is. PlexSidebar's facet
Chip (glass-card resting) deferred — wants a .pill soft-glass variant.
2026-08-10 23:25:21 +02:00
peter dcb91066f5 refactor(ui): R10 S4 — unify selected-paint into .pill/.is-current/.chip recipes
The ~14 hand-rolled "this is the selected/current/active one" idioms each
repeated the same `sel ? "bg-accent text-accent-fg border-accent" : "…"` inline
ternary, which had drifted. Three attribute-driven recipes in @layer components
now own that paint, keyed on the ARIA the control already carries: .pill
(aria-pressed segments/toggles), .is-current (aria-current nav/list items), and
.chip / .chip-solid (tag & filter tokens). The JSX just toggles the ARIA; the CSS
paints, so one control's selected look can't diverge from the next's. No
focus-visible in the recipes — the @layer base rule already rings every button/a.

Migrated 15 sites (Feed show/content, Channels tag filter, Sidebar or-and +
scope, PlexSidebar match, Tabs, SettingsPanel tab-rail, PlexPlaylistView layout
[soft], NavSidebar nav + settings, ActiveFilterChips, PlexSidebar RemovableChip,
ChannelTags, ChannelPicker). Deliberately left: PlexSidebar facet Chip and
PlexPlaylistView already covered; SavedViews row (paint split across two
elements), the scheme swatch (border-only on a coloured swatch), and
ChannelPicker's option rows (text-only selection) — noted for a follow-up.
2026-08-09 21:06:58 +02:00
peter 13120163a4 release: 0.60.9 (improvement/popover-selected-highlight) 2026-08-09 17:47:38 +02:00
peter ead617fd31 chore(release): 0.60.9 — VERSION bump + public release note (popover weak-ring) 2026-08-09 17:47:05 +02:00
peter 5c5c045ed9 fix(popover): R10 weak-ring — checked rows get the full highlight fill
A hovered/roving CHECKED row got only a thin ring (R9 split), reading as a weak
afterthought next to an unchecked row's vivid accent fill. Now selected rows get the
SAME bg-accent/35 fill + a STRONGER ring (ring-2 accent/80 vs the unselected ring-1
accent/60); the child checkmark/checkbox stays legible on the tint. The one idiom that
DID rely on the R9 ring-only — the table-filter options, whose selected item paints its
own solid bg-accent ROW — opts back in via a new `keepSelectedPaint` PopoverItem prop,
so its solid paint isn't downgraded to a 35% tint. Scopes the R9 exception to the one
menu that needs it instead of penalising every checkbox menu.
2026-08-09 17:46:13 +02:00
peter eee7443e6f release: 0.60.8 (r10-recipe-consolidation) 2026-08-09 17:45:02 +02:00
peter 84046a2874 chore(release): 0.60.8 — VERSION bump + public release note (R10 S3) 2026-08-09 17:44:29 +02:00
peter 45c95442b6 refactor(input): R10 S3 review — fold Welcome's local inputCls into the shared recipe
Welcome kept the last local `const inputCls` (rounded-xl, py-2.5) after S3b consolidated
the other three, leaving its auth fields the sole inputs off the unified rounded-lg recipe.
Import the shared inputCls and drop the local const — now every input is truly one recipe.
(Auth fields go rounded-xl→rounded-lg, py-2.5→py-2 to match.)
2026-08-09 17:43:43 +02:00
peter 5a26499f1d refactor(header): R10 S3c — SyncStatus adopts the shared header-pill shell
The header row's three pills now share one shell: SyncStatus was glass/rounded-xl while
ModuleName and SearchBar were glass-menu/rounded-2xl/h-11. Aligned it (glass-menu, rounded-2xl,
h-11) so the pill row reads as one system, per user call.
2026-08-09 17:43:43 +02:00
peter 7ea93925fb fix(ui): R10 S3c — Banner action buttons gain hover feedback
The accent + warning Banner action buttons had no hover state (flagged during S1 UAT).
Add the house hover:opacity-90 idiom. (The header-pill .glass-chrome consolidation is
deferred — 2 of the 3 pills already share the glass-menu/rounded-2xl look; unifying the
odd one, SyncStatus, needs a visual call on whether it becomes a full-height pill.)
2026-08-09 17:43:43 +02:00
peter 0559e1a503 refactor(input): R10 S3b — one input recipe, ring focus everywhere
Unify text inputs onto the form.tsx inputCls look (surface bg, rounded-lg, 2px accent
RING focus), replacing the settings-family border-focus style app-wide:
- deleted 3 duplicate local `const inputCls` (KeyGate/SettingsPanel/SetupWizard) → import
  the shared one; a 4th (Welcome, py-2.5) kept local but re-styled to match.
- ~30 inline settings inputs: default full-size fields → inputCls; size variants (pager/
  table-filter/chat textarea/plex/glass-card) keep their width/radius/padding but swap
  bg-card→bg-surface and border-focus→ring-focus. glass-card/bg-transparent bgs left as-is.
- refreshed the form.tsx doc-comment (the old two-style split is gone).

The ring is the more visible / a11y-correct focus cue, matching R9's focus foundation.
2026-08-09 17:43:42 +02:00
peter 1955fc87ee refactor(glass): R10 S3a — glass-card notification rows + perf-aware .overlay-backdrop
- NotificationsPanel per-row .glass -> glass-card (the rows are cards, not panel surface).
- New .overlay-backdrop recipe (4px blur = backdrop-blur-xs) with a data-perf override that
  DROPS the blur. The viewport-sized modal scrim blur (Modal/PlayerModal/Welcome) was a raw
  backdrop-blur-xs utility that survived perf mode while every .glass had its blur dropped —
  the most expensive blur in the app. Overlays keep their bg-black/NN.
- gotcha: write ONLY the standard backdrop-filter; a manual -webkit- line makes lightningcss
  drop the standard prop (webkit-only output -> no blur in Firefox).
2026-08-09 17:43:42 +02:00