both auto-process toggles (Inbox, Queue) used a fully-controlled checkbox
whose checked prop was driven by data.autoProcessing / autoProcessQueue.
that made clicks feel frozen on anything slower than localhost: react
reconciles the DOM back to the pre-click value between onChange firing
and setData landing after loadAll, so a click could look like it snapped
back before the server answered. uncheck in particular showed up as
"can't turn it off" when the queue was idle.
mirror the prop in a local useState + sync via useEffect so the box
flips on click and the server value reconciles on the next pipeline
refresh. matches the pattern SettingsPage already uses for the same
toggle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
subtitle management (list/detail pages, /api/subtitles, subtitle_files
table, SubtitleFile types, predictExtractedFiles, nav link) moved to a
new sibling project at ~/Developer/netfelix-subtitles-manager/ where
it'll be rebuilt standalone later. this project now owns audio fixing
+ subtitle extraction only.
extraction still runs end to end: analyzeItem still marks every subtitle
stream as "remove from container", buildExtractionOutputs still wires
the -map 0:s:N + sidecar outputs into the ffmpeg command, and execute.ts
still flips review_plans.subs_extracted so verify.ts can check desired
state — just derived from the streams directly instead of by writing a
row per file to the now-gone subtitle_files table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
column headers are now a fixed three-row layout (title / subtitle / button
row). every column always reserves all three rows so headers line up
regardless of contents; actions render disabled when their column is
empty instead of disappearing, which keeps the header height stable as
state changes.
the processing column gets a new "Auto-process Queue" checkbox that
mirrors the inbox's "Auto-process Inbox" toggle. backend adds an
auto_process_queue config, a maybeStartQueueProcessor() helper, a
POST /api/settings/auto-process-queue endpoint, and a hook in
enqueueAudioJob so approvals drain the queue hands-off when the toggle
is on.
reopen-all and per-item reopen now send items to the Inbox (sorted=0)
instead of back to Review. the done column's label and tooltip become
"← Back to inbox" to match, and the clear button moves to the right
slot so the header pattern (left=backward, right=forward) stays
consistent across columns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sortInbox is now async, yields every 10 items, and emits inbox_sort_start
+ inbox_sort_progress via optional hooks. the pipeline route handler
wires those hooks to the existing job events stream and guards against
a second concurrent sort with a 409.
the inbox column swaps its Auto Review button for a live "Sorting N/T"
counter and progress bar while the sort is in flight; the auto-process
toggle hides to give the progress the full subtitle line. the previous
behaviour was a frozen button for the entire duration of the sort.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sortInbox used to distribute plans by the auto_class and stream_decisions
captured at scan time, which meant toggling an audio_languages entry and
then running "back to inbox" + "auto review" re-queued the item with the
stale decisions. now sortInbox re-runs the analyzer per plan against the
current audio_languages before distributing, matching the user's mental
model that auto review = re-apply the rules.
reanalyze() takes audioLanguages explicitly so callers can pass it in
once and tests can drive it without reaching into the singleton db.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rescan flagged every item where radarr/sonarr disagreed with jellyfin's
audio-track guess as needs_review=1, but the analyzer's authoritativeOg
check demands needs_review=0 — so the very items we had an authoritative
answer for were the ones dumped into the "Needs decision" bucket. Lost
(german dubs first on most files, sonarr authoritatively english) was
the perfect worst case: nearly every episode misclassified as manual.
trust the authoritative source unconditionally and reset needs_review=0
when it fires, mismatch or not.
use grid-cols-[auto_1fr_auto] so backward/forward take natural width
and the flexible middle column centers skip; add whitespace-nowrap;
bump column min-width 64→80 so all three fit on the skinniest column.
- execute/clear now also resets sorted=0 so cleared items land back in the
inbox where the distributor can re-classify them; previously they got
stranded in Review with auto_class='auto', unreachable by both
"Approve all ready" and "Auto Review"
- pipelinecard: action row moves to the top so Skip/Approve/Back-to-review
sit in the same place regardless of card body height; title row follows;
file info (copy/transcode reasons) gets a dedicated row with the
ready/needs-decision badge pushed to the right
- tests: clearQueue preserves running/completed jobs, only pending plans
flip back to sorted=0 pending
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- rescan: skip jellyfin/radarr/sonarr lookups when orig_lang_source='manual' so user pins survive webhook + full scans
- jellyfin: request SeriesProviderIds so episodes can resolve to the series-level tvdb id
- sonarr: drop the lookup[0] fallback that silently returned unrelated shows on tvdb misses
- seriescard: split badges+language and approve buttons onto separate rows; seasongroup header wraps with ml-auto so buttons don't overflow the narrow pipeline column
- tests: cover manual override preservation and episode → series tvdb resolution
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
snapshot decisions after decideAction, before deduplicateAudioByLanguage;
diff to identify tracks that flipped keep→remove due to the commentary
regex — language-driven removes no longer falsely upgrade auto_class to
auto_heuristic even when the title coincidentally matches
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The scalar subquery I added in 7d30e6c ran one aggregate scan of
media_streams per row. On a real library (33k items / 212k streams)
a single page took 500+ seconds synchronously, blocking the event
loop and timing out every other request — Library AND Pipeline both
stopped loading.
Swap it for a single batched `GROUP_CONCAT ... WHERE item_id IN (?...)`
query over the current page's ids (max 25), then merge back into rows.
v2026.04.15.10
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-row audio codec summary (distinct lowercased codecs across an
item's audio streams) via scalar subquery on media_streams, rendered
as "ac3 · aac" in a new monospace Audio column.
v2026.04.15.9
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously the input type was `revealed || !isMasked ? "text" : "password"` —
once revealed the value was no longer the "***" placeholder, so !isMasked
kept the input in text mode even with revealed=false. Type now depends on
`revealed` alone, so a second click re-dots the field.
v2026.04.15.6
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ProcessingColumn now anchors a local deadline when a 'sleeping' queue
status arrives and ticks a 1s timer. "Sleeping 60s between jobs"
becomes "Next job in 59s, 58s, …".
Settings: API key inputs now span the card's width (matching the URL
field), and the reveal affordance is a GNOME-style eye glyph sitting
inside the input's right edge. Uses an inline SVG so it inherits
currentColor and doesn't fight emoji rendering across OSes. When the
field is env-locked, the lock glyph takes the slot (eye hidden — no
edit possible anyway).
v2026.04.15.5
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Splitting the loader: SSE job_update events now only refetch the
pipeline payload (queue/processing/done), not the review groups.
loadAll (pipeline + groups) is still used for first mount and user-
driven mutations (approve/skip) via onMutate.
Before: a running job flushed stdout → job_update SSE → 1s debounced
load() refetched /groups?offset=0&limit=25 → ReviewColumn's
useEffect([initialResponse]) reset groups to page 0, wiping any
pages the user had scrolled through. Lazy load appeared to block
because every second the column snapped back to the top.
v2026.04.15.4
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Client changes paired with the earlier /groups endpoint:
- Types: drop review[]/reviewTotal from PipelineData, add ReviewGroup
and ReviewGroupsResponse.
- PipelinePage: parallel-fetch /pipeline and /groups?offset=0&limit=25.
- ReviewColumn: IntersectionObserver on a sentinel div fetches the
next page when it scrolls into view. No more "Showing first N of M"
banner — the column loads lazily until hasMore is false.
- SeriesCard: when a series has pending work in >1 season, render
collapsible season sub-groups each with an "Approve season" button
wired to POST /season/:key/:season/approve-all. Rename the series
button from "Approve all" to "Approve series" for clarity.
v2026.04.15.3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
/api/review/pipeline no longer ships the review array — it now only
carries queue state + reviewItemsTotal. Review items live behind
/api/review/groups?offset=N&limit=25 which returns complete series
(every pending non-noop episode, bucketed by season) so the UI never
sees a split group.
Lifted enrichWithStreamsAndReasons + PipelineAudioStream to module
scope so both /pipeline (queued column) and /groups (review page)
can share the same enrichment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GET /api/settings now returns jellyfin_api_key, radarr_api_key,
sonarr_api_key, mqtt_password as "***" when set (empty string when
unset). Real values only reach the client via an explicit
GET /api/settings/reveal?key=<key> call, wired to an eye icon on
each secret input in the Settings page.
Save endpoints treat an incoming "***" as a sentinel meaning "user
didn't touch this field, keep stored value", so saving without
revealing preserves the existing secret.
Addresses audit finding #3 (settings endpoint leaks secrets).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Queue previously processed a snapshot of pending jobs — anything approved
after Run-all clicked sat idle until the user clicked again. Now, when
the local queue drains, re-poll the DB once for newly-approved jobs
before exiting.
Also swap the looser local parseId (Number.parseInt accepted '42abc')
for the strict shared parseId in server/lib/validate.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>