From 325a20d3fb456ae0e5682f64af21feca88977546 Mon Sep 17 00:00:00 2001
From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com>
Date: Wed, 26 Aug 2026 13:20:40 +0200
Subject: [PATCH] feat: add automation control center UI
Preserve monotonic source-cleanup requirements while excluding ambiguous job identities from Main payloads, and reconcile mixed add responses only across globally unique jobs that were actually sent.
Render the automation status card, localized fixed-column metrics, read-only test overlay, queue-limit and reconciliation controls, and persistent finish-and-pause or resume action from immutable automation snapshots.
Add hidden Electron coverage for cleanup siblings, Main fingerprints, German and English states, keyboard focus, Escape and inert behavior, responsive 760-pixel layouts, and snapshot-gated upload controls.
---
renderer/app.js | 521 +++++++++++++++++++++---
renderer/i18n.js | 55 +++
renderer/styles.css | 126 +++++-
tests/i18n.test.js | 60 +++
tests/startup-renderer.test.js | 721 ++++++++++++++++++++++++++++++---
5 files changed, 1372 insertions(+), 111 deletions(-)
diff --git a/renderer/app.js b/renderer/app.js
index 73d7797..993e68c 100644
--- a/renderer/app.js
+++ b/renderer/app.js
@@ -38,6 +38,8 @@ function refreshLocalizedRuntimeUi() {
const historyContainer = document.getElementById('historyContainer');
if (historyContainer && historyRowsData.length) renderHistoryTable(historyContainer);
updateStatusBar();
+ refreshAutomationControlCenter();
+ refreshAutomationTestOverlay();
const activeRecentTab = document.querySelector('.recent-tab.active');
const hint = document.getElementById('recentFilesHint');
if (hint && activeRecentTab) hint.textContent = localizeUiText(activeRecentTab.dataset.panel === 'statsTab' ? 'Upload-Statistiken' : 'Zuletzt erzeugte Upload-Links');
@@ -64,6 +66,13 @@ let hosterSettings = {};
let uploading = false;
let healthCheckRunning = false;
let automationRuntimeStatus = Object.freeze({});
+let automationRuntimeStatusAvailable = false;
+let automationRuntimeStartedAt = null;
+let automationPauseResumeBusy = false;
+let automationTestGeneration = 0;
+let automationTestReturnFocus = null;
+let automationTestInertState = [];
+let automationTestViewState = Object.freeze({ loading: false, summary: null, error: '' });
let managedOnlineBackups = [];
let managedOnlineBackupsAuthoritative = false;
let managedOnlineBackupMutationGeneration = 0;
@@ -472,7 +481,25 @@ function automationSettings() {
}
function applyAutomationRuntimeStatus(value) {
- automationRuntimeStatus = freezeAutomationValue({ ...(value || {}) });
+ const next = { ...(value || {}) };
+ const incomingStartedAt = automationTimestamp(next.startedAt);
+ const wasRunning = automationRuntimeStatus.running === true && automationRuntimeStatus.paused !== true;
+ if (next.running === true && next.paused !== true) {
+ automationRuntimeStartedAt = incomingStartedAt || (wasRunning ? automationRuntimeStartedAt : null) || Date.now();
+ } else if (incomingStartedAt) {
+ automationRuntimeStartedAt = incomingStartedAt;
+ }
+ if (automationRuntimeStartedAt) next.startedAt = automationRuntimeStartedAt;
+ if (typeof next.paused === 'boolean' && config.globalSettings) {
+ const folderMonitor = config.globalSettings.folderMonitor || {};
+ config.globalSettings.folderMonitor = {
+ ...folderMonitor,
+ paused: next.paused,
+ pausedAt: next.paused ? (next.pausedAt ?? folderMonitor.pausedAt ?? Date.now()) : null
+ };
+ }
+ automationRuntimeStatus = freezeAutomationValue(next);
+ automationRuntimeStatusAvailable = true;
return automationRuntimeStatus;
}
@@ -496,6 +523,12 @@ function createAutomationStatusSnapshot() {
const currentJobCount = window.AutomationControl.countAutomaticQueueJobs(queueJobs);
const availableSlots = normalized.queueLimitJobs === 0 ? null : Math.max(0, normalized.queueLimitJobs - currentJobCount);
const telemetry = window.AutomationControl.rollDailyTelemetry(folderSettings.telemetry);
+ const error = String(automationRuntimeStatus.error || automationRuntimeStatus.monitorError || telemetry.lastError || '');
+ const startedAt = automationTimestamp(automationRuntimeStatus.startedAt) || automationRuntimeStartedAt;
+ const lastReconcileAt = automationTimestamp(automationRuntimeStatus.lastScanAt);
+ const nextReconcileAt = automationRuntimeStatus.running === true && automationRuntimeStatus.paused !== true && lastReconcileAt
+ ? lastReconcileAt + normalized.reconcileIntervalMinutes * 60000
+ : null;
const snapshot = {
...automationRuntimeStatus,
enabled: folderSettings.enabled === true,
@@ -506,7 +539,13 @@ function createAutomationStatusSnapshot() {
currentJobCount,
availableSlots,
queueLimited: normalized.queueLimitJobs !== 0 && availableSlots === 0,
- telemetry
+ telemetry,
+ error,
+ startedAt,
+ lastReconcileAt,
+ nextReconcileAt,
+ lastErrorAt: automationTimestamp(automationRuntimeStatus.lastErrorAt) || automationTimestamp(telemetry.lastErrorAt),
+ statusAvailable: automationRuntimeStatusAvailable
};
snapshot.state = window.AutomationControl.deriveAutomationState(snapshot);
return freezeAutomationValue(snapshot);
@@ -741,7 +780,7 @@ async function applyAutomationEvaluation(evaluation) {
persistQueueStateSoon(true);
return freezeAutomationValue({ ok: false, error: 'Jobs konnten nicht eindeutig bestätigt werden.', warning: null, admittedFiles: [], deferredFiles, paused: false, dryRun: false });
}
- if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
+ applyAddJobsFingerprints(addRequest, result?.sourceCleanupFingerprints);
} catch {
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
return freezeAutomationValue({ ok: false, error: 'Jobs konnten nicht hinzugefügt werden.', warning: null, admittedFiles: [], deferredFiles, paused: false, dryRun: false });
@@ -775,9 +814,331 @@ async function applyAutomationEvaluation(evaluation) {
async function runFolderMonitorTestScan() {
const result = await window.api.folderMonitorTestScan();
+ if (result?.error) throw new Error('Ordnerüberwachung konnte nicht getestet werden.');
return evaluateAutomationCandidates(result?.files || [], { dryRun: true, trigger: result?.trigger || 'test' });
}
+const automationStatePresentation = Object.freeze({
+ inactive: Object.freeze({ label: 'Inaktiv', className: 'state-inactive' }),
+ active: Object.freeze({ label: 'Aktiv', className: 'state-active' }),
+ paused: Object.freeze({ label: 'Pausiert', className: 'state-paused' }),
+ 'queue-limited': Object.freeze({ label: 'Queue-Limit erreicht', className: 'state-queue-limited' }),
+ disconnected: Object.freeze({ label: 'Ordner getrennt', className: 'state-disconnected' }),
+ error: Object.freeze({ label: 'Fehler', className: 'state-error' })
+});
+
+const automationTestMetricDefinitions = Object.freeze([
+ Object.freeze(['found', 'Gefundene Dateien']),
+ Object.freeze(['filterMatched', 'Passend zum Dateifilter']),
+ Object.freeze(['alreadyProcessed', 'Bereits verarbeitet']),
+ Object.freeze(['unavailable', 'Fehlend, leer oder nicht lesbar']),
+ Object.freeze(['sizeLimitedJobs', 'Durch Größenlimits ausgeschlossen']),
+ Object.freeze(['acceptedFiles', 'Akzeptierte Dateien']),
+ Object.freeze(['selectedTargets', 'Ausgewählte Ziele']),
+ Object.freeze(['resultingJobs', 'Entstehende Upload-Jobs']),
+ Object.freeze(['availableSlots', 'Verfügbare Jobs bis zum Queue-Limit']),
+ Object.freeze(['deferredFiles', 'Aktuell zurückzustellende Dateien'])
+]);
+
+function automationTimestamp(value) {
+ if (value === null || value === undefined || value === '') return null;
+ const numeric = Number(value);
+ if (Number.isFinite(numeric) && numeric > 0) return numeric;
+ const parsed = Date.parse(String(value));
+ return Number.isFinite(parsed) ? parsed : null;
+}
+
+function formatAutomationNumber(value) {
+ const numeric = Number(value);
+ return new Intl.NumberFormat(getUiLocale(), { maximumFractionDigits: 0 }).format(Number.isFinite(numeric) ? Math.max(0, numeric) : 0);
+}
+
+function formatAutomationDateTime(value) {
+ const timestamp = automationTimestamp(value);
+ if (!timestamp) return localizeUiText('Nie');
+ return new Intl.DateTimeFormat(getUiLocale(), { dateStyle: 'short', timeStyle: 'medium' }).format(new Date(timestamp));
+}
+
+function setAutomationText(id, value) {
+ const element = document.getElementById(id);
+ if (element) element.textContent = value;
+}
+
+function renderAutomationStatusSnapshot(snapshot) {
+ const card = document.getElementById('automationStatusCard');
+ if (!card || !snapshot) return snapshot;
+ const presentation = automationStatePresentation[snapshot.state] || automationStatePresentation.inactive;
+ const badge = document.getElementById('automationStateBadge');
+ if (badge) {
+ badge.className = `automation-state-badge ${presentation.className}`;
+ badge.textContent = localizeUiText(presentation.label);
+ }
+ card.dataset.state = snapshot.state;
+ const queueLimit = snapshot.queueLimitJobs === 0
+ ? localizeUiText('Unbegrenzt')
+ : formatAutomationNumber(snapshot.queueLimitJobs);
+ const queueText = `${formatAutomationNumber(snapshot.currentJobCount)} / ${queueLimit}`;
+ setAutomationText('automationQueueMeter', queueText);
+ const queueBar = document.getElementById('automationQueueMeterBar');
+ if (queueBar) {
+ const percentage = snapshot.queueLimitJobs === 0
+ ? 0
+ : Math.min(100, Math.max(0, snapshot.currentJobCount / Math.max(1, snapshot.queueLimitJobs) * 100));
+ queueBar.style.width = `${percentage}%`;
+ }
+ const queueTrack = document.getElementById('automationQueueMeterTrack');
+ if (queueTrack) {
+ queueTrack.setAttribute('aria-valuenow', String(snapshot.currentJobCount));
+ if (snapshot.queueLimitJobs === 0) queueTrack.removeAttribute('aria-valuemax');
+ else queueTrack.setAttribute('aria-valuemax', String(snapshot.queueLimitJobs));
+ queueTrack.setAttribute('aria-valuetext', queueText);
+ }
+ const telemetry = snapshot.telemetry || {};
+ setAutomationText('automationMonitoringSince', snapshot.running === true ? formatAutomationDateTime(snapshot.startedAt) : localizeUiText('Nie'));
+ setAutomationText('automationFolderReachable', snapshot.reachable === null || snapshot.reachable === undefined ? '—' : localizeUiText(snapshot.reachable ? 'Ja' : 'Nein'));
+ setAutomationText('automationLastDetectedFile', telemetry.lastDetectedName || localizeUiText('Keine Datei erkannt'));
+ setAutomationText('automationDetectedToday', formatAutomationNumber(telemetry.detected));
+ setAutomationText('automationQueuedToday', formatAutomationNumber(telemetry.queued));
+ setAutomationText('automationSkippedToday', formatAutomationNumber(telemetry.skipped));
+ setAutomationText('automationDeferredToday', formatAutomationNumber(telemetry.deferred));
+ setAutomationText('automationLastReconcile', formatAutomationDateTime(snapshot.lastReconcileAt));
+ setAutomationText('automationNextReconcile', formatAutomationDateTime(snapshot.nextReconcileAt));
+ const errorRow = document.getElementById('automationLastErrorRow');
+ const errorText = String(snapshot.error || telemetry.lastError || '');
+ if (errorRow) errorRow.hidden = errorText.length === 0;
+ setAutomationText('automationLastError', errorText);
+ return snapshot;
+}
+
+function ensureAutomationPauseResumeButton() {
+ const existing = document.getElementById('automationPauseResumeBtn') || document.getElementById('finishStopBtn');
+ if (!existing) return null;
+ existing.id = 'automationPauseResumeBtn';
+ existing.classList.add('automation-pause-resume-btn');
+ let label = existing.querySelector('.automation-pause-resume-label');
+ if (!label) {
+ label = document.createElement('span');
+ label.className = 'automation-pause-resume-label';
+ existing.appendChild(label);
+ }
+ return existing;
+}
+
+function syncAutomationPauseResumeButton(snapshot) {
+ const button = ensureAutomationPauseResumeButton();
+ if (!button || !snapshot) return;
+ const label = snapshot.paused === true ? 'Fortsetzen' : 'Abschließen und pausieren';
+ const localized = localizeUiText(label);
+ const text = button.querySelector('.automation-pause-resume-label');
+ if (text) text.textContent = localized;
+ button.title = localized;
+ button.setAttribute('aria-label', localized);
+ button.classList.toggle('automation-resume', snapshot.paused === true);
+ button.disabled = automationPauseResumeBusy;
+}
+
+function syncAutomationContextStartControls(blocked) {
+ document.querySelectorAll('[data-action="start-selected"], [data-action="retry-selected"]').forEach(element => {
+ element.setAttribute('aria-disabled', String(blocked));
+ element.classList.toggle('ctx-item-disabled', blocked);
+ });
+}
+
+function refreshAutomationControlCenter() {
+ const snapshot = createAutomationStatusSnapshot();
+ updateQueueActionButtons(snapshot);
+ return snapshot;
+}
+
+function setAutomationTestBackgroundInert(active) {
+ const overlay = document.getElementById('automationTestOverlay');
+ if (!overlay) return;
+ if (active) {
+ if (automationTestInertState.length > 0) return;
+ automationTestInertState = Array.from(document.body.children)
+ .filter(element => element !== overlay && 'inert' in element)
+ .map(element => ({ element, inert: element.inert }));
+ automationTestInertState.forEach(({ element }) => { element.inert = true; });
+ return;
+ }
+ automationTestInertState.forEach(({ element, inert }) => {
+ if (element.isConnected) element.inert = inert;
+ });
+ automationTestInertState = [];
+}
+
+function ensureAutomationTestOverlay() {
+ let overlay = document.getElementById('automationTestOverlay');
+ if (overlay) return overlay;
+ overlay = document.createElement('div');
+ overlay.id = 'automationTestOverlay';
+ overlay.className = 'modal-overlay automation-test-overlay';
+ overlay.style.display = 'none';
+ overlay.setAttribute('aria-hidden', 'true');
+ overlay.setAttribute('aria-busy', 'false');
+ overlay.innerHTML = `
+
+
+
+
+
+ Ordner wird geprüft…
+
+
+ ${automationTestMetricDefinitions.map(([key, label]) => `
+
+ ${label}
+ 0
+
`).join('')}
+
+
+
+
+ `;
+ document.body.appendChild(overlay);
+ document.getElementById('automationTestCloseBtn')?.addEventListener('click', closeAutomationTestOverlay);
+ overlay.addEventListener('click', event => {
+ if (event.target === overlay) closeAutomationTestOverlay();
+ });
+ document.addEventListener('keydown', handleAutomationTestKeydown, true);
+ return overlay;
+}
+
+function renderAutomationTestViewState(state = automationTestViewState) {
+ const overlay = ensureAutomationTestOverlay();
+ automationTestViewState = freezeAutomationValue({
+ loading: state.loading === true,
+ summary: state.summary || null,
+ error: String(state.error || '')
+ });
+ overlay.setAttribute('aria-busy', automationTestViewState.loading ? 'true' : 'false');
+ const spinner = document.getElementById('automationTestSpinner');
+ const metrics = document.getElementById('automationTestMetrics');
+ const error = document.getElementById('automationTestError');
+ if (spinner) spinner.hidden = !automationTestViewState.loading;
+ if (metrics) metrics.hidden = automationTestViewState.loading;
+ if (error) {
+ error.hidden = automationTestViewState.error.length === 0;
+ error.textContent = automationTestViewState.error ? localizeUiText(automationTestViewState.error) : '';
+ }
+ setAutomationText('automationTestTitle', localizeUiText('Test der Ordnerüberwachung'));
+ setAutomationText('automationTestDescription', localizeUiText('Der Test verändert weder Queue noch Einstellungen.'));
+ setAutomationText('automationTestCloseBtn', localizeUiText('Schließen'));
+ for (const [key, label] of automationTestMetricDefinitions) {
+ const row = document.querySelector(`[data-automation-test-metric="${key}"]`);
+ const labelElement = row?.querySelector('.automation-test-label');
+ const valueElement = row?.querySelector('.automation-test-value');
+ if (labelElement) labelElement.textContent = localizeUiText(label);
+ if (valueElement) {
+ const value = automationTestViewState.summary?.[key];
+ valueElement.textContent = key === 'availableSlots' && value === null
+ ? localizeUiText('Unbegrenzt')
+ : formatAutomationNumber(value);
+ }
+ }
+ const button = document.getElementById('automationTestBtn');
+ if (button) button.disabled = automationTestViewState.loading;
+ return automationTestViewState;
+}
+
+function refreshAutomationTestOverlay() {
+ if (!document.getElementById('automationTestOverlay')) return automationTestViewState;
+ return renderAutomationTestViewState(automationTestViewState);
+}
+
+function openAutomationTestOverlay() {
+ const overlay = ensureAutomationTestOverlay();
+ automationTestReturnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
+ overlay.style.display = 'flex';
+ overlay.setAttribute('aria-hidden', 'false');
+ setAutomationTestBackgroundInert(true);
+ document.getElementById('automationTestCloseBtn')?.focus();
+ return overlay;
+}
+
+function closeAutomationTestOverlay() {
+ const overlay = document.getElementById('automationTestOverlay');
+ if (!overlay || overlay.style.display === 'none') return;
+ const canceledLoading = automationTestViewState.loading === true;
+ automationTestGeneration++;
+ overlay.style.display = 'none';
+ overlay.setAttribute('aria-hidden', 'true');
+ if (canceledLoading) renderAutomationTestViewState({ loading: false, summary: null, error: '' });
+ setAutomationTestBackgroundInert(false);
+ const returnFocus = automationTestReturnFocus;
+ automationTestReturnFocus = null;
+ if (returnFocus?.isConnected) returnFocus.focus();
+}
+
+function handleAutomationTestKeydown(event) {
+ const overlay = document.getElementById('automationTestOverlay');
+ if (!overlay || overlay.style.display !== 'flex') return;
+ const dialog = overlay.querySelector('[role="dialog"]');
+ const closeButton = document.getElementById('automationTestCloseBtn');
+ if (event.key === 'Escape') {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ closeAutomationTestOverlay();
+ return;
+ }
+ if (event.key === 'Tab') {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ closeButton?.focus();
+ return;
+ }
+ if (dialog && !dialog.contains(event.target)) {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ closeButton?.focus();
+ }
+}
+
+async function runAutomationTestOverlay() {
+ openAutomationTestOverlay();
+ const generation = ++automationTestGeneration;
+ renderAutomationTestViewState({ loading: true, summary: null, error: '' });
+ try {
+ const evaluation = await runFolderMonitorTestScan();
+ if (generation !== automationTestGeneration) return;
+ renderAutomationTestViewState({ loading: false, summary: evaluation.summary, error: '' });
+ } catch {
+ if (generation !== automationTestGeneration) return;
+ renderAutomationTestViewState({ loading: false, summary: null, error: 'Ordnerüberwachung konnte nicht getestet werden.' });
+ }
+}
+
+async function toggleAutomationPauseResume() {
+ if (automationPauseResumeBusy) return;
+ const snapshot = createAutomationStatusSnapshot();
+ const resume = snapshot.paused === true;
+ automationPauseResumeBusy = true;
+ updateQueueActionButtons(snapshot);
+ try {
+ const result = resume
+ ? await window.api.automationResume()
+ : await window.api.automationPauseAfterActive();
+ if (result?.error) throw new Error(result.error);
+ applyAutomationRuntimeStatus({ ...result, paused: resume ? false : true });
+ if (!resume && uploading) {
+ lastUploadStats.state = 'stopping';
+ updateStatusBar();
+ }
+ } catch {
+ showCopyToast(resume ? 'Automatik konnte nicht fortgesetzt werden.' : 'Automatik konnte nicht pausiert werden.');
+ } finally {
+ automationPauseResumeBusy = false;
+ refreshAutomationControlCenter();
+ }
+}
+
window.evaluateAutomationCandidates = evaluateAutomationCandidates;
window.applyAutomationEvaluation = applyAutomationEvaluation;
window.createAutomationStatusSnapshot = createAutomationStatusSnapshot;
@@ -900,7 +1261,7 @@ async function init() {
if (typeof window.api.onAutomationStatus === 'function') {
window.api.onAutomationStatus(status => {
applyAutomationRuntimeStatus(status);
- updateQueueActionButtons();
+ refreshAutomationControlCenter();
});
}
@@ -1543,7 +1904,7 @@ async function applyHosterSelection() {
cleanupStates: cleanupPreparation.rollbackStates
});
if (!outcome.consistent) regularInjectionFailure = 'Jobs konnten nicht eindeutig bestätigt werden.';
- else if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
+ else applyAddJobsFingerprints(addRequest, result?.sourceCleanupFingerprints);
}
} catch {
regularInjectionFailure = 'Jobs konnten nicht hinzugefügt werden.';
@@ -2038,19 +2399,21 @@ function updateUploadView(options = {}) {
updateQueueActionButtons();
}
-function updateStartButton() {
+function updateStartButton(snapshot = createAutomationStatusSnapshot()) {
const btn = document.getElementById('startUploadBtn');
+ if (!btn) return;
const hosters = getSelectedHosters();
const hasQueuedJobs = queueJobs.some(isStartableQueueJob);
const canBuildQueueFromSelection = selectedFiles.length > 0 && hosters.length > 0;
- btn.disabled = uploading || !(hasQueuedJobs || canBuildQueueFromSelection);
+ const startBlocked = snapshot.paused === true || snapshot.statusAvailable !== true;
+ btn.disabled = startBlocked || uploading || !(hasQueuedJobs || canBuildQueueFromSelection);
}
const _UPLOAD_SELECTION_STATUSES = new Set(['done', 'error', 'aborted', 'skipped']);
const _ABORT_SELECTION_STATUSES = new Set(['preview', 'queued', 'getting-server', 'uploading', 'retrying']);
-function updateQueueActionButtons() {
- updateStartButton();
+function updateQueueActionButtons(snapshot = createAutomationStatusSnapshot()) {
+ updateStartButton(snapshot);
_normalizeQueueSelectionToVisible();
const hasSelection = selectedJobIds.size > 0;
@@ -2072,22 +2435,27 @@ function updateQueueActionButtons() {
const startSelectedBtn = document.getElementById('startSelectedBtn');
const reuploadBtn = document.getElementById('reuploadSelectedBtn');
const abortSelectedBtn = document.getElementById('abortSelectedBtn');
- const finishStopBtn = document.getElementById('finishStopBtn');
+ ensureAutomationPauseResumeButton();
const abortAllBtn = document.getElementById('abortAllBtn');
const moveTopBtn = document.getElementById('moveTopBtn');
const moveUpBtn = document.getElementById('moveUpBtn');
const moveDownBtn = document.getElementById('moveDownBtn');
const moveBottomBtn = document.getElementById('moveBottomBtn');
- if (startSelectedBtn) startSelectedBtn.disabled = uploading || !hasStartableSelection;
- if (reuploadBtn) reuploadBtn.disabled = !hasUploadSelection;
+ const startBlocked = snapshot.paused === true || snapshot.statusAvailable !== true;
+ if (startSelectedBtn) startSelectedBtn.disabled = startBlocked || uploading || !hasStartableSelection;
+ if (reuploadBtn) reuploadBtn.disabled = startBlocked || !hasUploadSelection;
if (abortSelectedBtn) abortSelectedBtn.disabled = !hasAbortSelection;
- if (finishStopBtn) finishStopBtn.disabled = !uploading;
+ syncAutomationPauseResumeButton(snapshot);
if (abortAllBtn) abortAllBtn.disabled = !uploading;
if (moveTopBtn) moveTopBtn.disabled = !hasMovableSelection;
if (moveUpBtn) moveUpBtn.disabled = !hasMovableSelection;
if (moveDownBtn) moveDownBtn.disabled = !hasMovableSelection;
if (moveBottomBtn) moveBottomBtn.disabled = !hasMovableSelection;
+ const retryFailedBtn = document.getElementById('retryFailedBtn');
+ if (retryFailedBtn) retryFailedBtn.disabled = startBlocked || !queueJobs.some(job => job.status === 'error');
+ syncAutomationContextStartControls(startBlocked);
+ renderAutomationStatusSnapshot(snapshot);
syncDataActionState();
}
@@ -3628,6 +3996,10 @@ document.getElementById('contextMenu').addEventListener('click', (e) => {
async function handleContextAction(action, targetJobId = null) {
_normalizeQueueSelectionToVisible();
+ if (['start-selected', 'retry-selected'].includes(action)) {
+ const snapshot = createAutomationStatusSnapshot();
+ if (snapshot.paused === true || snapshot.statusAvailable !== true) return;
+ }
if (action === 'start-selected') {
startSelectedUpload();
} else if (action === 'copy-links') {
@@ -3824,10 +4196,7 @@ function analyzeAddJobsInput(jobs, relevantJobs = jobs) {
function prepareAddSourceCleanup(analysis) {
const rollbackStates = captureSourceCleanupStates();
if (!config.globalSettings?.deleteSourceAfterSuccessfulUpload || !window.SourceCleanupPolicy) return { groups: [], rollbackStates };
- const cleanupJobs = analysis.confirmableJobs.map(job => ({
- ...job,
- sourceCleanupRequiredHosters: []
- }));
+ const cleanupJobs = analysis.confirmableJobs.map(job => ({ ...job }));
const cleanupJobsById = new Map(cleanupJobs.map(job => [job.id, job]));
const prepared = window.SourceCleanupPolicy.prepareGroups(
cleanupJobs,
@@ -3892,7 +4261,7 @@ function resolveAddJobsOutcome(jobs, result, analysis = null) {
const added = Number(result?.added);
const confirmationConsistent = valid && Number.isInteger(added) && added >= 0 && added === remainingJobs.length;
return {
- consistent: unconfirmableJobs.length === 0 && confirmationConsistent,
+ consistent: confirmationConsistent,
added: Number.isInteger(added) && added >= 0 ? added : 0,
addedJobs: confirmationConsistent ? remainingJobs : [],
alreadyJobs: [...alreadyIds].map(id => jobsById.get(id)),
@@ -3919,6 +4288,11 @@ function applyAddJobsOutcome(jobs, result, options = {}) {
return outcome;
}
+function applyAddJobsFingerprints(analysis, fingerprints) {
+ if (!fingerprints || !window.SourceCleanupPolicy) return [];
+ return window.SourceCleanupPolicy.applyFingerprints(analysis?.confirmableJobs || [], fingerprints);
+}
+
async function startUpload(opts) {
if (uploading) return { ok: false, error: 'Upload läuft bereits.' };
if (await isAutomationPaused()) return { ok: false, error: 'Automatik ist pausiert' };
@@ -4086,9 +4460,7 @@ async function startSelectedUpload(explicitJobs) {
showCopyToast(error);
return { ok: false, error };
}
- if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) {
- window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
- }
+ applyAddJobsFingerprints(addRequest, result?.sourceCleanupFingerprints);
persistQueueStateSoon();
const added = outcome.added;
// Use ASCII-only toast text here to avoid encoding artifacts on some systems.
@@ -4804,13 +5176,6 @@ async function abortSelectedJobs() {
persistQueueStateSoon(true);
}
-async function finishUploadsInProgress() {
- if (!uploading) return;
- await window.api.finishAfterActive();
- lastUploadStats.state = 'stopping';
- updateStatusBar();
-}
-
async function abortAllUploads() {
if (!uploading) return;
if (!await showAppConfirm({ title: 'Alle Uploads abbrechen?', message: 'Alle laufenden Uploads werden abgebrochen und in die Warteschlange zurückgesetzt.', confirmText: 'Alle abbrechen', danger: true })) return;
@@ -5563,6 +5928,7 @@ function renderSettings() {
const globalSettings = config.globalSettings || {};
const configuredAccounts = getAvailableHosters();
const fm = globalSettings.folderMonitor || {};
+ const normalizedFm = window.AutomationControl.normalizeAutomationSettings(fm);
const remoteSettings = globalSettings.remote || {};
const pageDefinitions = [
@@ -5715,6 +6081,33 @@ function renderSettings() {
pages.automatik.innerHTML = `
${pageHeader('Automatik', 'Wiederholungen und überwachte Ordner für unbeaufsichtigte Uploads.')}
+
+
+
+
Überwachung läuft seitNie
+
Ordner erreichbar—
+
Letzte erkannte DateiKeine Datei erkannt
+
Heute erkannt0
+
Heute eingereiht0
+
Heute übersprungen0
+
Wegen Queue-Limit zurückgestellt0
+
Letzter AbgleichNie
+
Nächster AbgleichNie
+
+
+ Letzter Fehler
+
+
+
Unbeaufsichtigter Betrieb
@@ -5730,12 +6123,30 @@ function renderSettings() {
Minuten · jede weitere Runde wartet entsprechend länger
- Ordnerüberwachung ${fm.enabled && fm.folderPath ? 'Aktiv' : 'Inaktiv'}
+ Ordnerüberwachung
+
+
+
+ 0 = unbegrenzt
+
+
+
+
+
+
+
+ Ordnerüberwachung testen
+ Prüft den aktuellen Ordner schreibgeschützt mit denselben Regeln.
+
+
+