From 818b0d1396cd8636c17ee7d0dd49869c494b0745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Wed, 4 Mar 2026 17:22:14 +0100 Subject: [PATCH] add version badge in nav, apply path mappings at execution time, clear done/error jobs - 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 --- package.json | 2 +- server/api/execute.ts | 16 ++++++++++++---- server/db/index.ts | 21 ++++++++++++++++++--- server/index.tsx | 3 +++ src/features/execute/ExecutePage.tsx | 2 ++ src/routes/__root.tsx | 18 ++++++++++++++++++ vite.config.ts | 4 ++++ 7 files changed, 58 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 78c8542..33da99f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "netfelix-audio-fix", - "version": "2026.02.26", + "version": "2026.03.04", "scripts": { "dev:server": "NODE_ENV=development bun --hot server/index.tsx", "dev:client": "vite", diff --git a/server/api/execute.ts b/server/api/execute.ts index 9724cba..43d7447 100644 --- a/server/api/execute.ts +++ b/server/api/execute.ts @@ -1,6 +1,6 @@ import { Hono } from 'hono'; import { stream } from 'hono/streaming'; -import { getDb } from '../db/index'; +import { getDb, applyPathMappings, applyPathMappingsToCommand } from '../db/index'; import { execStream } from '../services/ssh'; import type { Job, Node, MediaItem, MediaStream } from '../types'; import { predictExtractedFiles } from '../services/ffmpeg'; @@ -129,6 +129,12 @@ app.post('/clear', (c) => { return c.json({ ok: true, cleared: result.changes }); }); +app.post('/clear-completed', (c) => { + const db = getDb(); + const result = db.prepare("DELETE FROM jobs WHERE status IN ('done', 'error')").run(); + return c.json({ ok: true, cleared: result.changes }); +}); + // ─── SSE ────────────────────────────────────────────────────────────────────── app.get('/events', (c) => { @@ -167,8 +173,9 @@ async function runJob(job: Job): Promise { if (!job.node_id) { const itemRow = db.prepare('SELECT file_path FROM media_items WHERE id = ?').get(job.item_id) as { file_path: string } | undefined; if (itemRow?.file_path) { - try { accessSync(itemRow.file_path, constants.R_OK | constants.W_OK); } catch (fsErr) { - const msg = `File not accessible: ${itemRow.file_path}\n${(fsErr as Error).message}`; + const mappedPath = applyPathMappings(itemRow.file_path); + try { accessSync(mappedPath, constants.R_OK | constants.W_OK); } catch (fsErr) { + const msg = `File not accessible: ${mappedPath}\n${(fsErr as Error).message}`; db.prepare("UPDATE jobs SET status = 'error', output = ?, exit_code = 1, completed_at = datetime('now') WHERE id = ?").run(msg, job.id); emitJobUpdate(job.id, 'error', msg); db.prepare("UPDATE review_plans SET status = 'error' WHERE item_id = ?").run(job.item_id); @@ -197,7 +204,8 @@ async function runJob(job: Job): Promise { if (node.series_path) cmd = cmd.replaceAll('/series/', node.series_path.replace(/\/$/, '') + '/'); for await (const line of execStream(node, cmd)) { outputLines.push(line); flush(); } } else { - const proc = Bun.spawn(['sh', '-c', job.command], { stdout: 'pipe', stderr: 'pipe' }); + const mappedCmd = applyPathMappingsToCommand(job.command); + const proc = Bun.spawn(['sh', '-c', mappedCmd], { stdout: 'pipe', stderr: 'pipe' }); const readStream = async (readable: ReadableStream, prefix = '') => { const reader = readable.getReader(); const decoder = new TextDecoder(); diff --git a/server/db/index.ts b/server/db/index.ts index 80f71e5..3260a97 100644 --- a/server/db/index.ts +++ b/server/db/index.ts @@ -111,12 +111,27 @@ export function getAllConfig(): Record { return result; } +function getPathMappings(): [string, string][] { + const raw = getConfig('path_mappings'); + if (!raw) return []; + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed.filter(([f, t]: [string, string]) => f && t) : []; + } catch { return []; } +} + /** Apply path_mappings config to translate a Jellyfin path to a local container path. */ export function applyPathMappings(path: string): string { - const raw = getConfig('path_mappings') ?? '[]'; - const mappings = JSON.parse(raw) as [string, string][]; - for (const [from, to] of mappings) { + for (const [from, to] of getPathMappings()) { if (path.startsWith(from)) return to + path.slice(from.length); } return path; } + +/** Apply all path_mappings as replaceAll on a command string (for baked-in paths). */ +export function applyPathMappingsToCommand(cmd: string): string { + for (const [from, to] of getPathMappings()) { + cmd = cmd.replaceAll(from, to); + } + return cmd; +} diff --git a/server/index.tsx b/server/index.tsx index 633fd66..f769a09 100644 --- a/server/index.tsx +++ b/server/index.tsx @@ -19,6 +19,9 @@ app.use('/api/*', cors({ origin: ['http://localhost:5173', 'http://localhost:300 // ─── API routes ─────────────────────────────────────────────────────────────── +import pkg from '../package.json'; + +app.get('/api/version', (c) => c.json({ version: pkg.version })); app.route('/api/dashboard', dashboardRoutes); app.route('/api/setup', setupRoutes); app.route('/api/scan', scanRoutes); diff --git a/src/features/execute/ExecutePage.tsx b/src/features/execute/ExecutePage.tsx index 4a12eab..b12386c 100644 --- a/src/features/execute/ExecutePage.tsx +++ b/src/features/execute/ExecutePage.tsx @@ -55,6 +55,7 @@ export function ExecutePage() { const startAll = async () => { await api.post('/api/execute/start'); load(); }; const clearQueue = async () => { await api.post('/api/execute/clear'); load(); }; + const clearCompleted = async () => { await api.post('/api/execute/clear-completed'); load(); }; const runJob = async (id: number) => { await api.post(`/api/execute/job/${id}/run`); load(); }; const cancelJob = async (id: number) => { await api.post(`/api/execute/job/${id}/cancel`); load(); }; const assignNode = async (jobId: number, nodeId: number | null) => { @@ -88,6 +89,7 @@ export function ExecutePage() {
{pending > 0 && } {pending > 0 && } + {(done > 0 || errors > 0) && } {jobs.length === 0 && (

No jobs yet. Go to Review and approve items first. diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 8536757..b6e29d9 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,5 +1,9 @@ import { createRootRoute, Link, Outlet } from '@tanstack/react-router'; +import { useEffect, useState } from 'react'; import { cn } from '~/shared/lib/utils'; +import { api } from '~/shared/lib/api'; + +declare const __APP_VERSION__: string; export const Route = createRootRoute({ component: RootLayout, @@ -17,6 +21,19 @@ function NavLink({ to, children }: { to: string; children: React.ReactNode }) { ); } +function VersionBadge() { + const [serverVersion, setServerVersion] = useState(null); + useEffect(() => { api.get<{ version: string }>('/api/version').then((d) => setServerVersion(d.version)).catch(() => {}); }, []); + const buildVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : null; + const mismatch = buildVersion && serverVersion && buildVersion !== serverVersion; + return ( + + v{serverVersion ?? buildVersion ?? '?'} + {mismatch && } + + ); +} + function RootLayout() { const isDev = import.meta.env.DEV; return ( @@ -30,6 +47,7 @@ function RootLayout() { 🎬 netfelix-audio-fix + Scan Audio Subtitles diff --git a/vite.config.ts b/vite.config.ts index d537522..196d56e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react-swc'; import tailwindcss from '@tailwindcss/vite'; import { TanStackRouterVite } from '@tanstack/router-plugin/vite'; import { resolve } from 'node:path'; +import pkg from './package.json' with { type: 'json' }; export default defineConfig({ plugins: [ @@ -21,6 +22,9 @@ export default defineConfig({ '/api': { target: 'http://localhost:3000', changeOrigin: true }, }, }, + define: { + __APP_VERSION__: JSON.stringify(pkg.version), + }, build: { outDir: 'dist', emptyOutDir: true,