This commit is contained in:
2026-03-13 15:15:50 +00:00
parent f38081649f
commit b6cac5efb4
7 changed files with 205 additions and 122 deletions

View File

@@ -855,6 +855,21 @@ async function migrateSettingsSchemaMetadata(db) {
logger.info('migrate:settings-schema-category-moved', { key: move.key, category: move.category }); logger.info('migrate:settings-schema-category-moved', { key: move.key, category: move.category });
} }
} }
const rawDirCdLabel = 'CD RAW-Ordner';
const rawDirCdDescription = 'Basisordner für CD-Rips. Enthält die WAV-Rohdaten (RAW) sowie den encodierten Audio-Output. Leer = Standardpfad (data/output/cd).';
const rawDirCdResult = await db.run(
`UPDATE settings_schema
SET label = ?, description = ?, updated_at = CURRENT_TIMESTAMP
WHERE key = 'raw_dir_cd' AND (label != ? OR description != ?)`,
[rawDirCdLabel, rawDirCdDescription, rawDirCdLabel, rawDirCdDescription]
);
if (rawDirCdResult?.changes > 0) {
logger.info('migrate:settings-schema-cd-raw-updated', {
key: 'raw_dir_cd',
label: rawDirCdLabel
});
}
} }
async function getDb() { async function getDb() {

View File

@@ -321,6 +321,20 @@ function formatCommandLine(cmd, args = []) {
return [quoteShellArg(cmd), ...normalizedArgs.map((arg) => quoteShellArg(arg))].join(' '); return [quoteShellArg(cmd), ...normalizedArgs.map((arg) => quoteShellArg(arg))].join(' ');
} }
function copyFilePreservingRaw(sourcePath, targetPath) {
const rawSource = String(sourcePath || '').trim();
const rawTarget = String(targetPath || '').trim();
if (!rawSource || !rawTarget) {
return;
}
const source = path.resolve(rawSource);
const target = path.resolve(rawTarget);
if (source === target) {
return;
}
fs.copyFileSync(source, target);
}
async function runProcessTracked({ async function runProcessTracked({
cmd, cmd,
args, args,
@@ -492,7 +506,7 @@ async function ripAndEncode(options) {
// ── Phase 2: Encode WAVs to target format ───────────────────────────────── // ── Phase 2: Encode WAVs to target format ─────────────────────────────────
if (format === 'wav') { if (format === 'wav') {
// Just move WAV files to output dir with proper names // Keep RAW WAVs in place and copy them to the final output structure.
for (let i = 0; i < tracksToRip.length; i++) { for (let i = 0; i < tracksToRip.length; i++) {
assertNotCancelled(isCancelled); assertNotCancelled(isCancelled);
const track = tracksToRip[i]; const track = tracksToRip[i];
@@ -508,8 +522,8 @@ async function ripAndEncode(options) {
percent: 50 + ((i / tracksToRip.length) * 50) percent: 50 + ((i / tracksToRip.length) * 50)
}); });
ensureDir(path.dirname(outFile)); ensureDir(path.dirname(outFile));
log('info', `Promptkette [Move ${i + 1}/${tracksToRip.length}]: mv ${quoteShellArg(wavFile)} ${quoteShellArg(outFile)}`); log('info', `Promptkette [Copy ${i + 1}/${tracksToRip.length}]: cp ${quoteShellArg(wavFile)} ${quoteShellArg(outFile)}`);
fs.renameSync(wavFile, outFile); copyFilePreservingRaw(wavFile, outFile);
onProgress && onProgress({ onProgress && onProgress({
phase: 'encode', phase: 'encode',
trackEvent: 'complete', trackEvent: 'complete',

View File

@@ -421,7 +421,7 @@ class HardwareMonitorService {
} else { } else {
addPath('raw_dir', 'RAW-Verzeichnis', blurayRawPath || dvdRawPath || sourceMap.raw_dir); addPath('raw_dir', 'RAW-Verzeichnis', blurayRawPath || dvdRawPath || sourceMap.raw_dir);
} }
addPath('raw_dir_cd', 'CD-Verzeichnis', cdRawPath || sourceMap.raw_dir_cd); addPath('raw_dir_cd', 'CD RAW-Ordner', cdRawPath || sourceMap.raw_dir_cd);
if (blurayMoviePath && dvdMoviePath && blurayMoviePath !== dvdMoviePath) { if (blurayMoviePath && dvdMoviePath && blurayMoviePath !== dvdMoviePath) {
addPath('movie_dir_bluray', 'Movie-Verzeichnis (Blu-ray)', blurayMoviePath); addPath('movie_dir_bluray', 'Movie-Verzeichnis (Blu-ray)', blurayMoviePath);

View File

@@ -21,7 +21,7 @@ function parseJsonSafe(raw, fallback = null) {
const PROCESS_LOG_TAIL_MAX_BYTES = 1024 * 1024; const PROCESS_LOG_TAIL_MAX_BYTES = 1024 * 1024;
const processLogStreams = new Map(); const processLogStreams = new Map();
const PROFILE_PATH_SUFFIXES = ['bluray', 'dvd', 'other']; const PROFILE_PATH_SUFFIXES = ['bluray', 'dvd', 'cd', 'other'];
const RAW_INCOMPLETE_PREFIX = 'Incomplete_'; const RAW_INCOMPLETE_PREFIX = 'Incomplete_';
const RAW_RIP_COMPLETE_PREFIX = 'Rip_Complete_'; const RAW_RIP_COMPLETE_PREFIX = 'Rip_Complete_';
@@ -356,12 +356,46 @@ function toProcessLogStreamKey(jobId) {
return String(Math.trunc(normalizedId)); return String(Math.trunc(normalizedId));
} }
function resolveEffectiveRawPath(storedPath, rawDir) { function resolveEffectiveRawPath(storedPath, rawDir, extraDirs = []) {
const stored = String(storedPath || '').trim(); const stored = String(storedPath || '').trim();
if (!stored || !rawDir) return stored; if (!stored) return stored;
const folderName = path.basename(stored); const folderName = path.basename(stored);
if (!folderName) return stored; if (!folderName) return stored;
return path.join(String(rawDir).trim(), folderName);
const candidates = [];
const seen = new Set();
const pushCandidate = (candidatePath) => {
const normalized = String(candidatePath || '').trim();
if (!normalized) {
return;
}
const comparable = normalizeComparablePath(normalized);
if (!comparable || seen.has(comparable)) {
return;
}
seen.add(comparable);
candidates.push(normalized);
};
pushCandidate(stored);
if (rawDir) {
pushCandidate(path.join(String(rawDir).trim(), folderName));
}
for (const extraDir of Array.isArray(extraDirs) ? extraDirs : []) {
pushCandidate(path.join(String(extraDir || '').trim(), folderName));
}
for (const candidate of candidates) {
try {
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
return candidate;
}
} catch (_error) {
// ignore fs errors and continue with fallbacks
}
}
return rawDir ? path.join(String(rawDir).trim(), folderName) : stored;
} }
function resolveEffectiveOutputPath(storedPath, movieDir) { function resolveEffectiveOutputPath(storedPath, movieDir) {
@@ -406,11 +440,11 @@ function resolveEffectiveStoragePathsForJob(settings = null, job = {}, parsed =
const rawDir = String(effectiveSettings?.raw_dir || '').trim(); const rawDir = String(effectiveSettings?.raw_dir || '').trim();
const configuredMovieDir = String(effectiveSettings?.movie_dir || '').trim(); const configuredMovieDir = String(effectiveSettings?.movie_dir || '').trim();
const movieDir = mediaType === 'cd' ? rawDir : configuredMovieDir; const movieDir = mediaType === 'cd' ? rawDir : configuredMovieDir;
const effectiveRawPath = mediaType === 'cd' const rawLookupDirs = getConfiguredMediaPathList(settings || {}, 'raw_dir')
? (job?.raw_path || null) .filter((candidate) => normalizeComparablePath(candidate) !== normalizeComparablePath(rawDir));
: (rawDir && job?.raw_path const effectiveRawPath = job?.raw_path
? resolveEffectiveRawPath(job.raw_path, rawDir) ? resolveEffectiveRawPath(job.raw_path, rawDir, rawLookupDirs)
: (job?.raw_path || null)); : (job?.raw_path || null);
const effectiveOutputPath = mediaType === 'cd' const effectiveOutputPath = mediaType === 'cd'
? (job?.output_path || null) ? (job?.output_path || null)
: (configuredMovieDir && job?.output_path : (configuredMovieDir && job?.output_path

View File

@@ -3694,6 +3694,46 @@ class PipelineService extends EventEmitter {
return existingDirectories[0]; return existingDirectories[0];
} }
buildRawPathLookupConfig(settingsMap = {}, mediaProfile = null) {
const sourceMap = settingsMap && typeof settingsMap === 'object' ? settingsMap : {};
const normalizedMediaProfile = normalizeMediaProfile(mediaProfile);
const effectiveSettings = settingsService.resolveEffectiveToolSettings(sourceMap, normalizedMediaProfile);
const preferredDefaultRawDir = normalizedMediaProfile === 'cd'
? settingsService.DEFAULT_CD_DIR
: settingsService.DEFAULT_RAW_DIR;
const uniqueRawDirs = Array.from(
new Set(
[
effectiveSettings?.raw_dir,
sourceMap?.raw_dir,
sourceMap?.raw_dir_bluray,
sourceMap?.raw_dir_dvd,
sourceMap?.raw_dir_cd,
preferredDefaultRawDir,
settingsService.DEFAULT_RAW_DIR,
settingsService.DEFAULT_CD_DIR
]
.map((item) => String(item || '').trim())
.filter(Boolean)
)
);
return {
effectiveSettings,
rawBaseDir: uniqueRawDirs[0] || String(preferredDefaultRawDir || '').trim() || null,
rawExtraDirs: uniqueRawDirs.slice(1)
};
}
resolveCurrentRawPathForSettings(settingsMap = {}, mediaProfile = null, storedRawPath = null) {
const stored = String(storedRawPath || '').trim();
if (!stored) {
return null;
}
const { rawBaseDir, rawExtraDirs } = this.buildRawPathLookupConfig(settingsMap, mediaProfile);
return this.resolveCurrentRawPath(rawBaseDir, stored, rawExtraDirs);
}
async migrateRawFolderNamingOnStartup(db) { async migrateRawFolderNamingOnStartup(db) {
const settings = await settingsService.getSettingsMap(); const settings = await settingsService.getSettingsMap();
const rawBaseDir = String(settings?.raw_dir || settingsService.DEFAULT_RAW_DIR || '').trim(); const rawBaseDir = String(settings?.raw_dir || settingsService.DEFAULT_RAW_DIR || '').trim();
@@ -5385,15 +5425,17 @@ class PipelineService extends EventEmitter {
}; };
} }
const existingPlan = this.safeParseJson(job.encode_plan_json);
const refreshSettings = await settingsService.getSettingsMap(); const refreshSettings = await settingsService.getSettingsMap();
const refreshRawBaseDir = settingsService.DEFAULT_RAW_DIR; const refreshMediaProfile = this.resolveMediaProfileForJob(job, {
const refreshRawExtraDirs = [ encodePlan: existingPlan,
refreshSettings?.raw_dir_bluray, rawPath: job.raw_path
refreshSettings?.raw_dir_dvd });
].map((d) => String(d || '').trim()).filter(Boolean); const resolvedRefreshRawPath = this.resolveCurrentRawPathForSettings(
const resolvedRefreshRawPath = job.raw_path refreshSettings,
? this.resolveCurrentRawPath(refreshRawBaseDir, job.raw_path, refreshRawExtraDirs) refreshMediaProfile,
: null; job.raw_path
);
if (!resolvedRefreshRawPath) { if (!resolvedRefreshRawPath) {
return { return {
@@ -5409,7 +5451,6 @@ class PipelineService extends EventEmitter {
await historyService.updateJob(activeJobId, { raw_path: resolvedRefreshRawPath }); await historyService.updateJob(activeJobId, { raw_path: resolvedRefreshRawPath });
} }
const existingPlan = this.safeParseJson(job.encode_plan_json);
const mode = existingPlan?.mode || this.snapshot.context?.mode || 'rip'; const mode = existingPlan?.mode || this.snapshot.context?.mode || 'rip';
const sourceJobId = existingPlan?.sourceJobId || this.snapshot.context?.sourceJobId || null; const sourceJobId = existingPlan?.sourceJobId || this.snapshot.context?.sourceJobId || null;
@@ -7339,14 +7380,11 @@ class PipelineService extends EventEmitter {
encodePlan: confirmedPlan encodePlan: confirmedPlan
}); });
const confirmSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile); const confirmSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile);
const confirmRawBaseDir = String(confirmSettings?.raw_dir || '').trim(); const resolvedConfirmRawPath = this.resolveCurrentRawPathForSettings(
const confirmRawExtraDirs = [ confirmSettings,
confirmSettings?.raw_dir_bluray, readyMediaProfile,
confirmSettings?.raw_dir_dvd job.raw_path
].map((d) => String(d || '').trim()).filter(Boolean); );
const resolvedConfirmRawPath = job.raw_path
? this.resolveCurrentRawPath(confirmRawBaseDir, job.raw_path, confirmRawExtraDirs)
: null;
const activeConfirmRawPath = resolvedConfirmRawPath || String(job.raw_path || '').trim() || null; const activeConfirmRawPath = resolvedConfirmRawPath || String(job.raw_path || '').trim() || null;
let inputPath = isPreRipMode let inputPath = isPreRipMode
@@ -7460,13 +7498,16 @@ class PipelineService extends EventEmitter {
throw error; throw error;
} }
const reencodeMediaProfile = this.resolveMediaProfileForJob(sourceJob, {
makemkvInfo: mkInfo,
rawPath: sourceJob.raw_path
});
const reencodeSettings = await settingsService.getSettingsMap(); const reencodeSettings = await settingsService.getSettingsMap();
const reencodeRawBaseDir = settingsService.DEFAULT_RAW_DIR; const resolvedReencodeRawPath = this.resolveCurrentRawPathForSettings(
const reencodeRawExtraDirs = [ reencodeSettings,
reencodeSettings?.raw_dir_bluray, reencodeMediaProfile,
reencodeSettings?.raw_dir_dvd sourceJob.raw_path
].map((d) => String(d || '').trim()).filter(Boolean); );
const resolvedReencodeRawPath = this.resolveCurrentRawPath(reencodeRawBaseDir, sourceJob.raw_path, reencodeRawExtraDirs);
if (!resolvedReencodeRawPath) { if (!resolvedReencodeRawPath) {
const error = new Error(`Re-Encode nicht möglich: RAW-Pfad existiert nicht (${sourceJob.raw_path}).`); const error = new Error(`Re-Encode nicht möglich: RAW-Pfad existiert nicht (${sourceJob.raw_path}).`);
error.statusCode = 400; error.statusCode = 400;
@@ -8339,14 +8380,7 @@ class PipelineService extends EventEmitter {
rawPath: job.raw_path rawPath: job.raw_path
}); });
const settings = await settingsService.getEffectiveSettingsMap(mediaProfile); const settings = await settingsService.getEffectiveSettingsMap(mediaProfile);
const rawBaseDir = String(settings.raw_dir || '').trim(); const resolvedRawPath = this.resolveCurrentRawPathForSettings(settings, mediaProfile, job.raw_path);
const rawExtraDirs = [
settings.raw_dir_bluray,
settings.raw_dir_dvd
].map((item) => String(item || '').trim()).filter(Boolean);
const resolvedRawPath = job.raw_path
? this.resolveCurrentRawPath(rawBaseDir, job.raw_path, rawExtraDirs)
: null;
const activeRawPath = resolvedRawPath || String(job.raw_path || '').trim() || null; const activeRawPath = resolvedRawPath || String(job.raw_path || '').trim() || null;
if (activeRawPath && normalizeComparablePath(activeRawPath) !== normalizeComparablePath(job.raw_path)) { if (activeRawPath && normalizeComparablePath(activeRawPath) !== normalizeComparablePath(job.raw_path)) {
await historyService.updateJob(jobId, { raw_path: activeRawPath }); await historyService.updateJob(jobId, { raw_path: activeRawPath });
@@ -9251,14 +9285,15 @@ class PipelineService extends EventEmitter {
}; };
} else { } else {
const retrySettings = await settingsService.getEffectiveSettingsMap(mediaProfile); const retrySettings = await settingsService.getEffectiveSettingsMap(mediaProfile);
const retryRawBaseDir = String(retrySettings?.raw_dir || '').trim(); const { rawBaseDir: retryRawBaseDir, rawExtraDirs: retryRawExtraDirs } = this.buildRawPathLookupConfig(
const retryRawExtraDirs = [ retrySettings,
retrySettings?.raw_dir_bluray, mediaProfile
retrySettings?.raw_dir_dvd );
].map((dirPath) => String(dirPath || '').trim()).filter(Boolean); const resolvedOldRawPath = this.resolveCurrentRawPathForSettings(
const resolvedOldRawPath = sourceJob.raw_path retrySettings,
? this.resolveCurrentRawPath(retryRawBaseDir, sourceJob.raw_path, retryRawExtraDirs) mediaProfile,
: null; sourceJob.raw_path
);
if (resolvedOldRawPath) { if (resolvedOldRawPath) {
const oldRawFolderName = path.basename(resolvedOldRawPath); const oldRawFolderName = path.basename(resolvedOldRawPath);
@@ -9419,15 +9454,13 @@ class PipelineService extends EventEmitter {
const mode = String(encodePlan?.mode || 'rip').trim().toLowerCase(); const mode = String(encodePlan?.mode || 'rip').trim().toLowerCase();
const isPreRipMode = mode === 'pre_rip' || Boolean(encodePlan?.preRip); const isPreRipMode = mode === 'pre_rip' || Boolean(encodePlan?.preRip);
const reviewConfirmed = Boolean(Number(job.encode_review_confirmed || 0) || encodePlan?.reviewConfirmed); const reviewConfirmed = Boolean(Number(job.encode_review_confirmed || 0) || encodePlan?.reviewConfirmed);
const resumeSettings = await settingsService.getEffectiveSettingsMap(this.resolveMediaProfileForJob(job, { encodePlan })); const readyMediaProfile = this.resolveMediaProfileForJob(job, { encodePlan });
const resumeRawBaseDir = String(resumeSettings?.raw_dir || '').trim(); const resumeSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile);
const resumeRawExtraDirs = [ const resolvedResumeRawPath = this.resolveCurrentRawPathForSettings(
resumeSettings?.raw_dir_bluray, resumeSettings,
resumeSettings?.raw_dir_dvd readyMediaProfile,
].map((d) => String(d || '').trim()).filter(Boolean); job.raw_path
const resolvedResumeRawPath = job.raw_path );
? this.resolveCurrentRawPath(resumeRawBaseDir, job.raw_path, resumeRawExtraDirs)
: null;
const activeResumeRawPath = resolvedResumeRawPath || String(job.raw_path || '').trim() || null; const activeResumeRawPath = resolvedResumeRawPath || String(job.raw_path || '').trim() || null;
let inputPath = isPreRipMode let inputPath = isPreRipMode
@@ -9463,10 +9496,6 @@ class PipelineService extends EventEmitter {
imdbId: job.imdb_id || null, imdbId: job.imdb_id || null,
poster: job.poster_url || null poster: job.poster_url || null
}; };
const readyMediaProfile = this.resolveMediaProfileForJob(job, {
encodePlan
});
await this.setState('READY_TO_ENCODE', { await this.setState('READY_TO_ENCODE', {
activeJobId: jobId, activeJobId: jobId,
progress: 0, progress: 0,
@@ -9613,14 +9642,11 @@ class PipelineService extends EventEmitter {
encodePlan: restartPlan encodePlan: restartPlan
}); });
const restartSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile); const restartSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile);
const restartRawBaseDir = String(restartSettings?.raw_dir || '').trim(); const resolvedRestartRawPath = this.resolveCurrentRawPathForSettings(
const restartRawExtraDirs = [ restartSettings,
restartSettings?.raw_dir_bluray, readyMediaProfile,
restartSettings?.raw_dir_dvd job.raw_path
].map((d) => String(d || '').trim()).filter(Boolean); );
const resolvedRestartRawPath = job.raw_path
? this.resolveCurrentRawPath(restartRawBaseDir, job.raw_path, restartRawExtraDirs)
: null;
const activeRestartRawPath = resolvedRestartRawPath || String(job.raw_path || '').trim() || null; const activeRestartRawPath = resolvedRestartRawPath || String(job.raw_path || '').trim() || null;
let inputPath = isPreRipMode let inputPath = isPreRipMode
@@ -9761,13 +9787,19 @@ class PipelineService extends EventEmitter {
throw error; throw error;
} }
const reviewMakemkvInfo = this.safeParseJson(sourceJob.makemkv_info_json);
const reviewEncodePlan = this.safeParseJson(sourceJob.encode_plan_json);
const reviewMediaProfile = this.resolveMediaProfileForJob(sourceJob, {
makemkvInfo: reviewMakemkvInfo,
encodePlan: reviewEncodePlan,
rawPath: sourceJob.raw_path
});
const reviewSettings = await settingsService.getSettingsMap(); const reviewSettings = await settingsService.getSettingsMap();
const reviewRawBaseDir = settingsService.DEFAULT_RAW_DIR; const resolvedReviewRawPath = this.resolveCurrentRawPathForSettings(
const reviewRawExtraDirs = [ reviewSettings,
reviewSettings?.raw_dir_bluray, reviewMediaProfile,
reviewSettings?.raw_dir_dvd sourceJob.raw_path
].map((d) => String(d || '').trim()).filter(Boolean); );
const resolvedReviewRawPath = this.resolveCurrentRawPath(reviewRawBaseDir, sourceJob.raw_path, reviewRawExtraDirs);
if (!resolvedReviewRawPath) { if (!resolvedReviewRawPath) {
const error = new Error(`Review-Neustart nicht möglich: RAW-Pfad existiert nicht (${sourceJob.raw_path}).`); const error = new Error(`Review-Neustart nicht möglich: RAW-Pfad existiert nicht (${sourceJob.raw_path}).`);
error.statusCode = 400; error.statusCode = 400;

View File

@@ -380,7 +380,7 @@ function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effect
const blurayMovies = ep.bluray?.movies || defaultMovies; const blurayMovies = ep.bluray?.movies || defaultMovies;
const dvdRaw = ep.dvd?.raw || defaultRaw; const dvdRaw = ep.dvd?.raw || defaultRaw;
const dvdMovies = ep.dvd?.movies || defaultMovies; const dvdMovies = ep.dvd?.movies || defaultMovies;
const cdOutput = ep.cd?.raw || defaultCd; const cdRaw = ep.cd?.raw || defaultCd;
const isDefault = (path, def) => path === def; const isDefault = (path, def) => path === def;
@@ -424,10 +424,10 @@ function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effect
</td> </td>
</tr> </tr>
<tr> <tr>
<td><strong>CD / Audio</strong></td> <td><strong>CD / Audio (RAW + Output)</strong></td>
<td colSpan={2}> <td colSpan={2}>
<code>{cdOutput}</code> <code>{cdRaw}</code>
{isDefault(cdOutput, defaultCd) && <span className="path-default-badge">Standard</span>} {isDefault(cdRaw, defaultCd) && <span className="path-default-badge">Standard</span>}
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@@ -768,20 +768,6 @@ export default function DashboardPage({
const memoryMetrics = monitoringSample?.memory || null; const memoryMetrics = monitoringSample?.memory || null;
const gpuMetrics = monitoringSample?.gpu || null; const gpuMetrics = monitoringSample?.gpu || null;
const storageMetrics = Array.isArray(monitoringSample?.storage) ? monitoringSample.storage : []; const storageMetrics = Array.isArray(monitoringSample?.storage) ? monitoringSample.storage : [];
const storageGroups = useMemo(() => {
const groups = [];
const mountMap = new Map();
for (const entry of storageMetrics) {
const groupKey = entry?.mountPoint || `__no_mount_${entry?.key}`;
if (!mountMap.has(groupKey)) {
const group = { mountPoint: entry?.mountPoint || null, entries: [], representative: entry };
mountMap.set(groupKey, group);
groups.push(group);
}
mountMap.get(groupKey).entries.push(entry);
}
return groups;
}, [storageMetrics]);
const cpuPerCoreMetrics = Array.isArray(cpuMetrics?.perCore) ? cpuMetrics.perCore : []; const cpuPerCoreMetrics = Array.isArray(cpuMetrics?.perCore) ? cpuMetrics.perCore : [];
const gpuDevices = Array.isArray(gpuMetrics?.devices) ? gpuMetrics.devices : []; const gpuDevices = Array.isArray(gpuMetrics?.devices) ? gpuMetrics.devices : [];
@@ -1868,55 +1854,57 @@ export default function DashboardPage({
<section className="hardware-monitor-block"> <section className="hardware-monitor-block">
<h4>Freier Speicher in Pfaden</h4> <h4>Freier Speicher in Pfaden</h4>
<div className="hardware-storage-list"> <div className="hardware-storage-list">
{storageGroups.map((group) => { {storageMetrics.map((entry) => {
const rep = group.representative; const tone = getStorageUsageTone(entry?.usagePercent);
const tone = getStorageUsageTone(rep?.usagePercent); const usagePercent = Number(entry?.usagePercent);
const usagePercent = Number(rep?.usagePercent);
const barValue = Number.isFinite(usagePercent) const barValue = Number.isFinite(usagePercent)
? Math.max(0, Math.min(100, usagePercent)) ? Math.max(0, Math.min(100, usagePercent))
: 0; : 0;
const hasError = group.entries.every((e) => e?.error); const hasError = Boolean(entry?.error);
const groupKey = group.mountPoint || group.entries.map((e) => e?.key).join('-'); const entryKey = entry?.key || entry?.path || 'storage-entry';
return ( return (
<div <div
key={`storage-group-${groupKey}`} key={`storage-entry-${entryKey}`}
className={`hardware-storage-item compact${hasError ? ' has-error' : ''}`} className={`hardware-storage-item compact${hasError ? ' has-error' : ''}`}
> >
<div className="hardware-storage-head"> <div className="hardware-storage-head">
<strong>{group.entries.map((e) => e?.label || e?.key || 'Pfad').join(' · ')}</strong> <strong>{entry?.label || entry?.key || 'Pfad'}</strong>
<span className={`hardware-storage-percent tone-${tone}`}> <span className={`hardware-storage-percent tone-${tone}`}>
{hasError ? 'Fehler' : formatPercent(rep?.usagePercent)} {hasError ? 'Fehler' : formatPercent(entry?.usagePercent)}
</span> </span>
</div> </div>
{hasError ? ( {hasError ? (
<small className="error-text">{rep?.error}</small> <small className="error-text">{entry?.error}</small>
) : ( ) : (
<> <>
<div className={`hardware-storage-bar tone-${tone}`}> <div className={`hardware-storage-bar tone-${tone}`}>
<ProgressBar value={barValue} showValue={false} /> <ProgressBar value={barValue} showValue={false} />
</div> </div>
<div className="hardware-storage-summary"> <div className="hardware-storage-summary">
<small>Frei: {formatBytes(rep?.freeBytes)}</small> <small>Frei: {formatBytes(entry?.freeBytes)}</small>
<small>Gesamt: {formatBytes(rep?.totalBytes)}</small> <small>Gesamt: {formatBytes(entry?.totalBytes)}</small>
</div> </div>
</> </>
)} )}
{group.entries.map((entry) => ( <div className="hardware-storage-paths">
<div key={entry?.key} className="hardware-storage-paths"> <small className="hardware-storage-label-tag">Pfad:</small>
<small className="hardware-storage-label-tag">{entry?.label || entry?.key}:</small>
<small className="hardware-storage-path" title={entry?.path || '-'}> <small className="hardware-storage-path" title={entry?.path || '-'}>
{entry?.path || '-'} {entry?.path || '-'}
</small> </small>
{entry?.mountPoint ? (
<small className="hardware-storage-path" title={entry.mountPoint}>
Mount: {entry.mountPoint}
</small>
) : null}
{entry?.queryPath && entry.queryPath !== entry.path ? ( {entry?.queryPath && entry.queryPath !== entry.path ? (
<small className="hardware-storage-path" title={entry.queryPath}> <small className="hardware-storage-path" title={entry.queryPath}>
(Parent: {entry.queryPath}) Parent: {entry.queryPath}
</small> </small>
) : null} ) : null}
{entry?.note ? <small className="hardware-storage-path">{entry.note}</small> : null} {entry?.note ? <small className="hardware-storage-path">{entry.note}</small> : null}
</div> </div>
))}
</div> </div>
); );
})} })}