0.11.0-5 multi lw
This commit is contained in:
@@ -16,7 +16,13 @@
|
|||||||
"Bash(node --check /home/michael/ripster/backend/src/services/settingsService.js)",
|
"Bash(node --check /home/michael/ripster/backend/src/services/settingsService.js)",
|
||||||
"Bash(node --check /home/michael/ripster/backend/src/services/pipelineService.js)",
|
"Bash(node --check /home/michael/ripster/backend/src/services/pipelineService.js)",
|
||||||
"Bash(node --check /home/michael/ripster/backend/src/db/database.js)",
|
"Bash(node --check /home/michael/ripster/backend/src/db/database.js)",
|
||||||
"Bash(node --check /home/michael/ripster/backend/src/routes/settingsRoutes.js)"
|
"Bash(node --check /home/michael/ripster/backend/src/routes/settingsRoutes.js)",
|
||||||
|
"Bash(sqlite3 /home/michael/ripster/backend/ripster.db \"SELECT key, value FROM settings_values WHERE key LIKE ''%drive%'' OR key LIKE ''%disc%'' ORDER BY key;\")",
|
||||||
|
"Bash(sqlite3 /home/michael/ripster/backend/data/ripster.db \"SELECT key, value FROM settings_values WHERE key LIKE ''%drive%'' OR key LIKE ''%disc%'' ORDER BY key;\")",
|
||||||
|
"Bash(lsblk -J -o NAME,PATH,TYPE,MOUNTPOINT,FSTYPE,LABEL,MODEL)",
|
||||||
|
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); [print\\(e\\) for e in d.get\\(''''blockdevices'''',[]\\)]\")",
|
||||||
|
"Bash(lsblk -o NAME,TYPE,MODEL)",
|
||||||
|
"Bash(node --check /home/michael/ripster/backend/src/routes/pipelineRoutes.js)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.11.0-4",
|
"version": "0.11.0-5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.11.0-4",
|
"version": "0.11.0-5",
|
||||||
"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.11.0-4",
|
"version": "0.11.0-5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -54,6 +54,21 @@ router.post(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/rescan-drive',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const devicePath = String(req.body?.devicePath || '').trim();
|
||||||
|
logger.info('post:rescan-drive', { reqId: req.reqId, devicePath });
|
||||||
|
if (!devicePath) {
|
||||||
|
const err = new Error('devicePath ist erforderlich');
|
||||||
|
err.statusCode = 400;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const result = await diskDetectionService.rescanDriveAndEmit(devicePath);
|
||||||
|
res.json({ result });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/omdb/search',
|
'/omdb/search',
|
||||||
asyncHandler(async (req, res) => {
|
asyncHandler(async (req, res) => {
|
||||||
|
|||||||
@@ -464,7 +464,65 @@ class DiskDetectionService extends EventEmitter {
|
|||||||
return results.filter(Boolean);
|
return results.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.detectAllAuto();
|
// Auto mode: scan all ROM drives via lsblk
|
||||||
|
const autoResults = await this.detectAllAuto();
|
||||||
|
|
||||||
|
// Fallback: if lsblk found no ROM drives but drive_device is configured, try it directly
|
||||||
|
if (autoResults.length === 0) {
|
||||||
|
const legacy = String(settingsMap.drive_device || '').trim();
|
||||||
|
if (legacy) {
|
||||||
|
logger.debug('detect:auto:lsblk-empty-fallback-explicit', { legacy });
|
||||||
|
const legacyResult = await this.detectExplicit(legacy);
|
||||||
|
if (legacyResult) {
|
||||||
|
return [legacyResult];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return autoResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rescan a single specific drive and emit the appropriate event
|
||||||
|
async rescanDriveAndEmit(devicePath) {
|
||||||
|
const normalized = this.normalizeDevicePath(devicePath);
|
||||||
|
if (!normalized) {
|
||||||
|
return { present: false, emitted: 'none', device: null };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
logger.info('rescan-drive:requested', { devicePath: normalized });
|
||||||
|
const detected = await this.detectExplicit(normalized);
|
||||||
|
const previouslyTracked = this.detectedDiscs.has(normalized);
|
||||||
|
|
||||||
|
if (detected) {
|
||||||
|
const previous = this.detectedDiscs.get(normalized);
|
||||||
|
const changed = previous ? buildSignature(previous) !== buildSignature(detected) : false;
|
||||||
|
this.detectedDiscs.set(normalized, detected);
|
||||||
|
this.lastDetected = detected;
|
||||||
|
this.lastPresent = true;
|
||||||
|
logger.info('rescan-drive:inserted', { devicePath: normalized, changed });
|
||||||
|
this.emit('discInserted', detected);
|
||||||
|
return { present: true, emitted: 'discInserted', device: detected };
|
||||||
|
}
|
||||||
|
|
||||||
|
// No disc found
|
||||||
|
if (previouslyTracked) {
|
||||||
|
const old = this.detectedDiscs.get(normalized);
|
||||||
|
this.detectedDiscs.delete(normalized);
|
||||||
|
if (this.detectedDiscs.size === 0) {
|
||||||
|
this.lastDetected = null;
|
||||||
|
this.lastPresent = false;
|
||||||
|
}
|
||||||
|
logger.info('rescan-drive:removed', { devicePath: normalized });
|
||||||
|
this.emit('discRemoved', old || { path: normalized });
|
||||||
|
return { present: false, emitted: 'discRemoved', device: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('rescan-drive:empty', { devicePath: normalized });
|
||||||
|
return { present: false, emitted: 'none', device: null };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('rescan-drive:error', { devicePath: normalized, error: errorToMeta(error) });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multi-drive: tracks per-device state and emits insert/remove events for each
|
// Multi-drive: tracks per-device state and emits insert/remove events for each
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.11.0-4",
|
"version": "0.11.0-5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.11.0-4",
|
"version": "0.11.0-5",
|
||||||
"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.11.0-4",
|
"version": "0.11.0-5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -489,6 +489,13 @@ export const api = {
|
|||||||
afterMutationInvalidate(['/history', '/pipeline/queue']);
|
afterMutationInvalidate(['/history', '/pipeline/queue']);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
async rescanDrive(devicePath) {
|
||||||
|
const result = await request('/pipeline/rescan-drive', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ devicePath })
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
searchOmdb(q) {
|
searchOmdb(q) {
|
||||||
return request(`/pipeline/omdb/search?q=${encodeURIComponent(q)}`);
|
return request(`/pipeline/omdb/search?q=${encodeURIComponent(q)}`);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -870,6 +870,8 @@ export default function DashboardPage({
|
|||||||
const [activationBytesBusy, setActivationBytesBusy] = useState(false);
|
const [activationBytesBusy, setActivationBytesBusy] = useState(false);
|
||||||
const [pendingActivationJobIds, setPendingActivationJobIds] = useState(() => new Set());
|
const [pendingActivationJobIds, setPendingActivationJobIds] = useState(() => new Set());
|
||||||
const [cpuCoresExpanded, setCpuCoresExpanded] = useState(false);
|
const [cpuCoresExpanded, setCpuCoresExpanded] = useState(false);
|
||||||
|
const [knownDrives, setKnownDrives] = useState([]);
|
||||||
|
const [driveRescanBusy, setDriveRescanBusy] = useState(() => new Set());
|
||||||
const [expandedQueueScriptKeys, setExpandedQueueScriptKeys] = useState(() => new Set());
|
const [expandedQueueScriptKeys, setExpandedQueueScriptKeys] = useState(() => new Set());
|
||||||
const [queueCatalog, setQueueCatalog] = useState({ scripts: [], chains: [] });
|
const [queueCatalog, setQueueCatalog] = useState({ scripts: [], chains: [] });
|
||||||
const [insertWaitSeconds, setInsertWaitSeconds] = useState(30);
|
const [insertWaitSeconds, setInsertWaitSeconds] = useState(30);
|
||||||
@@ -1062,6 +1064,12 @@ export default function DashboardPage({
|
|||||||
void loadDashboardJobs();
|
void loadDashboardJobs();
|
||||||
}, [pipeline?.state, pipeline?.activeJobId, pipeline?.context?.jobId, jobsRefreshToken]);
|
}, [pipeline?.state, pipeline?.activeJobId, pipeline?.context?.jobId, jobsRefreshToken]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.getDetectedDrives()
|
||||||
|
.then((res) => setKnownDrives(Array.isArray(res?.drives) ? res.drives : []))
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const requestedJobId = normalizeJobId(pendingExpandedJobId);
|
const requestedJobId = normalizeJobId(pendingExpandedJobId);
|
||||||
if (!requestedJobId) {
|
if (!requestedJobId) {
|
||||||
@@ -1327,20 +1335,29 @@ export default function DashboardPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshKnownDrives = () => {
|
||||||
|
api.getDetectedDrives()
|
||||||
|
.then((res) => setKnownDrives(Array.isArray(res?.drives) ? res.drives : []))
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
const handleRescan = async () => {
|
const handleRescan = async () => {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const response = await api.rescanDisc();
|
const response = await api.rescanDisc();
|
||||||
|
refreshKnownDrives();
|
||||||
|
const allDetected = response?.result?.allDetected || [];
|
||||||
const emitted = response?.result?.emitted || 'none';
|
const emitted = response?.result?.emitted || 'none';
|
||||||
|
const count = allDetected.length;
|
||||||
toastRef.current?.show({
|
toastRef.current?.show({
|
||||||
severity: emitted === 'discInserted' ? 'success' : 'info',
|
severity: count > 0 ? 'success' : 'info',
|
||||||
summary: 'Laufwerk neu gelesen',
|
summary: 'Laufwerke neu gelesen',
|
||||||
detail:
|
detail: count > 0
|
||||||
emitted === 'discInserted'
|
? `${count} Medium${count > 1 ? 'en' : ''} erkannt.`
|
||||||
? 'Disk-Event wurde neu ausgelöst.'
|
: 'Kein Medium erkannt.',
|
||||||
: 'Kein Medium erkannt.',
|
|
||||||
life: 2800
|
life: 2800
|
||||||
});
|
});
|
||||||
|
void emitted; // used implicitly via count
|
||||||
await refreshPipeline();
|
await refreshPipeline();
|
||||||
await loadDashboardJobs();
|
await loadDashboardJobs();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1350,6 +1367,32 @@ export default function DashboardPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRescanDrive = async (devicePath) => {
|
||||||
|
setDriveRescanBusy((prev) => new Set([...prev, devicePath]));
|
||||||
|
try {
|
||||||
|
const response = await api.rescanDrive(devicePath);
|
||||||
|
refreshKnownDrives();
|
||||||
|
const emitted = response?.result?.emitted || 'none';
|
||||||
|
toastRef.current?.show({
|
||||||
|
severity: emitted === 'discInserted' ? 'success' : 'info',
|
||||||
|
summary: devicePath,
|
||||||
|
detail: emitted === 'discInserted'
|
||||||
|
? 'Medium erkannt.'
|
||||||
|
: emitted === 'discRemoved' ? 'Medium entfernt.' : 'Leer.',
|
||||||
|
life: 2500
|
||||||
|
});
|
||||||
|
await refreshPipeline();
|
||||||
|
} catch (error) {
|
||||||
|
showError(error);
|
||||||
|
} finally {
|
||||||
|
setDriveRescanBusy((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(devicePath);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCancel = async (jobId = null, jobState = null) => {
|
const handleCancel = async (jobId = null, jobState = null) => {
|
||||||
const cancelledJobId = normalizeJobId(jobId) || currentPipelineJobId;
|
const cancelledJobId = normalizeJobId(jobId) || currentPipelineJobId;
|
||||||
const cancelledJob = dashboardJobs.find((item) => normalizeJobId(item?.id) === cancelledJobId) || null;
|
const cancelledJob = dashboardJobs.find((item) => normalizeJobId(item?.id) === cancelledJobId) || null;
|
||||||
@@ -1987,6 +2030,23 @@ export default function DashboardPage({
|
|||||||
const contextDevice = pipeline?.context?.device;
|
const contextDevice = pipeline?.context?.device;
|
||||||
const device = lastNonCdDiscEvent || (contextDevice?.mediaProfile !== 'cd' ? contextDevice : null);
|
const device = lastNonCdDiscEvent || (contextDevice?.mediaProfile !== 'cd' ? contextDevice : null);
|
||||||
const cdDriveEntries = Object.entries(pipeline?.cdDrives || {});
|
const cdDriveEntries = Object.entries(pipeline?.cdDrives || {});
|
||||||
|
|
||||||
|
// Unified list of all known drives: merge lsblk drives + CD state + non-CD pipeline state
|
||||||
|
const allDrives = useMemo(() => {
|
||||||
|
const driveMap = new Map();
|
||||||
|
for (const d of knownDrives) {
|
||||||
|
driveMap.set(d.path, { path: d.path, model: d.model, discArg: d.discArg });
|
||||||
|
}
|
||||||
|
for (const [drivePath, drive] of Object.entries(pipeline?.cdDrives || {})) {
|
||||||
|
const base = driveMap.get(drivePath) || { path: drivePath };
|
||||||
|
driveMap.set(drivePath, { ...base, cdDrive: drive });
|
||||||
|
}
|
||||||
|
if (device?.path) {
|
||||||
|
const base = driveMap.get(device.path) || { path: device.path, model: device.model };
|
||||||
|
driveMap.set(device.path, { ...base, pipelineDevice: device, pipelineState: state });
|
||||||
|
}
|
||||||
|
return Array.from(driveMap.values()).sort((a, b) => (a.path || '').localeCompare(b.path || ''));
|
||||||
|
}, [knownDrives, pipeline?.cdDrives, device, state]);
|
||||||
const isDriveActive = driveActiveStates.includes(state);
|
const isDriveActive = driveActiveStates.includes(state);
|
||||||
const canRescan = !isDriveActive;
|
const canRescan = !isDriveActive;
|
||||||
const canReanalyze = !isDriveActive && (state === 'ENCODING' ? Boolean(device) : !processingStates.includes(state));
|
const canReanalyze = !isDriveActive && (state === 'ENCODING' ? Boolean(device) : !processingStates.includes(state));
|
||||||
@@ -2229,9 +2289,10 @@ export default function DashboardPage({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card title="Disk-Information">
|
<Card title="Disk-Information">
|
||||||
|
{/* Global action buttons (non-CD / DVD / Bluray) */}
|
||||||
<div className="actions-row">
|
<div className="actions-row">
|
||||||
<Button
|
<Button
|
||||||
label="Laufwerk neu lesen"
|
label="Alle neu lesen"
|
||||||
icon="pi pi-refresh"
|
icon="pi pi-refresh"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
onClick={handleRescan}
|
onClick={handleRescan}
|
||||||
@@ -2254,106 +2315,125 @@ export default function DashboardPage({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Per-drive CD sections */}
|
{/* Compact per-drive list */}
|
||||||
{cdDriveEntries.length > 0 && (
|
{allDrives.length > 0 ? (
|
||||||
<div className="cd-drives-section">
|
<div className="drive-list">
|
||||||
{cdDriveEntries.map(([drivePath, drive]) => {
|
{allDrives.map((drv) => {
|
||||||
const driveState = String(drive?.state || '').trim().toUpperCase();
|
const drivePath = drv.path;
|
||||||
const driveDevice = drive?.device || {};
|
const cdDrive = drv.cdDrive;
|
||||||
const driveJobId = normalizeJobId(drive?.jobId);
|
const pipelineDevice = drv.pipelineDevice;
|
||||||
const driveCtx = drive?.context && typeof drive.context === 'object' ? drive.context : {};
|
const isRescanBusy = driveRescanBusy.has(drivePath);
|
||||||
const cdStateSeverity = driveState === 'FINISHED' ? 'success'
|
|
||||||
: driveState === 'ERROR' || driveState === 'CANCELLED' ? 'danger'
|
// Determine display state
|
||||||
: ['CD_RIPPING', 'CD_ENCODING'].includes(driveState) ? 'warning'
|
let stateLabel = null;
|
||||||
: 'info';
|
let stateSeverity = 'secondary';
|
||||||
const driveProgress = Number(drive?.progress ?? 0);
|
let discInfo = null;
|
||||||
const driveStatusText = drive?.statusText || null;
|
let showProgress = false;
|
||||||
const isActiveRip = ['CD_RIPPING', 'CD_ENCODING', 'CD_ANALYZING'].includes(driveState);
|
let progressValue = 0;
|
||||||
|
let statusText = null;
|
||||||
|
|
||||||
|
if (cdDrive) {
|
||||||
|
const s = String(cdDrive.state || '').toUpperCase();
|
||||||
|
stateLabel = s;
|
||||||
|
stateSeverity = s === 'FINISHED' ? 'success'
|
||||||
|
: s === 'ERROR' || s === 'CANCELLED' ? 'danger'
|
||||||
|
: ['CD_RIPPING', 'CD_ENCODING'].includes(s) ? 'warning'
|
||||||
|
: 'info';
|
||||||
|
const cd = cdDrive.device || {};
|
||||||
|
discInfo = cd.discLabel || cd.model || null;
|
||||||
|
showProgress = ['CD_RIPPING', 'CD_ENCODING', 'CD_ANALYZING'].includes(s);
|
||||||
|
progressValue = Number(cdDrive.progress ?? 0);
|
||||||
|
statusText = cdDrive.statusText || null;
|
||||||
|
} else if (pipelineDevice) {
|
||||||
|
const s = String(drv.pipelineState || '').toUpperCase();
|
||||||
|
stateLabel = s || 'DISC_DETECTED';
|
||||||
|
stateSeverity = s === 'FINISHED' ? 'success'
|
||||||
|
: s === 'ERROR' ? 'danger'
|
||||||
|
: processingStates.includes(s) ? 'warning'
|
||||||
|
: 'info';
|
||||||
|
discInfo = pipelineDevice.discLabel || pipelineDevice.label || null;
|
||||||
|
} else {
|
||||||
|
stateLabel = 'LEER';
|
||||||
|
stateSeverity = 'secondary';
|
||||||
|
}
|
||||||
|
|
||||||
|
const driveJobId = normalizeJobId(cdDrive?.jobId);
|
||||||
|
const driveCtx = cdDrive?.context && typeof cdDrive.context === 'object' ? cdDrive.context : {};
|
||||||
|
const cdState = cdDrive ? String(cdDrive.state || '').toUpperCase() : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={drivePath} className="cd-drive-item">
|
<div key={drivePath} className="drive-list-item">
|
||||||
<div className="cd-drive-header">
|
<div className="drive-list-row">
|
||||||
<code className="cd-drive-path">{drivePath}</code>
|
<div className="drive-list-info">
|
||||||
<Tag value={driveState} severity={cdStateSeverity} />
|
<code className="drive-list-path">{drivePath}</code>
|
||||||
</div>
|
{drv.model && <span className="drive-list-model">{drv.model}</span>}
|
||||||
{(driveDevice.discLabel || driveDevice.model) && (
|
{drv.discArg && <span className="drive-list-disc-arg">{drv.discArg}</span>}
|
||||||
<div className="cd-drive-disc-label">
|
|
||||||
{driveDevice.discLabel || driveDevice.model}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="drive-list-state">
|
||||||
{isActiveRip && (
|
{stateLabel && <Tag value={stateLabel} severity={stateSeverity} />}
|
||||||
<ProgressBar value={driveProgress} style={{ height: '6px', margin: '0.2rem 0' }} showValue={false} />
|
</div>
|
||||||
)}
|
<div className="drive-list-actions">
|
||||||
{isActiveRip && driveStatusText && (
|
|
||||||
<div className="cd-drive-status-text">{driveStatusText}</div>
|
|
||||||
)}
|
|
||||||
<div className="cd-drive-actions">
|
|
||||||
{driveState === 'DISC_DETECTED' && (
|
|
||||||
<Button
|
<Button
|
||||||
label="CD analysieren"
|
|
||||||
icon="pi pi-search"
|
|
||||||
size="small"
|
|
||||||
onClick={() => handleAnalyzeForDrive(drivePath)}
|
|
||||||
loading={busy}
|
|
||||||
disabled={busy}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{driveState === 'CD_METADATA_SELECTION' && (
|
|
||||||
<Button
|
|
||||||
label="Metadaten auswählen"
|
|
||||||
icon="pi pi-list"
|
|
||||||
size="small"
|
|
||||||
severity="info"
|
|
||||||
onClick={() => {
|
|
||||||
setCdMetadataDialogContext({ ...driveCtx, jobId: driveJobId });
|
|
||||||
setCdMetadataDialogVisible(true);
|
|
||||||
}}
|
|
||||||
disabled={!driveJobId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{(driveState === 'ERROR' || driveState === 'CANCELLED') && driveJobId && (
|
|
||||||
<Button
|
|
||||||
label="Erneut analysieren"
|
|
||||||
icon="pi pi-refresh"
|
icon="pi pi-refresh"
|
||||||
|
text
|
||||||
|
rounded
|
||||||
size="small"
|
size="small"
|
||||||
severity="warning"
|
severity="secondary"
|
||||||
onClick={() => handleAnalyzeForDrive(drivePath)}
|
loading={isRescanBusy}
|
||||||
loading={busy}
|
disabled={isRescanBusy || isDriveActive}
|
||||||
disabled={busy}
|
onClick={() => handleRescanDrive(drivePath)}
|
||||||
|
title="Laufwerk neu lesen"
|
||||||
|
aria-label="Laufwerk neu lesen"
|
||||||
/>
|
/>
|
||||||
)}
|
{cdState === 'DISC_DETECTED' && (
|
||||||
|
<Button
|
||||||
|
label="Analysieren"
|
||||||
|
icon="pi pi-search"
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleAnalyzeForDrive(drivePath)}
|
||||||
|
loading={busy}
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{cdState === 'CD_METADATA_SELECTION' && (
|
||||||
|
<Button
|
||||||
|
label="Metadaten"
|
||||||
|
icon="pi pi-list"
|
||||||
|
size="small"
|
||||||
|
severity="info"
|
||||||
|
onClick={() => {
|
||||||
|
setCdMetadataDialogContext({ ...driveCtx, jobId: driveJobId });
|
||||||
|
setCdMetadataDialogVisible(true);
|
||||||
|
}}
|
||||||
|
disabled={!driveJobId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(cdState === 'ERROR' || cdState === 'CANCELLED') && driveJobId && (
|
||||||
|
<Button
|
||||||
|
label="Retry"
|
||||||
|
icon="pi pi-replay"
|
||||||
|
size="small"
|
||||||
|
severity="warning"
|
||||||
|
onClick={() => handleAnalyzeForDrive(drivePath)}
|
||||||
|
loading={busy}
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{discInfo && <div className="drive-list-disc-label">{discInfo}</div>}
|
||||||
|
{showProgress && (
|
||||||
|
<ProgressBar value={progressValue} style={{ height: '5px', margin: '0.2rem 0 0' }} showValue={false} />
|
||||||
|
)}
|
||||||
|
{showProgress && statusText && (
|
||||||
|
<div className="drive-list-status-text">{statusText}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Non-CD / legacy single-drive display */}
|
|
||||||
<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 ? (
|
|
||||||
<div className="device-meta">
|
|
||||||
<div>
|
|
||||||
<strong>Pfad:</strong> {device.path || '-'}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>Modell:</strong> {device.model || '-'}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>Disk-Label:</strong> {device.discLabel || '-'}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>Laufwerks-Label:</strong> {device.label || '-'}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>Mount:</strong> {device.mountpoint || '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
cdDriveEntries.length === 0 ? <p>Aktuell keine Disk erkannt.</p> : null
|
<p className="drive-list-empty">Keine Laufwerke erkannt.</p>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -1802,6 +1802,90 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Per-drive CD sections in Dashboard */
|
/* Per-drive CD sections in Dashboard */
|
||||||
|
/* Unified per-drive list (Disk-Information card) */
|
||||||
|
.drive-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-empty {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--rip-muted);
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-item {
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
border: 1px solid var(--rip-border);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background: var(--rip-panel-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-path {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--rip-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-model {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--rip-muted);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-disc-arg {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--rip-muted);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-state {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-disc-label {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-status-text {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--rip-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legacy per-CD-drive section (kept for compat) */
|
||||||
.cd-drives-section {
|
.cd-drives-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.11.0-4",
|
"version": "0.11.0-5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.11.0-4",
|
"version": "0.11.0-5",
|
||||||
"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.11.0-4",
|
"version": "0.11.0-5",
|
||||||
"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