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.
This commit is contained in:
+457
-46
@@ -38,6 +38,8 @@ function refreshLocalizedRuntimeUi() {
|
|||||||
const historyContainer = document.getElementById('historyContainer');
|
const historyContainer = document.getElementById('historyContainer');
|
||||||
if (historyContainer && historyRowsData.length) renderHistoryTable(historyContainer);
|
if (historyContainer && historyRowsData.length) renderHistoryTable(historyContainer);
|
||||||
updateStatusBar();
|
updateStatusBar();
|
||||||
|
refreshAutomationControlCenter();
|
||||||
|
refreshAutomationTestOverlay();
|
||||||
const activeRecentTab = document.querySelector('.recent-tab.active');
|
const activeRecentTab = document.querySelector('.recent-tab.active');
|
||||||
const hint = document.getElementById('recentFilesHint');
|
const hint = document.getElementById('recentFilesHint');
|
||||||
if (hint && activeRecentTab) hint.textContent = localizeUiText(activeRecentTab.dataset.panel === 'statsTab' ? 'Upload-Statistiken' : 'Zuletzt erzeugte Upload-Links');
|
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 uploading = false;
|
||||||
let healthCheckRunning = false;
|
let healthCheckRunning = false;
|
||||||
let automationRuntimeStatus = Object.freeze({});
|
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 managedOnlineBackups = [];
|
||||||
let managedOnlineBackupsAuthoritative = false;
|
let managedOnlineBackupsAuthoritative = false;
|
||||||
let managedOnlineBackupMutationGeneration = 0;
|
let managedOnlineBackupMutationGeneration = 0;
|
||||||
@@ -472,7 +481,25 @@ function automationSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyAutomationRuntimeStatus(value) {
|
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;
|
return automationRuntimeStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,6 +523,12 @@ function createAutomationStatusSnapshot() {
|
|||||||
const currentJobCount = window.AutomationControl.countAutomaticQueueJobs(queueJobs);
|
const currentJobCount = window.AutomationControl.countAutomaticQueueJobs(queueJobs);
|
||||||
const availableSlots = normalized.queueLimitJobs === 0 ? null : Math.max(0, normalized.queueLimitJobs - currentJobCount);
|
const availableSlots = normalized.queueLimitJobs === 0 ? null : Math.max(0, normalized.queueLimitJobs - currentJobCount);
|
||||||
const telemetry = window.AutomationControl.rollDailyTelemetry(folderSettings.telemetry);
|
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 = {
|
const snapshot = {
|
||||||
...automationRuntimeStatus,
|
...automationRuntimeStatus,
|
||||||
enabled: folderSettings.enabled === true,
|
enabled: folderSettings.enabled === true,
|
||||||
@@ -506,7 +539,13 @@ function createAutomationStatusSnapshot() {
|
|||||||
currentJobCount,
|
currentJobCount,
|
||||||
availableSlots,
|
availableSlots,
|
||||||
queueLimited: normalized.queueLimitJobs !== 0 && availableSlots === 0,
|
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);
|
snapshot.state = window.AutomationControl.deriveAutomationState(snapshot);
|
||||||
return freezeAutomationValue(snapshot);
|
return freezeAutomationValue(snapshot);
|
||||||
@@ -741,7 +780,7 @@ async function applyAutomationEvaluation(evaluation) {
|
|||||||
persistQueueStateSoon(true);
|
persistQueueStateSoon(true);
|
||||||
return freezeAutomationValue({ ok: false, error: 'Jobs konnten nicht eindeutig bestätigt werden.', warning: null, admittedFiles: [], deferredFiles, paused: false, dryRun: false });
|
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 {
|
} catch {
|
||||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||||
return freezeAutomationValue({ ok: false, error: 'Jobs konnten nicht hinzugefügt werden.', warning: null, admittedFiles: [], deferredFiles, paused: false, dryRun: false });
|
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() {
|
async function runFolderMonitorTestScan() {
|
||||||
const result = await window.api.folderMonitorTestScan();
|
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' });
|
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 = `
|
||||||
|
<section class="modal-card automation-test-modal" role="dialog" aria-modal="true" aria-labelledby="automationTestTitle" aria-describedby="automationTestDescription" tabindex="-1">
|
||||||
|
<header class="modal-header automation-test-header">
|
||||||
|
<div>
|
||||||
|
<h3 id="automationTestTitle">Test der Ordnerüberwachung</h3>
|
||||||
|
<p id="automationTestDescription">Der Test verändert weder Queue noch Einstellungen.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="modal-body automation-test-body">
|
||||||
|
<div class="automation-test-loading" id="automationTestSpinner" hidden>
|
||||||
|
<span class="automation-test-spinner" aria-hidden="true"></span>
|
||||||
|
<span>Ordner wird geprüft…</span>
|
||||||
|
</div>
|
||||||
|
<div class="automation-test-metrics" id="automationTestMetrics">
|
||||||
|
${automationTestMetricDefinitions.map(([key, label]) => `
|
||||||
|
<div class="automation-test-metric" data-automation-test-metric="${key}">
|
||||||
|
<span class="automation-test-label">${label}</span>
|
||||||
|
<strong class="automation-test-value">0</strong>
|
||||||
|
</div>`).join('')}
|
||||||
|
</div>
|
||||||
|
<p class="automation-test-error" id="automationTestError" role="alert" hidden></p>
|
||||||
|
</div>
|
||||||
|
<footer class="modal-footer automation-test-footer">
|
||||||
|
<button class="btn btn-secondary" id="automationTestCloseBtn" type="button">Schließen</button>
|
||||||
|
</footer>
|
||||||
|
</section>`;
|
||||||
|
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.evaluateAutomationCandidates = evaluateAutomationCandidates;
|
||||||
window.applyAutomationEvaluation = applyAutomationEvaluation;
|
window.applyAutomationEvaluation = applyAutomationEvaluation;
|
||||||
window.createAutomationStatusSnapshot = createAutomationStatusSnapshot;
|
window.createAutomationStatusSnapshot = createAutomationStatusSnapshot;
|
||||||
@@ -900,7 +1261,7 @@ async function init() {
|
|||||||
if (typeof window.api.onAutomationStatus === 'function') {
|
if (typeof window.api.onAutomationStatus === 'function') {
|
||||||
window.api.onAutomationStatus(status => {
|
window.api.onAutomationStatus(status => {
|
||||||
applyAutomationRuntimeStatus(status);
|
applyAutomationRuntimeStatus(status);
|
||||||
updateQueueActionButtons();
|
refreshAutomationControlCenter();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1543,7 +1904,7 @@ async function applyHosterSelection() {
|
|||||||
cleanupStates: cleanupPreparation.rollbackStates
|
cleanupStates: cleanupPreparation.rollbackStates
|
||||||
});
|
});
|
||||||
if (!outcome.consistent) regularInjectionFailure = 'Jobs konnten nicht eindeutig bestätigt werden.';
|
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 {
|
} catch {
|
||||||
regularInjectionFailure = 'Jobs konnten nicht hinzugefügt werden.';
|
regularInjectionFailure = 'Jobs konnten nicht hinzugefügt werden.';
|
||||||
@@ -2038,19 +2399,21 @@ function updateUploadView(options = {}) {
|
|||||||
updateQueueActionButtons();
|
updateQueueActionButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStartButton() {
|
function updateStartButton(snapshot = createAutomationStatusSnapshot()) {
|
||||||
const btn = document.getElementById('startUploadBtn');
|
const btn = document.getElementById('startUploadBtn');
|
||||||
|
if (!btn) return;
|
||||||
const hosters = getSelectedHosters();
|
const hosters = getSelectedHosters();
|
||||||
const hasQueuedJobs = queueJobs.some(isStartableQueueJob);
|
const hasQueuedJobs = queueJobs.some(isStartableQueueJob);
|
||||||
const canBuildQueueFromSelection = selectedFiles.length > 0 && hosters.length > 0;
|
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 _UPLOAD_SELECTION_STATUSES = new Set(['done', 'error', 'aborted', 'skipped']);
|
||||||
const _ABORT_SELECTION_STATUSES = new Set(['preview', 'queued', 'getting-server', 'uploading', 'retrying']);
|
const _ABORT_SELECTION_STATUSES = new Set(['preview', 'queued', 'getting-server', 'uploading', 'retrying']);
|
||||||
|
|
||||||
function updateQueueActionButtons() {
|
function updateQueueActionButtons(snapshot = createAutomationStatusSnapshot()) {
|
||||||
updateStartButton();
|
updateStartButton(snapshot);
|
||||||
_normalizeQueueSelectionToVisible();
|
_normalizeQueueSelectionToVisible();
|
||||||
|
|
||||||
const hasSelection = selectedJobIds.size > 0;
|
const hasSelection = selectedJobIds.size > 0;
|
||||||
@@ -2072,22 +2435,27 @@ function updateQueueActionButtons() {
|
|||||||
const startSelectedBtn = document.getElementById('startSelectedBtn');
|
const startSelectedBtn = document.getElementById('startSelectedBtn');
|
||||||
const reuploadBtn = document.getElementById('reuploadSelectedBtn');
|
const reuploadBtn = document.getElementById('reuploadSelectedBtn');
|
||||||
const abortSelectedBtn = document.getElementById('abortSelectedBtn');
|
const abortSelectedBtn = document.getElementById('abortSelectedBtn');
|
||||||
const finishStopBtn = document.getElementById('finishStopBtn');
|
ensureAutomationPauseResumeButton();
|
||||||
const abortAllBtn = document.getElementById('abortAllBtn');
|
const abortAllBtn = document.getElementById('abortAllBtn');
|
||||||
const moveTopBtn = document.getElementById('moveTopBtn');
|
const moveTopBtn = document.getElementById('moveTopBtn');
|
||||||
const moveUpBtn = document.getElementById('moveUpBtn');
|
const moveUpBtn = document.getElementById('moveUpBtn');
|
||||||
const moveDownBtn = document.getElementById('moveDownBtn');
|
const moveDownBtn = document.getElementById('moveDownBtn');
|
||||||
const moveBottomBtn = document.getElementById('moveBottomBtn');
|
const moveBottomBtn = document.getElementById('moveBottomBtn');
|
||||||
|
|
||||||
if (startSelectedBtn) startSelectedBtn.disabled = uploading || !hasStartableSelection;
|
const startBlocked = snapshot.paused === true || snapshot.statusAvailable !== true;
|
||||||
if (reuploadBtn) reuploadBtn.disabled = !hasUploadSelection;
|
if (startSelectedBtn) startSelectedBtn.disabled = startBlocked || uploading || !hasStartableSelection;
|
||||||
|
if (reuploadBtn) reuploadBtn.disabled = startBlocked || !hasUploadSelection;
|
||||||
if (abortSelectedBtn) abortSelectedBtn.disabled = !hasAbortSelection;
|
if (abortSelectedBtn) abortSelectedBtn.disabled = !hasAbortSelection;
|
||||||
if (finishStopBtn) finishStopBtn.disabled = !uploading;
|
syncAutomationPauseResumeButton(snapshot);
|
||||||
if (abortAllBtn) abortAllBtn.disabled = !uploading;
|
if (abortAllBtn) abortAllBtn.disabled = !uploading;
|
||||||
if (moveTopBtn) moveTopBtn.disabled = !hasMovableSelection;
|
if (moveTopBtn) moveTopBtn.disabled = !hasMovableSelection;
|
||||||
if (moveUpBtn) moveUpBtn.disabled = !hasMovableSelection;
|
if (moveUpBtn) moveUpBtn.disabled = !hasMovableSelection;
|
||||||
if (moveDownBtn) moveDownBtn.disabled = !hasMovableSelection;
|
if (moveDownBtn) moveDownBtn.disabled = !hasMovableSelection;
|
||||||
if (moveBottomBtn) moveBottomBtn.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();
|
syncDataActionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3628,6 +3996,10 @@ document.getElementById('contextMenu').addEventListener('click', (e) => {
|
|||||||
|
|
||||||
async function handleContextAction(action, targetJobId = null) {
|
async function handleContextAction(action, targetJobId = null) {
|
||||||
_normalizeQueueSelectionToVisible();
|
_normalizeQueueSelectionToVisible();
|
||||||
|
if (['start-selected', 'retry-selected'].includes(action)) {
|
||||||
|
const snapshot = createAutomationStatusSnapshot();
|
||||||
|
if (snapshot.paused === true || snapshot.statusAvailable !== true) return;
|
||||||
|
}
|
||||||
if (action === 'start-selected') {
|
if (action === 'start-selected') {
|
||||||
startSelectedUpload();
|
startSelectedUpload();
|
||||||
} else if (action === 'copy-links') {
|
} else if (action === 'copy-links') {
|
||||||
@@ -3824,10 +4196,7 @@ function analyzeAddJobsInput(jobs, relevantJobs = jobs) {
|
|||||||
function prepareAddSourceCleanup(analysis) {
|
function prepareAddSourceCleanup(analysis) {
|
||||||
const rollbackStates = captureSourceCleanupStates();
|
const rollbackStates = captureSourceCleanupStates();
|
||||||
if (!config.globalSettings?.deleteSourceAfterSuccessfulUpload || !window.SourceCleanupPolicy) return { groups: [], rollbackStates };
|
if (!config.globalSettings?.deleteSourceAfterSuccessfulUpload || !window.SourceCleanupPolicy) return { groups: [], rollbackStates };
|
||||||
const cleanupJobs = analysis.confirmableJobs.map(job => ({
|
const cleanupJobs = analysis.confirmableJobs.map(job => ({ ...job }));
|
||||||
...job,
|
|
||||||
sourceCleanupRequiredHosters: []
|
|
||||||
}));
|
|
||||||
const cleanupJobsById = new Map(cleanupJobs.map(job => [job.id, job]));
|
const cleanupJobsById = new Map(cleanupJobs.map(job => [job.id, job]));
|
||||||
const prepared = window.SourceCleanupPolicy.prepareGroups(
|
const prepared = window.SourceCleanupPolicy.prepareGroups(
|
||||||
cleanupJobs,
|
cleanupJobs,
|
||||||
@@ -3892,7 +4261,7 @@ function resolveAddJobsOutcome(jobs, result, analysis = null) {
|
|||||||
const added = Number(result?.added);
|
const added = Number(result?.added);
|
||||||
const confirmationConsistent = valid && Number.isInteger(added) && added >= 0 && added === remainingJobs.length;
|
const confirmationConsistent = valid && Number.isInteger(added) && added >= 0 && added === remainingJobs.length;
|
||||||
return {
|
return {
|
||||||
consistent: unconfirmableJobs.length === 0 && confirmationConsistent,
|
consistent: confirmationConsistent,
|
||||||
added: Number.isInteger(added) && added >= 0 ? added : 0,
|
added: Number.isInteger(added) && added >= 0 ? added : 0,
|
||||||
addedJobs: confirmationConsistent ? remainingJobs : [],
|
addedJobs: confirmationConsistent ? remainingJobs : [],
|
||||||
alreadyJobs: [...alreadyIds].map(id => jobsById.get(id)),
|
alreadyJobs: [...alreadyIds].map(id => jobsById.get(id)),
|
||||||
@@ -3919,6 +4288,11 @@ function applyAddJobsOutcome(jobs, result, options = {}) {
|
|||||||
return outcome;
|
return outcome;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyAddJobsFingerprints(analysis, fingerprints) {
|
||||||
|
if (!fingerprints || !window.SourceCleanupPolicy) return [];
|
||||||
|
return window.SourceCleanupPolicy.applyFingerprints(analysis?.confirmableJobs || [], fingerprints);
|
||||||
|
}
|
||||||
|
|
||||||
async function startUpload(opts) {
|
async function startUpload(opts) {
|
||||||
if (uploading) return { ok: false, error: 'Upload läuft bereits.' };
|
if (uploading) return { ok: false, error: 'Upload läuft bereits.' };
|
||||||
if (await isAutomationPaused()) return { ok: false, error: 'Automatik ist pausiert' };
|
if (await isAutomationPaused()) return { ok: false, error: 'Automatik ist pausiert' };
|
||||||
@@ -4086,9 +4460,7 @@ async function startSelectedUpload(explicitJobs) {
|
|||||||
showCopyToast(error);
|
showCopyToast(error);
|
||||||
return { ok: false, error };
|
return { ok: false, error };
|
||||||
}
|
}
|
||||||
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) {
|
applyAddJobsFingerprints(addRequest, result?.sourceCleanupFingerprints);
|
||||||
window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
|
||||||
}
|
|
||||||
persistQueueStateSoon();
|
persistQueueStateSoon();
|
||||||
const added = outcome.added;
|
const added = outcome.added;
|
||||||
// Use ASCII-only toast text here to avoid encoding artifacts on some systems.
|
// Use ASCII-only toast text here to avoid encoding artifacts on some systems.
|
||||||
@@ -4804,13 +5176,6 @@ async function abortSelectedJobs() {
|
|||||||
persistQueueStateSoon(true);
|
persistQueueStateSoon(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function finishUploadsInProgress() {
|
|
||||||
if (!uploading) return;
|
|
||||||
await window.api.finishAfterActive();
|
|
||||||
lastUploadStats.state = 'stopping';
|
|
||||||
updateStatusBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function abortAllUploads() {
|
async function abortAllUploads() {
|
||||||
if (!uploading) return;
|
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;
|
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 globalSettings = config.globalSettings || {};
|
||||||
const configuredAccounts = getAvailableHosters();
|
const configuredAccounts = getAvailableHosters();
|
||||||
const fm = globalSettings.folderMonitor || {};
|
const fm = globalSettings.folderMonitor || {};
|
||||||
|
const normalizedFm = window.AutomationControl.normalizeAutomationSettings(fm);
|
||||||
const remoteSettings = globalSettings.remote || {};
|
const remoteSettings = globalSettings.remote || {};
|
||||||
|
|
||||||
const pageDefinitions = [
|
const pageDefinitions = [
|
||||||
@@ -5715,6 +6081,33 @@ function renderSettings() {
|
|||||||
|
|
||||||
pages.automatik.innerHTML = `
|
pages.automatik.innerHTML = `
|
||||||
${pageHeader('Automatik', 'Wiederholungen und überwachte Ordner für unbeaufsichtigte Uploads.')}
|
${pageHeader('Automatik', 'Wiederholungen und überwachte Ordner für unbeaufsichtigte Uploads.')}
|
||||||
|
<section class="automation-status-card" id="automationStatusCard" aria-live="polite">
|
||||||
|
<div class="automation-status-header">
|
||||||
|
<span class="automation-state-badge state-inactive" id="automationStateBadge">Inaktiv</span>
|
||||||
|
<div class="automation-queue-summary">
|
||||||
|
<span class="automation-queue-label">Aktuelle Queue-Auslastung</span>
|
||||||
|
<strong class="automation-status-value automation-queue-value" id="automationQueueMeter">0 / 15.000</strong>
|
||||||
|
<div class="automation-queue-track" id="automationQueueMeterTrack" role="progressbar" aria-label="Aktuelle Queue-Auslastung" aria-valuemin="0" aria-valuenow="0" aria-valuemax="15000">
|
||||||
|
<span class="automation-queue-bar" id="automationQueueMeterBar"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="automation-status-metrics">
|
||||||
|
<div class="automation-status-metric"><span>Überwachung läuft seit</span><strong class="automation-status-value" id="automationMonitoringSince">Nie</strong></div>
|
||||||
|
<div class="automation-status-metric"><span>Ordner erreichbar</span><strong class="automation-status-value" id="automationFolderReachable">—</strong></div>
|
||||||
|
<div class="automation-status-metric"><span>Letzte erkannte Datei</span><strong class="automation-status-value" id="automationLastDetectedFile">Keine Datei erkannt</strong></div>
|
||||||
|
<div class="automation-status-metric"><span>Heute erkannt</span><strong class="automation-status-value" id="automationDetectedToday">0</strong></div>
|
||||||
|
<div class="automation-status-metric"><span>Heute eingereiht</span><strong class="automation-status-value" id="automationQueuedToday">0</strong></div>
|
||||||
|
<div class="automation-status-metric"><span>Heute übersprungen</span><strong class="automation-status-value" id="automationSkippedToday">0</strong></div>
|
||||||
|
<div class="automation-status-metric"><span>Wegen Queue-Limit zurückgestellt</span><strong class="automation-status-value" id="automationDeferredToday">0</strong></div>
|
||||||
|
<div class="automation-status-metric"><span>Letzter Abgleich</span><strong class="automation-status-value" id="automationLastReconcile">Nie</strong></div>
|
||||||
|
<div class="automation-status-metric"><span>Nächster Abgleich</span><strong class="automation-status-value" id="automationNextReconcile">Nie</strong></div>
|
||||||
|
</div>
|
||||||
|
<div class="automation-status-error" id="automationLastErrorRow" hidden>
|
||||||
|
<span>Letzter Fehler</span>
|
||||||
|
<strong id="automationLastError"></strong>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<div class="settings-section-label">Unbeaufsichtigter Betrieb</div>
|
<div class="settings-section-label">Unbeaufsichtigter Betrieb</div>
|
||||||
<div class="settings-row automation-retry-row">
|
<div class="settings-row automation-retry-row">
|
||||||
<label for="autoRetryRoundsInput">Automatische Wiederholungsrunden</label>
|
<label for="autoRetryRoundsInput">Automatische Wiederholungsrunden</label>
|
||||||
@@ -5730,12 +6123,30 @@ function renderSettings() {
|
|||||||
<span class="hint">Minuten · jede weitere Runde wartet entsprechend länger</span>
|
<span class="hint">Minuten · jede weitere Runde wartet entsprechend länger</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-section-label">Ordnerüberwachung <span class="panel-status${fm.enabled && fm.folderPath ? ' active' : ''}" id="folderMonitorStatusBadge">${fm.enabled && fm.folderPath ? 'Aktiv' : 'Inaktiv'}</span></div>
|
<div class="settings-section-label">Ordnerüberwachung</div>
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label>Ordnerpfad</label>
|
<label>Ordnerpfad</label>
|
||||||
<input type="text" class="key-input settings-autosave" id="fmFolderPathInput" value="${escapeAttr(fm.folderPath || '')}" placeholder="Ordner wählen..." style="flex:1">
|
<input type="text" class="key-input settings-autosave" id="fmFolderPathInput" value="${escapeAttr(fm.folderPath || '')}" placeholder="Ordner wählen..." style="flex:1">
|
||||||
<button class="btn btn-xs btn-secondary" id="fmChooseFolderBtn">Wählen</button>
|
<button class="btn btn-xs btn-secondary" id="fmChooseFolderBtn">Wählen</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-row automation-capacity-row">
|
||||||
|
<label for="fmQueueLimitInput">Maximale automatische Queue-Größe</label>
|
||||||
|
<input type="number" class="hs-input settings-autosave" id="fmQueueLimitInput" value="${normalizedFm.queueLimitJobs}" min="0" step="1">
|
||||||
|
<span class="hint">0 = unbegrenzt</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row automation-interval-row">
|
||||||
|
<label for="fmReconcileIntervalInput">Abgleichintervall</label>
|
||||||
|
<select class="hs-input settings-autosave" id="fmReconcileIntervalInput">
|
||||||
|
${[1, 5, 15, 30, 60].map(value => `<option value="${value}" ${normalizedFm.reconcileIntervalMinutes === value ? 'selected' : ''}>${value === 1 ? '1 Minute' : `${value} Minuten`}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row automation-test-action-row">
|
||||||
|
<div class="automation-test-action-copy">
|
||||||
|
<strong>Ordnerüberwachung testen</strong>
|
||||||
|
<span class="hint">Prüft den aktuellen Ordner schreibgeschützt mit denselben Regeln.</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" id="automationTestBtn" type="button">Ordnerüberwachung testen</button>
|
||||||
|
</div>
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label>Dateierweiterungen</label>
|
<label>Dateierweiterungen</label>
|
||||||
<select class="hs-input settings-autosave" id="fmFilterModeInput" style="width:auto;margin-right:6px">
|
<select class="hs-input settings-autosave" id="fmFilterModeInput" style="width:auto;margin-right:6px">
|
||||||
@@ -6240,25 +6651,16 @@ function renderSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFmBadge = () => {
|
|
||||||
const b = document.getElementById('folderMonitorStatusBadge');
|
|
||||||
if (!b) return;
|
|
||||||
const enabled = document.getElementById('fmEnabledInput')?.checked;
|
|
||||||
const hasPath = (document.getElementById('fmFolderPathInput')?.value || '').trim();
|
|
||||||
if (enabled && hasPath) { b.textContent = 'Aktiv'; b.className = 'panel-status active'; }
|
|
||||||
else { b.textContent = 'Inaktiv'; b.className = 'panel-status'; }
|
|
||||||
};
|
|
||||||
document.getElementById('fmEnabledInput')?.addEventListener('change', updateFmBadge);
|
|
||||||
document.getElementById('fmFolderPathInput')?.addEventListener('input', updateFmBadge);
|
|
||||||
|
|
||||||
document.getElementById('fmChooseFolderBtn')?.addEventListener('click', async () => {
|
document.getElementById('fmChooseFolderBtn')?.addEventListener('click', async () => {
|
||||||
const folder = await window.api.folderMonitorSelectFolder();
|
const folder = await window.api.folderMonitorSelectFolder();
|
||||||
if (folder) {
|
if (folder) {
|
||||||
document.getElementById('fmFolderPathInput').value = folder;
|
document.getElementById('fmFolderPathInput').value = folder;
|
||||||
updateFmBadge();
|
|
||||||
scheduleSettingsSave();
|
scheduleSettingsSave();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
document.getElementById('automationTestBtn')?.addEventListener('click', runAutomationTestOverlay);
|
||||||
|
ensureAutomationTestOverlay();
|
||||||
|
refreshAutomationControlCenter();
|
||||||
|
|
||||||
document.getElementById('remoteCopyTokenBtn').addEventListener('click', async () => {
|
document.getElementById('remoteCopyTokenBtn').addEventListener('click', async () => {
|
||||||
const token = document.getElementById('remoteTokenInput').value;
|
const token = document.getElementById('remoteTokenInput').value;
|
||||||
@@ -6549,6 +6951,11 @@ async function performSaveSettings(options = {}) {
|
|||||||
const n = parseInt(el.value || String(dflt), 10) || dflt;
|
const n = parseInt(el.value || String(dflt), 10) || dflt;
|
||||||
return Math.max(lo, Math.min(hi, n));
|
return Math.max(lo, Math.min(hi, n));
|
||||||
};
|
};
|
||||||
|
const normalizedAutomationInputs = window.AutomationControl.normalizeAutomationSettings({
|
||||||
|
...curFm,
|
||||||
|
queueLimitJobs: document.getElementById('fmQueueLimitInput')?.value ?? curFm.queueLimitJobs,
|
||||||
|
reconcileIntervalMinutes: document.getElementById('fmReconcileIntervalInput')?.value ?? curFm.reconcileIntervalMinutes
|
||||||
|
});
|
||||||
|
|
||||||
const globalSettings = {
|
const globalSettings = {
|
||||||
...cur,
|
...cur,
|
||||||
@@ -6591,6 +6998,8 @@ async function performSaveSettings(options = {}) {
|
|||||||
skipDuplicates: elChk('fmSkipDuplicatesInput', curFm.skipDuplicates !== false),
|
skipDuplicates: elChk('fmSkipDuplicatesInput', curFm.skipDuplicates !== false),
|
||||||
delaySec: elInt('fmDelaySecInput', curFm.delaySec ?? 3, 3, 1, 300),
|
delaySec: elInt('fmDelaySecInput', curFm.delaySec ?? 3, 3, 1, 300),
|
||||||
autoStart: elChk('fmAutoStartInput', curFm.autoStart !== false),
|
autoStart: elChk('fmAutoStartInput', curFm.autoStart !== false),
|
||||||
|
queueLimitJobs: normalizedAutomationInputs.queueLimitJobs,
|
||||||
|
reconcileIntervalMinutes: normalizedAutomationInputs.reconcileIntervalMinutes,
|
||||||
hosters: document.querySelector('.fm-hoster-checkbox')
|
hosters: document.querySelector('.fm-hoster-checkbox')
|
||||||
? Array.from(document.querySelectorAll('.fm-hoster-checkbox:checked')).map(el => el.dataset.fmHoster)
|
? Array.from(document.querySelectorAll('.fm-hoster-checkbox:checked')).map(el => el.dataset.fmHoster)
|
||||||
: (curFm.hosters || [])
|
: (curFm.hosters || [])
|
||||||
@@ -6658,8 +7067,8 @@ async function performSaveSettings(options = {}) {
|
|||||||
|
|
||||||
// Start/stop folder monitor based on settings
|
// Start/stop folder monitor based on settings
|
||||||
const fmSettings = globalSettings.folderMonitor;
|
const fmSettings = globalSettings.folderMonitor;
|
||||||
const badge = document.getElementById('folderMonitorStatusBadge');
|
|
||||||
if (fmSettings && fmSettings.enabled && fmSettings.folderPath) {
|
if (fmSettings && fmSettings.enabled && fmSettings.folderPath) {
|
||||||
|
if (fmSettings.paused !== true) {
|
||||||
try {
|
try {
|
||||||
const folderStart = await window.api.folderMonitorStart(fmSettings);
|
const folderStart = await window.api.folderMonitorStart(fmSettings);
|
||||||
if (folderStart?.includesExisting) {
|
if (folderStart?.includesExisting) {
|
||||||
@@ -6669,14 +7078,15 @@ async function performSaveSettings(options = {}) {
|
|||||||
const includeExistingInput = document.getElementById('fmIncludeExistingInput');
|
const includeExistingInput = document.getElementById('fmIncludeExistingInput');
|
||||||
if (includeExistingInput) includeExistingInput.checked = false;
|
if (includeExistingInput) includeExistingInput.checked = false;
|
||||||
}
|
}
|
||||||
if (badge) { badge.textContent = 'Aktiv'; badge.className = 'panel-status active'; }
|
|
||||||
} catch {
|
} catch {
|
||||||
if (badge) { badge.textContent = 'Fehler'; badge.className = 'panel-status'; }
|
applyAutomationRuntimeStatus({ ...automationRuntimeStatus, error: 'Ordnerüberwachung fehlgeschlagen' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await window.api.folderMonitorStop();
|
await window.api.folderMonitorStop();
|
||||||
if (badge) { badge.textContent = 'Inaktiv'; badge.className = 'panel-status'; }
|
|
||||||
}
|
}
|
||||||
|
try { await refreshAutomationRuntimeStatus(); } catch {}
|
||||||
|
refreshAutomationControlCenter();
|
||||||
|
|
||||||
// Start/stop remote server based on settings
|
// Start/stop remote server based on settings
|
||||||
const remoteSettings = globalSettings.remote;
|
const remoteSettings = globalSettings.remote;
|
||||||
@@ -8394,6 +8804,7 @@ function prepareForWindowClose(attempt) {
|
|||||||
|
|
||||||
// --- Setup Listeners ---
|
// --- Setup Listeners ---
|
||||||
function setupListeners() {
|
function setupListeners() {
|
||||||
|
ensureAutomationPauseResumeButton();
|
||||||
try { initMenuBar(); } catch (err) { console.error('menu bar init failed', err); }
|
try { initMenuBar(); } catch (err) { console.error('menu bar init failed', err); }
|
||||||
document.querySelectorAll('[data-upload-sidebar-target]').forEach(button => {
|
document.querySelectorAll('[data-upload-sidebar-target]').forEach(button => {
|
||||||
button.addEventListener('click', () => setUploadSidebarFilter(button.dataset.uploadSidebarTarget));
|
button.addEventListener('click', () => setUploadSidebarFilter(button.dataset.uploadSidebarTarget));
|
||||||
@@ -8480,7 +8891,7 @@ function setupListeners() {
|
|||||||
});
|
});
|
||||||
document.getElementById('reuploadSelectedBtn').addEventListener('click', retrySelectedJobs);
|
document.getElementById('reuploadSelectedBtn').addEventListener('click', retrySelectedJobs);
|
||||||
document.getElementById('abortSelectedBtn').addEventListener('click', abortSelectedJobs);
|
document.getElementById('abortSelectedBtn').addEventListener('click', abortSelectedJobs);
|
||||||
document.getElementById('finishStopBtn').addEventListener('click', finishUploadsInProgress);
|
document.getElementById('automationPauseResumeBtn').addEventListener('click', toggleAutomationPauseResume);
|
||||||
document.getElementById('abortAllBtn').addEventListener('click', abortAllUploads);
|
document.getElementById('abortAllBtn').addEventListener('click', abortAllUploads);
|
||||||
document.getElementById('moveTopBtn').addEventListener('click', () => moveSelectedJobs('top'));
|
document.getElementById('moveTopBtn').addEventListener('click', () => moveSelectedJobs('top'));
|
||||||
document.getElementById('moveUpBtn').addEventListener('click', () => moveSelectedJobs('up'));
|
document.getElementById('moveUpBtn').addEventListener('click', () => moveSelectedJobs('up'));
|
||||||
|
|||||||
@@ -250,6 +250,61 @@
|
|||||||
['Wartezeit zwischen Runden', 'Delay between rounds'],
|
['Wartezeit zwischen Runden', 'Delay between rounds'],
|
||||||
['Minuten · jede weitere Runde wartet entsprechend länger', 'Minutes · each additional round waits proportionally longer'],
|
['Minuten · jede weitere Runde wartet entsprechend länger', 'Minutes · each additional round waits proportionally longer'],
|
||||||
['Ordnerüberwachung', 'Folder monitoring'],
|
['Ordnerüberwachung', 'Folder monitoring'],
|
||||||
|
['Ordnerüberwachung testen', 'Test folder monitoring'],
|
||||||
|
['Test der Ordnerüberwachung', 'Folder monitoring test'],
|
||||||
|
['Ordner wird geprüft…', 'Scanning folder…'],
|
||||||
|
['Der Test verändert weder Queue noch Einstellungen.', 'The test does not change the queue or settings.'],
|
||||||
|
['Prüft den aktuellen Ordner schreibgeschützt mit denselben Regeln.', 'Checks the current folder read-only with the same rules.'],
|
||||||
|
['Ordnerüberwachung konnte nicht getestet werden.', 'Folder monitoring could not be tested.'],
|
||||||
|
['Maximale automatische Queue-Größe', 'Maximum automatic queue size'],
|
||||||
|
['Abgleichintervall', 'Reconciliation interval'],
|
||||||
|
['Abschließen und pausieren', 'Finish and pause'],
|
||||||
|
['Fortsetzen', 'Resume'],
|
||||||
|
['Pausiert', 'Paused'],
|
||||||
|
['Queue-Limit erreicht', 'Queue limit reached'],
|
||||||
|
['Ordner getrennt', 'Folder disconnected'],
|
||||||
|
['Überwachung läuft seit', 'Monitoring since'],
|
||||||
|
['Ordner erreichbar', 'Folder reachable'],
|
||||||
|
['Letzte erkannte Datei', 'Last detected file'],
|
||||||
|
['Heute erkannt', 'Detected today'],
|
||||||
|
['Heute eingereiht', 'Queued today'],
|
||||||
|
['Heute übersprungen', 'Skipped today'],
|
||||||
|
['Wegen Queue-Limit zurückgestellt', 'Deferred by queue limit'],
|
||||||
|
['Aktuelle Queue-Auslastung', 'Current queue usage'],
|
||||||
|
['Letzter Abgleich', 'Last reconciliation'],
|
||||||
|
['Nächster Abgleich', 'Next reconciliation'],
|
||||||
|
['Letzter Fehler', 'Last error'],
|
||||||
|
['Nie', 'Never'],
|
||||||
|
['Ja', 'Yes'],
|
||||||
|
['Nein', 'No'],
|
||||||
|
['Keine Datei erkannt', 'No file detected'],
|
||||||
|
['Gefundene Dateien', 'Files found'],
|
||||||
|
['Passend zum Dateifilter', 'Matching file filter'],
|
||||||
|
['Bereits verarbeitet', 'Already processed'],
|
||||||
|
['Fehlend, leer oder nicht lesbar', 'Missing, empty, or unreadable'],
|
||||||
|
['Durch Größenlimits ausgeschlossen', 'Excluded by size limits'],
|
||||||
|
['Entstehende Upload-Jobs', 'Resulting upload jobs'],
|
||||||
|
['Verfügbare Jobs bis zum Queue-Limit', 'Available jobs before queue limit'],
|
||||||
|
['Aktuell zurückzustellende Dateien', 'Files currently deferred'],
|
||||||
|
['0 = unbegrenzt', '0 = unlimited'],
|
||||||
|
['Unbegrenzt', 'Unlimited'],
|
||||||
|
['1 Minute', '1 minute'],
|
||||||
|
['5 Minuten', '5 minutes'],
|
||||||
|
['15 Minuten', '15 minutes'],
|
||||||
|
['30 Minuten', '30 minutes'],
|
||||||
|
['60 Minuten', '60 minutes'],
|
||||||
|
['Automatik konnte nicht pausiert werden.', 'Automation could not be paused.'],
|
||||||
|
['Automatik konnte nicht fortgesetzt werden.', 'Automation could not be resumed.'],
|
||||||
|
['Ordnerüberwachung konnte nicht pausiert werden', 'Folder monitoring could not be paused'],
|
||||||
|
['Ordnerüberwachung fehlgeschlagen', 'Folder monitoring failed'],
|
||||||
|
['Ordner nicht erreichbar', 'Folder unavailable'],
|
||||||
|
['Ordnerscan fehlgeschlagen', 'Folder scan failed'],
|
||||||
|
['Keine Ordnerkonfiguration vorhanden', 'No folder configuration is available'],
|
||||||
|
['Jobs konnten nicht eindeutig bestätigt werden.', 'Jobs could not be confirmed unambiguously.'],
|
||||||
|
['Jobs konnten nicht hinzugefügt werden.', 'Jobs could not be added.'],
|
||||||
|
['Automatische Aufnahme konnte nicht abgeschlossen werden.', 'Automatic admission could not be completed.'],
|
||||||
|
['Upload läuft bereits.', 'An upload is already running.'],
|
||||||
|
['Keine startbaren Jobs vorhanden.', 'No startable jobs are available.'],
|
||||||
['Inaktiv', 'Inactive'],
|
['Inaktiv', 'Inactive'],
|
||||||
['Ordnerpfad', 'Folder path'],
|
['Ordnerpfad', 'Folder path'],
|
||||||
['Wählen', 'Choose'],
|
['Wählen', 'Choose'],
|
||||||
|
|||||||
+125
-1
@@ -1258,6 +1258,114 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
|||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
.settings-page-header + .settings-section-label { margin-top: 0; }
|
.settings-page-header + .settings-section-label { margin-top: 0; }
|
||||||
|
.automation-status-card {
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0 0 22px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: linear-gradient(145deg, color-mix(in srgb, var(--bg-raised) 94%, var(--accent) 6%), var(--bg-secondary));
|
||||||
|
box-shadow: 0 14px 32px rgba(0, 0, 0, .14);
|
||||||
|
}
|
||||||
|
.automation-status-card * { min-width: 0; }
|
||||||
|
.automation-status-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(240px, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.automation-state-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 26px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.automation-state-badge.state-inactive { border-color: var(--border-hover); background: rgba(255, 255, 255, .05); color: var(--text-muted); }
|
||||||
|
.automation-state-badge.state-active { border-color: rgba(16, 185, 129, .42); background: rgba(16, 185, 129, .14); color: #50e3a4; }
|
||||||
|
.automation-state-badge.state-paused { border-color: rgba(59, 130, 246, .46); background: rgba(59, 130, 246, .16); color: #7db5ff; }
|
||||||
|
.automation-state-badge.state-queue-limited { border-color: rgba(245, 158, 11, .5); background: rgba(245, 158, 11, .15); color: #fbbf24; }
|
||||||
|
.automation-state-badge.state-disconnected { border-color: rgba(249, 115, 22, .5); background: rgba(249, 115, 22, .15); color: #fb923c; }
|
||||||
|
.automation-state-badge.state-error { border-color: rgba(239, 68, 68, .5); background: rgba(239, 68, 68, .15); color: #f87171; }
|
||||||
|
.automation-queue-summary { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 5px 12px; }
|
||||||
|
.automation-queue-label { color: var(--text-muted); font-size: 11px; font-weight: 600; }
|
||||||
|
.automation-queue-value { text-align: right; white-space: nowrap; }
|
||||||
|
.automation-queue-track {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
height: 7px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border) 78%, transparent);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(0, 0, 0, .28);
|
||||||
|
}
|
||||||
|
.automation-queue-bar { display: block; width: 0; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--success), var(--accent)); transition: width .2s ease; }
|
||||||
|
.automation-status-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 1px;
|
||||||
|
margin-top: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
.automation-status-metric {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: minmax(30px, auto) minmax(22px, auto);
|
||||||
|
align-content: start;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 9px 10px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
.automation-status-metric > span { color: var(--text-dim); font-size: 10px; line-height: 1.35; overflow-wrap: anywhere; }
|
||||||
|
.automation-status-value { color: var(--text); font-size: 12px; font-variant-numeric: tabular-nums; line-height: 1.35; overflow-wrap: anywhere; }
|
||||||
|
.automation-status-error {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(120px, auto) minmax(0, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 9px 10px;
|
||||||
|
border: 1px solid rgba(239, 68, 68, .38);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(239, 68, 68, .1);
|
||||||
|
color: #fca5a5;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.automation-status-error[hidden] { display: none; }
|
||||||
|
.automation-test-action-row { display: grid !important; grid-template-columns: minmax(0, 1fr) auto; }
|
||||||
|
.automation-test-action-copy { display: grid; gap: 4px; }
|
||||||
|
.automation-test-action-copy > strong { color: var(--text); font-size: 12px; }
|
||||||
|
.automation-capacity-row > .hs-input { width: 120px; }
|
||||||
|
.automation-interval-row > .hs-input { min-width: 150px; }
|
||||||
|
.toolbar-btn.automation-pause-resume-btn { width: auto; min-width: 150px; gap: 7px; padding: 3px 9px; color: var(--text); }
|
||||||
|
.automation-pause-resume-label { font-size: 11px; font-weight: 700; white-space: nowrap; }
|
||||||
|
.toolbar-btn.automation-pause-resume-btn.automation-resume { border-color: rgba(16, 185, 129, .48); background: rgba(16, 185, 129, .17); color: #5ee6ac; }
|
||||||
|
.toolbar-btn.automation-pause-resume-btn.automation-resume:hover { border-color: rgba(16, 185, 129, .7); background: rgba(16, 185, 129, .26); }
|
||||||
|
.ctx-item-disabled { opacity: .42; pointer-events: none; }
|
||||||
|
.automation-test-overlay[aria-hidden="true"] { display: none; }
|
||||||
|
.automation-test-modal { width: min(720px, 100%); }
|
||||||
|
.automation-test-header p { margin: 4px 0 0; color: var(--text-muted); font-size: 11px; line-height: 1.45; }
|
||||||
|
.automation-test-body { overflow-y: auto; }
|
||||||
|
.automation-test-loading { min-height: 160px; display: grid; place-items: center; align-content: center; gap: 12px; color: var(--text-muted); font-size: 12px; }
|
||||||
|
.automation-test-loading[hidden] { display: none; }
|
||||||
|
.automation-test-spinner { width: 28px; height: 28px; border: 3px solid rgba(255, 255, 255, .14); border-top-color: var(--accent); border-radius: 50%; animation: automation-test-spin .8s linear infinite; }
|
||||||
|
.automation-test-metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
||||||
|
.automation-test-metrics[hidden] { display: none; }
|
||||||
|
.automation-test-metric { display: grid; grid-template-columns: minmax(0, 1fr) minmax(64px, auto); align-items: center; gap: 12px; min-height: 44px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 7px; background: var(--bg-secondary); }
|
||||||
|
.automation-test-label { color: var(--text-muted); font-size: 11px; line-height: 1.35; overflow-wrap: anywhere; }
|
||||||
|
.automation-test-value { color: var(--text); font-size: 13px; font-variant-numeric: tabular-nums; text-align: right; }
|
||||||
|
.automation-test-error { margin: 12px 0 0; padding: 10px; border: 1px solid rgba(239, 68, 68, .4); border-radius: 7px; background: rgba(239, 68, 68, .1); color: #fca5a5; font-size: 11px; }
|
||||||
|
.automation-test-error[hidden] { display: none; }
|
||||||
|
.automation-test-footer { justify-content: flex-end; }
|
||||||
|
@keyframes automation-test-spin { to { transform: rotate(360deg); } }
|
||||||
.settings-option {
|
.settings-option {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
@@ -4649,9 +4757,25 @@ input[type="checkbox"] {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.automation-status-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.automation-status-header,
|
||||||
|
.automation-test-action-row { grid-template-columns: minmax(0, 1fr); }
|
||||||
|
.automation-state-badge { justify-self: start; white-space: normal; }
|
||||||
|
.automation-status-metrics,
|
||||||
|
.automation-test-metrics { grid-template-columns: minmax(0, 1fr); }
|
||||||
|
.automation-test-action-row .btn { width: 100%; }
|
||||||
|
.toolbar-btn.automation-pause-resume-btn { min-width: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.header-update-button.is-checking .header-action-icon,
|
.header-update-button.is-checking .header-action-icon,
|
||||||
.header-update-button.is-downloading .header-action-icon {
|
.header-update-button.is-downloading .header-action-icon,
|
||||||
|
.automation-test-spinner {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
.automation-queue-bar { transition: none; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,66 @@ test('translates settings search result labels in both directions', () => {
|
|||||||
assert.equal(translateText('Suchergebnisse', 'en'), 'Search results');
|
assert.equal(translateText('Suchergebnisse', 'en'), 'Search results');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('translates every automation control center label in both directions', () => {
|
||||||
|
const pairs = [
|
||||||
|
['Ordnerüberwachung testen', 'Test folder monitoring'],
|
||||||
|
['Maximale automatische Queue-Größe', 'Maximum automatic queue size'],
|
||||||
|
['Abgleichintervall', 'Reconciliation interval'],
|
||||||
|
['Abschließen und pausieren', 'Finish and pause'],
|
||||||
|
['Fortsetzen', 'Resume'],
|
||||||
|
['Queue-Limit erreicht', 'Queue limit reached'],
|
||||||
|
['Ordner getrennt', 'Folder disconnected'],
|
||||||
|
['Pausiert', 'Paused'],
|
||||||
|
['Wegen Queue-Limit zurückgestellt', 'Deferred by queue limit'],
|
||||||
|
['Überwachung läuft seit', 'Monitoring since'],
|
||||||
|
['Ordner erreichbar', 'Folder reachable'],
|
||||||
|
['Letzte erkannte Datei', 'Last detected file'],
|
||||||
|
['Heute erkannt', 'Detected today'],
|
||||||
|
['Heute eingereiht', 'Queued today'],
|
||||||
|
['Heute übersprungen', 'Skipped today'],
|
||||||
|
['Aktuelle Queue-Auslastung', 'Current queue usage'],
|
||||||
|
['Letzter Abgleich', 'Last reconciliation'],
|
||||||
|
['Nächster Abgleich', 'Next reconciliation'],
|
||||||
|
['Letzter Fehler', 'Last error'],
|
||||||
|
['Nie', 'Never'],
|
||||||
|
['Ja', 'Yes'],
|
||||||
|
['Nein', 'No'],
|
||||||
|
['Keine Datei erkannt', 'No file detected'],
|
||||||
|
['Test der Ordnerüberwachung', 'Folder monitoring test'],
|
||||||
|
['Ordner wird geprüft…', 'Scanning folder…'],
|
||||||
|
['Der Test verändert weder Queue noch Einstellungen.', 'The test does not change the queue or settings.'],
|
||||||
|
['Prüft den aktuellen Ordner schreibgeschützt mit denselben Regeln.', 'Checks the current folder read-only with the same rules.'],
|
||||||
|
['Ordnerüberwachung konnte nicht getestet werden.', 'Folder monitoring could not be tested.'],
|
||||||
|
['Gefundene Dateien', 'Files found'],
|
||||||
|
['Passend zum Dateifilter', 'Matching file filter'],
|
||||||
|
['Bereits verarbeitet', 'Already processed'],
|
||||||
|
['Fehlend, leer oder nicht lesbar', 'Missing, empty, or unreadable'],
|
||||||
|
['Durch Größenlimits ausgeschlossen', 'Excluded by size limits'],
|
||||||
|
['Entstehende Upload-Jobs', 'Resulting upload jobs'],
|
||||||
|
['Verfügbare Jobs bis zum Queue-Limit', 'Available jobs before queue limit'],
|
||||||
|
['Aktuell zurückzustellende Dateien', 'Files currently deferred'],
|
||||||
|
['0 = unbegrenzt', '0 = unlimited'],
|
||||||
|
['Unbegrenzt', 'Unlimited'],
|
||||||
|
['1 Minute', '1 minute'],
|
||||||
|
['5 Minuten', '5 minutes'],
|
||||||
|
['15 Minuten', '15 minutes'],
|
||||||
|
['30 Minuten', '30 minutes'],
|
||||||
|
['60 Minuten', '60 minutes'],
|
||||||
|
['Automatik konnte nicht pausiert werden.', 'Automation could not be paused.'],
|
||||||
|
['Automatik konnte nicht fortgesetzt werden.', 'Automation could not be resumed.'],
|
||||||
|
['Ordnerüberwachung konnte nicht pausiert werden', 'Folder monitoring could not be paused'],
|
||||||
|
['Ordnerüberwachung fehlgeschlagen', 'Folder monitoring failed'],
|
||||||
|
['Ordner nicht erreichbar', 'Folder unavailable'],
|
||||||
|
['Ordnerscan fehlgeschlagen', 'Folder scan failed'],
|
||||||
|
['Keine Ordnerkonfiguration vorhanden', 'No folder configuration is available']
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [german, english] of pairs) {
|
||||||
|
assert.equal(translateText(german, 'en'), english, german);
|
||||||
|
assert.equal(translateText(english, 'de'), german, english);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('translates account cooldown and manual pause labels', () => {
|
test('translates account cooldown and manual pause labels', () => {
|
||||||
const pairs = [
|
const pairs = [
|
||||||
['Pausiert – noch', 'Paused –'],
|
['Pausiert – noch', 'Paused –'],
|
||||||
|
|||||||
+664
-53
@@ -64,10 +64,13 @@ test('Windows compositor paints the full hidden surface with an RDP session envi
|
|||||||
const { contextBridge } = require('electron');
|
const { contextBridge } = require('electron');
|
||||||
const managedOnlineBackupProbeCalls = [];
|
const managedOnlineBackupProbeCalls = [];
|
||||||
const folderMonitorProbeCalls = [];
|
const folderMonitorProbeCalls = [];
|
||||||
|
const automationStatusListeners = [];
|
||||||
|
let pendingAutomationTestScan = null;
|
||||||
let automationProbe = {
|
let automationProbe = {
|
||||||
history: [],
|
history: [],
|
||||||
uploadLog: [],
|
uploadLog: [],
|
||||||
paused: false,
|
paused: false,
|
||||||
|
runtimeStatus: {},
|
||||||
automationStatusSequence: [],
|
automationStatusSequence: [],
|
||||||
historyError: '',
|
historyError: '',
|
||||||
addResult: null,
|
addResult: null,
|
||||||
@@ -76,6 +79,8 @@ let automationProbe = {
|
|||||||
startResult: null,
|
startResult: null,
|
||||||
startError: '',
|
startError: '',
|
||||||
saveSettingsError: '',
|
saveSettingsError: '',
|
||||||
|
testScanError: '',
|
||||||
|
deferTestScan: false,
|
||||||
dryScan: { files: [], reachable: true, trigger: 'test' },
|
dryScan: { files: [], reachable: true, trigger: 'test' },
|
||||||
readCalls: { history: 0, uploadLog: 0, inspect: 0, status: 0, testScan: 0, reconcile: 0 },
|
readCalls: { history: 0, uploadLog: 0, inspect: 0, status: 0, testScan: 0, reconcile: 0 },
|
||||||
mutationCalls: [],
|
mutationCalls: [],
|
||||||
@@ -163,10 +168,12 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
},
|
},
|
||||||
getManagedOnlineBackupProbeCalls() { return managedOnlineBackupProbeCalls; },
|
getManagedOnlineBackupProbeCalls() { return managedOnlineBackupProbeCalls; },
|
||||||
configureAutomationProbe(value = {}) {
|
configureAutomationProbe(value = {}) {
|
||||||
|
pendingAutomationTestScan = null;
|
||||||
automationProbe = {
|
automationProbe = {
|
||||||
history: Array.isArray(value.history) ? value.history : [],
|
history: Array.isArray(value.history) ? value.history : [],
|
||||||
uploadLog: Array.isArray(value.uploadLog) ? value.uploadLog : [],
|
uploadLog: Array.isArray(value.uploadLog) ? value.uploadLog : [],
|
||||||
paused: value.paused === true,
|
paused: value.paused === true,
|
||||||
|
runtimeStatus: value.runtimeStatus && typeof value.runtimeStatus === 'object' ? { ...value.runtimeStatus } : {},
|
||||||
automationStatusSequence: Array.isArray(value.automationStatusSequence) ? value.automationStatusSequence.map(entry => ({ ...entry })) : [],
|
automationStatusSequence: Array.isArray(value.automationStatusSequence) ? value.automationStatusSequence.map(entry => ({ ...entry })) : [],
|
||||||
historyError: String(value.historyError || ''),
|
historyError: String(value.historyError || ''),
|
||||||
addResult: value.addResult || null,
|
addResult: value.addResult || null,
|
||||||
@@ -175,6 +182,8 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
startResult: value.startResult || null,
|
startResult: value.startResult || null,
|
||||||
startError: String(value.startError || ''),
|
startError: String(value.startError || ''),
|
||||||
saveSettingsError: String(value.saveSettingsError || ''),
|
saveSettingsError: String(value.saveSettingsError || ''),
|
||||||
|
testScanError: String(value.testScanError || ''),
|
||||||
|
deferTestScan: value.deferTestScan === true,
|
||||||
dryScan: value.dryScan || { files: [], reachable: true, trigger: 'test' },
|
dryScan: value.dryScan || { files: [], reachable: true, trigger: 'test' },
|
||||||
readCalls: { history: 0, uploadLog: 0, inspect: 0, status: 0, testScan: 0, reconcile: 0 },
|
readCalls: { history: 0, uploadLog: 0, inspect: 0, status: 0, testScan: 0, reconcile: 0 },
|
||||||
mutationCalls: [],
|
mutationCalls: [],
|
||||||
@@ -229,12 +238,41 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
automationGetStatus() {
|
automationGetStatus() {
|
||||||
automationProbe.readCalls.status++;
|
automationProbe.readCalls.status++;
|
||||||
if (automationProbe.automationStatusSequence.length > 0) return Promise.resolve(automationProbe.automationStatusSequence.shift());
|
if (automationProbe.automationStatusSequence.length > 0) return Promise.resolve(automationProbe.automationStatusSequence.shift());
|
||||||
return Promise.resolve({ paused: automationProbe.paused });
|
return Promise.resolve({ ...automationProbe.runtimeStatus, paused: automationProbe.paused });
|
||||||
|
},
|
||||||
|
automationPauseAfterActive() {
|
||||||
|
automationProbe.mutationCalls.push(['pause']);
|
||||||
|
automationProbe.paused = true;
|
||||||
|
return Promise.resolve({ ...automationProbe.runtimeStatus, paused: true, pausedAt: 1787712000000 });
|
||||||
|
},
|
||||||
|
automationResume() {
|
||||||
|
automationProbe.mutationCalls.push(['resume']);
|
||||||
|
automationProbe.paused = false;
|
||||||
|
return Promise.resolve({ ...automationProbe.runtimeStatus, paused: false, pausedAt: null });
|
||||||
|
},
|
||||||
|
onAutomationStatus(listener) {
|
||||||
|
automationStatusListeners.push(listener);
|
||||||
|
return () => {
|
||||||
|
const index = automationStatusListeners.indexOf(listener);
|
||||||
|
if (index >= 0) automationStatusListeners.splice(index, 1);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
emitAutomationStatus(status) {
|
||||||
|
automationStatusListeners.forEach(listener => listener({ ...status }));
|
||||||
},
|
},
|
||||||
folderMonitorTestScan() {
|
folderMonitorTestScan() {
|
||||||
automationProbe.readCalls.testScan++;
|
automationProbe.readCalls.testScan++;
|
||||||
|
if (automationProbe.testScanError) return Promise.reject(new Error(automationProbe.testScanError));
|
||||||
|
if (automationProbe.deferTestScan) {
|
||||||
|
return new Promise(resolve => { pendingAutomationTestScan = resolve; });
|
||||||
|
}
|
||||||
return Promise.resolve(automationProbe.dryScan);
|
return Promise.resolve(automationProbe.dryScan);
|
||||||
},
|
},
|
||||||
|
releaseAutomationTestScan() {
|
||||||
|
const resolve = pendingAutomationTestScan;
|
||||||
|
pendingAutomationTestScan = null;
|
||||||
|
if (resolve) resolve(automationProbe.dryScan);
|
||||||
|
},
|
||||||
folderMonitorReconcile() {
|
folderMonitorReconcile() {
|
||||||
automationProbe.readCalls.reconcile++;
|
automationProbe.readCalls.reconcile++;
|
||||||
return Promise.resolve(automationProbe.dryScan);
|
return Promise.resolve(automationProbe.dryScan);
|
||||||
@@ -1224,7 +1262,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
job.sourceCleanupFingerprint = { index, nested: ['before-' + index] };
|
job.sourceCleanupFingerprint = { index, nested: ['before-' + index] };
|
||||||
return job;
|
return job;
|
||||||
};
|
};
|
||||||
const summarizeCollisionPath = async (jobs, invalidJobs, result, before) => {
|
const summarizeCollisionPath = async (jobs, invalidJobs, validJobs, result, before) => {
|
||||||
const probe = await window.api.getAutomationProbeState();
|
const probe = await window.api.getAutomationProbeState();
|
||||||
const inject = probe.mutationCalls.find(call => call[0] === 'inject');
|
const inject = probe.mutationCalls.find(call => call[0] === 'inject');
|
||||||
return {
|
return {
|
||||||
@@ -1233,7 +1271,13 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
sentJobs: inject?.[3] || [],
|
sentJobs: inject?.[3] || [],
|
||||||
sourceCleanupGroups: inject?.[4] || [],
|
sourceCleanupGroups: inject?.[4] || [],
|
||||||
statuses: jobs.map(job => job.status),
|
statuses: jobs.map(job => job.status),
|
||||||
invalidRestored: invalidJobs.map((job, index) => JSON.stringify(job) === before[index])
|
invalidRestored: invalidJobs.map((job, index) => JSON.stringify(job) === before[index]),
|
||||||
|
validJobs: validJobs.map(job => ({
|
||||||
|
id: job.id,
|
||||||
|
status: job.status,
|
||||||
|
requiredHosters: [...(job.sourceCleanupRequiredHosters || [])].sort(),
|
||||||
|
fingerprint: clone(job.sourceCleanupFingerprint)
|
||||||
|
}))
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1248,10 +1292,11 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
{ ...makePauseRaceJob('active-duplicate-b.mkv'), id: 'active-duplicate' },
|
{ ...makePauseRaceJob('active-duplicate-b.mkv'), id: 'active-duplicate' },
|
||||||
activeMissingJob,
|
activeMissingJob,
|
||||||
{ ...makePauseRaceJob('active-shadowed.mkv'), id: 'active-shadowed' },
|
{ ...makePauseRaceJob('active-shadowed.mkv'), id: 'active-shadowed' },
|
||||||
{ ...makePauseRaceJob('active-unique.mkv'), id: 'active-unique' }
|
{ ...makePauseRaceJob('active-unique.mkv'), id: 'active-unique' },
|
||||||
|
{ ...makePauseRaceJob('active-already.mkv'), id: 'active-already' }
|
||||||
].map((job, index) => {
|
].map((job, index) => {
|
||||||
job.file = activeCollisionFile;
|
job.file = activeCollisionFile;
|
||||||
job.hoster = [hosters[0], hosters[1], hosters[2], hosters[0], hosters[3]][index];
|
job.hoster = [hosters[0], hosters[1], hosters[2], hosters[0], hosters[3], hosters[1]][index];
|
||||||
return applyCollisionCleanupFixture(job, index);
|
return applyCollisionCleanupFixture(job, index);
|
||||||
});
|
});
|
||||||
const activeShadowSibling = applyCollisionCleanupFixture({
|
const activeShadowSibling = applyCollisionCleanupFixture({
|
||||||
@@ -1261,13 +1306,42 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
hoster: hosters[1],
|
hoster: hosters[1],
|
||||||
status: 'done'
|
status: 'done'
|
||||||
}, 5);
|
}, 5);
|
||||||
queueJobs = [...activeCollisionJobs, activeShadowSibling];
|
const activeMainFingerprint = { size: 11, mtimeMs: 22, headHash: 'active-main' };
|
||||||
|
const activeValidToken = 'active-valid-token';
|
||||||
|
activeCollisionJobs[0].sourceCleanupToken = activeValidToken;
|
||||||
|
activeCollisionJobs[4].sourceCleanupToken = activeValidToken;
|
||||||
|
activeCollisionJobs[4].sourceCleanupRequiredHosters = [];
|
||||||
|
activeCollisionJobs[5].sourceCleanupToken = activeValidToken;
|
||||||
|
activeCollisionJobs[5].sourceCleanupRequiredHosters = [];
|
||||||
|
const activeValidSibling = applyCollisionCleanupFixture({
|
||||||
|
...makePauseRaceJob('active-running-sibling.mkv'),
|
||||||
|
id: 'active-running-sibling',
|
||||||
|
file: activeCollisionFile,
|
||||||
|
hoster: 'clouddrop.cc',
|
||||||
|
status: 'uploading'
|
||||||
|
}, 6);
|
||||||
|
activeValidSibling.sourceCleanupToken = activeValidToken;
|
||||||
|
activeValidSibling.sourceCleanupRequiredHosters = ['removed.example'];
|
||||||
|
queueJobs = [...activeCollisionJobs, activeShadowSibling, activeValidSibling];
|
||||||
rebuildJobIndex();
|
rebuildJobIndex();
|
||||||
const activeInvalidJobs = [...activeCollisionJobs.slice(0, 4), activeShadowSibling];
|
const activeInvalidJobs = [...activeCollisionJobs.slice(0, 4), activeShadowSibling];
|
||||||
const activeCollisionBefore = activeInvalidJobs.map(job => JSON.stringify(job));
|
const activeCollisionBefore = activeInvalidJobs.map(job => JSON.stringify(job));
|
||||||
window.api.configureAutomationProbe({ paused: false, addResult: { added: 1 } });
|
window.api.configureAutomationProbe({
|
||||||
|
paused: false,
|
||||||
|
addResult: {
|
||||||
|
added: 1,
|
||||||
|
alreadyInBatchJobIds: ['active-already'],
|
||||||
|
sourceCleanupFingerprints: { [activeValidToken]: activeMainFingerprint }
|
||||||
|
}
|
||||||
|
});
|
||||||
const activeCollisionResult = await startSelectedUpload(activeCollisionJobs);
|
const activeCollisionResult = await startSelectedUpload(activeCollisionJobs);
|
||||||
const activeCollision = await summarizeCollisionPath(activeCollisionJobs, activeInvalidJobs, activeCollisionResult, activeCollisionBefore);
|
const activeCollision = await summarizeCollisionPath(
|
||||||
|
activeCollisionJobs,
|
||||||
|
activeInvalidJobs,
|
||||||
|
[activeCollisionJobs[4], activeCollisionJobs[5], activeValidSibling],
|
||||||
|
activeCollisionResult,
|
||||||
|
activeCollisionBefore
|
||||||
|
);
|
||||||
|
|
||||||
configureAtomicState(0);
|
configureAtomicState(0);
|
||||||
config.globalSettings.deleteSourceAfterSuccessfulUpload = true;
|
config.globalSettings.deleteSourceAfterSuccessfulUpload = true;
|
||||||
@@ -1298,6 +1372,20 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
manualCollisionJobs[2].id = 'manual-shadowed';
|
manualCollisionJobs[2].id = 'manual-shadowed';
|
||||||
manualCollisionJobs[3].id = 'manual-unique';
|
manualCollisionJobs[3].id = 'manual-unique';
|
||||||
manualCollisionJobs.forEach(applyCollisionCleanupFixture);
|
manualCollisionJobs.forEach(applyCollisionCleanupFixture);
|
||||||
|
const manualValidToken = 'manual-valid-token';
|
||||||
|
manualCollisionJobs[0].sourceCleanupToken = manualValidToken;
|
||||||
|
manualCollisionJobs[3].sourceCleanupToken = manualValidToken;
|
||||||
|
manualCollisionJobs[3].sourceCleanupRequiredHosters = [];
|
||||||
|
const manualAlready = applyCollisionCleanupFixture({
|
||||||
|
...makePauseRaceJob('manual-already.mkv'),
|
||||||
|
id: 'manual-already',
|
||||||
|
file: manualCollisionFile.path,
|
||||||
|
hoster: 'clouddrop.cc',
|
||||||
|
status: 'preview'
|
||||||
|
}, 6);
|
||||||
|
manualAlready.sourceCleanupToken = manualValidToken;
|
||||||
|
manualAlready.sourceCleanupRequiredHosters = [];
|
||||||
|
manualCollisionJobs.push(manualAlready);
|
||||||
const manualShadowSibling = applyCollisionCleanupFixture({
|
const manualShadowSibling = applyCollisionCleanupFixture({
|
||||||
...makePauseRaceJob('manual-shadow-sibling.mkv'),
|
...makePauseRaceJob('manual-shadow-sibling.mkv'),
|
||||||
id: 'manual-shadowed',
|
id: 'manual-shadowed',
|
||||||
@@ -1312,19 +1400,43 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
status: 'done'
|
status: 'done'
|
||||||
}, 5);
|
}, 5);
|
||||||
delete manualMissingSibling.id;
|
delete manualMissingSibling.id;
|
||||||
queueJobs.push(manualShadowSibling, manualMissingSibling);
|
const manualValidSibling = applyCollisionCleanupFixture({
|
||||||
|
...makePauseRaceJob('manual-running-sibling.mkv'),
|
||||||
|
id: 'manual-running-sibling',
|
||||||
|
file: manualCollisionFile.path,
|
||||||
|
hoster: 'voe.sx',
|
||||||
|
status: 'uploading'
|
||||||
|
}, 7);
|
||||||
|
manualValidSibling.sourceCleanupToken = manualValidToken;
|
||||||
|
manualValidSibling.sourceCleanupRequiredHosters = ['removed.example'];
|
||||||
|
queueJobs.push(manualAlready, manualShadowSibling, manualMissingSibling, manualValidSibling);
|
||||||
manualInvalidJobs = [...manualCollisionJobs.slice(0, 3), manualShadowSibling, manualMissingSibling];
|
manualInvalidJobs = [...manualCollisionJobs.slice(0, 3), manualShadowSibling, manualMissingSibling];
|
||||||
manualCollisionBefore = manualInvalidJobs.map(job => JSON.stringify(job));
|
manualCollisionBefore = manualInvalidJobs.map(job => JSON.stringify(job));
|
||||||
|
manualCollisionJobs.validJobs = [manualCollisionJobs[3], manualAlready, manualValidSibling];
|
||||||
rebuildJobIndex();
|
rebuildJobIndex();
|
||||||
};
|
};
|
||||||
window.api.configureAutomationProbe({ paused: false, addResult: { added: 1 } });
|
const manualMainFingerprint = { size: 33, mtimeMs: 44, headHash: 'manual-main' };
|
||||||
|
window.api.configureAutomationProbe({
|
||||||
|
paused: false,
|
||||||
|
addResult: {
|
||||||
|
added: 1,
|
||||||
|
alreadyInBatchJobIds: ['manual-already'],
|
||||||
|
sourceCleanupFingerprints: { 'manual-valid-token': manualMainFingerprint }
|
||||||
|
}
|
||||||
|
});
|
||||||
const manualCollisionResult = await applyHosterSelection();
|
const manualCollisionResult = await applyHosterSelection();
|
||||||
buildQueuePreview = originalBuildQueuePreviewForCollision;
|
buildQueuePreview = originalBuildQueuePreviewForCollision;
|
||||||
const manualCollision = await summarizeCollisionPath(manualCollisionJobs, manualInvalidJobs, manualCollisionResult, manualCollisionBefore);
|
const manualCollision = await summarizeCollisionPath(
|
||||||
|
manualCollisionJobs,
|
||||||
|
manualInvalidJobs,
|
||||||
|
manualCollisionJobs.validJobs || [],
|
||||||
|
manualCollisionResult,
|
||||||
|
manualCollisionBefore
|
||||||
|
);
|
||||||
|
|
||||||
configureAtomicState(0);
|
configureAtomicState(0);
|
||||||
config.globalSettings.deleteSourceAfterSuccessfulUpload = true;
|
config.globalSettings.deleteSourceAfterSuccessfulUpload = true;
|
||||||
config.globalSettings.folderMonitor.hosters = hosters.slice();
|
config.globalSettings.folderMonitor.hosters = HOSTERS.slice();
|
||||||
config.globalSettings.folderMonitor.autoStart = true;
|
config.globalSettings.folderMonitor.autoStart = true;
|
||||||
selectedFiles = [];
|
selectedFiles = [];
|
||||||
uploading = true;
|
uploading = true;
|
||||||
@@ -1345,7 +1457,16 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
delete automationMissingSibling.id;
|
delete automationMissingSibling.id;
|
||||||
automationShadowSibling.sourceCleanupToken = 'automation-collision-token';
|
automationShadowSibling.sourceCleanupToken = 'automation-collision-token';
|
||||||
automationMissingSibling.sourceCleanupToken = 'automation-collision-token';
|
automationMissingSibling.sourceCleanupToken = 'automation-collision-token';
|
||||||
queueJobs.push(automationShadowSibling, automationMissingSibling);
|
const automationValidSibling = applyCollisionCleanupFixture({
|
||||||
|
...makePauseRaceJob('automation-running-sibling.mkv'),
|
||||||
|
id: 'automation-running-sibling',
|
||||||
|
file: 'C:\\collision\\automation-running-existing.mkv',
|
||||||
|
hoster: 'voe.sx',
|
||||||
|
status: 'uploading'
|
||||||
|
}, 6);
|
||||||
|
automationValidSibling.sourceCleanupToken = 'automation-collision-token';
|
||||||
|
automationValidSibling.sourceCleanupRequiredHosters = ['removed.example'];
|
||||||
|
queueJobs.push(automationShadowSibling, automationMissingSibling, automationValidSibling);
|
||||||
rebuildJobIndex();
|
rebuildJobIndex();
|
||||||
const automationExternalBefore = [automationShadowSibling, automationMissingSibling].map(job => JSON.stringify(job));
|
const automationExternalBefore = [automationShadowSibling, automationMissingSibling].map(job => JSON.stringify(job));
|
||||||
const originalCreateAutomationPreviewJobForCollision = createAutomationPreviewJob;
|
const originalCreateAutomationPreviewJobForCollision = createAutomationPreviewJob;
|
||||||
@@ -1356,23 +1477,39 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
const index = automationCollisionJobs.length;
|
const index = automationCollisionJobs.length;
|
||||||
if (index < 2) job.id = 'automation-duplicate';
|
if (index < 2) job.id = 'automation-duplicate';
|
||||||
else if (index === 2) job.id = 'automation-shadowed';
|
else if (index === 2) job.id = 'automation-shadowed';
|
||||||
else job.id = 'automation-unique';
|
else if (index === 3) job.id = 'automation-unique';
|
||||||
|
else job.id = 'automation-already';
|
||||||
applyCollisionCleanupFixture(job, index);
|
applyCollisionCleanupFixture(job, index);
|
||||||
job.sourceCleanupToken = 'automation-collision-token';
|
job.sourceCleanupToken = 'automation-collision-token';
|
||||||
|
if (index >= 3) job.sourceCleanupRequiredHosters = [];
|
||||||
automationCollisionJobs.push(job);
|
automationCollisionJobs.push(job);
|
||||||
if (index < 3) automationCollisionBefore.push(JSON.stringify(job));
|
if (index < 3) automationCollisionBefore.push(JSON.stringify(job));
|
||||||
return job;
|
return job;
|
||||||
};
|
};
|
||||||
window.api.configureAutomationProbe({ paused: false, addResult: { added: 1 } });
|
const automationMainFingerprint = { size: 55, mtimeMs: 66, headHash: 'automation-main' };
|
||||||
|
window.api.configureAutomationProbe({
|
||||||
|
paused: false,
|
||||||
|
addResult: {
|
||||||
|
added: 1,
|
||||||
|
alreadyInBatchJobIds: ['automation-already'],
|
||||||
|
sourceCleanupFingerprints: { 'automation-collision-token': automationMainFingerprint }
|
||||||
|
}
|
||||||
|
});
|
||||||
const automationCollisionEvaluation = await evaluateAutomationCandidates([automationCollisionFile], { dryRun: false, trigger: 'watcher' });
|
const automationCollisionEvaluation = await evaluateAutomationCandidates([automationCollisionFile], { dryRun: false, trigger: 'watcher' });
|
||||||
const automationCollisionResult = await applyAutomationEvaluation(automationCollisionEvaluation);
|
const automationCollisionResult = await applyAutomationEvaluation(automationCollisionEvaluation);
|
||||||
createAutomationPreviewJob = originalCreateAutomationPreviewJobForCollision;
|
createAutomationPreviewJob = originalCreateAutomationPreviewJobForCollision;
|
||||||
const automationInvalidJobs = [...automationCollisionJobs.slice(0, 3), automationShadowSibling, automationMissingSibling];
|
const automationInvalidJobs = [...automationCollisionJobs.slice(0, 3), automationShadowSibling, automationMissingSibling];
|
||||||
const automationCollision = await summarizeCollisionPath(automationCollisionJobs, automationInvalidJobs, {
|
const automationCollision = await summarizeCollisionPath(
|
||||||
|
automationCollisionJobs,
|
||||||
|
automationInvalidJobs,
|
||||||
|
[automationCollisionJobs[3], automationCollisionJobs[4], automationValidSibling],
|
||||||
|
{
|
||||||
ok: automationCollisionResult.ok,
|
ok: automationCollisionResult.ok,
|
||||||
error: automationCollisionResult.error,
|
error: automationCollisionResult.error || null,
|
||||||
admitted: automationCollisionResult.admittedFiles.map(file => file.name)
|
admitted: automationCollisionResult.admittedFiles.map(file => file.name)
|
||||||
}, [...automationCollisionBefore, ...automationExternalBefore]);
|
},
|
||||||
|
[...automationCollisionBefore, ...automationExternalBefore]
|
||||||
|
);
|
||||||
uploading = false;
|
uploading = false;
|
||||||
const collisionAdmission = { active: activeCollision, manual: manualCollision, automation: automationCollision };
|
const collisionAdmission = { active: activeCollision, manual: manualCollision, automation: automationCollision };
|
||||||
|
|
||||||
@@ -1570,6 +1707,293 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
};
|
};
|
||||||
return { dry, manualTest, historyEvidence, pendingDedup, manualHostTransactional, atomic, status, persistedQueueExactness, stale, replannedEligibility, mainPauseResponses, cleanupRollback, crossPathCleanupRollback, partialAddOutcomes, collisionResolver, collisionAdmission, pauseBetweenApplyAndStart, startAcceptance, fulfilledFeedback, injectionOutcomes, paused };
|
return { dry, manualTest, historyEvidence, pendingDedup, manualHostTransactional, atomic, status, persistedQueueExactness, stale, replannedEligibility, mainPauseResponses, cleanupRollback, crossPathCleanupRollback, partialAddOutcomes, collisionResolver, collisionAdmission, pauseBetweenApplyAndStart, startAcceptance, fulfilledFeedback, injectionOutcomes, paused };
|
||||||
})()`;
|
})()`;
|
||||||
|
const automationControlCenterScript = `(async () => {
|
||||||
|
const waitFor = async predicate => {
|
||||||
|
for (let attempt = 0; attempt < 80; attempt++) {
|
||||||
|
if (await predicate()) return true;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const fixedNow = 1787712600000;
|
||||||
|
const runtimeStatus = {
|
||||||
|
running: true,
|
||||||
|
reachable: true,
|
||||||
|
scanning: false,
|
||||||
|
folderPath: 'C:\\\\watch',
|
||||||
|
lastScanAt: fixedNow - 60000,
|
||||||
|
startedAt: fixedNow - 3600000,
|
||||||
|
error: ''
|
||||||
|
};
|
||||||
|
setUiLanguage('de');
|
||||||
|
config = {
|
||||||
|
hosters: Object.fromEntries(HOSTERS.map(hoster => [hoster, []])),
|
||||||
|
hosterSettings: {},
|
||||||
|
globalSettings: {
|
||||||
|
language: 'de',
|
||||||
|
folderMonitor: {
|
||||||
|
enabled: true,
|
||||||
|
folderPath: 'C:\\\\watch',
|
||||||
|
hosters: ['doodstream.com'],
|
||||||
|
autoStart: false,
|
||||||
|
reconcileIntervalMinutes: 5,
|
||||||
|
paused: false,
|
||||||
|
pausedAt: null,
|
||||||
|
telemetry: {
|
||||||
|
dateKey: new Date().toLocaleDateString('en-CA'),
|
||||||
|
detected: 23,
|
||||||
|
queued: 17,
|
||||||
|
skipped: 5,
|
||||||
|
deferred: 2,
|
||||||
|
lastDetectedName: 'episode-08.mkv',
|
||||||
|
lastDetectedAt: fixedNow - 120000,
|
||||||
|
lastError: '',
|
||||||
|
lastErrorAt: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
hosterSettings = {};
|
||||||
|
selectedFiles = [];
|
||||||
|
selectedUploadHosters = ['doodstream.com'];
|
||||||
|
queueJobs = Array.from({ length: 8420 }, (_, index) => ({
|
||||||
|
id: 'ui-capacity-' + index,
|
||||||
|
file: 'C:\\\\queue\\\\' + index + '.mkv',
|
||||||
|
fileName: index + '.mkv',
|
||||||
|
hoster: 'doodstream.com',
|
||||||
|
status: 'queued',
|
||||||
|
bytesTotal: 1
|
||||||
|
}));
|
||||||
|
rebuildJobIndex();
|
||||||
|
applyAutomationRuntimeStatus(runtimeStatus);
|
||||||
|
renderSettings();
|
||||||
|
document.querySelector('[data-settings-page="automatik"]')?.click();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
const automationPage = document.querySelector('[data-subpage="automatik"]');
|
||||||
|
const pageHeader = automationPage?.querySelector('.settings-page-header');
|
||||||
|
const queueLimitInput = document.getElementById('fmQueueLimitInput');
|
||||||
|
const intervalInput = document.getElementById('fmReconcileIntervalInput');
|
||||||
|
const initial = {
|
||||||
|
cardImmediatelyAfterHeader: pageHeader?.nextElementSibling?.id === 'automationStatusCard',
|
||||||
|
stateBadge: {
|
||||||
|
text: document.getElementById('automationStateBadge')?.textContent.trim() || null,
|
||||||
|
classes: [...(document.getElementById('automationStateBadge')?.classList || [])]
|
||||||
|
},
|
||||||
|
queueMeter: document.getElementById('automationQueueMeter')?.textContent.trim() || null,
|
||||||
|
lastErrorHidden: document.getElementById('automationLastErrorRow')?.hidden ?? null,
|
||||||
|
queueLimitDefault: queueLimitInput?.value || null,
|
||||||
|
queueLimitMin: queueLimitInput?.min || null,
|
||||||
|
intervalDefault: intervalInput?.value || null,
|
||||||
|
intervalOptions: [...(intervalInput?.options || [])].map(option => option.value),
|
||||||
|
snapshotFrozen: Object.isFrozen(createAutomationStatusSnapshot()) && Object.isFrozen(createAutomationStatusSnapshot().telemetry)
|
||||||
|
};
|
||||||
|
if (queueLimitInput) {
|
||||||
|
queueLimitInput.value = '0';
|
||||||
|
queueLimitInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
}
|
||||||
|
initial.queueLimitAcceptsZero = queueLimitInput?.value === '0' && queueLimitInput.checkValidity();
|
||||||
|
const stateDefinitions = [
|
||||||
|
['inactive', 'Inaktiv', 'state-inactive'],
|
||||||
|
['active', 'Aktiv', 'state-active'],
|
||||||
|
['paused', 'Pausiert', 'state-paused'],
|
||||||
|
['queue-limited', 'Queue-Limit erreicht', 'state-queue-limited'],
|
||||||
|
['disconnected', 'Ordner getrennt', 'state-disconnected'],
|
||||||
|
['error', 'Fehler', 'state-error']
|
||||||
|
];
|
||||||
|
const states = [];
|
||||||
|
if (typeof renderAutomationStatusSnapshot === 'function') {
|
||||||
|
const baseSnapshot = createAutomationStatusSnapshot();
|
||||||
|
for (const [state, label, className] of stateDefinitions) {
|
||||||
|
renderAutomationStatusSnapshot(Object.freeze({ ...baseSnapshot, state, error: state === 'error' ? 'Fehler beim Scan' : '' }));
|
||||||
|
const badge = document.getElementById('automationStateBadge');
|
||||||
|
states.push({ state, expectedLabel: label, text: badge?.textContent.trim(), classApplied: badge?.classList.contains(className) });
|
||||||
|
}
|
||||||
|
renderAutomationStatusSnapshot(baseSnapshot);
|
||||||
|
}
|
||||||
|
const originalSnapshotFactory = createAutomationStatusSnapshot;
|
||||||
|
let snapshotCalls = 0;
|
||||||
|
const pausedSnapshot = Object.freeze({
|
||||||
|
...originalSnapshotFactory(),
|
||||||
|
paused: true,
|
||||||
|
state: 'paused',
|
||||||
|
telemetry: Object.freeze({ ...originalSnapshotFactory().telemetry })
|
||||||
|
});
|
||||||
|
if (typeof refreshAutomationControlCenter === 'function') {
|
||||||
|
createAutomationStatusSnapshot = () => {
|
||||||
|
snapshotCalls++;
|
||||||
|
return pausedSnapshot;
|
||||||
|
};
|
||||||
|
refreshAutomationControlCenter();
|
||||||
|
createAutomationStatusSnapshot = originalSnapshotFactory;
|
||||||
|
}
|
||||||
|
queueJobs = [
|
||||||
|
{ id: 'ui-preview', file: 'C:\\\\ui-preview.mkv', fileName: 'ui-preview.mkv', hoster: 'doodstream.com', status: 'preview', bytesTotal: 1 },
|
||||||
|
{ id: 'ui-error', file: 'C:\\\\ui-error.mkv', fileName: 'ui-error.mkv', hoster: 'doodstream.com', status: 'error', bytesTotal: 1 }
|
||||||
|
];
|
||||||
|
uploadSidebarFilter = 'all';
|
||||||
|
queueSearchQuery = '';
|
||||||
|
queueHosterFilter = '';
|
||||||
|
queueStatusFilter = '';
|
||||||
|
_queueFilterCache = { filter: '', source: null, result: [] };
|
||||||
|
selectedJobIds.clear();
|
||||||
|
selectedJobIds.add('ui-preview');
|
||||||
|
selectedJobIds.add('ui-error');
|
||||||
|
rebuildJobIndex();
|
||||||
|
updateUploadView({ rebuildPreview: false });
|
||||||
|
config.globalSettings.folderMonitor.paused = true;
|
||||||
|
applyAutomationRuntimeStatus({ ...runtimeStatus, paused: true, pausedAt: fixedNow });
|
||||||
|
updateQueueActionButtons();
|
||||||
|
document.querySelector('[data-view="upload"]')?.click();
|
||||||
|
const pauseButton = document.getElementById('automationPauseResumeBtn');
|
||||||
|
const pausedControls = {
|
||||||
|
snapshotCalls,
|
||||||
|
pauseButtonDisabled: pauseButton?.disabled ?? null,
|
||||||
|
pauseButtonText: pauseButton?.textContent.trim() || null,
|
||||||
|
pauseButtonLabel: pauseButton?.getAttribute('aria-label') || null,
|
||||||
|
pauseButtonGreen: pauseButton?.classList.contains('automation-resume') ?? null,
|
||||||
|
pauseButtonFits: pauseButton ? pauseButton.scrollWidth <= pauseButton.clientWidth + 1 && pauseButton.getBoundingClientRect().width > 34 : null,
|
||||||
|
startDisabled: Object.fromEntries(['startUploadBtn', 'startSelectedBtn', 'reuploadSelectedBtn', 'retryFailedBtn'].map(id => [id, document.getElementById(id)?.disabled ?? null])),
|
||||||
|
contextStartDisabled: document.querySelector('[data-action="start-selected"]')?.getAttribute('aria-disabled') || null,
|
||||||
|
contextRetryDisabled: document.querySelector('[data-action="retry-selected"]')?.getAttribute('aria-disabled') || null
|
||||||
|
};
|
||||||
|
window.api.configureAutomationProbe({ paused: true, runtimeStatus });
|
||||||
|
pauseButton?.click();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
const afterResumeProbe = await window.api.getAutomationProbeState();
|
||||||
|
const resumedLabel = document.getElementById('automationPauseResumeBtn')?.textContent.trim() || null;
|
||||||
|
selectedJobIds.clear();
|
||||||
|
selectedJobIds.add('ui-preview');
|
||||||
|
selectedJobIds.add('ui-error');
|
||||||
|
updateQueueActionButtons();
|
||||||
|
const startDisabledAfterResume = Object.fromEntries(['startUploadBtn', 'startSelectedBtn', 'reuploadSelectedBtn', 'retryFailedBtn'].map(id => [id, document.getElementById(id)?.disabled ?? null]));
|
||||||
|
document.getElementById('automationPauseResumeBtn')?.click();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
const afterPauseProbe = await window.api.getAutomationProbeState();
|
||||||
|
const pausedLabel = document.getElementById('automationPauseResumeBtn')?.textContent.trim() || null;
|
||||||
|
setUiLanguage('en');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
const pausedLabelEnglish = document.getElementById('automationPauseResumeBtn')?.textContent.trim() || null;
|
||||||
|
setUiLanguage('de');
|
||||||
|
const pauseResumeActions = {
|
||||||
|
calls: afterPauseProbe.mutationCalls.filter(call => call[0] === 'resume' || call[0] === 'pause').map(call => call[0]),
|
||||||
|
resumedLabel,
|
||||||
|
startDisabledAfterResume,
|
||||||
|
pausedLabel,
|
||||||
|
pausedLabelEnglish,
|
||||||
|
configPaused: config.globalSettings.folderMonitor.paused
|
||||||
|
};
|
||||||
|
document.querySelector('[data-view="settings"]')?.click();
|
||||||
|
document.querySelector('[data-settings-page="automatik"]')?.click();
|
||||||
|
config.globalSettings.folderMonitor.paused = false;
|
||||||
|
applyAutomationRuntimeStatus(runtimeStatus);
|
||||||
|
queueJobs = Array.from({ length: 8420 }, (_, index) => ({
|
||||||
|
id: 'ui-test-' + index,
|
||||||
|
file: 'C:\\\\queue-test\\\\' + index + '.mkv',
|
||||||
|
fileName: index + '.mkv',
|
||||||
|
hoster: 'doodstream.com',
|
||||||
|
status: 'queued',
|
||||||
|
bytesTotal: 1
|
||||||
|
}));
|
||||||
|
rebuildJobIndex();
|
||||||
|
if (typeof refreshAutomationControlCenter === 'function') refreshAutomationControlCenter();
|
||||||
|
const dryFiles = [
|
||||||
|
{ path: 'C:\\\\watch\\\\accepted.mkv', name: 'accepted.mkv', size: 1, mtimeMs: 1, filterMatched: true },
|
||||||
|
{ path: 'C:\\\\watch\\\\filtered.txt', name: 'filtered.txt', size: 1, mtimeMs: 2, filterMatched: false },
|
||||||
|
{ path: 'C:\\\\watch\\\\unavailable.mkv', name: 'unavailable.mkv', size: 1, mtimeMs: 3, filterMatched: true, unavailable: true },
|
||||||
|
{ path: 'C:\\\\watch\\\\processed.mkv', name: 'processed.mkv', size: 1, mtimeMs: 4, filterMatched: true }
|
||||||
|
];
|
||||||
|
window.api.configureAutomationProbe({
|
||||||
|
paused: false,
|
||||||
|
runtimeStatus,
|
||||||
|
deferTestScan: true,
|
||||||
|
dryScan: { files: dryFiles, reachable: true, trigger: 'test' },
|
||||||
|
history: [{ id: 'ui-history', files: [{ path: dryFiles[3].path, name: dryFiles[3].name, results: [{ hoster: 'doodstream.com', status: 'done' }] }] }]
|
||||||
|
});
|
||||||
|
const beforeTestProbe = await window.api.getAutomationProbeState();
|
||||||
|
const testButton = document.getElementById('automationTestBtn');
|
||||||
|
testButton?.focus();
|
||||||
|
testButton?.click();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
const overlay = document.getElementById('automationTestOverlay');
|
||||||
|
const loading = {
|
||||||
|
visible: overlay?.style.display === 'flex' && overlay?.getAttribute('aria-hidden') === 'false',
|
||||||
|
busy: overlay?.getAttribute('aria-busy') || null,
|
||||||
|
spinnerVisible: document.getElementById('automationTestSpinner')?.hidden === false,
|
||||||
|
focusInside: Boolean(overlay?.contains(document.activeElement)),
|
||||||
|
backgroundInert: Array.from(document.body.children).filter(element => element !== overlay && 'inert' in element).every(element => element.inert)
|
||||||
|
};
|
||||||
|
window.api.releaseAutomationTestScan();
|
||||||
|
await waitFor(() => overlay?.getAttribute('aria-busy') === 'false');
|
||||||
|
const metricValues = Object.fromEntries([...document.querySelectorAll('[data-automation-test-metric]')].map(row => [
|
||||||
|
row.dataset.automationTestMetric,
|
||||||
|
row.querySelector('.automation-test-value')?.textContent.trim() || ''
|
||||||
|
]));
|
||||||
|
const germanMetricLabels = [...document.querySelectorAll('[data-automation-test-metric] .automation-test-label')].map(element => element.textContent.trim());
|
||||||
|
const afterTestProbe = await window.api.getAutomationProbeState();
|
||||||
|
const completed = {
|
||||||
|
metricValues,
|
||||||
|
metricCount: Object.keys(metricValues).length,
|
||||||
|
germanMetricLabels,
|
||||||
|
errorHidden: document.getElementById('automationTestError')?.hidden ?? null,
|
||||||
|
actionIds: [...(overlay?.querySelectorAll('button') || [])].map(button => button.id),
|
||||||
|
mutationFree: JSON.stringify(afterTestProbe.mutationCalls) === JSON.stringify(beforeTestProbe.mutationCalls)
|
||||||
|
};
|
||||||
|
setUiLanguage('en');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
const english = {
|
||||||
|
queueMeter: document.getElementById('automationQueueMeter')?.textContent.trim() || null,
|
||||||
|
availableSlots: document.querySelector('[data-automation-test-metric="availableSlots"] .automation-test-value')?.textContent.trim() || null,
|
||||||
|
metricLabels: [...document.querySelectorAll('[data-automation-test-metric] .automation-test-label')].map(element => element.textContent.trim())
|
||||||
|
};
|
||||||
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));
|
||||||
|
const closed = {
|
||||||
|
hidden: overlay?.style.display === 'none' && overlay?.getAttribute('aria-hidden') === 'true',
|
||||||
|
focusReturned: document.activeElement?.id === 'automationTestBtn',
|
||||||
|
backgroundRestored: Array.from(document.body.children).filter(element => element !== overlay && 'inert' in element).every(element => !element.inert)
|
||||||
|
};
|
||||||
|
setUiLanguage('de');
|
||||||
|
window.api.configureAutomationProbe({ paused: false, runtimeStatus, testScanError: 'token=secret-value' });
|
||||||
|
document.getElementById('automationTestBtn')?.click();
|
||||||
|
await waitFor(() => document.getElementById('automationTestOverlay')?.getAttribute('aria-busy') === 'false');
|
||||||
|
const errorText = document.getElementById('automationTestError')?.textContent.trim() || '';
|
||||||
|
const errorState = {
|
||||||
|
visible: document.getElementById('automationTestError')?.hidden === false,
|
||||||
|
text: errorText,
|
||||||
|
secretExposed: /secret-value|token=/i.test(errorText)
|
||||||
|
};
|
||||||
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));
|
||||||
|
window.api.configureAutomationProbe({
|
||||||
|
paused: false,
|
||||||
|
runtimeStatus,
|
||||||
|
deferTestScan: true,
|
||||||
|
dryScan: { files: dryFiles, reachable: true, trigger: 'test' }
|
||||||
|
});
|
||||||
|
document.getElementById('automationTestBtn')?.click();
|
||||||
|
await waitFor(() => document.getElementById('automationTestOverlay')?.getAttribute('aria-busy') === 'true');
|
||||||
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));
|
||||||
|
const enabledAfterCancel = document.getElementById('automationTestBtn')?.disabled === false;
|
||||||
|
window.api.releaseAutomationTestScan();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
const cancelLoading = {
|
||||||
|
hidden: document.getElementById('automationTestOverlay')?.style.display === 'none',
|
||||||
|
enabledAfterCancel,
|
||||||
|
lateResultStayedClosed: document.getElementById('automationTestOverlay')?.style.display === 'none'
|
||||||
|
};
|
||||||
|
return { initial, states, pausedControls, pauseResumeActions, loading, completed, english, closed, errorState, cancelLoading };
|
||||||
|
})()`;
|
||||||
|
const automationControlCenterLayoutScript = `(() => {
|
||||||
|
const card = document.getElementById('automationStatusCard');
|
||||||
|
const metrics = document.querySelector('.automation-status-metrics');
|
||||||
|
const metricValue = document.querySelector('.automation-status-value');
|
||||||
|
return {
|
||||||
|
viewportWidth: document.documentElement.clientWidth,
|
||||||
|
documentOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1,
|
||||||
|
cardOverflow: card ? card.scrollWidth > card.clientWidth + 1 : null,
|
||||||
|
metricsOverflow: metrics ? metrics.scrollWidth > metrics.clientWidth + 1 : null,
|
||||||
|
gridTemplateColumns: metrics ? getComputedStyle(metrics).gridTemplateColumns : null,
|
||||||
|
tabularNumbers: metricValue ? getComputedStyle(metricValue).fontVariantNumeric.includes('tabular-nums') : null
|
||||||
|
};
|
||||||
|
})()`;
|
||||||
const onlineBackupBehaviorScript = `(async () => {
|
const onlineBackupBehaviorScript = `(async () => {
|
||||||
const ids = {
|
const ids = {
|
||||||
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
||||||
@@ -1701,41 +2125,52 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
const copy = language === 'de' ? 'Schlüssel kopieren' : 'Copy key';
|
const copy = language === 'de' ? 'Schlüssel kopieren' : 'Copy key';
|
||||||
const remove = language === 'de' ? 'Online-Backup löschen' : 'Delete online backup';
|
const remove = language === 'de' ? 'Online-Backup löschen' : 'Delete online backup';
|
||||||
document.documentElement.lang = language;
|
document.documentElement.lang = language;
|
||||||
document.body.innerHTML = '<section class="online-backup-panel"><section class="online-backup-managed"><h4>Managed</h4><div class="online-backup-managed-list"><article class="online-backup-managed-row"><span class="online-backup-managed-key">ABCDEFGH…1234</span><span class="online-backup-managed-created">22.08.2026 12:00</span><div class="online-backup-managed-actions"><button class="btn btn-secondary">' + copy + '</button><button class="btn btn-danger">' + remove + '</button></div></article><article class="online-backup-managed-row"><span class="online-backup-managed-key">ZYXWVUTS…9876</span><span class="online-backup-managed-created">21.08.2026 11:00</span><div class="online-backup-managed-actions"><button class="btn btn-secondary">' + copy + '</button><button class="btn btn-danger">' + remove + '</button></div></article></div></section><footer class="online-backup-footer"><button class="btn btn-primary">Generate new key</button></footer></section>';
|
const fixture = document.createElement('div');
|
||||||
const panel = document.querySelector('.online-backup-panel');
|
fixture.innerHTML = '<section class="online-backup-panel"><section class="online-backup-managed"><h4>Managed</h4><div class="online-backup-managed-list"><article class="online-backup-managed-row"><span class="online-backup-managed-key">ABCDEFGH…1234</span><span class="online-backup-managed-created">22.08.2026 12:00</span><div class="online-backup-managed-actions"><button class="btn btn-secondary">' + copy + '</button><button class="btn btn-danger">' + remove + '</button></div></article><article class="online-backup-managed-row"><span class="online-backup-managed-key">ZYXWVUTS…9876</span><span class="online-backup-managed-created">21.08.2026 11:00</span><div class="online-backup-managed-actions"><button class="btn btn-secondary">' + copy + '</button><button class="btn btn-danger">' + remove + '</button></div></article></div></section><footer class="online-backup-footer"><button class="btn btn-primary">Generate new key</button></footer></section>';
|
||||||
|
document.body.appendChild(fixture);
|
||||||
|
const panel = fixture.querySelector('.online-backup-panel');
|
||||||
const panelRect = panel.getBoundingClientRect();
|
const panelRect = panel.getBoundingClientRect();
|
||||||
const panelStyle = getComputedStyle(panel);
|
const panelStyle = getComputedStyle(panel);
|
||||||
const rows = [...document.querySelectorAll('.online-backup-managed-row')].map(row => {
|
const rows = [...fixture.querySelectorAll('.online-backup-managed-row')].map(row => {
|
||||||
const key = row.querySelector('.online-backup-managed-key').getBoundingClientRect();
|
const key = row.querySelector('.online-backup-managed-key').getBoundingClientRect();
|
||||||
const created = row.querySelector('.online-backup-managed-created').getBoundingClientRect();
|
const created = row.querySelector('.online-backup-managed-created').getBoundingClientRect();
|
||||||
const actions = row.querySelector('.online-backup-managed-actions').getBoundingClientRect();
|
const actions = row.querySelector('.online-backup-managed-actions').getBoundingClientRect();
|
||||||
return { keyLeft: key.left, createdLeft: created.left, actionsRight: actions.right };
|
return { keyLeft: key.left, createdLeft: created.left, actionsRight: actions.right };
|
||||||
});
|
});
|
||||||
return {
|
const result = {
|
||||||
rows,
|
rows,
|
||||||
contentRight: panelRect.right - parseFloat(panelStyle.paddingRight),
|
contentRight: panelRect.right - parseFloat(panelStyle.paddingRight),
|
||||||
createRight: document.querySelector('.online-backup-footer button').getBoundingClientRect().right
|
createRight: fixture.querySelector('.online-backup-footer button').getBoundingClientRect().right
|
||||||
};
|
};
|
||||||
|
fixture.remove();
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
return { german: measure('de'), english: measure('en') };
|
return { german: measure('de'), english: measure('en') };
|
||||||
})()`;
|
})()`;
|
||||||
const onlineBackupNarrowLayoutScript = `(() => {
|
const onlineBackupNarrowLayoutScript = `(() => {
|
||||||
document.body.innerHTML = '<section class="online-backup-panel"><section class="online-backup-managed"><div class="online-backup-managed-list"><article class="online-backup-managed-row"><span class="online-backup-managed-key">ABCDEFGH…1234</span><span class="online-backup-managed-created">22/08/2026, 12:00</span><div class="online-backup-managed-actions"><button class="btn btn-secondary">Copy key</button><button class="btn btn-danger">Delete online backup</button></div></article></div></section><footer class="online-backup-footer"><button class="btn btn-primary">Generate new key</button></footer></section>';
|
const fixture = document.createElement('div');
|
||||||
const row = document.querySelector('.online-backup-managed-row').getBoundingClientRect();
|
fixture.innerHTML = '<section class="online-backup-panel"><section class="online-backup-managed"><div class="online-backup-managed-list"><article class="online-backup-managed-row"><span class="online-backup-managed-key">ABCDEFGH…1234</span><span class="online-backup-managed-created">22/08/2026, 12:00</span><div class="online-backup-managed-actions"><button class="btn btn-secondary">Copy key</button><button class="btn btn-danger">Delete online backup</button></div></article></div></section><footer class="online-backup-footer"><button class="btn btn-primary">Generate new key</button></footer></section>';
|
||||||
const key = document.querySelector('.online-backup-managed-key').getBoundingClientRect();
|
document.body.appendChild(fixture);
|
||||||
const created = document.querySelector('.online-backup-managed-created').getBoundingClientRect();
|
const rowElement = fixture.querySelector('.online-backup-managed-row');
|
||||||
const actions = document.querySelector('.online-backup-managed-actions').getBoundingClientRect();
|
const row = rowElement.getBoundingClientRect();
|
||||||
const rowStyle = getComputedStyle(document.querySelector('.online-backup-managed-row'));
|
const key = fixture.querySelector('.online-backup-managed-key').getBoundingClientRect();
|
||||||
|
const created = fixture.querySelector('.online-backup-managed-created').getBoundingClientRect();
|
||||||
|
const actions = fixture.querySelector('.online-backup-managed-actions').getBoundingClientRect();
|
||||||
|
const rowStyle = getComputedStyle(rowElement);
|
||||||
const rowContentWidth = row.width - parseFloat(rowStyle.paddingLeft) - parseFloat(rowStyle.paddingRight) - parseFloat(rowStyle.borderLeftWidth) - parseFloat(rowStyle.borderRightWidth);
|
const rowContentWidth = row.width - parseFloat(rowStyle.paddingLeft) - parseFloat(rowStyle.paddingRight) - parseFloat(rowStyle.borderLeftWidth) - parseFloat(rowStyle.borderRightWidth);
|
||||||
const footer = document.querySelector('.online-backup-footer').getBoundingClientRect();
|
const footer = fixture.querySelector('.online-backup-footer').getBoundingClientRect();
|
||||||
const create = document.querySelector('.online-backup-footer button').getBoundingClientRect();
|
const create = fixture.querySelector('.online-backup-footer button').getBoundingClientRect();
|
||||||
return {
|
const result = {
|
||||||
|
innerWidth,
|
||||||
|
narrowMedia: matchMedia('(max-width: 820px)').matches,
|
||||||
horizontalOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1,
|
horizontalOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1,
|
||||||
rowOverflow: document.querySelector('.online-backup-managed-row').scrollWidth > document.querySelector('.online-backup-managed-row').clientWidth + 1,
|
rowOverflow: rowElement.scrollWidth > rowElement.clientWidth + 1,
|
||||||
stacked: key.top < created.top && created.top < actions.top,
|
stacked: key.top < created.top && created.top < actions.top,
|
||||||
actionsStretched: Math.abs(actions.width - rowContentWidth) <= 1,
|
actionsStretched: Math.abs(actions.width - rowContentWidth) <= 1,
|
||||||
createStretched: Math.abs(create.width - footer.width) <= 1
|
createStretched: Math.abs(create.width - footer.width) <= 1
|
||||||
};
|
};
|
||||||
|
fixture.remove();
|
||||||
|
return result;
|
||||||
})()`;
|
})()`;
|
||||||
const probeSource = `
|
const probeSource = `
|
||||||
const { app, BrowserWindow, screen } = require('electron');
|
const { app, BrowserWindow, screen } = require('electron');
|
||||||
@@ -1746,6 +2181,15 @@ function pixelAt(bitmap, width, x, y) {
|
|||||||
const offset = (y * width + x) * 4;
|
const offset = (y * width + x) * 4;
|
||||||
return [bitmap[offset + 2], bitmap[offset + 1], bitmap[offset], bitmap[offset + 3]];
|
return [bitmap[offset + 2], bitmap[offset + 1], bitmap[offset], bitmap[offset + 3]];
|
||||||
}
|
}
|
||||||
|
async function waitForContentWidth(browserWindow, target) {
|
||||||
|
let width = 0;
|
||||||
|
for (let attempt = 0; attempt < 80; attempt++) {
|
||||||
|
width = await browserWindow.webContents.executeJavaScript('innerWidth');
|
||||||
|
if (Math.abs(width - target) <= 1) return width;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
}
|
||||||
|
return width;
|
||||||
|
}
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
const display = screen.getPrimaryDisplay();
|
const display = screen.getPrimaryDisplay();
|
||||||
const requestedContentWidth = Math.min(2544, display.workAreaSize.width);
|
const requestedContentWidth = Math.min(2544, display.workAreaSize.width);
|
||||||
@@ -1777,9 +2221,12 @@ app.whenReady().then(async () => {
|
|||||||
const settingsSearchBehavior = await window.webContents.executeJavaScript(${JSON.stringify(settingsSearchBehaviorScript)});
|
const settingsSearchBehavior = await window.webContents.executeJavaScript(${JSON.stringify(settingsSearchBehaviorScript)});
|
||||||
const folderMonitorBehavior = await window.webContents.executeJavaScript(${JSON.stringify(folderMonitorBehaviorScript)});
|
const folderMonitorBehavior = await window.webContents.executeJavaScript(${JSON.stringify(folderMonitorBehaviorScript)});
|
||||||
const automationPipeline = await window.webContents.executeJavaScript(${JSON.stringify(automationPipelineScript)});
|
const automationPipeline = await window.webContents.executeJavaScript(${JSON.stringify(automationPipelineScript)});
|
||||||
|
const automationControlCenter = await window.webContents.executeJavaScript(${JSON.stringify(automationControlCenterScript)});
|
||||||
|
const automationControlCenterWideLayout = await window.webContents.executeJavaScript(${JSON.stringify(automationControlCenterLayoutScript)});
|
||||||
const onlineBackupLayout = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupLayoutScript)});
|
const onlineBackupLayout = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupLayoutScript)});
|
||||||
window.setContentSize(760, Math.min(900, display.workAreaSize.height));
|
window.setContentSize(760, Math.min(900, display.workAreaSize.height));
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
await waitForContentWidth(window, 760);
|
||||||
|
const automationControlCenterNarrowLayout = await window.webContents.executeJavaScript(${JSON.stringify(automationControlCenterLayoutScript)});
|
||||||
const onlineBackupNarrowLayout = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupNarrowLayoutScript)});
|
const onlineBackupNarrowLayout = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupNarrowLayoutScript)});
|
||||||
fs.writeFileSync(outputPath, JSON.stringify({
|
fs.writeFileSync(outputPath, JSON.stringify({
|
||||||
size,
|
size,
|
||||||
@@ -1796,6 +2243,9 @@ app.whenReady().then(async () => {
|
|||||||
settingsSearchBehavior,
|
settingsSearchBehavior,
|
||||||
folderMonitorBehavior,
|
folderMonitorBehavior,
|
||||||
automationPipeline,
|
automationPipeline,
|
||||||
|
automationControlCenter,
|
||||||
|
automationControlCenterWideLayout,
|
||||||
|
automationControlCenterNarrowLayout,
|
||||||
onlineBackupBehavior,
|
onlineBackupBehavior,
|
||||||
onlineBackupLayout,
|
onlineBackupLayout,
|
||||||
onlineBackupNarrowLayout
|
onlineBackupNarrowLayout
|
||||||
@@ -2149,7 +2599,7 @@ app.whenReady().then(async () => {
|
|||||||
});
|
});
|
||||||
assert.deepEqual(result.automationPipeline.collisionResolver, {
|
assert.deepEqual(result.automationPipeline.collisionResolver, {
|
||||||
clean: {
|
clean: {
|
||||||
consistent: false,
|
consistent: true,
|
||||||
added: ['resolver-unique.mkv'],
|
added: ['resolver-unique.mkv'],
|
||||||
already: [],
|
already: [],
|
||||||
skipped: [],
|
skipped: [],
|
||||||
@@ -2165,28 +2615,61 @@ app.whenReady().then(async () => {
|
|||||||
});
|
});
|
||||||
assert.deepEqual(result.automationPipeline.collisionAdmission, {
|
assert.deepEqual(result.automationPipeline.collisionAdmission, {
|
||||||
active: {
|
active: {
|
||||||
result: { ok: false, error: 'Jobs konnten nicht eindeutig bestätigt werden.' },
|
result: { ok: true, added: 1 },
|
||||||
sentIds: ['active-unique'],
|
sentIds: ['active-unique', 'active-already'],
|
||||||
sentJobs: [{ id: 'active-unique', requiredHosters: ['byse.sx'] }],
|
sentJobs: [
|
||||||
sourceCleanupGroups: [{ requiredHosters: ['byse.sx'], jobIds: ['active-unique'] }],
|
{ id: 'active-unique', requiredHosters: ['removed.example', 'byse.sx', 'voe.sx', 'clouddrop.cc'] },
|
||||||
statuses: ['preview', 'preview', 'preview', 'preview', 'queued'],
|
{ id: 'active-already', requiredHosters: ['removed.example', 'byse.sx', 'voe.sx', 'clouddrop.cc'] }
|
||||||
invalidRestored: [true, true, true, true, true]
|
],
|
||||||
|
sourceCleanupGroups: [{
|
||||||
|
requiredHosters: ['removed.example', 'byse.sx', 'voe.sx', 'clouddrop.cc'],
|
||||||
|
jobIds: ['active-unique', 'active-already', 'active-running-sibling']
|
||||||
|
}],
|
||||||
|
statuses: ['preview', 'preview', 'preview', 'preview', 'queued', 'queued'],
|
||||||
|
invalidRestored: [true, true, true, true, true],
|
||||||
|
validJobs: [
|
||||||
|
{ id: 'active-unique', status: 'queued', requiredHosters: ['byse.sx', 'clouddrop.cc', 'removed.example', 'voe.sx'], fingerprint: { size: 11, mtimeMs: 22, headHash: 'active-main' } },
|
||||||
|
{ id: 'active-already', status: 'queued', requiredHosters: ['byse.sx', 'clouddrop.cc', 'removed.example', 'voe.sx'], fingerprint: { size: 11, mtimeMs: 22, headHash: 'active-main' } },
|
||||||
|
{ id: 'active-running-sibling', status: 'uploading', requiredHosters: ['byse.sx', 'clouddrop.cc', 'removed.example', 'voe.sx'], fingerprint: { size: 11, mtimeMs: 22, headHash: 'active-main' } }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
manual: {
|
manual: {
|
||||||
result: { ok: false, error: 'Jobs konnten nicht eindeutig bestätigt werden.' },
|
result: true,
|
||||||
sentIds: ['manual-unique'],
|
sentIds: ['manual-unique', 'manual-already'],
|
||||||
sentJobs: [{ id: 'manual-unique', requiredHosters: ['byse.sx'] }],
|
sentJobs: [
|
||||||
sourceCleanupGroups: [{ requiredHosters: ['byse.sx'], jobIds: ['manual-unique'] }],
|
{ id: 'manual-unique', requiredHosters: ['removed.example', 'byse.sx', 'clouddrop.cc', 'voe.sx'] },
|
||||||
statuses: ['preview', 'preview', 'preview', 'queued'],
|
{ id: 'manual-already', requiredHosters: ['removed.example', 'byse.sx', 'clouddrop.cc', 'voe.sx'] }
|
||||||
invalidRestored: [true, true, true, true, true]
|
],
|
||||||
|
sourceCleanupGroups: [{
|
||||||
|
requiredHosters: ['removed.example', 'byse.sx', 'clouddrop.cc', 'voe.sx'],
|
||||||
|
jobIds: ['manual-unique', 'manual-already', 'manual-running-sibling']
|
||||||
|
}],
|
||||||
|
statuses: ['preview', 'preview', 'preview', 'queued', 'queued'],
|
||||||
|
invalidRestored: [true, true, true, true, true],
|
||||||
|
validJobs: [
|
||||||
|
{ id: 'manual-unique', status: 'queued', requiredHosters: ['byse.sx', 'clouddrop.cc', 'removed.example', 'voe.sx'], fingerprint: { size: 33, mtimeMs: 44, headHash: 'manual-main' } },
|
||||||
|
{ id: 'manual-already', status: 'queued', requiredHosters: ['byse.sx', 'clouddrop.cc', 'removed.example', 'voe.sx'], fingerprint: { size: 33, mtimeMs: 44, headHash: 'manual-main' } },
|
||||||
|
{ id: 'manual-running-sibling', status: 'uploading', requiredHosters: ['byse.sx', 'clouddrop.cc', 'removed.example', 'voe.sx'], fingerprint: { size: 33, mtimeMs: 44, headHash: 'manual-main' } }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
automation: {
|
automation: {
|
||||||
result: { ok: false, error: 'Jobs konnten nicht eindeutig bestätigt werden.', admitted: [] },
|
result: { ok: true, error: null, admitted: ['automation.mkv'] },
|
||||||
sentIds: ['automation-unique'],
|
sentIds: ['automation-unique', 'automation-already'],
|
||||||
sentJobs: [{ id: 'automation-unique', requiredHosters: ['byse.sx'] }],
|
sentJobs: [
|
||||||
sourceCleanupGroups: [{ requiredHosters: ['byse.sx'], jobIds: ['automation-unique'] }],
|
{ id: 'automation-unique', requiredHosters: ['removed.example', 'voe.sx', 'byse.sx', 'clouddrop.cc'] },
|
||||||
statuses: ['preview', 'preview', 'preview', 'queued'],
|
{ id: 'automation-already', requiredHosters: ['removed.example', 'voe.sx', 'byse.sx', 'clouddrop.cc'] }
|
||||||
invalidRestored: [true, true, true, true, true]
|
],
|
||||||
|
sourceCleanupGroups: [{
|
||||||
|
requiredHosters: ['removed.example', 'voe.sx', 'byse.sx', 'clouddrop.cc'],
|
||||||
|
jobIds: ['automation-running-sibling', 'automation-unique', 'automation-already']
|
||||||
|
}],
|
||||||
|
statuses: ['preview', 'preview', 'preview', 'queued', 'queued'],
|
||||||
|
invalidRestored: [true, true, true, true, true],
|
||||||
|
validJobs: [
|
||||||
|
{ id: 'automation-unique', status: 'queued', requiredHosters: ['byse.sx', 'clouddrop.cc', 'removed.example', 'voe.sx'], fingerprint: { size: 55, mtimeMs: 66, headHash: 'automation-main' } },
|
||||||
|
{ id: 'automation-already', status: 'queued', requiredHosters: ['byse.sx', 'clouddrop.cc', 'removed.example', 'voe.sx'], fingerprint: { size: 55, mtimeMs: 66, headHash: 'automation-main' } },
|
||||||
|
{ id: 'automation-running-sibling', status: 'uploading', requiredHosters: ['byse.sx', 'clouddrop.cc', 'removed.example', 'voe.sx'], fingerprint: { size: 55, mtimeMs: 66, headHash: 'automation-main' } }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
assert.deepEqual(result.automationPipeline.pauseBetweenApplyAndStart, {
|
assert.deepEqual(result.automationPipeline.pauseBetweenApplyAndStart, {
|
||||||
@@ -2285,6 +2768,132 @@ app.whenReady().then(async () => {
|
|||||||
startCalls: 0,
|
startCalls: 0,
|
||||||
injectCalls: 0
|
injectCalls: 0
|
||||||
});
|
});
|
||||||
|
assert.equal(result.automationControlCenter.initial.cardImmediatelyAfterHeader, true);
|
||||||
|
assert.equal(result.automationControlCenter.initial.stateBadge.text, 'Aktiv');
|
||||||
|
assert.equal(result.automationControlCenter.initial.stateBadge.classes.includes('state-active'), true);
|
||||||
|
assert.equal(result.automationControlCenter.initial.queueMeter, '8.420 / 15.000');
|
||||||
|
assert.equal(result.automationControlCenter.initial.lastErrorHidden, true);
|
||||||
|
assert.equal(result.automationControlCenter.initial.queueLimitDefault, '15000');
|
||||||
|
assert.equal(result.automationControlCenter.initial.queueLimitMin, '0');
|
||||||
|
assert.equal(result.automationControlCenter.initial.queueLimitAcceptsZero, true);
|
||||||
|
assert.equal(result.automationControlCenter.initial.intervalDefault, '5');
|
||||||
|
assert.deepEqual(result.automationControlCenter.initial.intervalOptions, ['1', '5', '15', '30', '60']);
|
||||||
|
assert.equal(result.automationControlCenter.initial.snapshotFrozen, true);
|
||||||
|
assert.deepEqual(result.automationControlCenter.states, [
|
||||||
|
{ state: 'inactive', expectedLabel: 'Inaktiv', text: 'Inaktiv', classApplied: true },
|
||||||
|
{ state: 'active', expectedLabel: 'Aktiv', text: 'Aktiv', classApplied: true },
|
||||||
|
{ state: 'paused', expectedLabel: 'Pausiert', text: 'Pausiert', classApplied: true },
|
||||||
|
{ state: 'queue-limited', expectedLabel: 'Queue-Limit erreicht', text: 'Queue-Limit erreicht', classApplied: true },
|
||||||
|
{ state: 'disconnected', expectedLabel: 'Ordner getrennt', text: 'Ordner getrennt', classApplied: true },
|
||||||
|
{ state: 'error', expectedLabel: 'Fehler', text: 'Fehler', classApplied: true }
|
||||||
|
]);
|
||||||
|
assert.deepEqual(result.automationControlCenter.pausedControls, {
|
||||||
|
snapshotCalls: 1,
|
||||||
|
pauseButtonDisabled: false,
|
||||||
|
pauseButtonText: 'Fortsetzen',
|
||||||
|
pauseButtonLabel: 'Fortsetzen',
|
||||||
|
pauseButtonGreen: true,
|
||||||
|
pauseButtonFits: true,
|
||||||
|
startDisabled: {
|
||||||
|
startUploadBtn: true,
|
||||||
|
startSelectedBtn: true,
|
||||||
|
reuploadSelectedBtn: true,
|
||||||
|
retryFailedBtn: true
|
||||||
|
},
|
||||||
|
contextStartDisabled: 'true',
|
||||||
|
contextRetryDisabled: 'true'
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationControlCenter.pauseResumeActions, {
|
||||||
|
calls: ['resume', 'pause'],
|
||||||
|
resumedLabel: 'Abschließen und pausieren',
|
||||||
|
startDisabledAfterResume: {
|
||||||
|
startUploadBtn: false,
|
||||||
|
startSelectedBtn: false,
|
||||||
|
reuploadSelectedBtn: false,
|
||||||
|
retryFailedBtn: false
|
||||||
|
},
|
||||||
|
pausedLabel: 'Fortsetzen',
|
||||||
|
pausedLabelEnglish: 'Resume',
|
||||||
|
configPaused: true
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationControlCenter.loading, {
|
||||||
|
visible: true,
|
||||||
|
busy: 'true',
|
||||||
|
spinnerVisible: true,
|
||||||
|
focusInside: true,
|
||||||
|
backgroundInert: true
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationControlCenter.completed, {
|
||||||
|
metricValues: {
|
||||||
|
found: '4',
|
||||||
|
filterMatched: '3',
|
||||||
|
alreadyProcessed: '1',
|
||||||
|
unavailable: '1',
|
||||||
|
sizeLimitedJobs: '0',
|
||||||
|
acceptedFiles: '1',
|
||||||
|
selectedTargets: '1',
|
||||||
|
resultingJobs: '1',
|
||||||
|
availableSlots: '6.580',
|
||||||
|
deferredFiles: '0'
|
||||||
|
},
|
||||||
|
metricCount: 10,
|
||||||
|
germanMetricLabels: [
|
||||||
|
'Gefundene Dateien',
|
||||||
|
'Passend zum Dateifilter',
|
||||||
|
'Bereits verarbeitet',
|
||||||
|
'Fehlend, leer oder nicht lesbar',
|
||||||
|
'Durch Größenlimits ausgeschlossen',
|
||||||
|
'Akzeptierte Dateien',
|
||||||
|
'Ausgewählte Ziele',
|
||||||
|
'Entstehende Upload-Jobs',
|
||||||
|
'Verfügbare Jobs bis zum Queue-Limit',
|
||||||
|
'Aktuell zurückzustellende Dateien'
|
||||||
|
],
|
||||||
|
errorHidden: true,
|
||||||
|
actionIds: ['automationTestCloseBtn'],
|
||||||
|
mutationFree: true
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationControlCenter.english, {
|
||||||
|
queueMeter: '8,420 / 15,000',
|
||||||
|
availableSlots: '6,580',
|
||||||
|
metricLabels: [
|
||||||
|
'Files found',
|
||||||
|
'Matching file filter',
|
||||||
|
'Already processed',
|
||||||
|
'Missing, empty, or unreadable',
|
||||||
|
'Excluded by size limits',
|
||||||
|
'Accepted files',
|
||||||
|
'Selected destinations',
|
||||||
|
'Resulting upload jobs',
|
||||||
|
'Available jobs before queue limit',
|
||||||
|
'Files currently deferred'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationControlCenter.closed, {
|
||||||
|
hidden: true,
|
||||||
|
focusReturned: true,
|
||||||
|
backgroundRestored: true
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationControlCenter.errorState, {
|
||||||
|
visible: true,
|
||||||
|
text: 'Ordnerüberwachung konnte nicht getestet werden.',
|
||||||
|
secretExposed: false
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationControlCenter.cancelLoading, {
|
||||||
|
hidden: true,
|
||||||
|
enabledAfterCancel: true,
|
||||||
|
lateResultStayedClosed: true
|
||||||
|
});
|
||||||
|
assert.equal(result.automationControlCenterWideLayout.documentOverflow, false);
|
||||||
|
assert.equal(result.automationControlCenterWideLayout.cardOverflow, false);
|
||||||
|
assert.equal(result.automationControlCenterWideLayout.metricsOverflow, false);
|
||||||
|
assert.equal(result.automationControlCenterWideLayout.tabularNumbers, true);
|
||||||
|
assert.ok(result.automationControlCenterWideLayout.gridTemplateColumns);
|
||||||
|
assert.equal(result.automationControlCenterNarrowLayout.viewportWidth, 760);
|
||||||
|
assert.equal(result.automationControlCenterNarrowLayout.documentOverflow, false);
|
||||||
|
assert.equal(result.automationControlCenterNarrowLayout.cardOverflow, false);
|
||||||
|
assert.equal(result.automationControlCenterNarrowLayout.metricsOverflow, false);
|
||||||
|
assert.equal(result.automationControlCenterNarrowLayout.tabularNumbers, true);
|
||||||
assert.deepEqual(result.onlineBackupBehavior.initialKeys, ['MHU2-ZYXW…9876', 'MHU2-ABCD…1234']);
|
assert.deepEqual(result.onlineBackupBehavior.initialKeys, ['MHU2-ZYXW…9876', 'MHU2-ABCD…1234']);
|
||||||
assert.deepEqual(result.onlineBackupBehavior.initialWarning, {
|
assert.deepEqual(result.onlineBackupBehavior.initialWarning, {
|
||||||
hidden: false,
|
hidden: false,
|
||||||
@@ -2357,6 +2966,8 @@ app.whenReady().then(async () => {
|
|||||||
assert.ok(Math.abs(result.onlineBackupLayout.german.rows[0].keyLeft - result.onlineBackupLayout.english.rows[0].keyLeft) <= 1);
|
assert.ok(Math.abs(result.onlineBackupLayout.german.rows[0].keyLeft - result.onlineBackupLayout.english.rows[0].keyLeft) <= 1);
|
||||||
assert.ok(Math.abs(result.onlineBackupLayout.german.rows[0].createdLeft - result.onlineBackupLayout.english.rows[0].createdLeft) <= 1);
|
assert.ok(Math.abs(result.onlineBackupLayout.german.rows[0].createdLeft - result.onlineBackupLayout.english.rows[0].createdLeft) <= 1);
|
||||||
assert.ok(Math.abs(result.onlineBackupLayout.german.rows[0].actionsRight - result.onlineBackupLayout.english.rows[0].actionsRight) <= 1);
|
assert.ok(Math.abs(result.onlineBackupLayout.german.rows[0].actionsRight - result.onlineBackupLayout.english.rows[0].actionsRight) <= 1);
|
||||||
|
assert.equal(result.onlineBackupNarrowLayout.innerWidth, 760);
|
||||||
|
assert.equal(result.onlineBackupNarrowLayout.narrowMedia, true);
|
||||||
assert.equal(result.onlineBackupNarrowLayout.horizontalOverflow, false);
|
assert.equal(result.onlineBackupNarrowLayout.horizontalOverflow, false);
|
||||||
assert.equal(result.onlineBackupNarrowLayout.rowOverflow, false);
|
assert.equal(result.onlineBackupNarrowLayout.rowOverflow, false);
|
||||||
assert.equal(result.onlineBackupNarrowLayout.stacked, true);
|
assert.equal(result.onlineBackupNarrowLayout.stacked, true);
|
||||||
|
|||||||
Reference in New Issue
Block a user