the previous TimeInput was a bespoke two-field widget. correct in behaviour
but off-policy: we don't roll our own ui primitives when a maintained
library solves it. swap for react-aria-components + @internationalized/date
pinned to hourCycle={24}, granularity=minute, shouldForceLeadingZeros so
the output is always strict HH:MM regardless of browser/OS locale.
wrapper lives at src/shared/components/ui/time-input.tsx and keeps the
existing string-based API (value: "HH:MM", onChange(next)) so callers don't
change.
also updates the stack docs: web-stack.md now pins react-aria-components
as THE required library for every date/time ui; iOS and Android entries
mark their canonical component as TBD and explicitly forbid rolling our
own without user sign-off.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
native <input type="time"> inherits the browser/OS locale; on chrome/macos in
en-US that means AM/PM in the settings schedule editor. no attribute turns
it off — lang="en-GB" is not honored. swap for a tiny headless TimeInput
(two number fields, HH clamp 0..23, MM clamp 0..59) that always emits HH:MM.
also drop toLocaleString() in ScanPage, which silently flips thousands
separators in de-DE / fr-FR. add formatThousands() that always produces the
1,234,567 form regardless of locale.
shared/components/ui/time-input.tsx + shared/lib/utils.ts#formatThousands are
now the canonical helpers — every future time/number render must use them.
captured as a forward-looking feedback memory so this doesn't regress.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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 reset button nukes every table and reseeds defaults, so on a schema
change we no longer need per-column ALTERs to keep old databases working.
fresh installs get SCHEMA; upgrading installs click Reset once and they're
on the new layout.
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>
upgraded containers (like the unraid one with a persistent ./data volume)
kept their old media_items/media_streams tables, so upsertJellyfinItem hit
"table media_items has no column named original_title" on first scan after
the canonical-language rewrite. sqlite has no native add-column-if-not-exists
so we try/catch each alter, same pattern we had before i deleted the shims.
also backports the older alters (stream_decisions.custom_title, review_plans
subs_extracted/confidence/apple_compat/job_type) so pre-rewrite installs
still converge without a wipe.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
the gitea workflow tags the image with the package.json version; docker oci
refs forbid '+', so 2026.04.13+2 broke buildx with "invalid reference format".
switch to 2026.04.13.3. this matches what project memory already documented;
the global AGENTS.md example using +n doesn't apply here because we ship via
a registry.
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>
ffmpeg now writes -metadata:s:a:i language=<iso3> on every kept audio track so
files end up with canonical 3-letter tags (en → eng, ger → deu, null → und).
analyzer passes stream.profile (not title) to transcodeTarget so lossless
dts-hd ma in mkv correctly targets flac. is_noop also checks og-is-default and
canonical-language so pipeline-would-change-it cases stop showing as done.
normalizeLanguage gains 2→3 mapping, and mapStream no longer normalizes at
ingest so the raw jellyfin tag survives for the canonical check.
per-item scan work runs in a single db.transaction for large sqlite speedups,
extracted into server/services/rescan.ts so execute.ts can reuse it.
on successful job, execute calls jellyfin /Items/{id}/Refresh, waits for
DateLastRefreshed to change, refetches the item, and upserts it through the
same pipeline; plan flips to done iff the fresh streams satisfy is_noop.
schema wiped + rewritten to carry jellyfin_raw, external_raw, profile,
bit_depth, date_last_refreshed, runtime_ticks, original_title, last_executed_at
— so future scans aren't required to stay correct. user must drop data/*.db.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- remove nodes table, ssh service, nodes api, NodesPage route
- execute.ts: local-only spawn, atomic CAS job claim via UPDATE status
- wrap job done + subtitle_files insert + review_plans status in db transaction
- stream ffmpeg output per line with 500ms throttled flush
- bump version to 2026.04.13
- server-side filter + LIMIT 200 + totalCounts on GET /api/execute
- shared FilterTabs component with status-colored active tabs
- execute page: filter tabs, SSE live count updates, module-level cache
- replace inline tab pills in AudioListPage, SubtitleListPage with FilterTabs
- fix buildExtractOnlyCommand: skip -map 0:a when no audio streams exist
- bump version
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- show version (from package.json) in nav bar, warn on frontend/server mismatch
- apply path_mappings to file access check and command string at execution time
so existing scans with old jellyfin paths work without re-scanning
- add clear done/errors button on execute page
- bump version to 2026.03.04
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rewrite from monolithic hono jsx to react 19 spa with tanstack router
+ hono json api backend. add scan, review, execute, nodes, and setup
pages. multi-stage dockerfile (node for vite build, bun for runtime).
previously, server/ and src/shared/lib/ were silently excluded by
global gitignore patterns (/server/ from emacs, lib/ from python).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>