Commit Graph

171 Commits

Author SHA1 Message Date
felixfoertsch fada511ecc pipeline: optimistic auto-process checkbox state
Build and Push Docker Image / build (push) Successful in 2m42s
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>
2026-04-19 23:47:05 +02:00
felixfoertsch 05a1345750 rip subtitle manager → sibling project, keep extraction only
Build and Push Docker Image / build (push) Successful in 2m31s
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>
2026-04-19 22:50:52 +02:00
felixfoertsch 0fd3624d9f pipeline: uniform column headers, auto-process queue toggle, reopen → inbox
Build and Push Docker Image / build (push) Successful in 4m3s
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>
2026-04-19 21:57:13 +02:00
felixfoertsch a21bcefb54 stream auto review progress over SSE so large inboxes don't feel frozen
Build and Push Docker Image / build (push) Successful in 1m18s
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>
2026-04-19 20:56:51 +02:00
felixfoertsch 76a16ba84c reanalyze plans during auto review so config changes take effect
Build and Push Docker Image / build (push) Successful in 4m0s
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>
2026-04-19 20:38:14 +02:00
felixfoertsch 91d8ed67b8 fix authoritative language sources getting demoted to needs_review
Build and Push Docker Image / build (push) Successful in 2m22s
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.
2026-04-19 15:20:18 +02:00
felixfoertsch 794680ec17 pipeline: align ↑ approve above next to approve (right side), not skip
Build and Push Docker Image / build (push) Successful in 4m11s
2026-04-19 13:32:58 +02:00
felixfoertsch 201c02c810 pipeline: stop header buttons from wrapping
Build and Push Docker Image / build (push) Successful in 2m11s
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.
2026-04-19 13:27:52 +02:00
felixfoertsch 495a40a6c6 restructure pipeline column headers with backward/skip/forward slots, rename ready → auto-approve
Build and Push Docker Image / build (push) Successful in 1m52s
- columnshell: three-row header (title, subtitle, backward · skip · forward grid)
- inbox: auto-processing checkbox moves into subtitle, page top-right toggle removed
- review: add ← back to inbox, rename approve-all-ready to approve auto
- queue: clear button relabeled ← back to inbox (matches existing behaviour)
- done: add ← back to review (reopen-all), clear moves to middle skip slot
- seriescard: action row mirrors movie card, now at the top
-  ready badge →  auto-approve with tooltip, review subtitle uses "auto · need decisions"
- new endpoints: /api/review/unsort-all, /api/review/reopen-all (+ tests)
2026-04-19 13:16:29 +02:00
felixfoertsch 84e669922b clear queue → inbox, move pipeline card actions to the top
Build and Push Docker Image / build (push) Successful in 3m54s
- 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>
2026-04-19 11:34:49 +02:00
felixfoertsch 0d560743f3 fix manual language overrides wiped on rescan, use series tvdb for sonarr, split seriescard controls
Build and Push Docker Image / build (push) Successful in 3m31s
- 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>
2026-04-19 07:41:08 +02:00
felixfoertsch 331cc7b58e v2026.04.18.1 — inbox column, auto-processing toggle, classifier-driven sort
Build and Push Docker Image / build (push) Successful in 1m16s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 10:53:06 +02:00
felixfoertsch d8d1b43556 settings: add auto-processing toggle 2026-04-18 10:52:26 +02:00
felixfoertsch 13b2630a09 pipeline: 5 columns, auto-processing checkbox, inbox_sorted SSE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 10:51:16 +02:00
felixfoertsch 56f16c5ad3 ReviewColumn: approve-all-ready action + bucket=review fetch 2026-04-18 10:49:51 +02:00
felixfoertsch 6265301d47 pipeline: add InboxColumn component (bucket=inbox) 2026-04-18 10:48:52 +02:00
felixfoertsch 583fa3e218 SeriesCard: fix missing flatEpisodes reference in single-season branch
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 10:47:54 +02:00
felixfoertsch 24d15a5057 pipeline cards: render auto_class badges, series readyCount
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 10:47:05 +02:00
felixfoertsch 330d3de425 ColumnShell: add optional subtitle slot 2026-04-18 10:44:47 +02:00
felixfoertsch ac8772e3bf types: replace confidence with auto_class, extend pipeline payload 2026-04-18 10:43:55 +02:00
felixfoertsch b9b4a50e8a rescan: auto-sort inbox after scan when auto_processing is on
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 10:43:03 +02:00
felixfoertsch da5bd6cac2 settings: POST /auto-processing toggles flag and optionally drains inbox 2026-04-18 10:41:54 +02:00
felixfoertsch 75104402fa review: /pipeline payload adds inbox + ready/manual counts 2026-04-18 10:41:11 +02:00
felixfoertsch 43b190a1a0 sse: broadcast inbox_sorted on distributor runs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 10:40:19 +02:00
felixfoertsch 6faa5986a3 review: add approveReady + POST /approve-ready endpoint
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 10:38:55 +02:00
felixfoertsch 114b6687c6 review: add sortInbox distributor + POST /sort-inbox endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 10:36:05 +02:00
felixfoertsch 82c8c89fb9 review: buildReviewGroups accepts bucket param, sorts by auto_class
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 10:30:43 +02:00
felixfoertsch 67f1b9440e review: persist auto_class via reanalyze + rescan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 10:24:22 +02:00
felixfoertsch 8e1deb39d5 analyzer tests: narrow orig_lang_source type to MediaItem union
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 10:20:22 +02:00
felixfoertsch 455796ebb6 analyzer: only count commentary-driven removes as heuristic
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>
2026-04-18 10:19:22 +02:00
felixfoertsch c595ad3792 analyzer: classify each plan as auto/auto_heuristic/manual
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 10:15:01 +02:00
felixfoertsch ef417bea09 schema: add auto_class + sorted to review_plans, drop confidence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 10:10:40 +02:00
felixfoertsch 68771bb980 plan: inbox column + auto-processing implementation plan 2026-04-18 10:05:48 +02:00
felixfoertsch b2d10bd3d4 spec: inbox column, auto-processing toggle, classifier-driven sort 2026-04-18 09:49:48 +02:00
felixfoertsch 0c595a787e library: batch audio-codec lookup — per-row subquery was O(page×streams)
Build and Push Docker Image / build (push) Successful in 1m11s
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>
2026-04-15 19:42:23 +02:00
felixfoertsch 7d30e6c1a6 library: rename Scan nav/page to Library, show audio codecs per row
Build and Push Docker Image / build (push) Successful in 1m4s
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>
2026-04-15 19:10:00 +02:00
felixfoertsch a2bdecd298 rework scan page, add ingest-source browsing, bump version to 2026.04.15.8
Build and Push Docker Image / build (push) Successful in 4m56s
2026-04-15 18:33:08 +02:00
felixfoertsch c6698db51a improve running-job responsiveness, bump version to 2026.04.15.7
Build and Push Docker Image / build (push) Successful in 1m41s
2026-04-15 16:58:53 +02:00
felixfoertsch 604fdc5c6c settings: clicking the eye again actually hides the secret
Build and Push Docker Image / build (push) Successful in 5m11s
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>
2026-04-15 14:51:57 +02:00
felixfoertsch c22642630d pipeline: live sleep countdown; settings: full-width fields, eye inside input
Build and Push Docker Image / build (push) Successful in 2m48s
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>
2026-04-15 14:29:35 +02:00
felixfoertsch ab65909e6e pipeline: stop wiping Review scroll state on every SSE tick
Build and Push Docker Image / build (push) Successful in 3m22s
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>
2026-04-15 12:26:04 +02:00
felixfoertsch 07c98f36f0 review: lazy-load groups with infinite scroll, nest seasons
Build and Push Docker Image / build (push) Successful in 29s
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>
2026-04-15 12:22:00 +02:00
felixfoertsch 4e96382097 test: buildReviewGroups — completeness, season buckets, sort, filters
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:10:54 +02:00
felixfoertsch 3f910873eb review: add /groups endpoint with server-side grouping + pagination
/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>
2026-04-15 12:09:27 +02:00
felixfoertsch 3f848c0d31 plan: review column lazy-load + season grouping
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:06:57 +02:00
felixfoertsch 967d2f56ad spec: review column lazy-load + season grouping
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:04:24 +02:00
felixfoertsch 45f4175929 v2026.04.15.2 — queue pump, strict parseId, settings secret masking
Build and Push Docker Image / build (push) Successful in 2m13s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:15:45 +02:00
felixfoertsch e040c9a234 settings: mask API keys in GET /api/settings, add eye-icon reveal
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>
2026-04-15 08:15:08 +02:00
felixfoertsch b0d06a1d8c execute: drain newly-approved jobs mid-run, use strict shared parseId
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>
2026-04-15 08:12:59 +02:00
felixfoertsch e49a04c576 v2026.04.15.1 — drop verify/checkmarks, merge jobs view into item details
Build and Push Docker Image / build (push) Successful in 1m58s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 07:07:06 +02:00