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 = ` + `; + 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.')} +
+
+ Inaktiv +
+ Aktuelle Queue-Auslastung + 0 / 15.000 +
+ +
+
+
+
+
Ü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
+
+ +
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. +
+ +