Files
Felix Förtsch c0bcbaec1b
All checks were successful
Build and Push Docker Image / build (push) Successful in 49s
time input: replace hand-rolled fields with react-aria-components TimeField
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>
2026-04-13 15:37:03 +02:00

4.7 KiB

netfelix-audio-fix — Project Memory

What it does

Bun + Hono JSON REST API + React 19 SPA to scan a Jellyfin library, compute which audio/subtitle tracks to remove from each file, let you review and edit those decisions, then execute FFmpeg (copy mode) to strip/reorder streams. Remote nodes via SSH.

Key technical decisions

  • Runtime: Bun + Hono JSON REST API backend; React 19 SPA frontend via Vite
  • DB: bun:sqlite WAL mode → data/netfelix.db (server-side; PGlite not applicable)
  • Frontend stack: React 19 + TanStack Router + Zustand + Tailwind v4 + cn() utilities
  • Code quality: Biome (formatting + linting) — no ESLint/Prettier
  • Path alias: ~/src/ (vite.config.ts resolve.alias + tsconfig.json paths)
  • Dev: bun run dev runs Hono API (:3000) + Vite (:5173) concurrently; Vite proxies /api/*
  • Prod: bun run build → Vite builds to dist/; Hono serves dist/ + /api/* routes
  • SSH keys: uploaded via file input, stored as PEM text in nodes.private_key
  • Two tsconfigs: tsconfig.json (frontend, DOM lib), tsconfig.server.json (backend, bun-types)

Project structure

server/                   ← Backend (Bun + Hono, JSON API at /api/*)
  index.tsx               ← entry point, Bun.serve, CORS for dev, static SPA serve
  types.ts                ← server-side interfaces
  db/index.ts             ← getDb(), getConfig(), setConfig(), getEnvLockedKeys()
  db/schema.ts            ← SCHEMA DDL string + DEFAULT_CONFIG
  services/
    jellyfin.ts / radarr.ts / sonarr.ts / analyzer.ts / ffmpeg.ts / ssh.ts
  api/
    dashboard.ts / scan.ts / review.ts / execute.ts / nodes.ts / setup.ts / subtitles.ts
src/                      ← Frontend (React SPA, built with Vite)
  main.tsx                ← entry, RouterProvider
  index.css               ← Tailwind v4 @import
  routeTree.gen.ts        ← auto-generated by TanStack Router (gitignored)
  routes/
    __root.tsx            ← nav layout with Link components
    index.tsx / scan.tsx / execute.tsx / nodes.tsx / setup.tsx
    review.tsx            ← layout route with Audio/Subtitles tab bar + Outlet
    review/index.tsx (redirect → /review/audio)
    review/audio/index.tsx ($filter) / review/audio/$id.tsx
    review/subtitles/index.tsx ($filter) / review/subtitles/$id.tsx
  features/
    dashboard/DashboardPage.tsx
    scan/ScanPage.tsx (SSE for live progress)
    review/AudioListPage.tsx / AudioDetailPage.tsx
    subtitles/SubtitleListPage.tsx / SubtitleDetailPage.tsx
    execute/ExecutePage.tsx (SSE for job updates)
    nodes/NodesPage.tsx
    setup/SetupPage.tsx
  shared/
    lib/utils.ts (cn()) / api.ts (typed fetch) / types.ts / lang.ts (LANG_NAMES)
    components/ui/badge.tsx / button.tsx / input.tsx / select.tsx / textarea.tsx / alert.tsx
biome.json / vite.config.ts / tsconfig.json / tsconfig.server.json / index.html

Rules: what gets kept

  • Video/Data streams: always keep
  • Audio: keep original_language + configured audio_languages (if OG unknown → keep all, flag needs_review)
  • Audio order: OG first, then additional languages in audio_languages config order
  • Subtitles: ALL removed from container, extracted to sidecar files on disk
  • subtitle_files table tracks extracted sidecar files (file manager UI)
  • review_plans.subs_extracted flag tracks extraction status
  • is_noop only considers audio changes (subtitle extraction is implicit)

Scan flow

  1. Jellyfin paginated API → upsert media_items + media_streams
  2. Cross-check with Radarr (movies) / Sonarr (episodes) for language
  3. analyzeItem() → upsert review_plans + stream_decisions
  4. SSE events stream progress to browser (React EventSource in ScanPage)

Running locally

mise exec bun -- bun run dev   # concurrent: Hono API :3000 + Vite :5173
mise exec bun -- bun run build # build frontend to dist/
mise exec bun -- bun start     # production: Hono serves dist/ + API on :3000

Forward-looking rules

Workflow rules

  • Always bump version in package.json before committing/pushing. CalVer with dot-suffix per global AGENTS.md (YYYY.MM.DD.N). .gitea/workflows/build.yml tags a Docker image with this version, so the +N form breaks CI with invalid reference format.

Docker deploy (Unraid)

docker compose up -d   # port 3000, data volume at ./data/

Note: Docker must serve the built dist/ — run bun run build before building the Docker image.