From 17b3aeb94e1d47e14b315484a6e9b3748f82a59c Mon Sep 17 00:00:00 2001 From: mboehmlaender Date: Tue, 17 Mar 2026 10:51:28 +0000 Subject: [PATCH] 0.10.2-16 Frontend Features --- backend/package-lock.json | 4 +- backend/package.json | 2 +- backend/src/db/database.js | 6 +++ backend/src/services/historyService.js | 11 +++-- backend/src/services/pipelineService.js | 28 +++++++++++++ frontend/package-lock.json | 4 +- frontend/package.json | 2 +- frontend/src/pages/DashboardPage.jsx | 56 +++++++++++++++++++++---- frontend/src/styles/app.css | 15 +++++++ package-lock.json | 4 +- package.json | 2 +- 11 files changed, 114 insertions(+), 20 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 1a1266e..b2676ea 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-backend", - "version": "0.10.2-15", + "version": "0.10.2-16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-backend", - "version": "0.10.2-15", + "version": "0.10.2-16", "dependencies": { "archiver": "^7.0.1", "cors": "^2.8.5", diff --git a/backend/package.json b/backend/package.json index 904dcef..e8a1aa0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-backend", - "version": "0.10.2-15", + "version": "0.10.2-16", "private": true, "type": "commonjs", "scripts": { diff --git a/backend/src/db/database.js b/backend/src/db/database.js index d81eb05..0477029 100644 --- a/backend/src/db/database.js +++ b/backend/src/db/database.js @@ -961,6 +961,12 @@ async function migrateSettingsSchemaMetadata(db) { VALUES ('download_dir_owner', 'Pfade', 'Eigentümer Download ZIP-Ordner', 'string', 0, 'Eigentümer der vorbereiteten ZIP-Dateien im Format user:gruppe. Leer = Standardbenutzer des Dienstes.', NULL, '[]', '{}', 1185)` ); await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('download_dir_owner', NULL)`); + + await db.run( + `INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) + VALUES ('auto_eject_after_rip', 'Laufwerk', 'Laufwerk nach Rip auswerfen', 'boolean', 1, 'Wenn aktiv, wird das Laufwerk nach erfolgreichem Abschluss eines Rip-Vorgangs automatisch ausgeworfen (eject).', 'false', '[]', '{}', 45)` + ); + await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('auto_eject_after_rip', 'false')`); } async function getDb() { diff --git a/backend/src/services/historyService.js b/backend/src/services/historyService.js index 369a599..546477f 100644 --- a/backend/src/services/historyService.js +++ b/backend/src/services/historyService.js @@ -2110,9 +2110,14 @@ class HistoryService { toNormalizedPath(resolvedPaths?.rawDir), ...explicitRawPaths.map((candidatePath) => toNormalizedPath(path.dirname(candidatePath))) ]); + // Only configured base dirs become protectedRoots (must never be deleted themselves) const movieRoots = sanitizeRoots([ ...getConfiguredMediaPathList(settings || {}, 'movie_dir'), - toNormalizedPath(resolvedPaths?.movieDir), + toNormalizedPath(resolvedPaths?.movieDir) + ]); + // Includes parent dirs of output files for addCandidate safety checks + const movieAllowedPaths = sanitizeRoots([ + ...movieRoots, ...explicitMoviePaths.map((candidatePath) => toNormalizedPath(path.dirname(candidatePath))) ]); @@ -2204,7 +2209,7 @@ class HistoryService { 'movie', outputPath, artifactMoviePathSet.has(outputPath) ? 'lineage_output_path' : 'output_path', - movieRoots + movieAllowedPaths ); const parentDir = toNormalizedPath(path.dirname(outputPath)); if (parentDir && !movieRoots.includes(parentDir)) { @@ -2213,7 +2218,7 @@ class HistoryService { 'movie', parentDir, artifactMoviePathSet.has(outputPath) ? 'lineage_output_parent' : 'output_parent', - movieRoots + movieAllowedPaths ); } } diff --git a/backend/src/services/pipelineService.js b/backend/src/services/pipelineService.js index ca7e4e5..b57fdc7 100644 --- a/backend/src/services/pipelineService.js +++ b/backend/src/services/pipelineService.js @@ -5201,6 +5201,32 @@ class PipelineService extends EventEmitter { } } + async ejectDriveIfEnabled(settingsMap, devicePath = null) { + try { + const enabled = String(settingsMap?.auto_eject_after_rip || '').trim().toLowerCase(); + if (enabled !== 'true' && enabled !== '1') { + return; + } + const device = devicePath + || (settingsMap?.drive_mode === 'explicit' ? String(settingsMap?.drive_device || '').trim() : null) + || String(settingsMap?.drive_device || '').trim() + || '/dev/sr0'; + logger.info('eject:drive', { device }); + await new Promise((resolve) => { + execFile('eject', [device], { timeout: 10000 }, (error) => { + if (error) { + logger.warn('eject:drive:failed', { device, error: errorToMeta(error) }); + } else { + logger.info('eject:drive:ok', { device }); + } + resolve(); + }); + }); + } catch (error) { + logger.warn('eject:drive:error', { error: errorToMeta(error) }); + } + } + normalizeDiscValue(value) { return String(value || '').trim().toLowerCase(); } @@ -9605,6 +9631,7 @@ class PipelineService extends EventEmitter { title: 'Ripster - Job abgeschlossen', message: `${job.title || job.detected_title || `Job #${jobId}`} -> ${finalizedOutputPath}` }); + void this.ejectDriveIfEnabled(settings); } setTimeout(async () => { @@ -13267,6 +13294,7 @@ class PipelineService extends EventEmitter { title: 'Ripster - CD Rip erfolgreich', message: `Job #${jobId}: ${selectedMeta?.title || 'Audio CD'}` }); + void this.ejectDriveIfEnabled(settings); } catch (error) { settleLifecycle(); const failedCdLive = buildLiveContext(currentTrackPosition || null); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2d5d7f5..0424c08 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-frontend", - "version": "0.10.2-15", + "version": "0.10.2-16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-frontend", - "version": "0.10.2-15", + "version": "0.10.2-16", "dependencies": { "primeicons": "^7.0.0", "primereact": "^10.9.2", diff --git a/frontend/package.json b/frontend/package.json index 4e5c80e..22ebfe4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-frontend", - "version": "0.10.2-15", + "version": "0.10.2-16", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx index 208bdc2..7b51fe6 100644 --- a/frontend/src/pages/DashboardPage.jsx +++ b/frontend/src/pages/DashboardPage.jsx @@ -143,7 +143,14 @@ function normalizeRuntimeActivitiesPayload(rawPayload) { jobId: Number.isFinite(Number(source.jobId)) && Number(source.jobId) > 0 ? Math.trunc(Number(source.jobId)) : null, cronJobId: Number.isFinite(Number(source.cronJobId)) && Number(source.cronJobId) > 0 ? Math.trunc(Number(source.cronJobId)) : null, canCancel: Boolean(source.canCancel), - canNextStep: Boolean(source.canNextStep) + canNextStep: Boolean(source.canNextStep), + parentActivityId: Number.isFinite(Number(source.parentActivityId)) && Number(source.parentActivityId) > 0 + ? Math.trunc(Number(source.parentActivityId)) + : null, + stepIndex: Number.isFinite(Number(source.stepIndex)) ? Math.trunc(Number(source.stepIndex)) : null, + stepTotal: Number.isFinite(Number(source.stepTotal)) && Number(source.stepTotal) > 0 + ? Math.trunc(Number(source.stepTotal)) + : null }; }; const active = (Array.isArray(payload.active) ? payload.active : []).map(normalizeItem); @@ -2109,6 +2116,18 @@ export default function DashboardPage({ ? runtimeActivities.recent.slice(0, 8) : []; + // Group chain activities with their current child script — hide child scripts from top level + const activeChainChildIds = new Set( + runtimeActiveItems.filter((i) => i.parentActivityId != null).map((i) => i.id) + ); + const activeItemsDisplay = runtimeActiveItems.filter((i) => !activeChainChildIds.has(i.id)); + const findActiveChainChild = (chainId) => runtimeActiveItems.find((i) => i.parentActivityId === chainId) || null; + + const recentChainChildIds = new Set( + runtimeRecentItems.filter((i) => i.parentActivityId != null).map((i) => i.id) + ); + const recentItemsDisplay = runtimeRecentItems.filter((i) => !recentChainChildIds.has(i.id)); + return (
@@ -2209,6 +2228,11 @@ export default function DashboardPage({ disabled={!canOpenMetadataModal} />
+
+ {device + ? + : } +
{device ? (
@@ -2690,12 +2714,20 @@ export default function DashboardPage({ Keine laufenden Skript-/Ketten-/Cron-Ausführungen. ) : (
- {runtimeActiveItems.map((item, index) => { + {activeItemsDisplay.map((item, index) => { + const isChain = String(item?.type || '').trim().toLowerCase() === 'chain'; + const chainChild = isChain ? findActiveChainChild(item?.id) : null; + const outputItem = chainChild || item; const statusMeta = runtimeStatusMeta(item?.status); const canCancel = Boolean(item?.canCancel); - const canNextStep = String(item?.type || '').trim().toLowerCase() === 'chain' && Boolean(item?.canNextStep); + const canNextStep = isChain && Boolean(item?.canNextStep); const cancelBusy = isRuntimeActionBusy(item?.id, 'cancel'); const nextStepBusy = isRuntimeActionBusy(item?.id, 'next-step'); + const stepLabel = item?.stepIndex != null && item?.stepTotal != null + ? `Schritt ${item.stepIndex + 1}/${item.stepTotal}` + : item?.currentStep + ? 'Schritt' + : null; return (
@@ -2710,11 +2742,19 @@ export default function DashboardPage({ {item?.jobId ? ` | Job #${item.jobId}` : ''} {item?.cronJobId ? ` | Cron #${item.cronJobId}` : ''} - {item?.currentStep ? Schritt: {item.currentStep} : null} - {item?.currentScriptName ? Laufendes Skript: {item.currentScriptName} : null} - {item?.message ? {item.message} : null} + {isChain && (chainChild || item?.currentStep || item?.currentScriptName) ? ( +
+ + {stepLabel ? {stepLabel}: : null} + {chainChild?.name || item?.currentScriptName || item?.currentStep || null} + + {chainChild?.message ? {chainChild.message} : null} +
+ ) : null} + {!isChain && item?.currentStep ? Schritt: {item.currentStep} : null} + {!isChain && item?.message ? {item.message} : null} Gestartet: {formatUpdatedAt(item?.startedAt)} @@ -2765,7 +2805,7 @@ export default function DashboardPage({ Keine abgeschlossenen Einträge vorhanden. ) : (
- {runtimeRecentItems.map((item, index) => { + {recentItemsDisplay.map((item, index) => { const outcomeMeta = runtimeOutcomeMeta(item?.outcome, item?.status); return (
diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index fd65416..607b8bc 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -367,6 +367,10 @@ body { flex-wrap: wrap; } +.dashboard-drive-state { + margin: 0.5rem 0; +} + .settings-expert-toggle { display: inline-flex; align-items: center; @@ -3808,6 +3812,17 @@ body { margin-top: 0.2rem; } +.runtime-chain-current { + display: flex; + flex-direction: column; + gap: 0.1rem; + padding: 0.3rem 0.45rem; + border-left: 2px solid var(--rip-border); + margin: 0.1rem 0; + background: var(--rip-surface); + border-radius: 0 0.3rem 0.3rem 0; +} + .runtime-activity-details summary { cursor: pointer; font-size: 0.82rem; diff --git a/package-lock.json b/package-lock.json index a06b6e7..64e4aed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster", - "version": "0.10.2-15", + "version": "0.10.2-16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster", - "version": "0.10.2-15", + "version": "0.10.2-16", "devDependencies": { "concurrently": "^9.1.2" } diff --git a/package.json b/package.json index 88e510a..fcce2a6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ripster", "private": true, - "version": "0.10.2-15", + "version": "0.10.2-16", "scripts": { "dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"", "dev:backend": "npm run dev --prefix backend",