worked through AUDIT.md. triage:
- finding 2 (subtitle rescan wipes decisions): confirmed. /:id/rescan now
snapshots custom_titles and calls reanalyze() after the stream delete/
insert, mirroring the review rescan flow. exported reanalyze + titleKey
from review.ts so both routes share the logic.
- finding 3 (scan limit accepts NaN/negatives): confirmed. extracted
parseScanLimit into a pure helper, added unit tests covering NaN,
negatives, floats, infinity, numeric strings. invalid input 400s and
releases the scan_running lock.
- finding 4 (parseId lenient): confirmed. tightened the regex to /^\d+$/
so "42abc", "abc42", "+42", "42.0" all return null. rewrote the test
that codified the old lossy behaviour.
- finding 5 (setup_complete set before jellyfin test passes): confirmed.
the /jellyfin endpoint still persists url+key unconditionally, but now
only flips setup_complete=1 on a successful connection test.
- finding 6 (swallowed errors): partial. the mqtt restart and version-
fetch swallows are intentional best-effort with downstream surfaces
(getMqttStatus, UI fallback). only the scan.ts db-update swallow was
a real visibility gap — logs via logError now.
- finding 1 (auth): left as-is. redacting secrets on GET without auth
on POST is security theater; real fix is an auth layer, which is a
design decision not a bugfix. audit removed from the tree.
- lint fail on ffmpeg.test.ts: formatted.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
two fixes based on actual behavior of the jellyfin webhook plugin:
- 'Webhook Url' setup value no longer re-serialized with mqtt://. show
the user's broker url verbatim so whatever protocol they use (ws://,
http://, etc.) survives the round trip
- dropped the server-side 'trigger a jellyfin rescan during the test'
machinery. a refresh that doesn't mutate metadata won't fire Item
Added, so relying on it produced false negatives. now we just wait
for any message on the topic; ui instructs the user to hit play on a
movie in jellyfin while the test runs — playback start is a
deterministic trigger, unlike library events
- setup panel now lists Notification Types as 'Item Added, Playback
Start'. playback start is for the test only; the production handler
still filters events down to item added / updated
- MqttSection now renders as a nested block inside the Jellyfin
ConnSection instead of its own card; ConnSection grew a children slot
- when the enable checkbox is off, broker/topic/credentials inputs and
the whole plugin setup panel are hidden; only the toggle + a small
save button remain
- 'Test Connection' became 'Test end-to-end': connects to the broker,
subscribes, picks a random scanned movie/episode, asks jellyfin to
refresh it, and waits for a matching webhook message. the UI walks
through all three steps (broker reachable → jellyfin rescan triggered
→ webhook received) with per-step success/failure so a broken
plugin config is obvious
- new mqtt_enabled config + toggle at top of the section; subscriber
only starts when the box is checked
- moved the whole MqttSection directly below the Jellyfin section so
all jellyfin-adjacent config lives together
- rewrote the plugin setup list to match the actual form order and
group it: 'Top of plugin page' (Server Url = jellyfin base URL),
'Generic destination', 'MQTT settings', 'Template'
- fields the user picks from a dropdown or toggles (Status,
Notification Type, Item Type, Use TLS, Use Credentials, QoS) now
render a 'select' hint instead of a broken Copy button
after ffmpeg finishes we used to block the queue on a jellyfin refresh
+ re-analyze round-trip. now we just kick jellyfin and return. a new
mqtt subscriber listens for library events from jellyfin's webhook
plugin and re-runs upsertJellyfinItem — flipping plans back to pending
when the on-disk streams still don't match, otherwise confirming done.
- execute.ts: hand-off is fire-and-forget; no more sync re-analyze
- rescan.ts: upsertJellyfinItem takes source: 'scan' | 'webhook'.
webhook-sourced rescans can reopen terminal 'done' plans when
is_noop flips back to 0; scan-sourced rescans still treat done as
terminal (keeps the dup-job fix from a06ab34 intact).
- mqtt.ts: long-lived client, auto-reconnect, status feed for UI badge
- webhook.ts: pure processWebhookEvent(db, deps) handler + 5s dedupe
map to kill jellyfin's burst re-fires during library scans
- settings: /api/settings/mqtt{,/status,/test} + /api/settings/
jellyfin/webhook-plugin (checks if the plugin is installed)
- ui: new Settings section with broker form, test button, copy-paste
setup panel for the Jellyfin plugin template. MQTT status badge on
the scan page.
analyzer removes every subtitle unconditionally (see case 'Subtitle' in
decideAction) and the pipeline extracts all of them to sidecars — the config
was purely informational and only subtitles.ts echoed it back as
'keepLanguages' for a subtitle-manager ui that doesn't exist yet. we'll
revive language preferences inside that manager when it ships.
removes: the settings card + ui state, POST /api/settings/subtitle-languages,
the config default, the SUBTITLE_LANGUAGES env mapping, AnalyzerConfig's
subtitleLanguages field, RescanConfig's subtitleLanguages field, every
caller site (scan.ts / execute.ts / review.ts), and the keepLanguages
surface in subtitles.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
the existing clear-scan button only drops media_items + related; settings
survived. useful when schema changes or corrupt state make you want a full
do-over on a running container without ssh-ing in to rm data/netfelix.db.
POST /api/settings/reset truncates everything (config included) then re-seeds
DEFAULT_CONFIG via the exported reseedDefaults helper. env-var overrides keep
working through getConfig's env fallback. ui lives next to clear-scan in the
danger zone with a double confirm and reload to /, so the setup wizard shows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
the old one-window scheduler gated only the job queue. now the scan loop and
the processing queue have independent windows — useful when the container
runs as an always-on service and we only want to hammer jellyfin + ffmpeg
at night.
config keys renamed from schedule_* to scan_schedule_* / process_schedule_*,
plus the existing job_sleep_seconds. scheduler.ts exposes parallel helpers
(isInScanWindow / isInProcessWindow, waitForScanWindow / waitForProcessWindow)
so each caller picks its window without cross-contamination.
scan.ts checks the scan window between items and emits paused/resumed sse.
execute.ts keeps its per-job pause + sleep-between-jobs but now on the
process window. /api/execute/scheduler moved to /api/settings/schedule.
frontend: ScheduleControls popup deleted from the pipeline header, replaced
with a plain Start queue button. settings page grows a Schedule section with
both windows and the job sleep input.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When I switched the settings UI to read result.saved to decide whether the
'✓ Saved & connected' / '⚠ Saved, but connection test failed' / '✗ error'
states should appear, I only updated the Radarr and Sonarr endpoints to
return that shape. Jellyfin still returned bare { ok: true } so the UI
saw saved=undefined and showed '✗ Save failed' even on a perfectly
successful save — making it look like Jellyfin had stopped working.
Bring Jellyfin in line:
- Save the URL+API key (and setup_complete) BEFORE running testConnection
so the input survives a failed probe (same fix as Radarr/Sonarr).
- Only do the admin-user discovery on test success.
- Return { ok, saved, testError }.
Two cleanups:
1. Rename the page from 'Setup' to 'Settings' all the way down. The H1
already said Settings; the file/component/api path were lying.
- src/features/setup/ → src/features/settings/
- SetupPage.tsx → SettingsPage.tsx, SetupPage → SettingsPage,
SetupData → SettingsData, setupCache → settingsCache
- server/api/setup.ts → server/api/settings.ts
- /api/setup → /api/settings (only consumer is our frontend)
- server/index.tsx import + route mount renamed
- ScanPage's local setupChecked → configChecked
2. Sonarr (and Radarr) save flow: persist the values BEFORE running the
connection test. The previous code returned early if the test failed,
silently dropping what the user typed — explained the user's report
that Sonarr 'forgets' the input. Now setConfig fires unconditionally
on a valid (non-empty) URL+key; the test result is returned as
{ ok, saved, testError } so the UI can show 'Saved & connected' on
success or '⚠ Saved, but connection test failed: …' on failure
instead of erasing the input.
Note: setup_complete config key kept as-is — it represents 'has the user
configured Jellyfin' which is conceptually setup and not user-visible.