fix: harden automatic queue admission
This commit is contained in:
+215
-59
@@ -442,16 +442,28 @@ function flattenAutomationHistoryRows(history) {
|
|||||||
const rows = [];
|
const rows = [];
|
||||||
for (const entry of Array.isArray(history) ? history : []) {
|
for (const entry of Array.isArray(history) ? history : []) {
|
||||||
if (!Array.isArray(entry?.files)) {
|
if (!Array.isArray(entry?.files)) {
|
||||||
rows.push(entry);
|
const status = String(entry?.status || '').toLowerCase();
|
||||||
|
const link = entry?.download_url || entry?.embed_url || entry?.link || entry?.file_code || '';
|
||||||
|
if (['done', 'success', 'completed'].includes(status) || link) rows.push({ ...entry, link });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const file of entry.files) {
|
for (const file of entry.files) {
|
||||||
|
const results = Array.isArray(file?.results) ? file.results : [];
|
||||||
|
for (const result of results) {
|
||||||
|
const status = String(result?.status || '').toLowerCase();
|
||||||
|
const link = result?.download_url || result?.embed_url || result?.link || result?.file_code || '';
|
||||||
|
if (!['done', 'success', 'completed'].includes(status) && !link) continue;
|
||||||
rows.push({
|
rows.push({
|
||||||
|
...result,
|
||||||
path: file?.path || file?.file || '',
|
path: file?.path || file?.file || '',
|
||||||
fileName: file?.fileName || file?.filename || file?.name || ''
|
fileName: file?.fileName || file?.filename || file?.name || '',
|
||||||
|
status,
|
||||||
|
hoster: result?.hoster || '',
|
||||||
|
link
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,9 +514,16 @@ function createAutomationStatusSnapshot() {
|
|||||||
|
|
||||||
async function evaluateAutomationCandidates(files, options = {}) {
|
async function evaluateAutomationCandidates(files, options = {}) {
|
||||||
const source = Array.isArray(files) ? files : [];
|
const source = Array.isArray(files) ? files : [];
|
||||||
const candidates = source.map(normalizeAutomationCandidate);
|
const normalizedCandidates = source.map(normalizeAutomationCandidate);
|
||||||
|
const candidateMap = new Map();
|
||||||
|
for (const candidate of normalizedCandidates) {
|
||||||
|
const key = normalizeAutomationPath(candidate.path);
|
||||||
|
if (key && !candidateMap.has(key)) candidateMap.set(key, candidate);
|
||||||
|
}
|
||||||
|
const candidates = [...candidateMap.values()];
|
||||||
const matched = candidates.filter(candidate => candidate.path && candidate.filterMatched);
|
const matched = candidates.filter(candidate => candidate.path && candidate.filterMatched);
|
||||||
const folderSettings = config.globalSettings?.folderMonitor || {};
|
const folderSettings = config.globalSettings?.folderMonitor || {};
|
||||||
|
const ownedPendingPaths = new Set((Array.isArray(options.ownedPendingPaths) ? options.ownedPendingPaths : []).map(normalizeAutomationPath));
|
||||||
const selectedHosters = Array.from(new Set((Array.isArray(options.selectedHosters) ? options.selectedHosters : folderSettings.hosters || [])
|
const selectedHosters = Array.from(new Set((Array.isArray(options.selectedHosters) ? options.selectedHosters : folderSettings.hosters || [])
|
||||||
.map(value => String(value || '').trim())
|
.map(value => String(value || '').trim())
|
||||||
.filter(Boolean)));
|
.filter(Boolean)));
|
||||||
@@ -520,7 +539,10 @@ async function evaluateAutomationCandidates(files, options = {}) {
|
|||||||
});
|
});
|
||||||
const processedPaths = new Set(processed.processedPaths.map(normalizeAutomationPath));
|
const processedPaths = new Set(processed.processedPaths.map(normalizeAutomationPath));
|
||||||
const unprocessed = matched.filter(candidate => !processedPaths.has(normalizeAutomationPath(candidate.path)));
|
const unprocessed = matched.filter(candidate => !processedPaths.has(normalizeAutomationPath(candidate.path)));
|
||||||
const inspection = await window.api.inspectImportFiles(unprocessed, []);
|
const inspection = await window.api.inspectImportFiles(
|
||||||
|
unprocessed,
|
||||||
|
_pendingFiles.filter(file => !ownedPendingPaths.has(normalizeAutomationPath(file.path))).map(file => file.path)
|
||||||
|
);
|
||||||
const metadata = new Map(unprocessed.map(candidate => [normalizeAutomationPath(candidate.path), candidate]));
|
const metadata = new Map(unprocessed.map(candidate => [normalizeAutomationPath(candidate.path), candidate]));
|
||||||
const accepted = (Array.isArray(inspection?.accepted) ? inspection.accepted : []).map(file => ({
|
const accepted = (Array.isArray(inspection?.accepted) ? inspection.accepted : []).map(file => ({
|
||||||
...(metadata.get(normalizeAutomationPath(file?.path)) || {}),
|
...(metadata.get(normalizeAutomationPath(file?.path)) || {}),
|
||||||
@@ -568,6 +590,7 @@ async function evaluateAutomationCandidates(files, options = {}) {
|
|||||||
queueJobCount: currentJobCount,
|
queueJobCount: currentJobCount,
|
||||||
queueLimitJobs: normalizedSettings.queueLimitJobs,
|
queueLimitJobs: normalizedSettings.queueLimitJobs,
|
||||||
selectedHosters,
|
selectedHosters,
|
||||||
|
ownedPendingPaths: [...ownedPendingPaths],
|
||||||
candidates: plannedCandidates,
|
candidates: plannedCandidates,
|
||||||
admittedFiles,
|
admittedFiles,
|
||||||
deferredFiles,
|
deferredFiles,
|
||||||
@@ -598,7 +621,8 @@ function createAutomationPreviewJob(file, hoster) {
|
|||||||
result: null,
|
result: null,
|
||||||
attempt: 0,
|
attempt: 0,
|
||||||
maxAttempts: 0,
|
maxAttempts: 0,
|
||||||
link: ''
|
link: '',
|
||||||
|
automationAdmission: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,8 +637,10 @@ async function persistAutomationTelemetry(delta) {
|
|||||||
config.globalSettings = nextSettings;
|
config.globalSettings = nextSettings;
|
||||||
try {
|
try {
|
||||||
await saveGlobalSettingsTracked(nextSettings);
|
await saveGlobalSettingsTracked(nextSettings);
|
||||||
} catch {}
|
return { telemetry, warning: '' };
|
||||||
return telemetry;
|
} catch {
|
||||||
|
return { telemetry, warning: 'Telemetrie konnte nicht gespeichert werden.' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyAutomationEvaluation(evaluation) {
|
async function applyAutomationEvaluation(evaluation) {
|
||||||
@@ -626,9 +652,30 @@ async function applyAutomationEvaluation(evaluation) {
|
|||||||
if (paused && !manualPreview) {
|
if (paused && !manualPreview) {
|
||||||
return freezeAutomationValue({ admittedFiles: [], deferredFiles: evaluation.candidates || [], paused: true, dryRun: false });
|
return freezeAutomationValue({ admittedFiles: [], deferredFiles: evaluation.candidates || [], paused: true, dryRun: false });
|
||||||
}
|
}
|
||||||
const currentPaths = new Set([...queueJobs.map(job => job.file), ...selectedFiles.map(file => file.path)].map(normalizeAutomationPath));
|
const selectedHosters = Array.from(new Set((manualPreview
|
||||||
const candidates = evaluation.candidates.filter(candidate => !currentPaths.has(normalizeAutomationPath(candidate.path)));
|
? evaluation.selectedHosters
|
||||||
if (evaluation.selectedHosters.length === 0) {
|
: config.globalSettings?.folderMonitor?.hosters || [])
|
||||||
|
.map(value => String(value || '').trim())
|
||||||
|
.filter(Boolean)));
|
||||||
|
const replannedCandidates = evaluation.candidates.map(file => {
|
||||||
|
const eligibleHosters = window.ImportPreflight.getEligibleImportHosters(file, selectedHosters, hosterSettings);
|
||||||
|
return {
|
||||||
|
path: file.path,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
mtimeMs: file.mtimeMs,
|
||||||
|
eligibleHosters,
|
||||||
|
eligibleJobCount: eligibleHosters.length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const ownedPendingPaths = new Set(Array.isArray(evaluation.ownedPendingPaths) ? evaluation.ownedPendingPaths : []);
|
||||||
|
const currentPaths = new Set([
|
||||||
|
...queueJobs.map(job => job.file),
|
||||||
|
...selectedFiles.map(file => file.path),
|
||||||
|
..._pendingFiles.filter(file => !ownedPendingPaths.has(normalizeAutomationPath(file.path))).map(file => file.path)
|
||||||
|
].map(normalizeAutomationPath));
|
||||||
|
const candidates = replannedCandidates.filter(candidate => !currentPaths.has(normalizeAutomationPath(candidate.path)));
|
||||||
|
if (selectedHosters.length === 0) {
|
||||||
if (candidates.length > 0) {
|
if (candidates.length > 0) {
|
||||||
_pendingFiles.push(...candidates.map(file => ({ path: file.path, name: file.name, size: file.size, mtimeMs: file.mtimeMs })));
|
_pendingFiles.push(...candidates.map(file => ({ path: file.path, name: file.name, size: file.size, mtimeMs: file.mtimeMs })));
|
||||||
mergePendingImportInspection({
|
mergePendingImportInspection({
|
||||||
@@ -657,11 +704,7 @@ async function applyAutomationEvaluation(evaluation) {
|
|||||||
if (admittedFiles.length === 0) {
|
if (admittedFiles.length === 0) {
|
||||||
return freezeAutomationValue({ admittedFiles: [], deferredFiles, paused, dryRun: false });
|
return freezeAutomationValue({ admittedFiles: [], deferredFiles, paused, dryRun: false });
|
||||||
}
|
}
|
||||||
const filePaths = new Set(admittedFiles.map(file => file.path));
|
|
||||||
const newJobs = admittedFiles.flatMap(file => file.eligibleHosters.map(hoster => createAutomationPreviewJob(file, hoster)));
|
const newJobs = admittedFiles.flatMap(file => file.eligibleHosters.map(hoster => createAutomationPreviewJob(file, hoster)));
|
||||||
selectedUploadHosters = evaluation.selectedHosters.slice();
|
|
||||||
clearDedupKeysForPaths(filePaths);
|
|
||||||
selectedFiles.push(...admittedFiles.map(file => ({ path: file.path, name: file.name, size: file.size })));
|
|
||||||
queueJobs.push(...newJobs);
|
queueJobs.push(...newJobs);
|
||||||
rebuildJobIndex();
|
rebuildJobIndex();
|
||||||
_queueStatsCache = null;
|
_queueStatsCache = null;
|
||||||
@@ -671,35 +714,59 @@ async function applyAutomationEvaluation(evaluation) {
|
|||||||
updateStatusBar();
|
updateStatusBar();
|
||||||
updateStatsPanel();
|
updateStatsPanel();
|
||||||
persistQueueStateSoon(true);
|
persistQueueStateSoon(true);
|
||||||
await persistAutomationTelemetry({
|
|
||||||
detected: evaluation.summary.found,
|
|
||||||
queued: admittedFiles.length,
|
|
||||||
skipped: evaluation.summary.alreadyProcessed + evaluation.summary.unavailable,
|
|
||||||
deferred: deferredFiles.length,
|
|
||||||
lastDetectedName: admittedFiles.at(-1)?.name || ''
|
|
||||||
});
|
|
||||||
const action = paused && manualPreview ? 'queue' : resolveFolderMonitorQueueAction({
|
const action = paused && manualPreview ? 'queue' : resolveFolderMonitorQueueAction({
|
||||||
autoStart: config.globalSettings?.folderMonitor?.autoStart === true,
|
autoStart: config.globalSettings?.folderMonitor?.autoStart === true,
|
||||||
uploading,
|
uploading,
|
||||||
healthCheckRunning
|
healthCheckRunning
|
||||||
});
|
});
|
||||||
if (action === 'inject' && !(await isAutomationPaused())) {
|
if (action === 'inject') {
|
||||||
|
if (await isAutomationPaused()) {
|
||||||
|
return freezeAutomationValue({ ok: false, error: 'Automatik ist pausiert', warning: null, admittedFiles: [], deferredFiles, paused: true, dryRun: false });
|
||||||
|
}
|
||||||
const cleanupPreparation = prepareSourceCleanup(newJobs);
|
const cleanupPreparation = prepareSourceCleanup(newJobs);
|
||||||
try {
|
try {
|
||||||
const result = await window.api.addJobsToBatch({
|
const result = await window.api.addJobsToBatch({
|
||||||
jobs: newJobs.map(serializeUploadJob),
|
jobs: newJobs.map(serializeUploadJob),
|
||||||
sourceCleanupGroups: cleanupPreparation.groups
|
sourceCleanupGroups: cleanupPreparation.groups
|
||||||
});
|
});
|
||||||
if (!result?.error) newJobs.forEach(job => { job.status = 'queued'; });
|
if (result?.error) {
|
||||||
|
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) {
|
||||||
|
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);
|
_markSkippedJobs(result);
|
||||||
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
||||||
} catch {}
|
} catch {
|
||||||
|
return freezeAutomationValue({ ok: false, error: 'Jobs konnten nicht hinzugefügt werden.', warning: null, admittedFiles: [], deferredFiles, paused: false, dryRun: false });
|
||||||
|
}
|
||||||
renderQueueTable();
|
renderQueueTable();
|
||||||
persistQueueStateSoon(true);
|
persistQueueStateSoon(true);
|
||||||
} else if (action === 'start') {
|
} else if (action === 'start') {
|
||||||
await startUpload();
|
const result = await startUpload();
|
||||||
|
if (result?.ok === false) {
|
||||||
|
return freezeAutomationValue({ ok: false, error: result.error, warning: null, admittedFiles: [], deferredFiles, paused: /pausiert/i.test(result.error), dryRun: false });
|
||||||
}
|
}
|
||||||
return freezeAutomationValue({ admittedFiles, deferredFiles, paused, dryRun: false, plannedJobs: admission.plannedJobs });
|
}
|
||||||
|
const telemetryResult = await persistAutomationTelemetry({
|
||||||
|
detected: evaluation.summary.found,
|
||||||
|
queued: admittedFiles.length,
|
||||||
|
skipped: evaluation.summary.alreadyProcessed + evaluation.summary.unavailable,
|
||||||
|
deferred: deferredFiles.length,
|
||||||
|
lastDetectedName: admittedFiles.at(-1)?.name || ''
|
||||||
|
});
|
||||||
|
return freezeAutomationValue({
|
||||||
|
ok: telemetryResult.warning === '',
|
||||||
|
error: null,
|
||||||
|
warning: telemetryResult.warning || null,
|
||||||
|
admittedFiles,
|
||||||
|
deferredFiles,
|
||||||
|
paused,
|
||||||
|
dryRun: false,
|
||||||
|
plannedJobs: admission.plannedJobs
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runFolderMonitorTestScan() {
|
async function runFolderMonitorTestScan() {
|
||||||
@@ -1433,15 +1500,17 @@ async function applyHosterSelection() {
|
|||||||
selectedUploadHosters = Array.from(document.querySelectorAll('input[data-hoster-modal]:checked'))
|
selectedUploadHosters = Array.from(document.querySelectorAll('input[data-hoster-modal]:checked'))
|
||||||
.map(input => input.dataset.hosterModal);
|
.map(input => input.dataset.hosterModal);
|
||||||
const pendingFiles = _pendingFiles.slice();
|
const pendingFiles = _pendingFiles.slice();
|
||||||
const automationFiles = pendingFiles.filter(file => _pendingFolderMonitorAutoStart.has(file.path));
|
const automationPathKeys = new Set([..._pendingFolderMonitorAutoStart.keys()].map(normalizeAutomationPath));
|
||||||
const regularFiles = pendingFiles.filter(file => !_pendingFolderMonitorAutoStart.has(file.path));
|
const automationFiles = pendingFiles.filter(file => automationPathKeys.has(normalizeAutomationPath(file.path)));
|
||||||
|
const regularFiles = pendingFiles.filter(file => !automationPathKeys.has(normalizeAutomationPath(file.path)));
|
||||||
const admittedFiles = regularFiles.filter(file => window.ImportPreflight
|
const admittedFiles = regularFiles.filter(file => window.ImportPreflight
|
||||||
.getEligibleImportHosters(file, selectedUploadHosters, hosterSettings).length > 0);
|
.getEligibleImportHosters(file, selectedUploadHosters, hosterSettings).length > 0);
|
||||||
const pendingPaths = new Set(admittedFiles.map(f => f.path));
|
const pendingPaths = new Set(admittedFiles.map(f => f.path));
|
||||||
|
let regularInjectionFailure = null;
|
||||||
if (admittedFiles.length > 0) {
|
if (admittedFiles.length > 0) {
|
||||||
selectedFiles.push(...admittedFiles);
|
selectedFiles.push(...admittedFiles);
|
||||||
}
|
}
|
||||||
_pendingFiles = [];
|
_pendingFiles = automationFiles.slice();
|
||||||
clearDedupKeysForPaths(pendingPaths);
|
clearDedupKeysForPaths(pendingPaths);
|
||||||
renderHosterSummary();
|
renderHosterSummary();
|
||||||
if (pendingPaths.size > 0) buildQueuePreview();
|
if (pendingPaths.size > 0) buildQueuePreview();
|
||||||
@@ -1454,27 +1523,64 @@ async function applyHosterSelection() {
|
|||||||
jobs: newJobs.map(serializeUploadJob),
|
jobs: newJobs.map(serializeUploadJob),
|
||||||
sourceCleanupGroups: cleanupPreparation.groups
|
sourceCleanupGroups: cleanupPreparation.groups
|
||||||
});
|
});
|
||||||
if (!result?.error) newJobs.forEach(job => { job.status = 'queued'; });
|
if (result?.error) {
|
||||||
|
regularInjectionFailure = sanitizeUploadControlError(result.error, 'Jobs konnten nicht hinzugefügt werden.');
|
||||||
|
} else if ((Number(result?.added) || 0) !== newJobs.length) {
|
||||||
|
regularInjectionFailure = 'Jobs wurden nicht vollständig hinzugefügt.';
|
||||||
|
} else {
|
||||||
|
newJobs.forEach(job => { job.status = 'queued'; });
|
||||||
_markSkippedJobs(result);
|
_markSkippedJobs(result);
|
||||||
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
||||||
} catch {}
|
}
|
||||||
|
} catch {
|
||||||
|
regularInjectionFailure = 'Jobs konnten nicht hinzugefügt werden.';
|
||||||
|
}
|
||||||
renderQueueTable();
|
renderQueueTable();
|
||||||
persistQueueStateSoon(true);
|
persistQueueStateSoon(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateUploadView();
|
updateUploadView();
|
||||||
persistQueueStateSoon(true);
|
persistQueueStateSoon(true);
|
||||||
|
if (regularInjectionFailure) {
|
||||||
|
showCopyToast(regularInjectionFailure, 6500);
|
||||||
|
return { ok: false, error: regularInjectionFailure };
|
||||||
|
}
|
||||||
const selectedHosters = selectedUploadHosters.slice();
|
const selectedHosters = selectedUploadHosters.slice();
|
||||||
_pendingFolderMonitorAutoStart.clear();
|
|
||||||
_pendingImportInspection = null;
|
|
||||||
document.getElementById('hosterModal').style.display = 'none';
|
|
||||||
if (automationFiles.length > 0) {
|
if (automationFiles.length > 0) {
|
||||||
|
try {
|
||||||
const evaluation = await evaluateAutomationCandidates(automationFiles, {
|
const evaluation = await evaluateAutomationCandidates(automationFiles, {
|
||||||
dryRun: false,
|
dryRun: false,
|
||||||
trigger: 'manual-host',
|
trigger: 'manual-host',
|
||||||
selectedHosters
|
selectedHosters,
|
||||||
|
ownedPendingPaths: automationFiles.map(file => file.path)
|
||||||
});
|
});
|
||||||
await applyAutomationEvaluation(evaluation);
|
const result = await applyAutomationEvaluation(evaluation);
|
||||||
|
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)));
|
||||||
|
for (const path of [..._pendingFolderMonitorAutoStart.keys()]) {
|
||||||
|
if (admittedPaths.has(normalizeAutomationPath(path))) _pendingFolderMonitorAutoStart.delete(path);
|
||||||
|
}
|
||||||
|
if (_pendingImportInspection) {
|
||||||
|
_pendingImportInspection = {
|
||||||
|
..._pendingImportInspection,
|
||||||
|
accepted: (_pendingImportInspection.accepted || []).filter(file => !admittedPaths.has(normalizeAutomationPath(file.path)))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
const failure = { ok: false, error: 'Automatische Aufnahme konnte nicht abgeschlossen werden.' };
|
||||||
|
showCopyToast(failure.error, 6500);
|
||||||
|
document.getElementById('hosterModal').style.display = 'flex';
|
||||||
|
renderImportPlanSummary();
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_pendingFiles.length === 0) {
|
||||||
|
_pendingImportInspection = null;
|
||||||
|
document.getElementById('hosterModal').style.display = 'none';
|
||||||
|
} else {
|
||||||
|
document.getElementById('hosterModal').style.display = 'flex';
|
||||||
|
renderImportPlanSummary();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1550,6 +1656,7 @@ function restoreQueueStateFromConfig() {
|
|||||||
sourceCleanupRequiredHosters: Array.isArray(job.sourceCleanupRequiredHosters) ? [...job.sourceCleanupRequiredHosters] : [],
|
sourceCleanupRequiredHosters: Array.isArray(job.sourceCleanupRequiredHosters) ? [...job.sourceCleanupRequiredHosters] : [],
|
||||||
sourceCleanupCompletedHosters: Array.isArray(job.sourceCleanupCompletedHosters) ? [...job.sourceCleanupCompletedHosters] : [],
|
sourceCleanupCompletedHosters: Array.isArray(job.sourceCleanupCompletedHosters) ? [...job.sourceCleanupCompletedHosters] : [],
|
||||||
sourceCleanupFingerprint: job.sourceCleanupFingerprint || null,
|
sourceCleanupFingerprint: job.sourceCleanupFingerprint || null,
|
||||||
|
automationAdmission: job.automationAdmission === true,
|
||||||
attempt: 0,
|
attempt: 0,
|
||||||
maxAttempts: job.maxAttempts || 0,
|
maxAttempts: job.maxAttempts || 0,
|
||||||
link: '',
|
link: '',
|
||||||
@@ -1576,7 +1683,7 @@ function buildPersistedQueueState() {
|
|||||||
const selectedFileMap = new Map(selectedFiles.map(file => [file.path, file]));
|
const selectedFileMap = new Map(selectedFiles.map(file => [file.path, file]));
|
||||||
|
|
||||||
for (const job of persistableJobs) {
|
for (const job of persistableJobs) {
|
||||||
if (job.file && !selectedFileMap.has(job.file)) {
|
if (job.file && job.automationAdmission !== true && !selectedFileMap.has(job.file)) {
|
||||||
selectedFileMap.set(job.file, {
|
selectedFileMap.set(job.file, {
|
||||||
path: job.file,
|
path: job.file,
|
||||||
name: job.fileName,
|
name: job.fileName,
|
||||||
@@ -1627,6 +1734,7 @@ function buildPersistedQueueState() {
|
|||||||
sourceCleanupRequiredHosters: Array.isArray(job.sourceCleanupRequiredHosters) ? [...job.sourceCleanupRequiredHosters] : [],
|
sourceCleanupRequiredHosters: Array.isArray(job.sourceCleanupRequiredHosters) ? [...job.sourceCleanupRequiredHosters] : [],
|
||||||
sourceCleanupCompletedHosters: Array.isArray(job.sourceCleanupCompletedHosters) ? [...job.sourceCleanupCompletedHosters] : [],
|
sourceCleanupCompletedHosters: Array.isArray(job.sourceCleanupCompletedHosters) ? [...job.sourceCleanupCompletedHosters] : [],
|
||||||
sourceCleanupFingerprint: job.sourceCleanupFingerprint || null,
|
sourceCleanupFingerprint: job.sourceCleanupFingerprint || null,
|
||||||
|
automationAdmission: job.automationAdmission === true,
|
||||||
maxAttempts: job.maxAttempts || 0
|
maxAttempts: job.maxAttempts || 0
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -1999,7 +2107,7 @@ function suppressPreviewKeysStillSelected(keys) {
|
|||||||
// Build preview jobs from selected files x selected hosters (before upload starts)
|
// Build preview jobs from selected files x selected hosters (before upload starts)
|
||||||
function buildQueuePreview() {
|
function buildQueuePreview() {
|
||||||
const hosters = getSelectedHosters();
|
const hosters = getSelectedHosters();
|
||||||
queueJobs = queueJobs.filter(j => j.status !== 'preview');
|
queueJobs = queueJobs.filter(j => j.status !== 'preview' || j.automationAdmission === true);
|
||||||
|
|
||||||
if (hosters.length > 0) {
|
if (hosters.length > 0) {
|
||||||
const existingKeys = new Set();
|
const existingKeys = new Set();
|
||||||
@@ -3604,6 +3712,24 @@ function serializeUploadJob(job) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function captureUploadJobStates(jobs) {
|
||||||
|
return jobs.map(job => [job, { ...job }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreUploadJobStates(states) {
|
||||||
|
for (const [job, state] of states) {
|
||||||
|
for (const key of Object.keys(job)) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(state, key)) delete job[key];
|
||||||
|
}
|
||||||
|
Object.assign(job, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeUploadControlError(error, fallback) {
|
||||||
|
const message = String(error?.message || error || '');
|
||||||
|
return /pausiert/i.test(message) ? 'Automatik ist pausiert' : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
async function startUpload(opts) {
|
async function startUpload(opts) {
|
||||||
if (uploading) return;
|
if (uploading) return;
|
||||||
if (await isAutomationPaused()) return false;
|
if (await isAutomationPaused()) return false;
|
||||||
@@ -3627,6 +3753,7 @@ async function startUpload(opts) {
|
|||||||
|
|
||||||
const jobsToStart = queueJobs.filter((job) => isStartableQueueStatus(job.status));
|
const jobsToStart = queueJobs.filter((job) => isStartableQueueStatus(job.status));
|
||||||
if (jobsToStart.length === 0) { uploading = false; updateQueueActionButtons(); return; }
|
if (jobsToStart.length === 0) { uploading = false; updateQueueActionButtons(); return; }
|
||||||
|
const originalStates = captureUploadJobStates(jobsToStart);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cleanupPreparation = prepareSourceCleanup(jobsToStart);
|
const cleanupPreparation = prepareSourceCleanup(jobsToStart);
|
||||||
@@ -3652,23 +3779,32 @@ async function startUpload(opts) {
|
|||||||
sourceCleanupGroups: cleanupPreparation.groups
|
sourceCleanupGroups: cleanupPreparation.groups
|
||||||
};
|
};
|
||||||
const result = await window.api.startUpload(uploadPayload);
|
const result = await window.api.startUpload(uploadPayload);
|
||||||
|
if (result && result.error) {
|
||||||
|
restoreUploadJobStates(originalStates);
|
||||||
|
const error = sanitizeUploadControlError(result.error, 'Upload konnte nicht gestartet werden.');
|
||||||
|
uploading = false;
|
||||||
|
renderQueueTable();
|
||||||
|
updateQueueActionButtons();
|
||||||
|
updateStatusBar();
|
||||||
|
await showAppAlert(error, 'Upload-Start fehlgeschlagen');
|
||||||
|
return { ok: false, error };
|
||||||
|
}
|
||||||
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) {
|
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) {
|
||||||
window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
||||||
}
|
}
|
||||||
_markSkippedJobs(result);
|
_markSkippedJobs(result);
|
||||||
persistQueueStateSoon();
|
persistQueueStateSoon();
|
||||||
|
|
||||||
if (result && result.error) {
|
return { ok: true };
|
||||||
await showAppAlert(result.error, 'Upload-Start fehlgeschlagen');
|
|
||||||
uploading = false;
|
|
||||||
updateQueueActionButtons();
|
|
||||||
updateStatusBar();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
restoreUploadJobStates(originalStates);
|
||||||
uploading = false;
|
uploading = false;
|
||||||
|
renderQueueTable();
|
||||||
updateQueueActionButtons();
|
updateQueueActionButtons();
|
||||||
updateStatusBar();
|
updateStatusBar();
|
||||||
await showAppAlert(`Upload-Start fehlgeschlagen: ${err.message}`, 'Upload-Start fehlgeschlagen');
|
const error = sanitizeUploadControlError(err, 'Upload konnte nicht gestartet werden.');
|
||||||
|
await showAppAlert(error, 'Upload-Start fehlgeschlagen');
|
||||||
|
return { ok: false, error };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3695,6 +3831,7 @@ async function startSelectedUpload(explicitJobs) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
const originalStates = captureUploadJobStates(addable);
|
||||||
const cleanupPreparation = prepareSourceCleanup(addable);
|
const cleanupPreparation = prepareSourceCleanup(addable);
|
||||||
addable.forEach(j => {
|
addable.forEach(j => {
|
||||||
j.status = 'queued'; j.error = null; j.result = null;
|
j.status = 'queued'; j.error = null; j.result = null;
|
||||||
@@ -3708,17 +3845,26 @@ async function startSelectedUpload(explicitJobs) {
|
|||||||
sourceCleanupGroups: cleanupPreparation.groups
|
sourceCleanupGroups: cleanupPreparation.groups
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showCopyToast(`Jobs konnten nicht hinzugefügt werden: ${err.message}`);
|
restoreUploadJobStates(originalStates);
|
||||||
return;
|
renderQueueTable();
|
||||||
|
const error = sanitizeUploadControlError(err, 'Jobs konnten nicht hinzugefügt werden.');
|
||||||
|
showCopyToast(error);
|
||||||
|
return { ok: false, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the batch ended between UI-state and IPC call, start a fresh batch immediately
|
|
||||||
if (result && result.error === 'Kein Upload aktiv') {
|
if (result && result.error === 'Kein Upload aktiv') {
|
||||||
|
restoreUploadJobStates(originalStates);
|
||||||
uploading = false;
|
uploading = false;
|
||||||
updateQueueActionButtons();
|
updateQueueActionButtons();
|
||||||
updateStatusBar();
|
updateStatusBar();
|
||||||
await startSelectedUpload(addable);
|
return startSelectedUpload(addable);
|
||||||
return;
|
}
|
||||||
|
if (result && result.error) {
|
||||||
|
restoreUploadJobStates(originalStates);
|
||||||
|
renderQueueTable();
|
||||||
|
const error = sanitizeUploadControlError(result.error, 'Jobs konnten nicht hinzugefügt werden.');
|
||||||
|
showCopyToast(error);
|
||||||
|
return { ok: false, error };
|
||||||
}
|
}
|
||||||
_markSkippedJobs(result);
|
_markSkippedJobs(result);
|
||||||
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) {
|
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) {
|
||||||
@@ -3742,7 +3888,7 @@ async function startSelectedUpload(explicitJobs) {
|
|||||||
} else {
|
} else {
|
||||||
showCopyToast('Keine Jobs hinzugefuegt');
|
showCopyToast('Keine Jobs hinzugefuegt');
|
||||||
}
|
}
|
||||||
return;
|
return { ok: true, added };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uploading = true; // set immediately to prevent double-click race
|
uploading = true; // set immediately to prevent double-click race
|
||||||
@@ -3751,6 +3897,7 @@ async function startSelectedUpload(explicitJobs) {
|
|||||||
const hosters = getSelectedHosters();
|
const hosters = getSelectedHosters();
|
||||||
const jobsToStart = scopedJobs.filter(job => isStartableQueueStatus(job.status));
|
const jobsToStart = scopedJobs.filter(job => isStartableQueueStatus(job.status));
|
||||||
if (jobsToStart.length === 0) { uploading = false; updateQueueActionButtons(); return; }
|
if (jobsToStart.length === 0) { uploading = false; updateQueueActionButtons(); return; }
|
||||||
|
const originalStates = captureUploadJobStates(jobsToStart);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cleanupPreparation = prepareSourceCleanup(jobsToStart);
|
const cleanupPreparation = prepareSourceCleanup(jobsToStart);
|
||||||
@@ -3773,23 +3920,32 @@ async function startSelectedUpload(explicitJobs) {
|
|||||||
sourceCleanupGroups: cleanupPreparation.groups
|
sourceCleanupGroups: cleanupPreparation.groups
|
||||||
};
|
};
|
||||||
const result = await window.api.startUpload(uploadPayload);
|
const result = await window.api.startUpload(uploadPayload);
|
||||||
|
if (result && result.error) {
|
||||||
|
restoreUploadJobStates(originalStates);
|
||||||
|
const error = sanitizeUploadControlError(result.error, 'Upload konnte nicht gestartet werden.');
|
||||||
|
uploading = false;
|
||||||
|
renderQueueTable();
|
||||||
|
updateQueueActionButtons();
|
||||||
|
updateStatusBar();
|
||||||
|
await showAppAlert(error, 'Upload-Start fehlgeschlagen');
|
||||||
|
return { ok: false, error };
|
||||||
|
}
|
||||||
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) {
|
if (result?.sourceCleanupFingerprints && window.SourceCleanupPolicy) {
|
||||||
window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
window.SourceCleanupPolicy.applyFingerprints(queueJobs, result.sourceCleanupFingerprints);
|
||||||
}
|
}
|
||||||
_markSkippedJobs(result);
|
_markSkippedJobs(result);
|
||||||
persistQueueStateSoon();
|
persistQueueStateSoon();
|
||||||
|
|
||||||
if (result && result.error) {
|
return { ok: true };
|
||||||
await showAppAlert(result.error, 'Upload-Start fehlgeschlagen');
|
|
||||||
uploading = false;
|
|
||||||
updateQueueActionButtons();
|
|
||||||
updateStatusBar();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
restoreUploadJobStates(originalStates);
|
||||||
uploading = false;
|
uploading = false;
|
||||||
|
renderQueueTable();
|
||||||
updateQueueActionButtons();
|
updateQueueActionButtons();
|
||||||
updateStatusBar();
|
updateStatusBar();
|
||||||
await showAppAlert(`Upload-Start fehlgeschlagen: ${err.message}`, 'Upload-Start fehlgeschlagen');
|
const error = sanitizeUploadControlError(err, 'Upload konnte nicht gestartet werden.');
|
||||||
|
await showAppAlert(error, 'Upload-Start fehlgeschlagen');
|
||||||
|
return { ok: false, error };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4463,7 +4619,7 @@ function moveSelectedJobs(direction) {
|
|||||||
function syncSelectedFilesFromQueue() {
|
function syncSelectedFilesFromQueue() {
|
||||||
const fileMap = new Map();
|
const fileMap = new Map();
|
||||||
queueJobs
|
queueJobs
|
||||||
.filter((job) => !['done', 'skipped', 'aborted'].includes(job.status))
|
.filter((job) => job.automationAdmission !== true && !['done', 'skipped', 'aborted'].includes(job.status))
|
||||||
.forEach((job) => {
|
.forEach((job) => {
|
||||||
if (!job.file || fileMap.has(job.file)) return;
|
if (!job.file || fileMap.has(job.file)) return;
|
||||||
fileMap.set(job.file, {
|
fileMap.set(job.file, {
|
||||||
|
|||||||
+452
-12
@@ -68,6 +68,11 @@ let automationProbe = {
|
|||||||
history: [],
|
history: [],
|
||||||
uploadLog: [],
|
uploadLog: [],
|
||||||
paused: false,
|
paused: false,
|
||||||
|
historyError: '',
|
||||||
|
addResult: null,
|
||||||
|
addError: '',
|
||||||
|
startResult: null,
|
||||||
|
saveSettingsError: '',
|
||||||
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: [],
|
||||||
@@ -159,6 +164,11 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
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,
|
||||||
|
historyError: String(value.historyError || ''),
|
||||||
|
addResult: value.addResult || null,
|
||||||
|
addError: String(value.addError || ''),
|
||||||
|
startResult: value.startResult || null,
|
||||||
|
saveSettingsError: String(value.saveSettingsError || ''),
|
||||||
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: [],
|
||||||
@@ -174,23 +184,36 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
savedSettings: automationProbe.savedSettings.map(value => JSON.parse(JSON.stringify(value)))
|
savedSettings: automationProbe.savedSettings.map(value => JSON.parse(JSON.stringify(value)))
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
inspectImportFiles(entries) {
|
inspectImportFiles(entries, existingPaths) {
|
||||||
automationProbe.readCalls.inspect++;
|
automationProbe.readCalls.inspect++;
|
||||||
const candidates = Array.isArray(entries) ? entries : [];
|
const candidates = Array.isArray(entries) ? entries : [];
|
||||||
const unavailable = candidates.filter(entry => entry?.unavailable).map(entry => ({ ...entry, reason: 'unreadable' }));
|
const normalize = value => String(value || '').replace(/\\\\/g, '/').toLowerCase();
|
||||||
const accepted = candidates.filter(entry => !entry?.unavailable).map(entry => ({ ...entry }));
|
const seen = new Set((Array.isArray(existingPaths) ? existingPaths : []).map(normalize));
|
||||||
|
const duplicates = [];
|
||||||
|
const unique = [];
|
||||||
|
for (const entry of candidates) {
|
||||||
|
const key = normalize(entry?.path);
|
||||||
|
if (seen.has(key)) duplicates.push({ ...entry });
|
||||||
|
else {
|
||||||
|
seen.add(key);
|
||||||
|
unique.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const unavailable = unique.filter(entry => entry?.unavailable).map(entry => ({ ...entry, reason: 'unreadable' }));
|
||||||
|
const accepted = unique.filter(entry => !entry?.unavailable).map(entry => ({ ...entry }));
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
candidateCount: candidates.length,
|
candidateCount: candidates.length,
|
||||||
duplicateCount: 0,
|
duplicateCount: duplicates.length,
|
||||||
unavailableCount: unavailable.length,
|
unavailableCount: unavailable.length,
|
||||||
acceptedCount: accepted.length,
|
acceptedCount: accepted.length,
|
||||||
accepted,
|
accepted,
|
||||||
duplicates: [],
|
duplicates,
|
||||||
unavailable
|
unavailable
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getHistory() {
|
getHistory() {
|
||||||
automationProbe.readCalls.history++;
|
automationProbe.readCalls.history++;
|
||||||
|
if (automationProbe.historyError) return Promise.reject(new Error(automationProbe.historyError));
|
||||||
return Promise.resolve(automationProbe.history);
|
return Promise.resolve(automationProbe.history);
|
||||||
},
|
},
|
||||||
readOwnUploadLog() {
|
readOwnUploadLog() {
|
||||||
@@ -213,6 +236,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
saveGlobalSettings(value) {
|
saveGlobalSettings(value) {
|
||||||
automationProbe.savedSettings.push(value);
|
automationProbe.savedSettings.push(value);
|
||||||
automationProbe.mutationCalls.push(['settings']);
|
automationProbe.mutationCalls.push(['settings']);
|
||||||
|
if (automationProbe.saveSettingsError) return Promise.reject(new Error(automationProbe.saveSettingsError));
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
},
|
},
|
||||||
savePendingQueue(payload) {
|
savePendingQueue(payload) {
|
||||||
@@ -223,11 +247,12 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
addJobsToBatch(payload) {
|
addJobsToBatch(payload) {
|
||||||
folderMonitorProbeCalls.push(['inject', payload?.jobs?.length || 0]);
|
folderMonitorProbeCalls.push(['inject', payload?.jobs?.length || 0]);
|
||||||
automationProbe.mutationCalls.push(['inject', payload?.jobs?.length || 0]);
|
automationProbe.mutationCalls.push(['inject', payload?.jobs?.length || 0]);
|
||||||
return Promise.resolve({ added: payload?.jobs?.length || 0 });
|
if (automationProbe.addError) return Promise.reject(new Error(automationProbe.addError));
|
||||||
|
return Promise.resolve(automationProbe.addResult || { added: payload?.jobs?.length || 0 });
|
||||||
},
|
},
|
||||||
startUpload(payload) {
|
startUpload(payload) {
|
||||||
automationProbe.mutationCalls.push(['start', payload?.jobs?.length || 0]);
|
automationProbe.mutationCalls.push(['start', payload?.jobs?.length || 0]);
|
||||||
return Promise.resolve({ started: true });
|
return Promise.resolve(automationProbe.startResult || { started: true });
|
||||||
},
|
},
|
||||||
getFolderMonitorProbeCalls() { return folderMonitorProbeCalls; }
|
getFolderMonitorProbeCalls() { return folderMonitorProbeCalls; }
|
||||||
});
|
});
|
||||||
@@ -536,6 +561,142 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
summary: manualTestPreview.summary,
|
summary: manualTestPreview.summary,
|
||||||
reads: manualTestProbe.readCalls
|
reads: manualTestProbe.readCalls
|
||||||
};
|
};
|
||||||
|
const historyCandidates = [
|
||||||
|
{ path: 'C:\\\\history\\\\success.mkv', name: 'success.mkv', size: 1 },
|
||||||
|
{ path: 'C:\\\\history\\\\link-success.mkv', name: 'link-success.mkv', size: 1 },
|
||||||
|
{ path: 'C:\\\\history\\\\error.mkv', name: 'error.mkv', size: 1 },
|
||||||
|
{ path: 'C:\\\\history\\\\aborted.mkv', name: 'aborted.mkv', size: 1 },
|
||||||
|
{ path: 'C:\\\\history\\\\skipped.mkv', name: 'skipped.mkv', size: 1 },
|
||||||
|
{ path: 'C:\\\\history\\\\all-failed.mkv', name: 'all-failed.mkv', size: 1 },
|
||||||
|
{ path: 'C:\\\\history-a\\\\same-name.mkv', name: 'same-name.mkv', size: 1 },
|
||||||
|
{ path: 'D:\\\\history-b\\\\same-name.mkv', name: 'same-name.mkv', size: 1 }
|
||||||
|
];
|
||||||
|
config.globalSettings.folderMonitor = {
|
||||||
|
enabled: true,
|
||||||
|
folderPath: 'C:\\\\history',
|
||||||
|
hosters: ['doodstream.com'],
|
||||||
|
autoStart: false,
|
||||||
|
queueLimitJobs: 15000,
|
||||||
|
paused: false
|
||||||
|
};
|
||||||
|
hosterSettings = {};
|
||||||
|
selectedFiles = [];
|
||||||
|
queueJobs = [];
|
||||||
|
rebuildJobIndex();
|
||||||
|
window.api.configureAutomationProbe({
|
||||||
|
paused: false,
|
||||||
|
history: [{
|
||||||
|
id: 'evidence',
|
||||||
|
files: [
|
||||||
|
{ path: historyCandidates[0].path, name: historyCandidates[0].name, results: [{ hoster: 'doodstream.com', status: 'done' }] },
|
||||||
|
{ path: historyCandidates[1].path, name: historyCandidates[1].name, results: [{ hoster: 'doodstream.com', status: 'error', download_url: 'https://example.test/link' }] },
|
||||||
|
{ path: historyCandidates[2].path, name: historyCandidates[2].name, results: [{ hoster: 'doodstream.com', status: 'error' }] },
|
||||||
|
{ path: historyCandidates[3].path, name: historyCandidates[3].name, results: [{ hoster: 'doodstream.com', status: 'aborted' }] },
|
||||||
|
{ path: historyCandidates[4].path, name: historyCandidates[4].name, results: [{ hoster: 'doodstream.com', status: 'skipped' }] },
|
||||||
|
{ path: historyCandidates[5].path, name: historyCandidates[5].name, results: [{ hoster: 'doodstream.com', status: 'error' }, { hoster: 'voe.sx', status: 'aborted' }] },
|
||||||
|
{ name: 'same-name.mkv', results: [{ hoster: 'doodstream.com', status: 'done' }] }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
const historyEvaluation = await evaluateAutomationCandidates(historyCandidates, { dryRun: true, trigger: 'test' });
|
||||||
|
const historyEvidence = {
|
||||||
|
alreadyProcessed: historyEvaluation.summary.alreadyProcessed,
|
||||||
|
acceptedNames: historyEvaluation.candidates.map(file => file.name).sort(),
|
||||||
|
resultingJobs: historyEvaluation.summary.resultingJobs
|
||||||
|
};
|
||||||
|
config.globalSettings.folderMonitor = {
|
||||||
|
enabled: true,
|
||||||
|
folderPath: 'C:\\\\pending',
|
||||||
|
hosters: [],
|
||||||
|
autoStart: false,
|
||||||
|
queueLimitJobs: 15000,
|
||||||
|
paused: false
|
||||||
|
};
|
||||||
|
selectedFiles = [];
|
||||||
|
queueJobs = [];
|
||||||
|
_pendingFiles = [{ path: 'C:\\\\pending\\\\existing.mkv', name: 'existing.mkv', size: 1 }];
|
||||||
|
_pendingImportInspection = { candidateCount: 1, duplicateCount: 0, unavailableCount: 0, accepted: _pendingFiles.slice() };
|
||||||
|
_pendingFolderMonitorAutoStart.clear();
|
||||||
|
rebuildJobIndex();
|
||||||
|
window.api.configureAutomationProbe({ paused: false });
|
||||||
|
const pendingEvaluation = await evaluateAutomationCandidates([
|
||||||
|
{ path: 'c:\\\\PENDING\\\\existing.mkv', name: 'existing.mkv', size: 1 },
|
||||||
|
{ path: 'C:\\\\pending\\\\new.mkv', name: 'new.mkv', size: 1 },
|
||||||
|
{ path: 'c:\\\\PENDING\\\\new.mkv', name: 'new.mkv', size: 1 }
|
||||||
|
], { dryRun: true, trigger: 'test' });
|
||||||
|
_pendingFiles = [];
|
||||||
|
_pendingImportInspection = null;
|
||||||
|
_pendingFolderMonitorAutoStart.clear();
|
||||||
|
const overlapping = { path: 'C:\\\\pending\\\\overlap.mkv', name: 'overlap.mkv', size: 1 };
|
||||||
|
await Promise.all([
|
||||||
|
handleFolderMonitorFiles([overlapping, { ...overlapping, path: 'c:\\\\PENDING\\\\overlap.mkv' }]),
|
||||||
|
handleFolderMonitorFiles([{ ...overlapping }])
|
||||||
|
]);
|
||||||
|
const pendingDedup = {
|
||||||
|
evaluatedNames: pendingEvaluation.candidates.map(file => file.name),
|
||||||
|
pendingPaths: _pendingFiles.map(file => normalizeAutomationPath(file.path)),
|
||||||
|
pendingAccepted: _pendingImportInspection?.accepted?.length || 0,
|
||||||
|
markerPaths: [..._pendingFolderMonitorAutoStart.keys()].map(normalizeAutomationPath)
|
||||||
|
};
|
||||||
|
const prepareManualHostFailure = ({ full, historyError }) => {
|
||||||
|
const file = { path: 'C:\\\\manual-host\\\\pending.mkv', name: 'pending.mkv', size: 1, mtimeMs: 1 };
|
||||||
|
config.globalSettings.folderMonitor = {
|
||||||
|
enabled: true,
|
||||||
|
folderPath: 'C:\\\\manual-host',
|
||||||
|
hosters: [],
|
||||||
|
autoStart: false,
|
||||||
|
queueLimitJobs: full ? 1 : 15000,
|
||||||
|
paused: false
|
||||||
|
};
|
||||||
|
hosterSettings = {};
|
||||||
|
selectedFiles = [];
|
||||||
|
selectedUploadHosters = [];
|
||||||
|
queueJobs = full ? [{ id: 'full', file: 'C:\\\\queue\\\\full.mkv', fileName: 'full.mkv', hoster: 'doodstream.com', status: 'queued', bytesTotal: 1 }] : [];
|
||||||
|
_pendingFiles = [file];
|
||||||
|
_pendingImportInspection = { candidateCount: 1, duplicateCount: 0, unavailableCount: 0, accepted: [file] };
|
||||||
|
_pendingImportInspections = 0;
|
||||||
|
_pendingFolderMonitorAutoStart.clear();
|
||||||
|
_pendingFolderMonitorAutoStart.set(file.path, false);
|
||||||
|
rebuildJobIndex();
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'checkbox';
|
||||||
|
input.dataset.hosterModal = 'doodstream.com';
|
||||||
|
input.checked = true;
|
||||||
|
document.getElementById('hosterModalList').replaceChildren(input);
|
||||||
|
document.getElementById('hosterModal').style.display = 'flex';
|
||||||
|
window.api.configureAutomationProbe({ paused: false, historyError });
|
||||||
|
};
|
||||||
|
const captureManualHostFailure = (result, thrown) => ({
|
||||||
|
result,
|
||||||
|
thrown,
|
||||||
|
pendingNames: _pendingFiles.map(file => file.name),
|
||||||
|
markerNames: [..._pendingFolderMonitorAutoStart.keys()].map(path => path.split('\\\\').pop()),
|
||||||
|
inspectionAccepted: _pendingImportInspection?.accepted?.length || 0,
|
||||||
|
modalOpen: document.getElementById('hosterModal').style.display === 'flex'
|
||||||
|
});
|
||||||
|
prepareManualHostFailure({ full: false, historyError: 'token=secret-value' });
|
||||||
|
let readFailureResult = null;
|
||||||
|
let readFailureThrown = null;
|
||||||
|
try {
|
||||||
|
readFailureResult = await applyHosterSelection();
|
||||||
|
} catch (error) {
|
||||||
|
readFailureThrown = error.message || String(error);
|
||||||
|
}
|
||||||
|
const readFailure = captureManualHostFailure(readFailureResult, readFailureThrown);
|
||||||
|
prepareManualHostFailure({ full: true, historyError: '' });
|
||||||
|
let applyFailureResult = null;
|
||||||
|
let applyFailureThrown = null;
|
||||||
|
try {
|
||||||
|
applyFailureResult = await applyHosterSelection();
|
||||||
|
} catch (error) {
|
||||||
|
applyFailureThrown = error.message || String(error);
|
||||||
|
}
|
||||||
|
const applyFailure = captureManualHostFailure(applyFailureResult, applyFailureThrown);
|
||||||
|
const manualHostTransactional = {
|
||||||
|
readFailure,
|
||||||
|
applyFailure,
|
||||||
|
secretExposed: JSON.stringify({ readFailure, applyFailure }).includes('secret-value')
|
||||||
|
};
|
||||||
|
|
||||||
const configureAtomicState = currentCount => {
|
const configureAtomicState = currentCount => {
|
||||||
config.globalSettings.folderMonitor = {
|
config.globalSettings.folderMonitor = {
|
||||||
@@ -551,8 +712,8 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
'doodstream.com': { maxSizeMb: 2 },
|
'doodstream.com': { maxSizeMb: 2 },
|
||||||
'voe.sx': { maxSizeMb: 2 }
|
'voe.sx': { maxSizeMb: 2 }
|
||||||
};
|
};
|
||||||
selectedUploadHosters = [];
|
selectedUploadHosters = ['clouddrop.cc'];
|
||||||
selectedFiles = [{ path: 'C:\\\\manual\\\\unplanned.mkv', name: 'unplanned.mkv', size: 1 }];
|
selectedFiles = [];
|
||||||
queueJobs = Array.from({ length: currentCount }, (_, index) => ({
|
queueJobs = Array.from({ length: currentCount }, (_, index) => ({
|
||||||
id: 'capacity-' + index,
|
id: 'capacity-' + index,
|
||||||
file: 'C:\\\\capacity\\\\existing-' + index + '.mkv',
|
file: 'C:\\\\capacity\\\\existing-' + index + '.mkv',
|
||||||
@@ -576,13 +737,21 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
configureAtomicState(14998);
|
configureAtomicState(14998);
|
||||||
const atomicEvaluation = await evaluateAutomationCandidates(atomicCandidates, { dryRun: false, trigger: 'watcher' });
|
const atomicEvaluation = await evaluateAutomationCandidates(atomicCandidates, { dryRun: false, trigger: 'watcher' });
|
||||||
const atomicResult = await applyAutomationEvaluation(atomicEvaluation);
|
const atomicResult = await applyAutomationEvaluation(atomicEvaluation);
|
||||||
|
const selectedHostersAfterApply = selectedUploadHosters.slice();
|
||||||
|
const manualSelectionFilesAfterApply = selectedFiles.map(file => file.name);
|
||||||
|
const plannedHostsBeforeRebuild = queueJobs.filter(job => job.file === atomicCandidates[1].path).map(job => job.hoster).sort();
|
||||||
|
selectedUploadHosters = ['clouddrop.cc'];
|
||||||
|
updateUploadView();
|
||||||
const atomic = {
|
const atomic = {
|
||||||
newQueueFiles: [...new Set(queueJobs.filter(job => job.file === atomicCandidates[0].path || job.file === atomicCandidates[1].path).map(job => job.fileName))],
|
newQueueFiles: [...new Set(queueJobs.filter(job => job.file === atomicCandidates[0].path || job.file === atomicCandidates[1].path).map(job => job.fileName))],
|
||||||
admittedFiles: atomicResult.admittedFiles.map(file => file.name),
|
admittedFiles: atomicResult.admittedFiles.map(file => file.name),
|
||||||
deferred: config.globalSettings.folderMonitor.telemetry.deferred,
|
deferred: config.globalSettings.folderMonitor.telemetry.deferred,
|
||||||
queued: config.globalSettings.folderMonitor.telemetry.queued,
|
queued: config.globalSettings.folderMonitor.telemetry.queued,
|
||||||
currentJobCount: window.AutomationControl.countAutomaticQueueJobs(queueJobs),
|
currentJobCount: window.AutomationControl.countAutomaticQueueJobs(queueJobs),
|
||||||
unplannedJobs: queueJobs.filter(job => job.fileName === 'unplanned.mkv').length
|
selectedHostersAfterApply,
|
||||||
|
manualSelectionFilesAfterApply,
|
||||||
|
plannedHostsBeforeRebuild,
|
||||||
|
hostsAfterRebuild: queueJobs.filter(job => job.file === atomicCandidates[1].path).map(job => job.hoster).sort()
|
||||||
};
|
};
|
||||||
const statusSnapshot = createAutomationStatusSnapshot();
|
const statusSnapshot = createAutomationStatusSnapshot();
|
||||||
const status = {
|
const status = {
|
||||||
@@ -593,6 +762,30 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
frozen: Object.isFrozen(statusSnapshot) && Object.isFrozen(statusSnapshot.telemetry)
|
frozen: Object.isFrozen(statusSnapshot) && Object.isFrozen(statusSnapshot.telemetry)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
configureAtomicState(0);
|
||||||
|
selectedFiles = [];
|
||||||
|
selectedUploadHosters = ['clouddrop.cc'];
|
||||||
|
config.globalSettings.folderMonitor.hosters = ['doodstream.com', 'voe.sx'];
|
||||||
|
hosterSettings = {};
|
||||||
|
const persistedFile = { path: 'C:\\\\persisted\\\\automation.mkv', name: 'automation.mkv', size: 1, mtimeMs: 1 };
|
||||||
|
const persistedEvaluation = await evaluateAutomationCandidates([persistedFile], { dryRun: false, trigger: 'watcher' });
|
||||||
|
await applyAutomationEvaluation(persistedEvaluation);
|
||||||
|
const pendingSnapshot = buildPersistedQueueState();
|
||||||
|
config.globalSettings.pendingQueue = pendingSnapshot;
|
||||||
|
selectedFiles = [];
|
||||||
|
selectedUploadHosters = [];
|
||||||
|
queueJobs = [];
|
||||||
|
rebuildJobIndex();
|
||||||
|
restoreQueueStateFromConfig();
|
||||||
|
syncSelectedFilesFromQueue();
|
||||||
|
selectedUploadHosters = ['clouddrop.cc'];
|
||||||
|
updateUploadView();
|
||||||
|
const persistedQueueExactness = {
|
||||||
|
restoredSelectedFiles: selectedFiles.map(file => file.name),
|
||||||
|
automationMarkers: queueJobs.filter(job => job.file === persistedFile.path && job.automationAdmission === true).length,
|
||||||
|
hostsAfterRebuild: queueJobs.filter(job => job.file === persistedFile.path).map(job => job.hoster).sort()
|
||||||
|
};
|
||||||
|
|
||||||
configureAtomicState(14994);
|
configureAtomicState(14994);
|
||||||
const staleEvaluation = await evaluateAutomationCandidates(atomicCandidates, { dryRun: false, trigger: 'watcher' });
|
const staleEvaluation = await evaluateAutomationCandidates(atomicCandidates, { dryRun: false, trigger: 'watcher' });
|
||||||
queueJobs.push(...Array.from({ length: 4 }, (_, index) => ({
|
queueJobs.push(...Array.from({ length: 4 }, (_, index) => ({
|
||||||
@@ -611,6 +804,147 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
newQueueFiles: [...new Set(queueJobs.filter(job => job.file === atomicCandidates[0].path || job.file === atomicCandidates[1].path).map(job => job.fileName))]
|
newQueueFiles: [...new Set(queueJobs.filter(job => job.file === atomicCandidates[0].path || job.file === atomicCandidates[1].path).map(job => job.fileName))]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
configureAtomicState(0);
|
||||||
|
selectedUploadHosters = ['clouddrop.cc'];
|
||||||
|
selectedFiles = [];
|
||||||
|
config.globalSettings.folderMonitor.hosters = ['doodstream.com', 'voe.sx'];
|
||||||
|
hosterSettings = {};
|
||||||
|
const changingFile = { path: 'C:\\\\watch\\\\changing.mkv', name: 'changing.mkv', size: 3 * 1024 * 1024, mtimeMs: 1 };
|
||||||
|
const changingEvaluation = await evaluateAutomationCandidates([changingFile], { dryRun: false, trigger: 'watcher' });
|
||||||
|
config.globalSettings.folderMonitor.hosters = ['byse.sx', 'vidmoly.me'];
|
||||||
|
hosterSettings = { 'byse.sx': { maxSizeMb: 2 } };
|
||||||
|
const changingResult = await applyAutomationEvaluation(changingEvaluation);
|
||||||
|
const watcherHosts = queueJobs.filter(job => job.file === changingFile.path).map(job => job.hoster).sort();
|
||||||
|
const immutableEvaluatedHosts = changingEvaluation.candidates[0].eligibleHosters.slice().sort();
|
||||||
|
configureAtomicState(0);
|
||||||
|
selectedFiles = [];
|
||||||
|
config.globalSettings.folderMonitor.hosters = [];
|
||||||
|
hosterSettings = {};
|
||||||
|
const manualChangingEvaluation = await evaluateAutomationCandidates([changingFile], {
|
||||||
|
dryRun: false,
|
||||||
|
trigger: 'manual-host',
|
||||||
|
selectedHosters: ['doodstream.com', 'voe.sx']
|
||||||
|
});
|
||||||
|
hosterSettings = {
|
||||||
|
'doodstream.com': { maxSizeMb: 2 },
|
||||||
|
'voe.sx': { maxSizeMb: 2 }
|
||||||
|
};
|
||||||
|
const manualChangingResult = await applyAutomationEvaluation(manualChangingEvaluation);
|
||||||
|
const replannedEligibility = {
|
||||||
|
watcherAdmitted: changingResult.admittedFiles.map(file => file.name),
|
||||||
|
watcherHosts,
|
||||||
|
immutableEvaluatedHosts,
|
||||||
|
manualAdmitted: manualChangingResult.admittedFiles.map(file => file.name),
|
||||||
|
manualJobs: queueJobs.filter(job => job.file === changingFile.path).length
|
||||||
|
};
|
||||||
|
|
||||||
|
const makePauseRaceJob = name => ({
|
||||||
|
id: 'pause-race-' + name,
|
||||||
|
file: 'C:\\\\pause-race\\\\' + name,
|
||||||
|
fileName: name,
|
||||||
|
hoster: 'doodstream.com',
|
||||||
|
status: 'preview',
|
||||||
|
bytesUploaded: 0,
|
||||||
|
bytesTotal: 1,
|
||||||
|
speedKbs: 0,
|
||||||
|
elapsed: 0,
|
||||||
|
remaining: 0,
|
||||||
|
error: null,
|
||||||
|
result: null,
|
||||||
|
progress: 0,
|
||||||
|
uploadId: null
|
||||||
|
});
|
||||||
|
configureAtomicState(0);
|
||||||
|
selectedFiles = [];
|
||||||
|
selectedUploadHosters = ['doodstream.com'];
|
||||||
|
const startRaceJob = makePauseRaceJob('start-paused.mkv');
|
||||||
|
queueJobs = [startRaceJob];
|
||||||
|
rebuildJobIndex();
|
||||||
|
window.api.configureAutomationProbe({ paused: false, startResult: { error: 'Automatik ist pausiert' } });
|
||||||
|
const originalShowAppAlertForPause = showAppAlert;
|
||||||
|
showAppAlert = async () => {};
|
||||||
|
const startRaceResult = await startUpload();
|
||||||
|
showAppAlert = originalShowAppAlertForPause;
|
||||||
|
const startRace = { result: startRaceResult, status: startRaceJob.status, uploading };
|
||||||
|
const addRaceJob = makePauseRaceJob('add-paused.mkv');
|
||||||
|
queueJobs = [addRaceJob];
|
||||||
|
uploading = true;
|
||||||
|
rebuildJobIndex();
|
||||||
|
window.api.configureAutomationProbe({ paused: false, addResult: { error: 'Automatik ist pausiert', added: 0 } });
|
||||||
|
const addRaceResult = await startSelectedUpload([addRaceJob]);
|
||||||
|
const addRace = { result: addRaceResult, status: addRaceJob.status, uploading };
|
||||||
|
uploading = false;
|
||||||
|
const selectedStartRaceJob = makePauseRaceJob('selected-start-paused.mkv');
|
||||||
|
queueJobs = [selectedStartRaceJob];
|
||||||
|
uploading = false;
|
||||||
|
rebuildJobIndex();
|
||||||
|
window.api.configureAutomationProbe({ paused: false, startResult: { error: 'Automatik ist pausiert' } });
|
||||||
|
showAppAlert = async () => {};
|
||||||
|
const selectedStartRaceResult = await startSelectedUpload([selectedStartRaceJob]);
|
||||||
|
showAppAlert = originalShowAppAlertForPause;
|
||||||
|
const selectedStartRace = { result: selectedStartRaceResult, status: selectedStartRaceJob.status, uploading };
|
||||||
|
const manualRaceFile = { path: 'C:\\\\manual-race\\\\manual-paused.mkv', name: 'manual-paused.mkv', size: 1 };
|
||||||
|
queueJobs = [];
|
||||||
|
selectedFiles = [];
|
||||||
|
uploading = true;
|
||||||
|
_pendingFiles = [manualRaceFile];
|
||||||
|
_pendingImportInspection = { candidateCount: 1, duplicateCount: 0, unavailableCount: 0, accepted: [manualRaceFile] };
|
||||||
|
_pendingImportInspections = 0;
|
||||||
|
_pendingFolderMonitorAutoStart.clear();
|
||||||
|
rebuildJobIndex();
|
||||||
|
const manualRaceInput = document.createElement('input');
|
||||||
|
manualRaceInput.type = 'checkbox';
|
||||||
|
manualRaceInput.dataset.hosterModal = 'doodstream.com';
|
||||||
|
manualRaceInput.checked = true;
|
||||||
|
document.getElementById('hosterModalList').replaceChildren(manualRaceInput);
|
||||||
|
document.getElementById('hosterModal').style.display = 'flex';
|
||||||
|
window.api.configureAutomationProbe({ paused: false, addResult: { error: 'Automatik ist pausiert', added: 0 } });
|
||||||
|
const manualRaceResult = await applyHosterSelection();
|
||||||
|
const manualRace = {
|
||||||
|
result: manualRaceResult,
|
||||||
|
status: queueJobs.find(job => job.file === manualRaceFile.path)?.status || null,
|
||||||
|
uploading
|
||||||
|
};
|
||||||
|
uploading = false;
|
||||||
|
const mainPauseResponses = { startRace, addRace, selectedStartRace, manualRace };
|
||||||
|
|
||||||
|
const runInjectionCase = async (name, probe) => {
|
||||||
|
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 = true;
|
||||||
|
window.api.configureAutomationProbe({ paused: false, ...probe });
|
||||||
|
const file = { path: 'C:\\\\injection\\\\' + name, name, size: 1, mtimeMs: 1 };
|
||||||
|
const evaluation = await evaluateAutomationCandidates([file], { dryRun: false, trigger: 'watcher' });
|
||||||
|
const result = await applyAutomationEvaluation(evaluation);
|
||||||
|
const state = await window.api.getAutomationProbeState();
|
||||||
|
const response = {
|
||||||
|
ok: result.ok,
|
||||||
|
error: result.error || null,
|
||||||
|
warning: result.warning || null,
|
||||||
|
admitted: result.admittedFiles.map(entry => entry.name),
|
||||||
|
status: queueJobs.find(job => job.file === file.path)?.status || null,
|
||||||
|
queuedTelemetry: config.globalSettings.folderMonitor.telemetry.queued,
|
||||||
|
telemetrySaveAttempted: state.mutationCalls.some(call => call[0] === 'settings')
|
||||||
|
};
|
||||||
|
uploading = false;
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
const pausedInjection = await runInjectionCase('paused.mkv', { addResult: { error: 'Automatik ist pausiert', added: 0 } });
|
||||||
|
const unconfirmedInjection = await runInjectionCase('unconfirmed.mkv', { addResult: { added: 0 } });
|
||||||
|
const exceptionInjection = await runInjectionCase('exception.mkv', { addError: 'token=secret-value' });
|
||||||
|
const telemetryFailure = await runInjectionCase('telemetry.mkv', { saveSettingsError: 'token=telemetry-secret' });
|
||||||
|
const injectionOutcomes = {
|
||||||
|
pausedInjection,
|
||||||
|
unconfirmedInjection,
|
||||||
|
exceptionInjection,
|
||||||
|
telemetryFailure,
|
||||||
|
secretExposed: JSON.stringify({ pausedInjection, unconfirmedInjection, exceptionInjection, telemetryFailure }).includes('secret')
|
||||||
|
};
|
||||||
|
|
||||||
configureAtomicState(0);
|
configureAtomicState(0);
|
||||||
config.globalSettings.folderMonitor.paused = true;
|
config.globalSettings.folderMonitor.paused = true;
|
||||||
window.api.configureAutomationProbe({ paused: true });
|
window.api.configureAutomationProbe({ paused: true });
|
||||||
@@ -651,7 +985,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
startCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'start').length,
|
startCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'start').length,
|
||||||
injectCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'inject').length
|
injectCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'inject').length
|
||||||
};
|
};
|
||||||
return { dry, manualTest, atomic, status, stale, paused };
|
return { dry, manualTest, historyEvidence, pendingDedup, manualHostTransactional, atomic, status, persistedQueueExactness, stale, replannedEligibility, mainPauseResponses, injectionOutcomes, paused };
|
||||||
})()`;
|
})()`;
|
||||||
const onlineBackupBehaviorScript = `(async () => {
|
const onlineBackupBehaviorScript = `(async () => {
|
||||||
const ids = {
|
const ids = {
|
||||||
@@ -1051,13 +1385,46 @@ app.whenReady().then(async () => {
|
|||||||
},
|
},
|
||||||
reads: { history: 1, uploadLog: 1, inspect: 1, status: 0, testScan: 1, reconcile: 0 }
|
reads: { history: 1, uploadLog: 1, inspect: 1, status: 0, testScan: 1, reconcile: 0 }
|
||||||
});
|
});
|
||||||
|
assert.deepEqual(result.automationPipeline.historyEvidence, {
|
||||||
|
alreadyProcessed: 2,
|
||||||
|
acceptedNames: ['aborted.mkv', 'all-failed.mkv', 'error.mkv', 'same-name.mkv', 'same-name.mkv', 'skipped.mkv'],
|
||||||
|
resultingJobs: 6
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationPipeline.pendingDedup, {
|
||||||
|
evaluatedNames: ['new.mkv'],
|
||||||
|
pendingPaths: ['c:/pending/overlap.mkv'],
|
||||||
|
pendingAccepted: 1,
|
||||||
|
markerPaths: ['c:/pending/overlap.mkv']
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationPipeline.manualHostTransactional, {
|
||||||
|
readFailure: {
|
||||||
|
result: { ok: false, error: 'Automatische Aufnahme konnte nicht abgeschlossen werden.' },
|
||||||
|
thrown: null,
|
||||||
|
pendingNames: ['pending.mkv'],
|
||||||
|
markerNames: ['pending.mkv'],
|
||||||
|
inspectionAccepted: 1,
|
||||||
|
modalOpen: true
|
||||||
|
},
|
||||||
|
applyFailure: {
|
||||||
|
result: { ok: false, error: 'Automatische Aufnahme konnte nicht abgeschlossen werden.' },
|
||||||
|
thrown: null,
|
||||||
|
pendingNames: ['pending.mkv'],
|
||||||
|
markerNames: ['pending.mkv'],
|
||||||
|
inspectionAccepted: 1,
|
||||||
|
modalOpen: true
|
||||||
|
},
|
||||||
|
secretExposed: false
|
||||||
|
});
|
||||||
assert.deepEqual(result.automationPipeline.atomic, {
|
assert.deepEqual(result.automationPipeline.atomic, {
|
||||||
newQueueFiles: ['b.mkv'],
|
newQueueFiles: ['b.mkv'],
|
||||||
admittedFiles: ['b.mkv'],
|
admittedFiles: ['b.mkv'],
|
||||||
deferred: 1,
|
deferred: 1,
|
||||||
queued: 1,
|
queued: 1,
|
||||||
currentJobCount: 15000,
|
currentJobCount: 15000,
|
||||||
unplannedJobs: 0
|
selectedHostersAfterApply: ['clouddrop.cc'],
|
||||||
|
manualSelectionFilesAfterApply: [],
|
||||||
|
plannedHostsBeforeRebuild: ['byse.sx', 'vidmoly.me'],
|
||||||
|
hostsAfterRebuild: ['byse.sx', 'vidmoly.me']
|
||||||
});
|
});
|
||||||
assert.deepEqual(result.automationPipeline.status, {
|
assert.deepEqual(result.automationPipeline.status, {
|
||||||
state: 'queue-limited',
|
state: 'queue-limited',
|
||||||
@@ -1066,11 +1433,84 @@ app.whenReady().then(async () => {
|
|||||||
queueLimited: true,
|
queueLimited: true,
|
||||||
frozen: true
|
frozen: true
|
||||||
});
|
});
|
||||||
|
assert.deepEqual(result.automationPipeline.persistedQueueExactness, {
|
||||||
|
restoredSelectedFiles: [],
|
||||||
|
automationMarkers: 2,
|
||||||
|
hostsAfterRebuild: ['doodstream.com', 'voe.sx']
|
||||||
|
});
|
||||||
assert.deepEqual(result.automationPipeline.stale, {
|
assert.deepEqual(result.automationPipeline.stale, {
|
||||||
plannedBeforeApply: ['a.mkv', 'b.mkv'],
|
plannedBeforeApply: ['a.mkv', 'b.mkv'],
|
||||||
admittedAfterApply: ['b.mkv'],
|
admittedAfterApply: ['b.mkv'],
|
||||||
newQueueFiles: ['b.mkv']
|
newQueueFiles: ['b.mkv']
|
||||||
});
|
});
|
||||||
|
assert.deepEqual(result.automationPipeline.replannedEligibility, {
|
||||||
|
watcherAdmitted: ['changing.mkv'],
|
||||||
|
watcherHosts: ['vidmoly.me'],
|
||||||
|
immutableEvaluatedHosts: ['doodstream.com', 'voe.sx'],
|
||||||
|
manualAdmitted: [],
|
||||||
|
manualJobs: 0
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationPipeline.mainPauseResponses, {
|
||||||
|
startRace: {
|
||||||
|
result: { ok: false, error: 'Automatik ist pausiert' },
|
||||||
|
status: 'preview',
|
||||||
|
uploading: false
|
||||||
|
},
|
||||||
|
addRace: {
|
||||||
|
result: { ok: false, error: 'Automatik ist pausiert' },
|
||||||
|
status: 'preview',
|
||||||
|
uploading: true
|
||||||
|
},
|
||||||
|
selectedStartRace: {
|
||||||
|
result: { ok: false, error: 'Automatik ist pausiert' },
|
||||||
|
status: 'preview',
|
||||||
|
uploading: false
|
||||||
|
},
|
||||||
|
manualRace: {
|
||||||
|
result: { ok: false, error: 'Automatik ist pausiert' },
|
||||||
|
status: 'preview',
|
||||||
|
uploading: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationPipeline.injectionOutcomes, {
|
||||||
|
pausedInjection: {
|
||||||
|
ok: false,
|
||||||
|
error: 'Automatik ist pausiert',
|
||||||
|
warning: null,
|
||||||
|
admitted: [],
|
||||||
|
status: 'preview',
|
||||||
|
queuedTelemetry: 0,
|
||||||
|
telemetrySaveAttempted: false
|
||||||
|
},
|
||||||
|
unconfirmedInjection: {
|
||||||
|
ok: false,
|
||||||
|
error: 'Jobs wurden nicht vollständig hinzugefügt.',
|
||||||
|
warning: null,
|
||||||
|
admitted: [],
|
||||||
|
status: 'preview',
|
||||||
|
queuedTelemetry: 0,
|
||||||
|
telemetrySaveAttempted: false
|
||||||
|
},
|
||||||
|
exceptionInjection: {
|
||||||
|
ok: false,
|
||||||
|
error: 'Jobs konnten nicht hinzugefügt werden.',
|
||||||
|
warning: null,
|
||||||
|
admitted: [],
|
||||||
|
status: 'preview',
|
||||||
|
queuedTelemetry: 0,
|
||||||
|
telemetrySaveAttempted: false
|
||||||
|
},
|
||||||
|
telemetryFailure: {
|
||||||
|
ok: false,
|
||||||
|
error: null,
|
||||||
|
warning: 'Telemetrie konnte nicht gespeichert werden.',
|
||||||
|
admitted: ['telemetry.mkv'],
|
||||||
|
status: 'queued',
|
||||||
|
queuedTelemetry: 1,
|
||||||
|
telemetrySaveAttempted: true
|
||||||
|
},
|
||||||
|
secretExposed: false
|
||||||
|
});
|
||||||
assert.deepEqual(result.automationPipeline.paused, {
|
assert.deepEqual(result.automationPipeline.paused, {
|
||||||
uploading: false,
|
uploading: false,
|
||||||
statuses: {
|
statuses: {
|
||||||
|
|||||||
Reference in New Issue
Block a user