fix: preserve automation rollback contracts
This commit is contained in:
+105
-14
@@ -730,16 +730,19 @@ async function applyAutomationEvaluation(evaluation) {
|
||||
sourceCleanupGroups: cleanupPreparation.groups
|
||||
});
|
||||
if (result?.error) {
|
||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||
const error = sanitizeUploadControlError(result.error, 'Jobs konnten nicht hinzugefügt werden.');
|
||||
return freezeAutomationValue({ ok: false, error, warning: null, admittedFiles: [], deferredFiles, paused: /pausiert/i.test(error), dryRun: false });
|
||||
}
|
||||
if ((Number(result?.added) || 0) !== newJobs.length) {
|
||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||
return freezeAutomationValue({ ok: false, error: 'Jobs wurden nicht vollständig hinzugefügt.', warning: null, admittedFiles: [], deferredFiles, paused: false, dryRun: false });
|
||||
}
|
||||
newJobs.forEach(job => { job.status = 'queued'; });
|
||||
_markSkippedJobs(result);
|
||||
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
||||
} catch {
|
||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||
return freezeAutomationValue({ ok: false, error: 'Jobs konnten nicht hinzugefügt werden.', warning: null, admittedFiles: [], deferredFiles, paused: false, dryRun: false });
|
||||
}
|
||||
renderQueueTable();
|
||||
@@ -779,10 +782,19 @@ window.applyAutomationEvaluation = applyAutomationEvaluation;
|
||||
window.createAutomationStatusSnapshot = createAutomationStatusSnapshot;
|
||||
window.runFolderMonitorTestScan = runFolderMonitorTestScan;
|
||||
|
||||
function surfaceAutomationOutcome(result) {
|
||||
if (result?.ok !== false) return '';
|
||||
const message = result.warning || result.error || 'Automatische Aufnahme konnte nicht abgeschlossen werden.';
|
||||
showCopyToast(message, 6500);
|
||||
return message;
|
||||
}
|
||||
|
||||
async function handleFolderMonitorFiles(files) {
|
||||
window.api.debugLog('folder-monitor: received ' + files.length + ' file(s)');
|
||||
const evaluation = await evaluateAutomationCandidates(files, { dryRun: false, trigger: 'watcher' });
|
||||
return applyAutomationEvaluation(evaluation);
|
||||
const result = await applyAutomationEvaluation(evaluation);
|
||||
surfaceAutomationOutcome(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// --- Init ---
|
||||
@@ -1535,6 +1547,7 @@ async function applyHosterSelection() {
|
||||
} catch {
|
||||
regularInjectionFailure = 'Jobs konnten nicht hinzugefügt werden.';
|
||||
}
|
||||
if (regularInjectionFailure) restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||
renderQueueTable();
|
||||
persistQueueStateSoon(true);
|
||||
}
|
||||
@@ -1546,6 +1559,7 @@ async function applyHosterSelection() {
|
||||
return { ok: false, error: regularInjectionFailure };
|
||||
}
|
||||
const selectedHosters = selectedUploadHosters.slice();
|
||||
let automationOutcome = null;
|
||||
if (automationFiles.length > 0) {
|
||||
try {
|
||||
const evaluation = await evaluateAutomationCandidates(automationFiles, {
|
||||
@@ -1555,6 +1569,7 @@ async function applyHosterSelection() {
|
||||
ownedPendingPaths: automationFiles.map(file => file.path)
|
||||
});
|
||||
const result = await applyAutomationEvaluation(evaluation);
|
||||
automationOutcome = result;
|
||||
const admittedPaths = new Set((result?.admittedFiles || []).map(file => normalizeAutomationPath(file.path)));
|
||||
if (result?.error || admittedPaths.size === 0) throw new Error('manual-host-apply-failed');
|
||||
_pendingFiles = _pendingFiles.filter(file => !admittedPaths.has(normalizeAutomationPath(file.path)));
|
||||
@@ -1582,6 +1597,10 @@ async function applyHosterSelection() {
|
||||
document.getElementById('hosterModal').style.display = 'flex';
|
||||
renderImportPlanSummary();
|
||||
}
|
||||
if (automationOutcome?.warning) {
|
||||
surfaceAutomationOutcome(automationOutcome);
|
||||
return { ok: false, warning: automationOutcome.warning, error: null };
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2108,6 +2127,10 @@ function suppressPreviewKeysStillSelected(keys) {
|
||||
function buildQueuePreview() {
|
||||
const hosters = getSelectedHosters();
|
||||
queueJobs = queueJobs.filter(j => j.status !== 'preview' || j.automationAdmission === true);
|
||||
const normalizedSettings = automationSettings();
|
||||
let availableSlots = normalizedSettings.queueLimitJobs === 0
|
||||
? null
|
||||
: Math.max(0, normalizedSettings.queueLimitJobs - window.AutomationControl.countAutomaticQueueJobs(queueJobs));
|
||||
|
||||
if (hosters.length > 0) {
|
||||
const existingKeys = new Set();
|
||||
@@ -2116,10 +2139,14 @@ function buildQueuePreview() {
|
||||
}
|
||||
|
||||
for (const file of selectedFiles) {
|
||||
for (const hoster of hosters) {
|
||||
if (!window.ImportPreflight.isImportPairEligible(file, hoster, hosterSettings)) continue;
|
||||
const eligibleHosters = hosters.filter(hoster => {
|
||||
if (!window.ImportPreflight.isImportPairEligible(file, hoster, hosterSettings)) return false;
|
||||
const key = `${file.path}|${hoster}`;
|
||||
return !existingKeys.has(key) && !_completedUploadKeys.has(key) && !_suppressedPreviewKeys.has(key);
|
||||
});
|
||||
if (availableSlots !== null && eligibleHosters.length > availableSlots) continue;
|
||||
for (const hoster of eligibleHosters) {
|
||||
const key = `${file.path}|${hoster}`;
|
||||
if (!existingKeys.has(key) && !_completedUploadKeys.has(key) && !_suppressedPreviewKeys.has(key)) {
|
||||
const job = {
|
||||
id: `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
file: file.path, fileName: file.name, hoster,
|
||||
@@ -2129,8 +2156,8 @@ function buildQueuePreview() {
|
||||
};
|
||||
queueJobs.push(job);
|
||||
existingKeys.add(key);
|
||||
}
|
||||
}
|
||||
if (availableSlots !== null) availableSlots -= eligibleHosters.length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3694,9 +3721,41 @@ function getSelectedJobLinks() {
|
||||
}
|
||||
|
||||
// --- Upload ---
|
||||
const sourceCleanupFields = [
|
||||
'sourceCleanupToken',
|
||||
'sourceCleanupRequiredHosters',
|
||||
'sourceCleanupCompletedHosters',
|
||||
'sourceCleanupFingerprint'
|
||||
];
|
||||
|
||||
function cloneSourceCleanupValue(value) {
|
||||
if (!value || typeof value !== 'object') return value;
|
||||
return structuredClone(value);
|
||||
}
|
||||
|
||||
function captureSourceCleanupStates(jobs) {
|
||||
const paths = new Set((Array.isArray(jobs) ? jobs : []).map(job => normalizeAutomationPath(job?.file)).filter(Boolean));
|
||||
return queueJobs
|
||||
.filter(job => paths.has(normalizeAutomationPath(job?.file)))
|
||||
.map(job => [job, Object.fromEntries(sourceCleanupFields.map(field => [field, {
|
||||
present: Object.prototype.hasOwnProperty.call(job, field),
|
||||
value: cloneSourceCleanupValue(job[field])
|
||||
}]))]);
|
||||
}
|
||||
|
||||
function restoreSourceCleanupStates(states) {
|
||||
for (const [job, fields] of states || []) {
|
||||
for (const field of sourceCleanupFields) {
|
||||
if (!fields[field].present) delete job[field];
|
||||
else job[field] = cloneSourceCleanupValue(fields[field].value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prepareSourceCleanup(jobs) {
|
||||
if (!config.globalSettings?.deleteSourceAfterSuccessfulUpload || !window.SourceCleanupPolicy) return { groups: [] };
|
||||
return window.SourceCleanupPolicy.prepareGroups(queueJobs, jobs, () => window.crypto.randomUUID(), 'win32');
|
||||
const rollbackStates = captureSourceCleanupStates(jobs);
|
||||
if (!config.globalSettings?.deleteSourceAfterSuccessfulUpload || !window.SourceCleanupPolicy) return { groups: [], rollbackStates };
|
||||
return { ...window.SourceCleanupPolicy.prepareGroups(queueJobs, jobs, () => window.crypto.randomUUID(), 'win32'), rollbackStates };
|
||||
}
|
||||
|
||||
function serializeUploadJob(job) {
|
||||
@@ -3731,8 +3790,8 @@ function sanitizeUploadControlError(error, fallback) {
|
||||
}
|
||||
|
||||
async function startUpload(opts) {
|
||||
if (uploading) return;
|
||||
if (await isAutomationPaused()) return false;
|
||||
if (uploading) return { ok: false, error: 'Upload läuft bereits.' };
|
||||
if (await isAutomationPaused()) return { ok: false, error: 'Automatik ist pausiert' };
|
||||
if (!(opts && opts._restoredAutoStart)) cancelStartupQueueAutoStart();
|
||||
if (!(opts && opts._autoRetry)) _cancelAutoRetry(true);
|
||||
else _cancelAutoRetry(false);
|
||||
@@ -3746,17 +3805,22 @@ async function startUpload(opts) {
|
||||
await showAppAlert('Bitte mindestens einen Hoster auswählen.');
|
||||
uploading = false;
|
||||
updateQueueActionButtons();
|
||||
return;
|
||||
return { ok: false, error: 'Bitte mindestens einen Hoster auswählen.' };
|
||||
}
|
||||
buildQueuePreview();
|
||||
}
|
||||
|
||||
const jobsToStart = queueJobs.filter((job) => isStartableQueueStatus(job.status));
|
||||
if (jobsToStart.length === 0) { uploading = false; updateQueueActionButtons(); return; }
|
||||
if (jobsToStart.length === 0) {
|
||||
uploading = false;
|
||||
updateQueueActionButtons();
|
||||
return { ok: false, error: 'Keine startbaren Jobs vorhanden.' };
|
||||
}
|
||||
const originalStates = captureUploadJobStates(jobsToStart);
|
||||
let cleanupPreparation = { groups: [], rollbackStates: [] };
|
||||
|
||||
try {
|
||||
const cleanupPreparation = prepareSourceCleanup(jobsToStart);
|
||||
cleanupPreparation = prepareSourceCleanup(jobsToStart);
|
||||
jobsToStart.forEach(j => {
|
||||
j.status = 'queued';
|
||||
j.error = null;
|
||||
@@ -3780,6 +3844,7 @@ async function startUpload(opts) {
|
||||
};
|
||||
const result = await window.api.startUpload(uploadPayload);
|
||||
if (result && result.error) {
|
||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||
restoreUploadJobStates(originalStates);
|
||||
const error = sanitizeUploadControlError(result.error, 'Upload konnte nicht gestartet werden.');
|
||||
uploading = false;
|
||||
@@ -3789,6 +3854,17 @@ async function startUpload(opts) {
|
||||
await showAppAlert(error, 'Upload-Start fehlgeschlagen');
|
||||
return { ok: false, error };
|
||||
}
|
||||
if (result?.started !== true) {
|
||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||
restoreUploadJobStates(originalStates);
|
||||
const error = 'Upload wurde nicht bestätigt.';
|
||||
uploading = false;
|
||||
renderQueueTable();
|
||||
updateQueueActionButtons();
|
||||
updateStatusBar();
|
||||
await showAppAlert(error, 'Upload-Start fehlgeschlagen');
|
||||
return { ok: false, error };
|
||||
}
|
||||
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) {
|
||||
window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
||||
}
|
||||
@@ -3797,6 +3873,7 @@ async function startUpload(opts) {
|
||||
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||
restoreUploadJobStates(originalStates);
|
||||
uploading = false;
|
||||
renderQueueTable();
|
||||
@@ -3821,7 +3898,7 @@ function _markSkippedJobs(result) {
|
||||
}
|
||||
|
||||
async function startSelectedUpload(explicitJobs) {
|
||||
if (await isAutomationPaused()) return false;
|
||||
if (await isAutomationPaused()) return { ok: false, error: 'Automatik ist pausiert' };
|
||||
const scopedJobs = Array.isArray(explicitJobs) ? explicitJobs : _getVisibleSelectedQueueJobs();
|
||||
if (uploading) {
|
||||
_hydrateMissingJobSizes();
|
||||
@@ -3845,6 +3922,7 @@ async function startSelectedUpload(explicitJobs) {
|
||||
sourceCleanupGroups: cleanupPreparation.groups
|
||||
});
|
||||
} catch (err) {
|
||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||
restoreUploadJobStates(originalStates);
|
||||
renderQueueTable();
|
||||
const error = sanitizeUploadControlError(err, 'Jobs konnten nicht hinzugefügt werden.');
|
||||
@@ -3853,6 +3931,7 @@ async function startSelectedUpload(explicitJobs) {
|
||||
}
|
||||
|
||||
if (result && result.error === 'Kein Upload aktiv') {
|
||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||
restoreUploadJobStates(originalStates);
|
||||
uploading = false;
|
||||
updateQueueActionButtons();
|
||||
@@ -3860,12 +3939,21 @@ async function startSelectedUpload(explicitJobs) {
|
||||
return startSelectedUpload(addable);
|
||||
}
|
||||
if (result && result.error) {
|
||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||
restoreUploadJobStates(originalStates);
|
||||
renderQueueTable();
|
||||
const error = sanitizeUploadControlError(result.error, 'Jobs konnten nicht hinzugefügt werden.');
|
||||
showCopyToast(error);
|
||||
return { ok: false, error };
|
||||
}
|
||||
if ((Number(result?.added) || 0) !== addable.length) {
|
||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||
restoreUploadJobStates(originalStates);
|
||||
renderQueueTable();
|
||||
const error = 'Jobs wurden nicht vollständig hinzugefügt.';
|
||||
showCopyToast(error);
|
||||
return { ok: false, error };
|
||||
}
|
||||
_markSkippedJobs(result);
|
||||
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) {
|
||||
window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
||||
@@ -3898,9 +3986,10 @@ async function startSelectedUpload(explicitJobs) {
|
||||
const jobsToStart = scopedJobs.filter(job => isStartableQueueStatus(job.status));
|
||||
if (jobsToStart.length === 0) { uploading = false; updateQueueActionButtons(); return; }
|
||||
const originalStates = captureUploadJobStates(jobsToStart);
|
||||
let cleanupPreparation = { groups: [], rollbackStates: [] };
|
||||
|
||||
try {
|
||||
const cleanupPreparation = prepareSourceCleanup(jobsToStart);
|
||||
cleanupPreparation = prepareSourceCleanup(jobsToStart);
|
||||
jobsToStart.forEach(j => {
|
||||
j.status = 'queued';
|
||||
j.error = null;
|
||||
@@ -3921,6 +4010,7 @@ async function startSelectedUpload(explicitJobs) {
|
||||
};
|
||||
const result = await window.api.startUpload(uploadPayload);
|
||||
if (result && result.error) {
|
||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||
restoreUploadJobStates(originalStates);
|
||||
const error = sanitizeUploadControlError(result.error, 'Upload konnte nicht gestartet werden.');
|
||||
uploading = false;
|
||||
@@ -3938,6 +4028,7 @@ async function startSelectedUpload(explicitJobs) {
|
||||
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
restoreSourceCleanupStates(cleanupPreparation.rollbackStates);
|
||||
restoreUploadJobStates(originalStates);
|
||||
uploading = false;
|
||||
renderQueueTable();
|
||||
|
||||
@@ -68,10 +68,12 @@ let automationProbe = {
|
||||
history: [],
|
||||
uploadLog: [],
|
||||
paused: false,
|
||||
automationStatusSequence: [],
|
||||
historyError: '',
|
||||
addResult: null,
|
||||
addError: '',
|
||||
startResult: null,
|
||||
startError: '',
|
||||
saveSettingsError: '',
|
||||
dryScan: { files: [], reachable: true, trigger: 'test' },
|
||||
readCalls: { history: 0, uploadLog: 0, inspect: 0, status: 0, testScan: 0, reconcile: 0 },
|
||||
@@ -164,10 +166,12 @@ contextBridge.exposeInMainWorld('api', {
|
||||
history: Array.isArray(value.history) ? value.history : [],
|
||||
uploadLog: Array.isArray(value.uploadLog) ? value.uploadLog : [],
|
||||
paused: value.paused === true,
|
||||
automationStatusSequence: Array.isArray(value.automationStatusSequence) ? value.automationStatusSequence.map(entry => ({ ...entry })) : [],
|
||||
historyError: String(value.historyError || ''),
|
||||
addResult: value.addResult || null,
|
||||
addError: String(value.addError || ''),
|
||||
startResult: value.startResult || null,
|
||||
startError: String(value.startError || ''),
|
||||
saveSettingsError: String(value.saveSettingsError || ''),
|
||||
dryScan: value.dryScan || { files: [], reachable: true, trigger: 'test' },
|
||||
readCalls: { history: 0, uploadLog: 0, inspect: 0, status: 0, testScan: 0, reconcile: 0 },
|
||||
@@ -222,6 +226,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
},
|
||||
automationGetStatus() {
|
||||
automationProbe.readCalls.status++;
|
||||
if (automationProbe.automationStatusSequence.length > 0) return Promise.resolve(automationProbe.automationStatusSequence.shift());
|
||||
return Promise.resolve({ paused: automationProbe.paused });
|
||||
},
|
||||
folderMonitorTestScan() {
|
||||
@@ -252,6 +257,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
},
|
||||
startUpload(payload) {
|
||||
automationProbe.mutationCalls.push(['start', payload?.jobs?.length || 0]);
|
||||
if (automationProbe.startError) return Promise.reject(new Error(automationProbe.startError));
|
||||
return Promise.resolve(automationProbe.startResult || { started: true });
|
||||
},
|
||||
getFolderMonitorProbeCalls() { return folderMonitorProbeCalls; }
|
||||
@@ -735,6 +741,8 @@ contextBridge.exposeInMainWorld('api', {
|
||||
{ path: 'C:\\\\watch\\\\b.mkv', name: 'b.mkv', size: 3 * 1024 * 1024, mtimeMs: 2, filterMatched: true }
|
||||
];
|
||||
configureAtomicState(14998);
|
||||
selectedFiles = [{ path: 'C:\\\\manual\\\\unplanned.mkv', name: 'unplanned.mkv', size: 1 }];
|
||||
selectedUploadHosters = ['clouddrop.cc'];
|
||||
const atomicEvaluation = await evaluateAutomationCandidates(atomicCandidates, { dryRun: false, trigger: 'watcher' });
|
||||
const atomicResult = await applyAutomationEvaluation(atomicEvaluation);
|
||||
const selectedHostersAfterApply = selectedUploadHosters.slice();
|
||||
@@ -748,6 +756,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
deferred: config.globalSettings.folderMonitor.telemetry.deferred,
|
||||
queued: config.globalSettings.folderMonitor.telemetry.queued,
|
||||
currentJobCount: window.AutomationControl.countAutomaticQueueJobs(queueJobs),
|
||||
unplannedJobs: queueJobs.filter(job => job.fileName === 'unplanned.mkv').length,
|
||||
selectedHostersAfterApply,
|
||||
manualSelectionFilesAfterApply,
|
||||
plannedHostsBeforeRebuild,
|
||||
@@ -908,6 +917,174 @@ contextBridge.exposeInMainWorld('api', {
|
||||
uploading = false;
|
||||
const mainPauseResponses = { startRace, addRace, selectedStartRace, manualRace };
|
||||
|
||||
const cleanupState = job => ({
|
||||
token: job.sourceCleanupToken,
|
||||
required: clone(job.sourceCleanupRequiredHosters),
|
||||
completed: clone(job.sourceCleanupCompletedHosters),
|
||||
fingerprint: clone(job.sourceCleanupFingerprint)
|
||||
});
|
||||
const runCleanupRollbackCase = async (name, probe) => {
|
||||
configureAtomicState(0);
|
||||
config.globalSettings.deleteSourceAfterSuccessfulUpload = true;
|
||||
const filePath = 'C:\\\\cleanup\\\\' + name + '.mkv';
|
||||
const target = {
|
||||
...makePauseRaceJob(name + '.mkv'),
|
||||
id: 'cleanup-target-' + name,
|
||||
file: filePath,
|
||||
sourceCleanupToken: 'preview-token-' + name,
|
||||
sourceCleanupRequiredHosters: ['doodstream.com'],
|
||||
sourceCleanupCompletedHosters: [],
|
||||
sourceCleanupFingerprint: { size: 11, mtimeMs: 22 }
|
||||
};
|
||||
const sibling = {
|
||||
...makePauseRaceJob(name + '-done.mkv'),
|
||||
id: 'cleanup-sibling-' + name,
|
||||
file: filePath,
|
||||
fileName: name + '.mkv',
|
||||
hoster: 'voe.sx',
|
||||
status: 'done',
|
||||
sourceCleanupToken: 'done-token-' + name,
|
||||
sourceCleanupRequiredHosters: ['voe.sx'],
|
||||
sourceCleanupCompletedHosters: ['voe.sx'],
|
||||
sourceCleanupFingerprint: { size: 33, mtimeMs: 44 }
|
||||
};
|
||||
queueJobs = [target, sibling];
|
||||
uploading = true;
|
||||
rebuildJobIndex();
|
||||
window.api.configureAutomationProbe({ paused: false, ...probe });
|
||||
const before = JSON.stringify([cleanupState(target), cleanupState(sibling)]);
|
||||
const result = await startSelectedUpload([target]);
|
||||
const after = JSON.stringify([cleanupState(target), cleanupState(sibling)]);
|
||||
uploading = false;
|
||||
return {
|
||||
cleanupByteIdentical: after === before,
|
||||
targetStatus: target.status,
|
||||
siblingStatus: sibling.status,
|
||||
result
|
||||
};
|
||||
};
|
||||
const cleanupRollback = {
|
||||
paused: await runCleanupRollbackCase('paused', { addResult: { error: 'Automatik ist pausiert', added: 0 } }),
|
||||
error: await runCleanupRollbackCase('error', { addResult: { error: 'token=secret-value', added: 0 } }),
|
||||
exception: await runCleanupRollbackCase('exception', { addError: 'token=secret-value' }),
|
||||
unconfirmed: await runCleanupRollbackCase('unconfirmed', { addResult: { added: 0 } })
|
||||
};
|
||||
|
||||
configureAtomicState(0);
|
||||
selectedFiles = [];
|
||||
config.globalSettings.folderMonitor.autoStart = true;
|
||||
config.globalSettings.folderMonitor.hosters = ['doodstream.com'];
|
||||
config.globalSettings.folderMonitor.telemetry = { dateKey: new Date().toLocaleDateString('en-CA'), detected: 0, queued: 0, skipped: 0, deferred: 0 };
|
||||
hosterSettings = {};
|
||||
uploading = false;
|
||||
window.api.configureAutomationProbe({
|
||||
paused: false,
|
||||
automationStatusSequence: [{ paused: false }, { paused: true }]
|
||||
});
|
||||
const betweenFile = { path: 'C:\\\\between\\\\paused.mkv', name: 'paused.mkv', size: 1, mtimeMs: 1 };
|
||||
const betweenEvaluation = await evaluateAutomationCandidates([betweenFile], { dryRun: false, trigger: 'watcher' });
|
||||
const betweenResult = await applyAutomationEvaluation(betweenEvaluation);
|
||||
const betweenProbe = await window.api.getAutomationProbeState();
|
||||
const pauseBetweenApplyAndStart = {
|
||||
result: {
|
||||
ok: betweenResult.ok,
|
||||
error: betweenResult.error || null,
|
||||
warning: betweenResult.warning || null,
|
||||
admitted: betweenResult.admittedFiles.map(file => file.name)
|
||||
},
|
||||
status: queueJobs.find(job => job.file === betweenFile.path)?.status || null,
|
||||
queuedTelemetry: config.globalSettings.folderMonitor.telemetry.queued,
|
||||
startCalls: betweenProbe.mutationCalls.filter(call => call[0] === 'start').length
|
||||
};
|
||||
const runStartAcceptanceCase = async (name, probe) => {
|
||||
configureAtomicState(0);
|
||||
selectedFiles = [];
|
||||
selectedUploadHosters = ['doodstream.com'];
|
||||
const job = makePauseRaceJob(name + '.mkv');
|
||||
queueJobs = [job];
|
||||
uploading = false;
|
||||
rebuildJobIndex();
|
||||
window.api.configureAutomationProbe({ paused: false, ...probe });
|
||||
const originalAlert = showAppAlert;
|
||||
showAppAlert = async () => {};
|
||||
const result = await startUpload();
|
||||
showAppAlert = originalAlert;
|
||||
return { result, status: job.status, uploading };
|
||||
};
|
||||
const startAcceptance = {
|
||||
accepted: await runStartAcceptanceCase('accepted', { startResult: { started: true } }),
|
||||
unconfirmed: await runStartAcceptanceCase('unconfirmed', { startResult: { started: false } }),
|
||||
exception: await runStartAcceptanceCase('exception', { startError: 'token=secret-value' })
|
||||
};
|
||||
|
||||
const originalShowCopyToastForAutomation = showCopyToast;
|
||||
const feedbackMessages = [];
|
||||
showCopyToast = message => { feedbackMessages.push(String(message)); };
|
||||
configureAtomicState(0);
|
||||
selectedFiles = [];
|
||||
config.globalSettings.folderMonitor.hosters = ['doodstream.com'];
|
||||
config.globalSettings.folderMonitor.autoStart = false;
|
||||
hosterSettings = {};
|
||||
window.api.configureAutomationProbe({ paused: false, saveSettingsError: 'token=telemetry-secret' });
|
||||
const watcherWarningResult = await handleFolderMonitorFiles([
|
||||
{ path: 'C:\\\\feedback\\\\watcher-warning.mkv', name: 'watcher-warning.mkv', size: 1, mtimeMs: 1 }
|
||||
]);
|
||||
const watcherWarningFeedback = feedbackMessages.splice(0);
|
||||
failedConfigWriteOperations.length = 0;
|
||||
configureAtomicState(0);
|
||||
selectedFiles = [];
|
||||
config.globalSettings.folderMonitor.hosters = ['doodstream.com'];
|
||||
config.globalSettings.folderMonitor.autoStart = true;
|
||||
hosterSettings = {};
|
||||
uploading = true;
|
||||
window.api.configureAutomationProbe({ paused: false, addResult: { error: 'token=secret-value', added: 0 } });
|
||||
const watcherErrorResult = await handleFolderMonitorFiles([
|
||||
{ path: 'C:\\\\feedback\\\\watcher-error.mkv', name: 'watcher-error.mkv', size: 1, mtimeMs: 1 }
|
||||
]);
|
||||
const watcherErrorFeedback = feedbackMessages.splice(0);
|
||||
uploading = false;
|
||||
configureAtomicState(0);
|
||||
selectedFiles = [];
|
||||
config.globalSettings.folderMonitor.hosters = [];
|
||||
config.globalSettings.folderMonitor.autoStart = false;
|
||||
hosterSettings = {};
|
||||
const modalWarningFile = { path: 'C:\\\\feedback\\\\modal-warning.mkv', name: 'modal-warning.mkv', size: 1, mtimeMs: 1 };
|
||||
_pendingFiles = [modalWarningFile];
|
||||
_pendingImportInspection = { candidateCount: 1, duplicateCount: 0, unavailableCount: 0, accepted: [modalWarningFile] };
|
||||
_pendingImportInspections = 0;
|
||||
_pendingFolderMonitorAutoStart.clear();
|
||||
_pendingFolderMonitorAutoStart.set(modalWarningFile.path, false);
|
||||
const modalWarningInput = document.createElement('input');
|
||||
modalWarningInput.type = 'checkbox';
|
||||
modalWarningInput.dataset.hosterModal = 'doodstream.com';
|
||||
modalWarningInput.checked = true;
|
||||
document.getElementById('hosterModalList').replaceChildren(modalWarningInput);
|
||||
document.getElementById('hosterModal').style.display = 'flex';
|
||||
window.api.configureAutomationProbe({ paused: false, saveSettingsError: 'token=telemetry-secret' });
|
||||
const modalWarningResult = await applyHosterSelection();
|
||||
const modalWarningFeedback = feedbackMessages.splice(0);
|
||||
failedConfigWriteOperations.length = 0;
|
||||
showCopyToast = originalShowCopyToastForAutomation;
|
||||
const fulfilledFeedback = {
|
||||
watcherWarning: {
|
||||
result: { ok: watcherWarningResult.ok, warning: watcherWarningResult.warning || null, error: watcherWarningResult.error || null },
|
||||
feedback: watcherWarningFeedback
|
||||
},
|
||||
watcherError: {
|
||||
result: { ok: watcherErrorResult.ok, warning: watcherErrorResult.warning || null, error: watcherErrorResult.error || null },
|
||||
feedback: watcherErrorFeedback
|
||||
},
|
||||
modalWarning: {
|
||||
result: modalWarningResult,
|
||||
feedback: modalWarningFeedback,
|
||||
pending: _pendingFiles.length,
|
||||
markers: _pendingFolderMonitorAutoStart.size,
|
||||
modalOpen: document.getElementById('hosterModal').style.display === 'flex',
|
||||
queueStatus: queueJobs.find(job => job.file === modalWarningFile.path)?.status || null
|
||||
},
|
||||
secretExposed: JSON.stringify({ watcherWarningFeedback, watcherErrorFeedback, modalWarningFeedback, modalWarningResult }).includes('secret')
|
||||
};
|
||||
|
||||
const runInjectionCase = async (name, probe) => {
|
||||
configureAtomicState(0);
|
||||
selectedFiles = [];
|
||||
@@ -985,7 +1162,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
startCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'start').length,
|
||||
injectCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'inject').length
|
||||
};
|
||||
return { dry, manualTest, historyEvidence, pendingDedup, manualHostTransactional, atomic, status, persistedQueueExactness, stale, replannedEligibility, mainPauseResponses, injectionOutcomes, paused };
|
||||
return { dry, manualTest, historyEvidence, pendingDedup, manualHostTransactional, atomic, status, persistedQueueExactness, stale, replannedEligibility, mainPauseResponses, cleanupRollback, pauseBetweenApplyAndStart, startAcceptance, fulfilledFeedback, injectionOutcomes, paused };
|
||||
})()`;
|
||||
const onlineBackupBehaviorScript = `(async () => {
|
||||
const ids = {
|
||||
@@ -1421,8 +1598,9 @@ app.whenReady().then(async () => {
|
||||
deferred: 1,
|
||||
queued: 1,
|
||||
currentJobCount: 15000,
|
||||
unplannedJobs: 0,
|
||||
selectedHostersAfterApply: ['clouddrop.cc'],
|
||||
manualSelectionFilesAfterApply: [],
|
||||
manualSelectionFilesAfterApply: ['unplanned.mkv'],
|
||||
plannedHostsBeforeRebuild: ['byse.sx', 'vidmoly.me'],
|
||||
hostsAfterRebuild: ['byse.sx', 'vidmoly.me']
|
||||
});
|
||||
@@ -1472,6 +1650,79 @@ app.whenReady().then(async () => {
|
||||
uploading: true
|
||||
}
|
||||
});
|
||||
assert.deepEqual(result.automationPipeline.cleanupRollback, {
|
||||
paused: {
|
||||
cleanupByteIdentical: true,
|
||||
targetStatus: 'preview',
|
||||
siblingStatus: 'done',
|
||||
result: { ok: false, error: 'Automatik ist pausiert' }
|
||||
},
|
||||
error: {
|
||||
cleanupByteIdentical: true,
|
||||
targetStatus: 'preview',
|
||||
siblingStatus: 'done',
|
||||
result: { ok: false, error: 'Jobs konnten nicht hinzugefügt werden.' }
|
||||
},
|
||||
exception: {
|
||||
cleanupByteIdentical: true,
|
||||
targetStatus: 'preview',
|
||||
siblingStatus: 'done',
|
||||
result: { ok: false, error: 'Jobs konnten nicht hinzugefügt werden.' }
|
||||
},
|
||||
unconfirmed: {
|
||||
cleanupByteIdentical: true,
|
||||
targetStatus: 'preview',
|
||||
siblingStatus: 'done',
|
||||
result: { ok: false, error: 'Jobs wurden nicht vollständig hinzugefügt.' }
|
||||
}
|
||||
});
|
||||
assert.deepEqual(result.automationPipeline.pauseBetweenApplyAndStart, {
|
||||
result: {
|
||||
ok: false,
|
||||
error: 'Automatik ist pausiert',
|
||||
warning: null,
|
||||
admitted: []
|
||||
},
|
||||
status: 'preview',
|
||||
queuedTelemetry: 0,
|
||||
startCalls: 0
|
||||
});
|
||||
assert.deepEqual(result.automationPipeline.startAcceptance, {
|
||||
accepted: {
|
||||
result: { ok: true },
|
||||
status: 'queued',
|
||||
uploading: true
|
||||
},
|
||||
unconfirmed: {
|
||||
result: { ok: false, error: 'Upload wurde nicht bestätigt.' },
|
||||
status: 'preview',
|
||||
uploading: false
|
||||
},
|
||||
exception: {
|
||||
result: { ok: false, error: 'Upload konnte nicht gestartet werden.' },
|
||||
status: 'preview',
|
||||
uploading: false
|
||||
}
|
||||
});
|
||||
assert.deepEqual(result.automationPipeline.fulfilledFeedback, {
|
||||
watcherWarning: {
|
||||
result: { ok: false, warning: 'Telemetrie konnte nicht gespeichert werden.', error: null },
|
||||
feedback: ['Telemetrie konnte nicht gespeichert werden.']
|
||||
},
|
||||
watcherError: {
|
||||
result: { ok: false, warning: null, error: 'Jobs konnten nicht hinzugefügt werden.' },
|
||||
feedback: ['Jobs konnten nicht hinzugefügt werden.']
|
||||
},
|
||||
modalWarning: {
|
||||
result: { ok: false, warning: 'Telemetrie konnte nicht gespeichert werden.', error: null },
|
||||
feedback: ['Telemetrie konnte nicht gespeichert werden.'],
|
||||
pending: 0,
|
||||
markers: 0,
|
||||
modalOpen: false,
|
||||
queueStatus: 'preview'
|
||||
},
|
||||
secretExposed: false
|
||||
});
|
||||
assert.deepEqual(result.automationPipeline.injectionOutcomes, {
|
||||
pausedInjection: {
|
||||
ok: false,
|
||||
|
||||
Reference in New Issue
Block a user