From da668b2d3629e162ed731a63342e3cff35dea472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Thu, 5 Mar 2026 10:31:05 +0100 Subject: [PATCH] add paths page to check volume accessibility after scan Co-Authored-By: Claude Opus 4.6 --- server/api/paths.ts | 35 +++++++++++++++ server/index.tsx | 2 + src/features/paths/PathsPage.tsx | 75 ++++++++++++++++++++++++++++++++ src/routes/__root.tsx | 1 + src/routes/paths.tsx | 6 +++ 5 files changed, 119 insertions(+) create mode 100644 server/api/paths.ts create mode 100644 src/features/paths/PathsPage.tsx create mode 100644 src/routes/paths.tsx diff --git a/server/api/paths.ts b/server/api/paths.ts new file mode 100644 index 0000000..1f79870 --- /dev/null +++ b/server/api/paths.ts @@ -0,0 +1,35 @@ +import { existsSync } from 'node:fs'; +import { Hono } from 'hono'; +import { getDb } from '../db/index'; + +const app = new Hono(); + +interface PathInfo { + prefix: string; + itemCount: number; + accessible: boolean; +} + +app.get('/', (c) => { + const db = getDb(); + const rows = db + .query<{ prefix: string; count: number }, []>( + `SELECT substr(file_path, 1, instr(substr(file_path, 2), '/') + 1) AS prefix, + COUNT(*) AS count + FROM media_items + WHERE file_path IS NOT NULL AND file_path != '' + GROUP BY prefix + ORDER BY prefix`, + ) + .all(); + + const paths: PathInfo[] = rows.map((r: { prefix: string; count: number }) => ({ + prefix: r.prefix, + itemCount: r.count, + accessible: existsSync(r.prefix), + })); + + return c.json({ paths }); +}); + +export default app; diff --git a/server/index.tsx b/server/index.tsx index 3eba5e2..23eb1a8 100644 --- a/server/index.tsx +++ b/server/index.tsx @@ -11,6 +11,7 @@ import executeRoutes from './api/execute'; import nodesRoutes from './api/nodes'; import subtitlesRoutes from './api/subtitles'; import dashboardRoutes from './api/dashboard'; +import pathsRoutes from './api/paths'; const app = new Hono(); @@ -41,6 +42,7 @@ app.route('/api/review', reviewRoutes); app.route('/api/execute', executeRoutes); app.route('/api/subtitles', subtitlesRoutes); app.route('/api/nodes', nodesRoutes); +app.route('/api/paths', pathsRoutes); // ─── Static assets (production: serve Vite build) ──────────────────────────── diff --git a/src/features/paths/PathsPage.tsx b/src/features/paths/PathsPage.tsx new file mode 100644 index 0000000..ab41577 --- /dev/null +++ b/src/features/paths/PathsPage.tsx @@ -0,0 +1,75 @@ +import { useEffect, useState } from 'react'; +import { api } from '~/shared/lib/api'; +import { Badge } from '~/shared/components/ui/badge'; +import { Button } from '~/shared/components/ui/button'; + +interface PathInfo { + prefix: string; + itemCount: number; + accessible: boolean; +} + +export function PathsPage() { + const [paths, setPaths] = useState([]); + const [loading, setLoading] = useState(true); + + const load = () => { + setLoading(true); + api.get<{ paths: PathInfo[] }>('/api/paths') + .then((d) => setPaths(d.paths)) + .finally(() => setLoading(false)); + }; + + useEffect(() => { load(); }, []); + + return ( +
+
+

Paths

+ +
+ + {paths.length === 0 && !loading && ( +

No media items scanned yet. Run a scan first.

+ )} + + {paths.length > 0 && ( + <> + + + + + + + + + + {paths.map((p) => ( + + + + + + ))} + +
PathItemsStatus
{p.prefix}{p.itemCount} + {p.accessible ? ( + Accessible + ) : ( + Not mounted + )} +
+ + {paths.some((p) => !p.accessible) && ( +

+ Paths marked "Not mounted" are not reachable from the container. + Add a Docker volume mount for the missing path, or configure a path mapping in Settings to translate the Jellyfin path to a local path. +

+ )} + + )} +
+ ); +} diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 505ffdb..fc27f27 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -50,6 +50,7 @@ function RootLayout() {
Scan + Paths Audio Subs Execute diff --git a/src/routes/paths.tsx b/src/routes/paths.tsx new file mode 100644 index 0000000..b2b819a --- /dev/null +++ b/src/routes/paths.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { PathsPage } from '~/features/paths/PathsPage'; + +export const Route = createFileRoute('/paths')({ + component: PathsPage, +});