0.10.2-11 Layout

This commit is contained in:
2026-03-16 11:30:09 +00:00
parent 42eceea521
commit bf72c40ae1
9 changed files with 214 additions and 293 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.10.2-10", "version": "0.10.2-11",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.10.2-10", "version": "0.10.2-11",
"dependencies": { "dependencies": {
"archiver": "^7.0.1", "archiver": "^7.0.1",
"cors": "^2.8.5", "cors": "^2.8.5",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.10.2-10", "version": "0.10.2-11",
"private": true, "private": true,
"type": "commonjs", "type": "commonjs",
"scripts": { "scripts": {
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.10.2-10", "version": "0.10.2-11",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.10.2-10", "version": "0.10.2-11",
"dependencies": { "dependencies": {
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primereact": "^10.9.2", "primereact": "^10.9.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.10.2-10", "version": "0.10.2-11",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+36 -108
View File
@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { TabView, TabPanel } from 'primereact/tabview'; import { TabView, TabPanel } from 'primereact/tabview';
import { Accordion, AccordionTab } from 'primereact/accordion';
import { InputText } from 'primereact/inputtext'; import { InputText } from 'primereact/inputtext';
import { InputNumber } from 'primereact/inputnumber'; import { InputNumber } from 'primereact/inputnumber';
import { InputSwitch } from 'primereact/inputswitch'; import { InputSwitch } from 'primereact/inputswitch';
@@ -68,7 +69,8 @@ const EXPERT_ONLY_SETTING_KEYS = new Set([
'makemkv_analyze_extra_args_dvd', 'makemkv_analyze_extra_args_dvd',
'makemkv_rip_extra_args_bluray', 'makemkv_rip_extra_args_bluray',
'makemkv_rip_extra_args_dvd', 'makemkv_rip_extra_args_dvd',
'cdparanoia_command' 'cdparanoia_command',
'script_test_timeout_ms'
]); ]);
function toBoolean(value) { function toBoolean(value) {
@@ -331,51 +333,6 @@ function SettingField({
); );
} }
function PathMediumCard({ title, pathSettings, settingsByKey, values, errors, dirtyKeys, onChange }) {
// Filter out _owner keys since they're rendered inline
const visibleSettings = pathSettings.filter(
(s) => !String(s?.key || '').endsWith('_owner')
);
if (visibleSettings.length === 0) {
return null;
}
return (
<div className="path-medium-card">
<div className="path-medium-card-header">
<h4>{title}</h4>
</div>
<div className="settings-grid">
{visibleSettings.map((setting) => {
const value = values?.[setting.key];
const error = errors?.[setting.key] || null;
const dirty = Boolean(dirtyKeys?.has?.(setting.key));
const ownerKey = `${setting.key}_owner`;
const ownerSetting = settingsByKey.get(ownerKey) || null;
const ownerValue = values?.[ownerKey];
const ownerError = errors?.[ownerKey] || null;
const ownerDirty = Boolean(dirtyKeys?.has?.(ownerKey));
return (
<SettingField
key={setting.key}
setting={setting}
value={value}
error={error}
dirty={dirty}
ownerSetting={ownerSetting}
ownerValue={ownerValue}
ownerError={ownerError}
ownerDirty={ownerDirty}
onChange={onChange}
/>
);
})}
</div>
</div>
);
}
function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effectivePaths }) { function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effectivePaths }) {
const list = Array.isArray(settings) ? settings : []; const list = Array.isArray(settings) ? settings : [];
@@ -478,66 +435,35 @@ function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effect
</div> </div>
</div> </div>
{/* Medium-Karten */} {/* Medium-Karten als Accordion */}
<div className="path-medium-cards"> <Accordion className="path-medium-accordion">
<PathMediumCard {[
title="Blu-ray" { title: 'Blu-ray', pathSettings: bluraySettings },
pathSettings={bluraySettings} { title: 'DVD', pathSettings: dvdSettings },
settingsByKey={settingsByKey} { title: 'CD / Audio', pathSettings: cdSettings },
values={values} { title: 'Audiobook', pathSettings: audiobookSettings },
errors={errors} { title: 'Downloads', pathSettings: downloadSettings },
dirtyKeys={dirtyKeys} ...(logSettings.length > 0 ? [{ title: 'Logs', pathSettings: logSettings }] : [])
onChange={onChange} ]
/> .filter(({ pathSettings }) =>
<PathMediumCard pathSettings.filter((s) => !String(s?.key || '').endsWith('_owner')).length > 0
title="DVD" )
pathSettings={dvdSettings} .map(({ title, pathSettings }) => {
settingsByKey={settingsByKey} const visibleSettings = pathSettings.filter(
values={values} (s) => !String(s?.key || '').endsWith('_owner')
errors={errors} );
dirtyKeys={dirtyKeys} return (
onChange={onChange} <AccordionTab key={title} header={title}>
/>
<PathMediumCard
title="CD / Audio"
pathSettings={cdSettings}
settingsByKey={settingsByKey}
values={values}
errors={errors}
dirtyKeys={dirtyKeys}
onChange={onChange}
/>
<PathMediumCard
title="Audiobook"
pathSettings={audiobookSettings}
settingsByKey={settingsByKey}
values={values}
errors={errors}
dirtyKeys={dirtyKeys}
onChange={onChange}
/>
<PathMediumCard
title="Downloads"
pathSettings={downloadSettings}
settingsByKey={settingsByKey}
values={values}
errors={errors}
dirtyKeys={dirtyKeys}
onChange={onChange}
/>
</div>
{/* Log-Ordner */}
{logSettings.length > 0 && (
<div className="path-medium-card">
<div className="path-medium-card-header">
<h4>Logs</h4>
</div>
<div className="settings-grid"> <div className="settings-grid">
{logSettings.map((setting) => { {visibleSettings.map((setting) => {
const value = values?.[setting.key]; const value = values?.[setting.key];
const error = errors?.[setting.key] || null; const error = errors?.[setting.key] || null;
const dirty = Boolean(dirtyKeys?.has?.(setting.key)); const dirty = Boolean(dirtyKeys?.has?.(setting.key));
const ownerKey = `${setting.key}_owner`;
const ownerSetting = settingsByKey.get(ownerKey) || null;
const ownerValue = values?.[ownerKey];
const ownerError = errors?.[ownerKey] || null;
const ownerDirty = Boolean(dirtyKeys?.has?.(ownerKey));
return ( return (
<SettingField <SettingField
key={setting.key} key={setting.key}
@@ -545,17 +471,19 @@ function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effect
value={value} value={value}
error={error} error={error}
dirty={dirty} dirty={dirty}
ownerSetting={null} ownerSetting={ownerSetting}
ownerValue={null} ownerValue={ownerValue}
ownerError={null} ownerError={ownerError}
ownerDirty={false} ownerDirty={ownerDirty}
onChange={onChange} onChange={onChange}
/> />
); );
})} })}
</div> </div>
</div> </AccordionTab>
)} );
})}
</Accordion>
</div> </div>
); );
} }
+152 -159
View File
@@ -2113,165 +2113,6 @@ export default function DashboardPage({
<div className="page-grid"> <div className="page-grid">
<Toast ref={toastRef} /> <Toast ref={toastRef} />
{monitoringState.enabled ? (
<Card title="Hardware Monitoring">
<div className="hardware-monitor-head">
<Tag value={`Letztes Update: ${formatUpdatedAt(monitoringState.updatedAt)}`} severity="warning" />
</div>
{monitoringState.error ? (
<small className="error-text">{monitoringState.error}</small>
) : null}
{!monitoringState.enabled ? (
<p>Monitoring ist deaktiviert. Aktivierung in den Settings unter Kategorie "Monitoring".</p>
) : !monitoringSample ? (
<p>Monitoring ist aktiv. Erste Messwerte werden gesammelt ...</p>
) : (
<div className="hardware-monitor-grid">
<section className="hardware-monitor-block">
<h4>CPU</h4>
<div className="hardware-cpu-summary">
<div className="hardware-cpu-chip" title="CPU Gesamtauslastung">
<i className="pi pi-chart-line" />
<span>{formatPercent(cpuMetrics?.overallUsagePercent)}</span>
</div>
<div className="hardware-cpu-chip" title="CPU Gesamttemperatur">
<i className="pi pi-bolt" />
<span>{formatTemperature(cpuMetrics?.overallTemperatureC)}</span>
</div>
<div className="hardware-cpu-load-group">
<div className="hardware-cpu-chip" title="CPU Load Average">
<i className="pi pi-chart-bar" />
<span>{Array.isArray(cpuMetrics?.loadAverage) ? cpuMetrics.loadAverage.join(' / ') : '-'}</span>
</div>
{cpuPerCoreMetrics.length > 0 ? (
<button
type="button"
className="hardware-cpu-core-toggle-btn"
onClick={() => setCpuCoresExpanded((prev) => !prev)}
aria-label={cpuCoresExpanded ? 'CPU-Kerne ausblenden' : 'CPU-Kerne einblenden'}
aria-expanded={cpuCoresExpanded}
>
<i className={`pi ${cpuCoresExpanded ? 'pi-angle-up' : 'pi-angle-down'}`} />
</button>
) : null}
</div>
</div>
{cpuPerCoreMetrics.length === 0 ? (
<small>Pro-Core-Daten sind noch nicht verfuegbar.</small>
) : null}
{cpuPerCoreMetrics.length > 0 && cpuCoresExpanded ? (
<div className="hardware-core-grid compact">
{cpuPerCoreMetrics.map((core) => (
<div key={`core-${core.index}`} className="hardware-core-item compact">
<div className="hardware-core-title">C{core.index}</div>
<div className="hardware-core-metric" title="Auslastung">
<i className="pi pi-chart-line" />
<small>{formatPercent(core.usagePercent)}</small>
</div>
<div className="hardware-core-metric" title="Temperatur">
<i className="pi pi-bolt" />
<small>{formatTemperature(core.temperatureC)}</small>
</div>
</div>
))}
</div>
) : null}
</section>
<section className="hardware-monitor-block">
<h4>RAM</h4>
<div className="hardware-cpu-summary">
<div className="hardware-cpu-chip" title="RAM Auslastung">
<i className="pi pi-chart-pie" />
<span>{formatPercent(memoryMetrics?.usagePercent)}</span>
</div>
<div className="hardware-cpu-chip" title="RAM Belegt">
<i className="pi pi-arrow-up" />
<span>{formatBytes(memoryMetrics?.usedBytes)}</span>
</div>
<div className="hardware-cpu-chip" title="RAM Frei">
<i className="pi pi-arrow-down" />
<span>{formatBytes(memoryMetrics?.freeBytes)}</span>
</div>
<div className="hardware-cpu-chip" title="RAM Gesamt">
<i className="pi pi-database" />
<span>{formatBytes(memoryMetrics?.totalBytes)}</span>
</div>
</div>
</section>
<section className="hardware-monitor-block">
<h4>GPU</h4>
{!gpuMetrics?.available ? (
<small>{gpuMetrics?.message || 'Keine GPU-Metriken verfuegbar.'}</small>
) : (
<div className="hardware-gpu-list">
{gpuDevices.map((gpu, index) => (
<div key={`gpu-${gpu?.index ?? index}`} className="hardware-gpu-item">
<strong>
GPU {gpu?.index ?? index}
{gpu?.name ? ` | ${gpu.name}` : ''}
</strong>
<small>Load: {formatPercent(gpu?.utilizationPercent)}</small>
<small>Mem-Load: {formatPercent(gpu?.memoryUtilizationPercent)}</small>
<small>Temp: {formatTemperature(gpu?.temperatureC)}</small>
<small>VRAM: {formatBytes(gpu?.memoryUsedBytes)} / {formatBytes(gpu?.memoryTotalBytes)}</small>
<small>Power: {Number.isFinite(Number(gpu?.powerDrawW)) ? `${gpu.powerDrawW} W` : 'n/a'} / {Number.isFinite(Number(gpu?.powerLimitW)) ? `${gpu.powerLimitW} W` : 'n/a'}</small>
</div>
))}
</div>
)}
</section>
<section className="hardware-monitor-block">
<h4>Freier Speicher</h4>
<div className="hardware-storage-list">
{storageGroups.map((group) => {
const rep = group.representative;
const tone = getStorageUsageTone(rep?.usagePercent);
const usagePercent = Number(rep?.usagePercent);
const barValue = Number.isFinite(usagePercent)
? Math.max(0, Math.min(100, usagePercent))
: 0;
const hasError = group.entries.every((e) => e?.error);
const groupKey = group.mountPoint || group.entries.map((e) => e?.key).join('-');
const labels = group.entries.map((e) => e?.label || e?.key || 'Pfad').join(' · ');
return (
<div
key={`storage-group-${groupKey}`}
className={`hardware-storage-item compact${hasError ? ' has-error' : ''}`}
>
<small className="hardware-storage-group-label">{labels}</small>
{hasError ? (
<small className="error-text">{rep?.error}</small>
) : (
<>
<div className="hardware-storage-bar-row">
<div className={`hardware-storage-bar tone-${tone}`}>
<ProgressBar value={barValue} showValue={false} />
</div>
<span className={`hardware-storage-percent tone-${tone}`}>
{formatPercent(rep?.usagePercent)}
</span>
</div>
<div className="hardware-storage-summary">
<small>Frei: {formatBytes(rep?.freeBytes)}</small>
<small>Gesamt: {formatBytes(rep?.totalBytes)}</small>
</div>
</>
)}
</div>
);
})}
</div>
</section>
</div>
)}
</Card>
) : null}
<div className="dashboard-3col-grid"> <div className="dashboard-3col-grid">
<div className="dashboard-col dashboard-col-left"> <div className="dashboard-col dashboard-col-left">
<Card title="Audiobook Upload"> <Card title="Audiobook Upload">
@@ -2390,6 +2231,48 @@ export default function DashboardPage({
<p>Aktuell keine Disk erkannt.</p> <p>Aktuell keine Disk erkannt.</p>
)} )}
</Card> </Card>
<Card title="Freier Speicher">
<div className="hardware-storage-list">
{storageGroups.map((group) => {
const rep = group.representative;
const tone = getStorageUsageTone(rep?.usagePercent);
const usagePercent = Number(rep?.usagePercent);
const barValue = Number.isFinite(usagePercent)
? Math.max(0, Math.min(100, usagePercent))
: 0;
const hasError = group.entries.every((e) => e?.error);
const groupKey = group.mountPoint || group.entries.map((e) => e?.key).join('-');
const labels = group.entries.map((e) => e?.label || e?.key || 'Pfad').join(' · ');
return (
<div
key={`storage-group-${groupKey}`}
className={`hardware-storage-item compact${hasError ? ' has-error' : ''}`}
>
<small className="hardware-storage-group-label">{labels}</small>
{hasError ? (
<small className="error-text">{rep?.error}</small>
) : (
<>
<div className="hardware-storage-bar-row">
<div className={`hardware-storage-bar tone-${tone}`}>
<ProgressBar value={barValue} showValue={false} />
</div>
<span className={`hardware-storage-percent tone-${tone}`}>
{formatPercent(rep?.usagePercent)}
</span>
</div>
<div className="hardware-storage-summary">
<small>Frei: {formatBytes(rep?.freeBytes)}</small>
<small>Gesamt: {formatBytes(rep?.totalBytes)}</small>
</div>
</>
)}
</div>
);
})}
</div>
</Card>
</div> </div>
<div className="dashboard-col dashboard-col-center"> <div className="dashboard-col dashboard-col-center">
@@ -2923,6 +2806,116 @@ export default function DashboardPage({
</div> </div>
</div> </div>
{monitoringState.enabled ? (
<Card title="Hardware Monitoring">
<div className="hardware-monitor-head">
{!monitoringState.enabled ? (
<small>Hardware-Monitoring ist deaktiviert.</small>
) : !monitoringSample ? (
<small>Warte auf erste Messung ...</small>
) : (
<div className="hardware-monitor-grid">
<section className="hardware-monitor-block">
<h4>CPU</h4>
<div className="hardware-cpu-summary">
<div className="hardware-cpu-chip" title="CPU Gesamtauslastung">
<i className="pi pi-chart-line" />
<span>{formatPercent(cpuMetrics?.overallUsagePercent)}</span>
</div>
<div className="hardware-cpu-chip" title="CPU Gesamttemperatur">
<i className="pi pi-bolt" />
<span>{formatTemperature(cpuMetrics?.overallTemperatureC)}</span>
</div>
<div className="hardware-cpu-load-group">
<div className="hardware-cpu-chip" title="CPU Load Average">
<i className="pi pi-chart-bar" />
<span>{Array.isArray(cpuMetrics?.loadAverage) ? cpuMetrics.loadAverage.join(' / ') : '-'}</span>
</div>
{cpuPerCoreMetrics.length > 0 ? (
<button
type="button"
className="hardware-cpu-core-toggle-btn"
onClick={() => setCpuCoresExpanded((prev) => !prev)}
aria-label={cpuCoresExpanded ? 'CPU-Kerne ausblenden' : 'CPU-Kerne einblenden'}
aria-expanded={cpuCoresExpanded}
>
<i className={`pi ${cpuCoresExpanded ? 'pi-angle-up' : 'pi-angle-down'}`} />
</button>
) : null}
</div>
</div>
{cpuPerCoreMetrics.length === 0 ? (
<small>Pro-Core-Daten sind noch nicht verfuegbar.</small>
) : null}
{cpuPerCoreMetrics.length > 0 && cpuCoresExpanded ? (
<div className="hardware-core-grid compact">
{cpuPerCoreMetrics.map((core) => (
<div key={`core-${core.index}`} className="hardware-core-item compact">
<div className="hardware-core-title">C{core.index}</div>
<div className="hardware-core-metric" title="Auslastung">
<i className="pi pi-chart-line" />
<small>{formatPercent(core.usagePercent)}</small>
</div>
<div className="hardware-core-metric" title="Temperatur">
<i className="pi pi-bolt" />
<small>{formatTemperature(core.temperatureC)}</small>
</div>
</div>
))}
</div>
) : null}
</section>
<section className="hardware-monitor-block">
<h4>RAM</h4>
<div className="hardware-cpu-summary">
<div className="hardware-cpu-chip" title="RAM Auslastung">
<i className="pi pi-chart-pie" />
<span>{formatPercent(memoryMetrics?.usagePercent)}</span>
</div>
<div className="hardware-cpu-chip" title="RAM Belegt">
<i className="pi pi-arrow-up" />
<span>{formatBytes(memoryMetrics?.usedBytes)}</span>
</div>
<div className="hardware-cpu-chip" title="RAM Frei">
<i className="pi pi-arrow-down" />
<span>{formatBytes(memoryMetrics?.freeBytes)}</span>
</div>
<div className="hardware-cpu-chip" title="RAM Gesamt">
<i className="pi pi-database" />
<span>{formatBytes(memoryMetrics?.totalBytes)}</span>
</div>
</div>
</section>
<section className="hardware-monitor-block">
<h4>GPU</h4>
{!gpuMetrics?.available ? (
<small>{gpuMetrics?.message || 'Keine GPU-Metriken verfuegbar.'}</small>
) : (
<div className="hardware-gpu-list">
{gpuDevices.map((gpu, index) => (
<div key={`gpu-${gpu?.index ?? index}`} className="hardware-gpu-item">
<strong>
GPU {gpu?.index ?? index}
{gpu?.name ? ` | ${gpu.name}` : ''}
</strong>
<small>Load: {formatPercent(gpu?.utilizationPercent)}</small>
<small>Mem-Load: {formatPercent(gpu?.memoryUtilizationPercent)}</small>
<small>Temp: {formatTemperature(gpu?.temperatureC)}</small>
<small>VRAM: {formatBytes(gpu?.memoryUsedBytes)} / {formatBytes(gpu?.memoryTotalBytes)}</small>
<small>Power: {Number.isFinite(Number(gpu?.powerDrawW)) ? `${gpu.powerDrawW} W` : 'n/a'} / {Number.isFinite(Number(gpu?.powerLimitW)) ? `${gpu.powerLimitW} W` : 'n/a'}</small>
</div>
))}
</div>
)}
</section>
</div>
)}
</div>
</Card>
) : null}
<MetadataSelectionDialog <MetadataSelectionDialog
visible={metadataDialogVisible} visible={metadataDialogVisible}
context={effectiveMetadataDialogContext} context={effectiveMetadataDialogContext}
+1 -1
View File
@@ -1744,7 +1744,7 @@ export default function SettingsPage() {
</TabView> </TabView>
</Card> </Card>
{activationBytes.length > 0 && ( {expertModeEnabled && activationBytes.length > 0 && (
<Card <Card
title="Activation Bytes Cache" title="Activation Bytes Cache"
subTitle="Lokal gespeicherte AAX-Activation Bytes. Werden beim ersten Upload automatisch über die Audible-Tools API ermittelt." subTitle="Lokal gespeicherte AAX-Activation Bytes. Werden beim ersten Upload automatisch über die Audible-Tools API ermittelt."
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster", "name": "ripster",
"version": "0.10.2-10", "version": "0.10.2-11",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster", "name": "ripster",
"version": "0.10.2-10", "version": "0.10.2-11",
"devDependencies": { "devDependencies": {
"concurrently": "^9.1.2" "concurrently": "^9.1.2"
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "ripster", "name": "ripster",
"private": true, "private": true,
"version": "0.10.2-10", "version": "0.10.2-11",
"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",