0.10.2-16 Frontend Features
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.10.2-15",
|
"version": "0.10.2-16",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.10.2-15",
|
"version": "0.10.2-16",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.10.2-15",
|
"version": "0.10.2-16",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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)`
|
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_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() {
|
async function getDb() {
|
||||||
|
|||||||
@@ -2110,9 +2110,14 @@ class HistoryService {
|
|||||||
toNormalizedPath(resolvedPaths?.rawDir),
|
toNormalizedPath(resolvedPaths?.rawDir),
|
||||||
...explicitRawPaths.map((candidatePath) => toNormalizedPath(path.dirname(candidatePath)))
|
...explicitRawPaths.map((candidatePath) => toNormalizedPath(path.dirname(candidatePath)))
|
||||||
]);
|
]);
|
||||||
|
// Only configured base dirs become protectedRoots (must never be deleted themselves)
|
||||||
const movieRoots = sanitizeRoots([
|
const movieRoots = sanitizeRoots([
|
||||||
...getConfiguredMediaPathList(settings || {}, 'movie_dir'),
|
...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)))
|
...explicitMoviePaths.map((candidatePath) => toNormalizedPath(path.dirname(candidatePath)))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -2204,7 +2209,7 @@ class HistoryService {
|
|||||||
'movie',
|
'movie',
|
||||||
outputPath,
|
outputPath,
|
||||||
artifactMoviePathSet.has(outputPath) ? 'lineage_output_path' : 'output_path',
|
artifactMoviePathSet.has(outputPath) ? 'lineage_output_path' : 'output_path',
|
||||||
movieRoots
|
movieAllowedPaths
|
||||||
);
|
);
|
||||||
const parentDir = toNormalizedPath(path.dirname(outputPath));
|
const parentDir = toNormalizedPath(path.dirname(outputPath));
|
||||||
if (parentDir && !movieRoots.includes(parentDir)) {
|
if (parentDir && !movieRoots.includes(parentDir)) {
|
||||||
@@ -2213,7 +2218,7 @@ class HistoryService {
|
|||||||
'movie',
|
'movie',
|
||||||
parentDir,
|
parentDir,
|
||||||
artifactMoviePathSet.has(outputPath) ? 'lineage_output_parent' : 'output_parent',
|
artifactMoviePathSet.has(outputPath) ? 'lineage_output_parent' : 'output_parent',
|
||||||
movieRoots
|
movieAllowedPaths
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
normalizeDiscValue(value) {
|
||||||
return String(value || '').trim().toLowerCase();
|
return String(value || '').trim().toLowerCase();
|
||||||
}
|
}
|
||||||
@@ -9605,6 +9631,7 @@ class PipelineService extends EventEmitter {
|
|||||||
title: 'Ripster - Job abgeschlossen',
|
title: 'Ripster - Job abgeschlossen',
|
||||||
message: `${job.title || job.detected_title || `Job #${jobId}`} -> ${finalizedOutputPath}`
|
message: `${job.title || job.detected_title || `Job #${jobId}`} -> ${finalizedOutputPath}`
|
||||||
});
|
});
|
||||||
|
void this.ejectDriveIfEnabled(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
@@ -13267,6 +13294,7 @@ class PipelineService extends EventEmitter {
|
|||||||
title: 'Ripster - CD Rip erfolgreich',
|
title: 'Ripster - CD Rip erfolgreich',
|
||||||
message: `Job #${jobId}: ${selectedMeta?.title || 'Audio CD'}`
|
message: `Job #${jobId}: ${selectedMeta?.title || 'Audio CD'}`
|
||||||
});
|
});
|
||||||
|
void this.ejectDriveIfEnabled(settings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
settleLifecycle();
|
settleLifecycle();
|
||||||
const failedCdLive = buildLiveContext(currentTrackPosition || null);
|
const failedCdLive = buildLiveContext(currentTrackPosition || null);
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.10.2-15",
|
"version": "0.10.2-16",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.10.2-15",
|
"version": "0.10.2-16",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primereact": "^10.9.2",
|
"primereact": "^10.9.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.10.2-15",
|
"version": "0.10.2-16",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -143,7 +143,14 @@ function normalizeRuntimeActivitiesPayload(rawPayload) {
|
|||||||
jobId: Number.isFinite(Number(source.jobId)) && Number(source.jobId) > 0 ? Math.trunc(Number(source.jobId)) : null,
|
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,
|
cronJobId: Number.isFinite(Number(source.cronJobId)) && Number(source.cronJobId) > 0 ? Math.trunc(Number(source.cronJobId)) : null,
|
||||||
canCancel: Boolean(source.canCancel),
|
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);
|
const active = (Array.isArray(payload.active) ? payload.active : []).map(normalizeItem);
|
||||||
@@ -2109,6 +2116,18 @@ export default function DashboardPage({
|
|||||||
? runtimeActivities.recent.slice(0, 8)
|
? 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 (
|
return (
|
||||||
<div className="page-grid">
|
<div className="page-grid">
|
||||||
<Toast ref={toastRef} />
|
<Toast ref={toastRef} />
|
||||||
@@ -2209,6 +2228,11 @@ export default function DashboardPage({
|
|||||||
disabled={!canOpenMetadataModal}
|
disabled={!canOpenMetadataModal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="dashboard-drive-state">
|
||||||
|
{device
|
||||||
|
? <Tag value="Disk eingelegt" severity="success" icon="pi pi-circle-fill" />
|
||||||
|
: <Tag value="Laufwerk leer" severity="secondary" icon="pi pi-circle" />}
|
||||||
|
</div>
|
||||||
{device ? (
|
{device ? (
|
||||||
<div className="device-meta">
|
<div className="device-meta">
|
||||||
<div>
|
<div>
|
||||||
@@ -2690,12 +2714,20 @@ export default function DashboardPage({
|
|||||||
<small className="queue-empty-hint">Keine laufenden Skript-/Ketten-/Cron-Ausführungen.</small>
|
<small className="queue-empty-hint">Keine laufenden Skript-/Ketten-/Cron-Ausführungen.</small>
|
||||||
) : (
|
) : (
|
||||||
<div className="runtime-activity-list">
|
<div className="runtime-activity-list">
|
||||||
{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 statusMeta = runtimeStatusMeta(item?.status);
|
||||||
const canCancel = Boolean(item?.canCancel);
|
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 cancelBusy = isRuntimeActionBusy(item?.id, 'cancel');
|
||||||
const nextStepBusy = isRuntimeActionBusy(item?.id, 'next-step');
|
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 (
|
return (
|
||||||
<div key={`runtime-active-${item?.id || index}`} className="runtime-activity-item running">
|
<div key={`runtime-active-${item?.id || index}`} className="runtime-activity-item running">
|
||||||
<div className="runtime-activity-head">
|
<div className="runtime-activity-head">
|
||||||
@@ -2710,11 +2742,19 @@ export default function DashboardPage({
|
|||||||
{item?.jobId ? ` | Job #${item.jobId}` : ''}
|
{item?.jobId ? ` | Job #${item.jobId}` : ''}
|
||||||
{item?.cronJobId ? ` | Cron #${item.cronJobId}` : ''}
|
{item?.cronJobId ? ` | Cron #${item.cronJobId}` : ''}
|
||||||
</small>
|
</small>
|
||||||
{item?.currentStep ? <small>Schritt: {item.currentStep}</small> : null}
|
{isChain && (chainChild || item?.currentStep || item?.currentScriptName) ? (
|
||||||
{item?.currentScriptName ? <small>Laufendes Skript: {item.currentScriptName}</small> : null}
|
<div className="runtime-chain-current">
|
||||||
{item?.message ? <small>{item.message}</small> : null}
|
<small>
|
||||||
|
{stepLabel ? <strong>{stepLabel}: </strong> : null}
|
||||||
|
{chainChild?.name || item?.currentScriptName || item?.currentStep || null}
|
||||||
|
</small>
|
||||||
|
{chainChild?.message ? <small>{chainChild.message}</small> : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{!isChain && item?.currentStep ? <small>Schritt: {item.currentStep}</small> : null}
|
||||||
|
{!isChain && item?.message ? <small>{item.message}</small> : null}
|
||||||
<RuntimeActivityDetails
|
<RuntimeActivityDetails
|
||||||
item={item}
|
item={outputItem}
|
||||||
summary="Live-Ausgabe anzeigen"
|
summary="Live-Ausgabe anzeigen"
|
||||||
/>
|
/>
|
||||||
<small>Gestartet: {formatUpdatedAt(item?.startedAt)}</small>
|
<small>Gestartet: {formatUpdatedAt(item?.startedAt)}</small>
|
||||||
@@ -2765,7 +2805,7 @@ export default function DashboardPage({
|
|||||||
<small className="queue-empty-hint">Keine abgeschlossenen Einträge vorhanden.</small>
|
<small className="queue-empty-hint">Keine abgeschlossenen Einträge vorhanden.</small>
|
||||||
) : (
|
) : (
|
||||||
<div className="runtime-activity-list">
|
<div className="runtime-activity-list">
|
||||||
{runtimeRecentItems.map((item, index) => {
|
{recentItemsDisplay.map((item, index) => {
|
||||||
const outcomeMeta = runtimeOutcomeMeta(item?.outcome, item?.status);
|
const outcomeMeta = runtimeOutcomeMeta(item?.outcome, item?.status);
|
||||||
return (
|
return (
|
||||||
<div key={`runtime-recent-${item?.id || index}`} className="runtime-activity-item done">
|
<div key={`runtime-recent-${item?.id || index}`} className="runtime-activity-item done">
|
||||||
|
|||||||
@@ -367,6 +367,10 @@ body {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-drive-state {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-expert-toggle {
|
.settings-expert-toggle {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -3808,6 +3812,17 @@ body {
|
|||||||
margin-top: 0.2rem;
|
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 {
|
.runtime-activity-details summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.10.2-15",
|
"version": "0.10.2-16",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.10.2-15",
|
"version": "0.10.2-16",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2"
|
"concurrently": "^9.1.2"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.10.2-15",
|
"version": "0.10.2-16",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
||||||
"dev:backend": "npm run dev --prefix backend",
|
"dev:backend": "npm run dev --prefix backend",
|
||||||
|
|||||||
Reference in New Issue
Block a user