0.11.0-5 multi lw

This commit is contained in:
2026-03-17 21:57:44 +00:00
parent 38fc1778f7
commit d00191324b
12 changed files with 356 additions and 106 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "ripster-frontend",
"version": "0.11.0-4",
"version": "0.11.0-5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ripster-frontend",
"version": "0.11.0-4",
"version": "0.11.0-5",
"dependencies": {
"primeicons": "^7.0.0",
"primereact": "^10.9.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ripster-frontend",
"version": "0.11.0-4",
"version": "0.11.0-5",
"private": true,
"type": "module",
"scripts": {
+7
View File
@@ -489,6 +489,13 @@ export const api = {
afterMutationInvalidate(['/history', '/pipeline/queue']);
return result;
},
async rescanDrive(devicePath) {
const result = await request('/pipeline/rescan-drive', {
method: 'POST',
body: JSON.stringify({ devicePath })
});
return result;
},
searchOmdb(q) {
return request(`/pipeline/omdb/search?q=${encodeURIComponent(q)}`);
},
+175 -95
View File
@@ -870,6 +870,8 @@ export default function DashboardPage({
const [activationBytesBusy, setActivationBytesBusy] = useState(false);
const [pendingActivationJobIds, setPendingActivationJobIds] = useState(() => new Set());
const [cpuCoresExpanded, setCpuCoresExpanded] = useState(false);
const [knownDrives, setKnownDrives] = useState([]);
const [driveRescanBusy, setDriveRescanBusy] = useState(() => new Set());
const [expandedQueueScriptKeys, setExpandedQueueScriptKeys] = useState(() => new Set());
const [queueCatalog, setQueueCatalog] = useState({ scripts: [], chains: [] });
const [insertWaitSeconds, setInsertWaitSeconds] = useState(30);
@@ -1062,6 +1064,12 @@ export default function DashboardPage({
void loadDashboardJobs();
}, [pipeline?.state, pipeline?.activeJobId, pipeline?.context?.jobId, jobsRefreshToken]);
useEffect(() => {
api.getDetectedDrives()
.then((res) => setKnownDrives(Array.isArray(res?.drives) ? res.drives : []))
.catch(() => {});
}, []);
useEffect(() => {
const requestedJobId = normalizeJobId(pendingExpandedJobId);
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 () => {
setBusy(true);
try {
const response = await api.rescanDisc();
refreshKnownDrives();
const allDetected = response?.result?.allDetected || [];
const emitted = response?.result?.emitted || 'none';
const count = allDetected.length;
toastRef.current?.show({
severity: emitted === 'discInserted' ? 'success' : 'info',
summary: 'Laufwerk neu gelesen',
detail:
emitted === 'discInserted'
? 'Disk-Event wurde neu ausgelöst.'
: 'Kein Medium erkannt.',
severity: count > 0 ? 'success' : 'info',
summary: 'Laufwerke neu gelesen',
detail: count > 0
? `${count} Medium${count > 1 ? 'en' : ''} erkannt.`
: 'Kein Medium erkannt.',
life: 2800
});
void emitted; // used implicitly via count
await refreshPipeline();
await loadDashboardJobs();
} 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 cancelledJobId = normalizeJobId(jobId) || currentPipelineJobId;
const cancelledJob = dashboardJobs.find((item) => normalizeJobId(item?.id) === cancelledJobId) || null;
@@ -1987,6 +2030,23 @@ export default function DashboardPage({
const contextDevice = pipeline?.context?.device;
const device = lastNonCdDiscEvent || (contextDevice?.mediaProfile !== 'cd' ? contextDevice : null);
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 canRescan = !isDriveActive;
const canReanalyze = !isDriveActive && (state === 'ENCODING' ? Boolean(device) : !processingStates.includes(state));
@@ -2229,9 +2289,10 @@ export default function DashboardPage({
</Card>
<Card title="Disk-Information">
{/* Global action buttons (non-CD / DVD / Bluray) */}
<div className="actions-row">
<Button
label="Laufwerk neu lesen"
label="Alle neu lesen"
icon="pi pi-refresh"
severity="secondary"
onClick={handleRescan}
@@ -2254,106 +2315,125 @@ export default function DashboardPage({
/>
</div>
{/* Per-drive CD sections */}
{cdDriveEntries.length > 0 && (
<div className="cd-drives-section">
{cdDriveEntries.map(([drivePath, drive]) => {
const driveState = String(drive?.state || '').trim().toUpperCase();
const driveDevice = drive?.device || {};
const driveJobId = normalizeJobId(drive?.jobId);
const driveCtx = drive?.context && typeof drive.context === 'object' ? drive.context : {};
const cdStateSeverity = driveState === 'FINISHED' ? 'success'
: driveState === 'ERROR' || driveState === 'CANCELLED' ? 'danger'
: ['CD_RIPPING', 'CD_ENCODING'].includes(driveState) ? 'warning'
: 'info';
const driveProgress = Number(drive?.progress ?? 0);
const driveStatusText = drive?.statusText || null;
const isActiveRip = ['CD_RIPPING', 'CD_ENCODING', 'CD_ANALYZING'].includes(driveState);
{/* Compact per-drive list */}
{allDrives.length > 0 ? (
<div className="drive-list">
{allDrives.map((drv) => {
const drivePath = drv.path;
const cdDrive = drv.cdDrive;
const pipelineDevice = drv.pipelineDevice;
const isRescanBusy = driveRescanBusy.has(drivePath);
// Determine display state
let stateLabel = null;
let stateSeverity = 'secondary';
let discInfo = null;
let showProgress = false;
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 (
<div key={drivePath} className="cd-drive-item">
<div className="cd-drive-header">
<code className="cd-drive-path">{drivePath}</code>
<Tag value={driveState} severity={cdStateSeverity} />
</div>
{(driveDevice.discLabel || driveDevice.model) && (
<div className="cd-drive-disc-label">
{driveDevice.discLabel || driveDevice.model}
<div key={drivePath} className="drive-list-item">
<div className="drive-list-row">
<div className="drive-list-info">
<code className="drive-list-path">{drivePath}</code>
{drv.model && <span className="drive-list-model">{drv.model}</span>}
{drv.discArg && <span className="drive-list-disc-arg">{drv.discArg}</span>}
</div>
)}
{isActiveRip && (
<ProgressBar value={driveProgress} style={{ height: '6px', margin: '0.2rem 0' }} showValue={false} />
)}
{isActiveRip && driveStatusText && (
<div className="cd-drive-status-text">{driveStatusText}</div>
)}
<div className="cd-drive-actions">
{driveState === 'DISC_DETECTED' && (
<div className="drive-list-state">
{stateLabel && <Tag value={stateLabel} severity={stateSeverity} />}
</div>
<div className="drive-list-actions">
<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"
text
rounded
size="small"
severity="warning"
onClick={() => handleAnalyzeForDrive(drivePath)}
loading={busy}
disabled={busy}
severity="secondary"
loading={isRescanBusy}
disabled={isRescanBusy || isDriveActive}
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>
{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>
)}
{/* 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>
+84
View File
@@ -1802,6 +1802,90 @@ body {
}
/* 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 {
display: flex;
flex-direction: column;