0.12.0-3 Plugin Integration

This commit is contained in:
2026-03-21 15:58:57 +00:00
parent d9969dfbe5
commit e99cdf1895
20 changed files with 928 additions and 115 deletions
+141 -18
View File
@@ -197,6 +197,40 @@ function runtimeOutcomeMeta(outcome, status) {
return runtimeStatusMeta(status);
}
function driveStateIconMeta(stateLabel) {
const normalized = String(stateLabel || '').trim().toUpperCase();
if (!normalized || normalized === 'LEER' || normalized === 'IDLE') {
return { icon: 'pi-stop', spin: false };
}
if (normalized === 'DISC_DETECTED') {
return { icon: 'pi-check', spin: false };
}
if (normalized === 'FINISHED') {
return { icon: 'pi-check-circle', spin: false };
}
if (normalized === 'ERROR' || normalized === 'CANCELLED') {
return { icon: 'pi-times-circle', spin: false };
}
if (processingStates.includes(normalized)) {
return { icon: 'pi-spinner', spin: true };
}
if (
normalized === 'METADATA_SELECTION'
|| normalized === 'CD_METADATA_SELECTION'
|| normalized === 'WAITING_FOR_USER_DECISION'
) {
return { icon: 'pi-list', spin: false };
}
if (
normalized === 'READY_TO_START'
|| normalized === 'READY_TO_ENCODE'
|| normalized === 'CD_READY_TO_RIP'
) {
return { icon: 'pi-play', spin: false };
}
return { icon: 'pi-question-circle', spin: false };
}
function hasRuntimeOutputDetails(item) {
if (!item || typeof item !== 'object') {
return false;
@@ -304,6 +338,20 @@ function normalizeQueue(queue) {
};
}
function buildCdDriveLifecycleKey(cdDrives) {
if (!cdDrives || typeof cdDrives !== 'object') {
return '';
}
const entries = Object.entries(cdDrives)
.map(([devicePath, driveState]) => {
const state = String(driveState?.state || '').trim().toUpperCase();
const jobId = normalizeJobId(driveState?.jobId || driveState?.context?.jobId) || 0;
return `${devicePath}|${jobId}|${state}`;
})
.sort();
return entries.join('::');
}
function getQueueActionResult(response) {
return response?.result && typeof response.result === 'object' ? response.result : {};
}
@@ -917,6 +965,10 @@ export default function DashboardPage({
() => normalizeHardwareMonitoringPayload(hardwareMonitoring),
[hardwareMonitoring]
);
const cdDriveLifecycleKey = useMemo(
() => buildCdDriveLifecycleKey(pipeline?.cdDrives),
[pipeline?.cdDrives]
);
const monitoringSample = monitoringState.sample;
const cpuMetrics = monitoringSample?.cpu || null;
const memoryMetrics = monitoringSample?.memory || null;
@@ -1115,13 +1167,43 @@ export default function DashboardPage({
}
}, [pipeline?.cdDrives]);
useEffect(() => {
if (!cdMetadataDialogVisible) {
return;
}
const dialogJobId = normalizeJobId(cdMetadataDialogContext?.jobId);
if (!dialogJobId) {
return;
}
const cdDrives = pipeline?.cdDrives;
if (!cdDrives || typeof cdDrives !== 'object') {
return;
}
let stillInMetadataSelection = false;
for (const drive of Object.values(cdDrives)) {
const driveState = String(drive?.state || '').trim().toUpperCase();
const ctx = drive?.context && typeof drive.context === 'object' ? drive.context : null;
const driveJobId = normalizeJobId(ctx?.jobId || drive?.jobId);
if (driveJobId === dialogJobId && driveState === 'CD_METADATA_SELECTION') {
stillInMetadataSelection = true;
break;
}
}
if (!stillInMetadataSelection) {
setCdMetadataDialogVisible(false);
setCdMetadataDialogContext(null);
}
}, [cdMetadataDialogVisible, cdMetadataDialogContext?.jobId, pipeline?.cdDrives]);
useEffect(() => {
setQueueState(normalizeQueue(pipeline?.queue));
}, [pipeline?.queue]);
useEffect(() => {
void loadDashboardJobs();
}, [pipeline?.state, pipeline?.activeJobId, pipeline?.context?.jobId, pipeline?.cdDrives, jobsRefreshToken]);
}, [pipeline?.state, pipeline?.activeJobId, pipeline?.context?.jobId, cdDriveLifecycleKey, jobsRefreshToken]);
useEffect(() => {
api.getDetectedDrives()
@@ -1553,18 +1635,28 @@ export default function DashboardPage({
try {
const response = await api.deleteJobFiles(jobId, effectiveTarget);
const summary = response?.summary || {};
const deletedFiles = effectiveTarget === 'raw'
? (summary.raw?.filesDeleted ?? 0)
: (summary.movie?.filesDeleted ?? 0);
const removedDirs = effectiveTarget === 'raw'
? (summary.raw?.dirsRemoved ?? 0)
: (summary.movie?.dirsRemoved ?? 0);
toastRef.current?.show({
severity: 'success',
summary: effectiveTarget === 'raw' ? 'RAW gelöscht' : 'Movie gelöscht',
detail: `Entfernt: ${deletedFiles} Datei(en), ${removedDirs} Ordner.`,
life: 4000
});
const targetSummary = effectiveTarget === 'raw'
? (summary.raw || {})
: (summary.movie || {});
const deletedFiles = targetSummary.filesDeleted ?? 0;
const removedDirs = targetSummary.dirsRemoved ?? 0;
const deletedSomething = Boolean(targetSummary.deleted);
if (!deletedSomething) {
toastRef.current?.show({
severity: 'warn',
summary: effectiveTarget === 'raw' ? 'RAW nicht gelöscht' : 'Movie nicht gelöscht',
detail: targetSummary.reason || 'Keine passenden Dateien/Ordner gefunden.',
life: 4200
});
} else {
toastRef.current?.show({
severity: 'success',
summary: effectiveTarget === 'raw' ? 'RAW gelöscht' : 'Movie gelöscht',
detail: `Entfernt: ${deletedFiles} Datei(en), ${removedDirs} Ordner.`,
life: 4000
});
}
await loadDashboardJobs();
await refreshPipeline();
setCancelCleanupDialog({ visible: false, jobId: null, target: null, path: null });
@@ -2503,6 +2595,8 @@ export default function DashboardPage({
const canAnalyzeDrive = cdState
? cdState === 'DISC_DETECTED'
: (Boolean(pipelineDevice) && pipelineState === 'DISC_DETECTED') || Boolean(drv.detectedDisc);
const stateIconMeta = driveStateIconMeta(stateLabel);
const discArg = String(drv.discArg || '').trim();
return (
<div key={drivePath} className="drive-list-item">
@@ -2510,10 +2604,27 @@ export default function DashboardPage({
<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>
<div className="drive-list-state">
{stateLabel && <Tag value={stateLabel} severity={stateSeverity} className="drive-list-status-tag" />}
{stateLabel ? (
<span
className={`drive-list-status-icon tone-${stateSeverity}`}
title={getStatusLabel(stateLabel)}
aria-label={`Status: ${getStatusLabel(stateLabel)}`}
>
<i className={`pi ${stateIconMeta.icon}${stateIconMeta.spin ? ' pi-spin' : ''}`} aria-hidden="true" />
</span>
) : null}
{isCdDriveLocked ? (
<span
className="drive-list-lock-indicator"
title="Laufwerk ist gesperrt, bis Rip/Encode abgeschlossen ist."
aria-label="Laufwerk gesperrt"
>
<i className="pi pi-lock" aria-hidden="true" />
<span>gesperrt</span>
</span>
) : null}
</div>
<div className="drive-list-actions">
<Button
@@ -2522,6 +2633,7 @@ export default function DashboardPage({
rounded
size="small"
severity="secondary"
className="drive-list-action-btn"
loading={isRescanBusy}
disabled={isRescanBusy || isDriveActive || isCdDriveLocked}
onClick={() => handleRescanDrive(drivePath)}
@@ -2535,6 +2647,7 @@ export default function DashboardPage({
rounded
size="small"
severity="secondary"
className="drive-list-action-btn"
loading={isAnalyzeBusy}
disabled={isAnalyzeBusy}
onClick={() => handleAnalyzeForDrive(drivePath)}
@@ -2544,15 +2657,19 @@ export default function DashboardPage({
)}
{cdState === 'CD_METADATA_SELECTION' && (
<Button
label="Metadaten"
icon="pi pi-list"
text
rounded
size="small"
severity="info"
className="drive-list-action-btn"
onClick={() => {
setCdMetadataDialogContext({ ...driveCtx, jobId: driveJobId });
setCdMetadataDialogVisible(true);
}}
disabled={!driveJobId}
title="Metadaten auswählen"
aria-label="Metadaten auswählen"
/>
)}
{(cdState === 'ERROR' || cdState === 'CANCELLED') && driveJobId && (
@@ -2562,6 +2679,7 @@ export default function DashboardPage({
rounded
size="small"
severity="warning"
className="drive-list-action-btn"
onClick={() => handleAnalyzeForDrive(drivePath)}
loading={isAnalyzeBusy}
disabled={isAnalyzeBusy}
@@ -2571,7 +2689,12 @@ export default function DashboardPage({
)}
</div>
</div>
{discInfo && <div className="drive-list-disc-label">{discInfo}</div>}
{(discArg || discInfo) ? (
<div className="drive-list-disc-meta">
{discArg ? <span className="drive-list-disc-arg">{discArg}</span> : null}
{discInfo ? <span className="drive-list-disc-label" title={discInfo}>{discInfo}</span> : null}
</div>
) : null}
{showProgress && (
<ProgressBar value={progressValue} style={{ height: '5px', margin: '0.2rem 0 0' }} showValue={false} />
)}
@@ -2663,7 +2786,7 @@ export default function DashboardPage({
</div>
) : (
<Card title="Job Übersicht" subTitle="Kompakte Liste; Klick auf Zeile öffnet die volle Job-Detailansicht mit passenden CTAs">
{jobsLoading ? (
{jobsLoading && dashboardJobs.length === 0 ? (
<p>Jobs werden geladen ...</p>
) : dashboardJobs.length === 0 ? (
<p>Keine relevanten Jobs im Dashboard (aktive/fortsetzbare Status).</p>