feat: enforce automatic queue admission
This commit is contained in:
+328
-62
@@ -63,6 +63,7 @@ let config = { hosters: {}, hosterSettings: {}, globalSettings: {} };
|
|||||||
let hosterSettings = {};
|
let hosterSettings = {};
|
||||||
let uploading = false;
|
let uploading = false;
|
||||||
let healthCheckRunning = false;
|
let healthCheckRunning = false;
|
||||||
|
let automationRuntimeStatus = Object.freeze({});
|
||||||
let managedOnlineBackups = [];
|
let managedOnlineBackups = [];
|
||||||
let managedOnlineBackupsAuthoritative = false;
|
let managedOnlineBackupsAuthoritative = false;
|
||||||
let managedOnlineBackupMutationGeneration = 0;
|
let managedOnlineBackupMutationGeneration = 0;
|
||||||
@@ -414,58 +415,307 @@ function resolveFolderMonitorQueueAction({ autoStart, uploading: isUploading, he
|
|||||||
return isHealthCheckRunning ? 'queue' : 'start';
|
return isHealthCheckRunning ? 'queue' : 'start';
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFolderMonitorFiles(files) {
|
function freezeAutomationValue(value) {
|
||||||
window.api.debugLog('folder-monitor: received ' + files.length + ' file(s)');
|
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
||||||
const fm = config.globalSettings && config.globalSettings.folderMonitor;
|
for (const nested of Object.values(value)) freezeAutomationValue(nested);
|
||||||
const fmHosters = fm && Array.isArray(fm.hosters) && fm.hosters.length > 0 ? fm.hosters : [];
|
return Object.freeze(value);
|
||||||
|
|
||||||
if (fmHosters.length === 0) {
|
|
||||||
addPathsToQueue(files, { folderMonitorAutoStart: !!fm.autoStart });
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedUploadHosters = fmHosters.slice();
|
function normalizeAutomationPath(value) {
|
||||||
const existing = new Set();
|
return String(value || '').replace(/\\/g, '/').toLowerCase();
|
||||||
for (const file of selectedFiles) existing.add(file.path);
|
|
||||||
for (const file of _pendingFiles) existing.add(file.path);
|
|
||||||
const newFiles = [];
|
|
||||||
for (const filePath of files) {
|
|
||||||
if (existing.has(filePath)) continue;
|
|
||||||
existing.add(filePath);
|
|
||||||
const name = filePath.split('\\').pop().split('/').pop();
|
|
||||||
newFiles.push({ path: filePath, name, size: null });
|
|
||||||
}
|
}
|
||||||
if (newFiles.length === 0) return;
|
|
||||||
|
|
||||||
const newPaths = new Set(newFiles.map(file => file.path));
|
function normalizeAutomationCandidate(value, index) {
|
||||||
clearDedupKeysForPaths(newPaths);
|
const source = value && typeof value === 'object' ? value : { path: value };
|
||||||
selectedFiles.push(...newFiles);
|
const filePath = String(source.path || '').trim();
|
||||||
buildQueuePreview();
|
return {
|
||||||
updateUploadView();
|
...source,
|
||||||
const action = resolveFolderMonitorQueueAction({
|
path: filePath,
|
||||||
autoStart: fm.autoStart,
|
name: String(source.name || filePath.split(/[\\/]/).pop() || ''),
|
||||||
|
size: Number.isFinite(Number(source.size)) ? Number(source.size) : null,
|
||||||
|
mtimeMs: Number(source.mtimeMs) || index,
|
||||||
|
filterMatched: source.filterMatched !== false && source.allowed !== false && source.classification?.allowed !== false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenAutomationHistoryRows(history) {
|
||||||
|
const rows = [];
|
||||||
|
for (const entry of Array.isArray(history) ? history : []) {
|
||||||
|
if (!Array.isArray(entry?.files)) {
|
||||||
|
rows.push(entry);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const file of entry.files) {
|
||||||
|
rows.push({
|
||||||
|
path: file?.path || file?.file || '',
|
||||||
|
fileName: file?.fileName || file?.filename || file?.name || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function automationSettings() {
|
||||||
|
return window.AutomationControl.normalizeAutomationSettings(config.globalSettings?.folderMonitor || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAutomationRuntimeStatus(value) {
|
||||||
|
automationRuntimeStatus = freezeAutomationValue({ ...(value || {}) });
|
||||||
|
return automationRuntimeStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAutomationRuntimeStatus() {
|
||||||
|
if (typeof window.api.automationGetStatus !== 'function') return automationRuntimeStatus;
|
||||||
|
return applyAutomationRuntimeStatus(await window.api.automationGetStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isAutomationPaused() {
|
||||||
|
try {
|
||||||
|
const status = await refreshAutomationRuntimeStatus();
|
||||||
|
return status.paused === true || config.globalSettings?.folderMonitor?.paused === true;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAutomationStatusSnapshot() {
|
||||||
|
const folderSettings = config.globalSettings?.folderMonitor || {};
|
||||||
|
const normalized = window.AutomationControl.normalizeAutomationSettings(folderSettings);
|
||||||
|
const currentJobCount = window.AutomationControl.countAutomaticQueueJobs(queueJobs);
|
||||||
|
const availableSlots = normalized.queueLimitJobs === 0 ? null : Math.max(0, normalized.queueLimitJobs - currentJobCount);
|
||||||
|
const telemetry = window.AutomationControl.rollDailyTelemetry(folderSettings.telemetry);
|
||||||
|
const snapshot = {
|
||||||
|
...automationRuntimeStatus,
|
||||||
|
enabled: folderSettings.enabled === true,
|
||||||
|
folderPath: String(folderSettings.folderPath || automationRuntimeStatus.folderPath || ''),
|
||||||
|
paused: automationRuntimeStatus.paused === true || normalized.paused,
|
||||||
|
pausedAt: automationRuntimeStatus.pausedAt ?? normalized.pausedAt,
|
||||||
|
queueLimitJobs: normalized.queueLimitJobs,
|
||||||
|
currentJobCount,
|
||||||
|
availableSlots,
|
||||||
|
queueLimited: normalized.queueLimitJobs !== 0 && availableSlots === 0,
|
||||||
|
telemetry
|
||||||
|
};
|
||||||
|
snapshot.state = window.AutomationControl.deriveAutomationState(snapshot);
|
||||||
|
return freezeAutomationValue(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function evaluateAutomationCandidates(files, options = {}) {
|
||||||
|
const source = Array.isArray(files) ? files : [];
|
||||||
|
const candidates = source.map(normalizeAutomationCandidate);
|
||||||
|
const matched = candidates.filter(candidate => candidate.path && candidate.filterMatched);
|
||||||
|
const folderSettings = config.globalSettings?.folderMonitor || {};
|
||||||
|
const selectedHosters = Array.from(new Set((Array.isArray(options.selectedHosters) ? options.selectedHosters : folderSettings.hosters || [])
|
||||||
|
.map(value => String(value || '').trim())
|
||||||
|
.filter(Boolean)));
|
||||||
|
const [history, uploadLog] = await Promise.all([
|
||||||
|
window.api.getHistory(),
|
||||||
|
window.api.readOwnUploadLog()
|
||||||
|
]);
|
||||||
|
const processed = window.AutomationControl.classifyProcessedCandidates({
|
||||||
|
candidates: matched,
|
||||||
|
queuePaths: [...queueJobs.map(job => job.file), ...selectedFiles.map(file => file.path)],
|
||||||
|
historyRows: flattenAutomationHistoryRows(history),
|
||||||
|
uploadLogRows: uploadLog
|
||||||
|
});
|
||||||
|
const processedPaths = new Set(processed.processedPaths.map(normalizeAutomationPath));
|
||||||
|
const unprocessed = matched.filter(candidate => !processedPaths.has(normalizeAutomationPath(candidate.path)));
|
||||||
|
const inspection = await window.api.inspectImportFiles(unprocessed, []);
|
||||||
|
const metadata = new Map(unprocessed.map(candidate => [normalizeAutomationPath(candidate.path), candidate]));
|
||||||
|
const accepted = (Array.isArray(inspection?.accepted) ? inspection.accepted : []).map(file => ({
|
||||||
|
...(metadata.get(normalizeAutomationPath(file?.path)) || {}),
|
||||||
|
...file
|
||||||
|
}));
|
||||||
|
const plannedCandidates = accepted.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 normalizedSettings = automationSettings();
|
||||||
|
const currentJobCount = window.AutomationControl.countAutomaticQueueJobs(queueJobs);
|
||||||
|
const availableSlots = normalizedSettings.queueLimitJobs === 0 ? null : Math.max(0, normalizedSettings.queueLimitJobs - currentJobCount);
|
||||||
|
const admission = window.AutomationControl.planAtomicAdmissions({
|
||||||
|
candidates: plannedCandidates,
|
||||||
|
currentJobCount,
|
||||||
|
queueLimitJobs: normalizedSettings.queueLimitJobs
|
||||||
|
});
|
||||||
|
const admittedPaths = new Set(admission.admittedPaths.map(normalizeAutomationPath));
|
||||||
|
const deferredPaths = new Set(admission.deferredPaths.map(normalizeAutomationPath));
|
||||||
|
const admittedFiles = plannedCandidates.filter(candidate => admittedPaths.has(normalizeAutomationPath(candidate.path)));
|
||||||
|
const deferredFiles = plannedCandidates.filter(candidate => deferredPaths.has(normalizeAutomationPath(candidate.path)));
|
||||||
|
const resultingJobs = plannedCandidates.reduce((total, candidate) => total + candidate.eligibleJobCount, 0);
|
||||||
|
const summary = {
|
||||||
|
found: candidates.length,
|
||||||
|
filterMatched: matched.length,
|
||||||
|
alreadyProcessed: processed.processedPaths.length,
|
||||||
|
unavailable: Number(inspection?.unavailableCount) || 0,
|
||||||
|
sizeLimitedJobs: plannedCandidates.length * selectedHosters.length - resultingJobs,
|
||||||
|
acceptedFiles: plannedCandidates.length,
|
||||||
|
selectedTargets: selectedHosters.length,
|
||||||
|
resultingJobs,
|
||||||
|
availableSlots,
|
||||||
|
deferredFiles: deferredFiles.length
|
||||||
|
};
|
||||||
|
return freezeAutomationValue({
|
||||||
|
dryRun: options.dryRun === true,
|
||||||
|
trigger: String(options.trigger || 'watcher'),
|
||||||
|
queueJobCount: currentJobCount,
|
||||||
|
queueLimitJobs: normalizedSettings.queueLimitJobs,
|
||||||
|
selectedHosters,
|
||||||
|
candidates: plannedCandidates,
|
||||||
|
admittedFiles,
|
||||||
|
deferredFiles,
|
||||||
|
summary,
|
||||||
|
telemetryDelta: {
|
||||||
|
detected: summary.found,
|
||||||
|
queued: admittedFiles.length,
|
||||||
|
skipped: summary.alreadyProcessed + summary.unavailable,
|
||||||
|
deferred: deferredFiles.length,
|
||||||
|
lastDetectedName: plannedCandidates.at(-1)?.name || ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAutomationPreviewJob(file, hoster) {
|
||||||
|
return {
|
||||||
|
id: `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
|
file: file.path,
|
||||||
|
fileName: file.name,
|
||||||
|
hoster,
|
||||||
|
status: 'preview',
|
||||||
|
bytesUploaded: 0,
|
||||||
|
bytesTotal: file.size || 0,
|
||||||
|
speedKbs: 0,
|
||||||
|
elapsed: 0,
|
||||||
|
remaining: 0,
|
||||||
|
error: null,
|
||||||
|
result: null,
|
||||||
|
attempt: 0,
|
||||||
|
maxAttempts: 0,
|
||||||
|
link: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistAutomationTelemetry(delta) {
|
||||||
|
const globalSettings = config.globalSettings || {};
|
||||||
|
const folderSettings = globalSettings.folderMonitor || {};
|
||||||
|
const telemetry = window.AutomationControl.applyTelemetryDelta(folderSettings.telemetry, delta);
|
||||||
|
const nextSettings = {
|
||||||
|
...globalSettings,
|
||||||
|
folderMonitor: { ...folderSettings, telemetry }
|
||||||
|
};
|
||||||
|
config.globalSettings = nextSettings;
|
||||||
|
try {
|
||||||
|
await saveGlobalSettingsTracked(nextSettings);
|
||||||
|
} catch {}
|
||||||
|
return telemetry;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyAutomationEvaluation(evaluation) {
|
||||||
|
if (!evaluation || evaluation.dryRun === true) {
|
||||||
|
return freezeAutomationValue({ admittedFiles: [], deferredFiles: [], paused: false, dryRun: true });
|
||||||
|
}
|
||||||
|
const manualPreview = evaluation.trigger === 'manual-host' || evaluation.trigger === 'manual-import';
|
||||||
|
const paused = await isAutomationPaused();
|
||||||
|
if (paused && !manualPreview) {
|
||||||
|
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 candidates = evaluation.candidates.filter(candidate => !currentPaths.has(normalizeAutomationPath(candidate.path)));
|
||||||
|
if (evaluation.selectedHosters.length === 0) {
|
||||||
|
if (candidates.length > 0) {
|
||||||
|
_pendingFiles.push(...candidates.map(file => ({ path: file.path, name: file.name, size: file.size, mtimeMs: file.mtimeMs })));
|
||||||
|
mergePendingImportInspection({
|
||||||
|
candidateCount: evaluation.summary.filterMatched,
|
||||||
|
duplicateCount: evaluation.summary.alreadyProcessed,
|
||||||
|
unavailableCount: evaluation.summary.unavailable,
|
||||||
|
accepted: candidates
|
||||||
|
});
|
||||||
|
markPendingFolderMonitorFiles(candidates, config.globalSettings?.folderMonitor?.autoStart === true);
|
||||||
|
if (document.getElementById('hosterModal')?.style.display === 'flex') renderHosterModal();
|
||||||
|
else openHosterModal();
|
||||||
|
}
|
||||||
|
return freezeAutomationValue({ admittedFiles: [], deferredFiles: [], paused, awaitingHostSelection: candidates.length > 0 });
|
||||||
|
}
|
||||||
|
const normalizedSettings = automationSettings();
|
||||||
|
const currentJobCount = window.AutomationControl.countAutomaticQueueJobs(queueJobs);
|
||||||
|
const admission = window.AutomationControl.planAtomicAdmissions({
|
||||||
|
candidates,
|
||||||
|
currentJobCount,
|
||||||
|
queueLimitJobs: normalizedSettings.queueLimitJobs
|
||||||
|
});
|
||||||
|
const admittedPaths = new Set(admission.admittedPaths.map(normalizeAutomationPath));
|
||||||
|
const deferredPaths = new Set(admission.deferredPaths.map(normalizeAutomationPath));
|
||||||
|
const admittedFiles = candidates.filter(candidate => admittedPaths.has(normalizeAutomationPath(candidate.path)));
|
||||||
|
const deferredFiles = candidates.filter(candidate => deferredPaths.has(normalizeAutomationPath(candidate.path)));
|
||||||
|
if (admittedFiles.length === 0) {
|
||||||
|
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)));
|
||||||
|
selectedUploadHosters = evaluation.selectedHosters.slice();
|
||||||
|
clearDedupKeysForPaths(filePaths);
|
||||||
|
selectedFiles.push(...admittedFiles.map(file => ({ path: file.path, name: file.name, size: file.size })));
|
||||||
|
queueJobs.push(...newJobs);
|
||||||
|
rebuildJobIndex();
|
||||||
|
_queueStatsCache = null;
|
||||||
|
renderHosterSummary();
|
||||||
|
renderQueueTable();
|
||||||
|
updateUploadView({ rebuildPreview: false });
|
||||||
|
updateStatusBar();
|
||||||
|
updateStatsPanel();
|
||||||
|
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({
|
||||||
|
autoStart: config.globalSettings?.folderMonitor?.autoStart === true,
|
||||||
uploading,
|
uploading,
|
||||||
healthCheckRunning
|
healthCheckRunning
|
||||||
});
|
});
|
||||||
if (action === 'start') {
|
if (action === 'inject' && !(await isAutomationPaused())) {
|
||||||
startUpload();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (action !== 'inject') return;
|
|
||||||
|
|
||||||
const newJobs = queueJobs.filter(job => job.status === 'preview' && newPaths.has(job.file));
|
|
||||||
if (newJobs.length === 0) return;
|
|
||||||
const cleanupPreparation = prepareSourceCleanup(newJobs);
|
const cleanupPreparation = prepareSourceCleanup(newJobs);
|
||||||
newJobs.forEach(job => { job.status = 'queued'; });
|
try {
|
||||||
renderQueueTable();
|
const result = await window.api.addJobsToBatch({
|
||||||
window.api.addJobsToBatch({
|
|
||||||
jobs: newJobs.map(serializeUploadJob),
|
jobs: newJobs.map(serializeUploadJob),
|
||||||
sourceCleanupGroups: cleanupPreparation.groups
|
sourceCleanupGroups: cleanupPreparation.groups
|
||||||
}).then(result => {
|
});
|
||||||
|
if (!result?.error) 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 {}
|
||||||
|
renderQueueTable();
|
||||||
persistQueueStateSoon(true);
|
persistQueueStateSoon(true);
|
||||||
|
} else if (action === 'start') {
|
||||||
|
await startUpload();
|
||||||
|
}
|
||||||
|
return freezeAutomationValue({ admittedFiles, deferredFiles, paused, dryRun: false, plannedJobs: admission.plannedJobs });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runFolderMonitorTestScan() {
|
||||||
|
const result = await window.api.folderMonitorTestScan();
|
||||||
|
return evaluateAutomationCandidates(result?.files || [], { dryRun: true, trigger: result?.trigger || 'test' });
|
||||||
|
}
|
||||||
|
|
||||||
|
window.evaluateAutomationCandidates = evaluateAutomationCandidates;
|
||||||
|
window.applyAutomationEvaluation = applyAutomationEvaluation;
|
||||||
|
window.createAutomationStatusSnapshot = createAutomationStatusSnapshot;
|
||||||
|
window.runFolderMonitorTestScan = runFolderMonitorTestScan;
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Init ---
|
// --- Init ---
|
||||||
@@ -476,6 +726,9 @@ async function init() {
|
|||||||
await showAppAlert(error.message || String(error), 'Zugangsdaten gesperrt');
|
await showAppAlert(error.message || String(error), 'Zugangsdaten gesperrt');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
await refreshAutomationRuntimeStatus();
|
||||||
|
} catch {}
|
||||||
setUiLanguage(config.globalSettings?.language);
|
setUiLanguage(config.globalSettings?.language);
|
||||||
hosterSettings = config.hosterSettings || {};
|
hosterSettings = config.hosterSettings || {};
|
||||||
autoHealthCheckEnabled = loadAutoCheckPreference();
|
autoHealthCheckEnabled = loadAutoCheckPreference();
|
||||||
@@ -561,7 +814,15 @@ async function init() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.api.onFolderMonitorNewFiles(handleFolderMonitorFiles);
|
window.api.onFolderMonitorNewFiles(files => {
|
||||||
|
handleFolderMonitorFiles(files).catch(error => window.api.debugLog(`folder-monitor renderer evaluation failed: ${error.message || String(error)}`));
|
||||||
|
});
|
||||||
|
if (typeof window.api.onAutomationStatus === 'function') {
|
||||||
|
window.api.onAutomationStatus(status => {
|
||||||
|
applyAutomationRuntimeStatus(status);
|
||||||
|
updateQueueActionButtons();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Account switched notification
|
// Account switched notification
|
||||||
window.api.onAccountSwitched((data) => {
|
window.api.onAccountSwitched((data) => {
|
||||||
@@ -1167,51 +1428,54 @@ function closeHosterModal() {
|
|||||||
if (modal) modal.style.display = 'none';
|
if (modal) modal.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyHosterSelection() {
|
async function applyHosterSelection() {
|
||||||
if (isImportConfirmationBlocked()) return false;
|
if (isImportConfirmationBlocked()) return false;
|
||||||
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 admittedFiles = _pendingFiles.filter(file => window.ImportPreflight
|
const pendingFiles = _pendingFiles.slice();
|
||||||
|
const automationFiles = pendingFiles.filter(file => _pendingFolderMonitorAutoStart.has(file.path));
|
||||||
|
const regularFiles = pendingFiles.filter(file => !_pendingFolderMonitorAutoStart.has(file.path));
|
||||||
|
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));
|
||||||
const pathsToInject = new Set(admittedFiles
|
|
||||||
.filter(file => !_pendingFolderMonitorAutoStart.has(file.path) || _pendingFolderMonitorAutoStart.get(file.path))
|
|
||||||
.map(file => file.path));
|
|
||||||
const shouldAutoStart = !uploading && admittedFiles.some(file => _pendingFolderMonitorAutoStart.get(file.path) === true);
|
|
||||||
if (admittedFiles.length > 0) {
|
if (admittedFiles.length > 0) {
|
||||||
selectedFiles.push(...admittedFiles);
|
selectedFiles.push(...admittedFiles);
|
||||||
}
|
}
|
||||||
_pendingFiles = [];
|
_pendingFiles = [];
|
||||||
clearDedupKeysForPaths(pendingPaths);
|
clearDedupKeysForPaths(pendingPaths);
|
||||||
renderHosterSummary();
|
renderHosterSummary();
|
||||||
|
|
||||||
// During an active upload, build preview jobs for the new files and inject
|
|
||||||
// them into the running batch immediately (otherwise they'd be lost on
|
|
||||||
// handleBatchDone via syncSelectedFilesFromQueue)
|
|
||||||
if (pendingPaths.size > 0) buildQueuePreview();
|
if (pendingPaths.size > 0) buildQueuePreview();
|
||||||
if (uploading && pathsToInject.size > 0) {
|
if (uploading && pendingPaths.size > 0 && !(await isAutomationPaused())) {
|
||||||
const newJobs = queueJobs.filter(j => j.status === 'preview' && pathsToInject.has(j.file));
|
const newJobs = queueJobs.filter(j => j.status === 'preview' && pendingPaths.has(j.file));
|
||||||
if (newJobs.length > 0) {
|
if (newJobs.length > 0) {
|
||||||
const cleanupPreparation = prepareSourceCleanup(newJobs);
|
const cleanupPreparation = prepareSourceCleanup(newJobs);
|
||||||
newJobs.forEach(j => { j.status = 'queued'; });
|
try {
|
||||||
renderQueueTable();
|
const result = await window.api.addJobsToBatch({
|
||||||
window.api.addJobsToBatch({
|
|
||||||
jobs: newJobs.map(serializeUploadJob),
|
jobs: newJobs.map(serializeUploadJob),
|
||||||
sourceCleanupGroups: cleanupPreparation.groups
|
sourceCleanupGroups: cleanupPreparation.groups
|
||||||
}).then(result => {
|
});
|
||||||
|
if (!result?.error) 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 {}
|
||||||
|
renderQueueTable();
|
||||||
persistQueueStateSoon(true);
|
persistQueueStateSoon(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUploadView();
|
updateUploadView();
|
||||||
persistQueueStateSoon(true); // immediate persist after adding files
|
persistQueueStateSoon(true);
|
||||||
|
const selectedHosters = selectedUploadHosters.slice();
|
||||||
_pendingFolderMonitorAutoStart.clear();
|
_pendingFolderMonitorAutoStart.clear();
|
||||||
_pendingImportInspection = null;
|
_pendingImportInspection = null;
|
||||||
document.getElementById('hosterModal').style.display = 'none';
|
document.getElementById('hosterModal').style.display = 'none';
|
||||||
if (shouldAutoStart) startUpload();
|
if (automationFiles.length > 0) {
|
||||||
|
const evaluation = await evaluateAutomationCandidates(automationFiles, {
|
||||||
|
dryRun: false,
|
||||||
|
trigger: 'manual-host',
|
||||||
|
selectedHosters
|
||||||
|
});
|
||||||
|
await applyAutomationEvaluation(evaluation);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1626,7 +1890,7 @@ function addPathsToQueue(paths, options) {
|
|||||||
return coordinateImportEntries(paths, options);
|
return coordinateImportEntries(paths, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUploadView() {
|
function updateUploadView(options = {}) {
|
||||||
const dropZone = document.getElementById('dropZone');
|
const dropZone = document.getElementById('dropZone');
|
||||||
const queueShell = document.getElementById('queueShell');
|
const queueShell = document.getElementById('queueShell');
|
||||||
const queueActions = document.getElementById('queueActions');
|
const queueActions = document.getElementById('queueActions');
|
||||||
@@ -1639,7 +1903,7 @@ function updateUploadView() {
|
|||||||
dropZone.style.display = 'none';
|
dropZone.style.display = 'none';
|
||||||
queueShell.style.display = 'flex';
|
queueShell.style.display = 'flex';
|
||||||
queueActions.style.display = 'flex';
|
queueActions.style.display = 'flex';
|
||||||
if (!uploading && selectedFiles.length > 0) {
|
if (options.rebuildPreview !== false && !uploading && selectedFiles.length > 0) {
|
||||||
buildQueuePreview();
|
buildQueuePreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3342,6 +3606,7 @@ function serializeUploadJob(job) {
|
|||||||
|
|
||||||
async function startUpload(opts) {
|
async function startUpload(opts) {
|
||||||
if (uploading) return;
|
if (uploading) return;
|
||||||
|
if (await isAutomationPaused()) return false;
|
||||||
if (!(opts && opts._restoredAutoStart)) cancelStartupQueueAutoStart();
|
if (!(opts && opts._restoredAutoStart)) cancelStartupQueueAutoStart();
|
||||||
if (!(opts && opts._autoRetry)) _cancelAutoRetry(true);
|
if (!(opts && opts._autoRetry)) _cancelAutoRetry(true);
|
||||||
else _cancelAutoRetry(false);
|
else _cancelAutoRetry(false);
|
||||||
@@ -3420,6 +3685,7 @@ function _markSkippedJobs(result) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startSelectedUpload(explicitJobs) {
|
async function startSelectedUpload(explicitJobs) {
|
||||||
|
if (await isAutomationPaused()) return false;
|
||||||
const scopedJobs = Array.isArray(explicitJobs) ? explicitJobs : _getVisibleSelectedQueueJobs();
|
const scopedJobs = Array.isArray(explicitJobs) ? explicitJobs : _getVisibleSelectedQueueJobs();
|
||||||
if (uploading) {
|
if (uploading) {
|
||||||
_hydrateMissingJobSizes();
|
_hydrateMissingJobSizes();
|
||||||
|
|||||||
@@ -64,6 +64,16 @@ test('Windows compositor paints the full hidden surface with an RDP session envi
|
|||||||
const { contextBridge } = require('electron');
|
const { contextBridge } = require('electron');
|
||||||
const managedOnlineBackupProbeCalls = [];
|
const managedOnlineBackupProbeCalls = [];
|
||||||
const folderMonitorProbeCalls = [];
|
const folderMonitorProbeCalls = [];
|
||||||
|
let automationProbe = {
|
||||||
|
history: [],
|
||||||
|
uploadLog: [],
|
||||||
|
paused: false,
|
||||||
|
dryScan: { files: [], reachable: true, trigger: 'test' },
|
||||||
|
readCalls: { history: 0, uploadLog: 0, inspect: 0, status: 0, testScan: 0, reconcile: 0 },
|
||||||
|
mutationCalls: [],
|
||||||
|
logs: [],
|
||||||
|
savedSettings: []
|
||||||
|
};
|
||||||
const managedOnlineBackupIds = {
|
const managedOnlineBackupIds = {
|
||||||
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
||||||
b: 'AQEBAQEBAQEBAQEBAQEBAQ',
|
b: 'AQEBAQEBAQEBAQEBAQEBAQ',
|
||||||
@@ -144,14 +154,80 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
pending.resolve({ ok: true, removedId: pending.id, notFound: false });
|
pending.resolve({ ok: true, removedId: pending.id, notFound: false });
|
||||||
},
|
},
|
||||||
getManagedOnlineBackupProbeCalls() { return managedOnlineBackupProbeCalls; },
|
getManagedOnlineBackupProbeCalls() { return managedOnlineBackupProbeCalls; },
|
||||||
debugLog() {},
|
configureAutomationProbe(value = {}) {
|
||||||
|
automationProbe = {
|
||||||
|
history: Array.isArray(value.history) ? value.history : [],
|
||||||
|
uploadLog: Array.isArray(value.uploadLog) ? value.uploadLog : [],
|
||||||
|
paused: value.paused === true,
|
||||||
|
dryScan: value.dryScan || { files: [], reachable: true, trigger: 'test' },
|
||||||
|
readCalls: { history: 0, uploadLog: 0, inspect: 0, status: 0, testScan: 0, reconcile: 0 },
|
||||||
|
mutationCalls: [],
|
||||||
|
logs: [],
|
||||||
|
savedSettings: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getAutomationProbeState() {
|
||||||
|
return {
|
||||||
|
readCalls: { ...automationProbe.readCalls },
|
||||||
|
mutationCalls: automationProbe.mutationCalls.map(value => [...value]),
|
||||||
|
logs: [...automationProbe.logs],
|
||||||
|
savedSettings: automationProbe.savedSettings.map(value => JSON.parse(JSON.stringify(value)))
|
||||||
|
};
|
||||||
|
},
|
||||||
|
inspectImportFiles(entries) {
|
||||||
|
automationProbe.readCalls.inspect++;
|
||||||
|
const candidates = Array.isArray(entries) ? entries : [];
|
||||||
|
const unavailable = candidates.filter(entry => entry?.unavailable).map(entry => ({ ...entry, reason: 'unreadable' }));
|
||||||
|
const accepted = candidates.filter(entry => !entry?.unavailable).map(entry => ({ ...entry }));
|
||||||
|
return Promise.resolve({
|
||||||
|
candidateCount: candidates.length,
|
||||||
|
duplicateCount: 0,
|
||||||
|
unavailableCount: unavailable.length,
|
||||||
|
acceptedCount: accepted.length,
|
||||||
|
accepted,
|
||||||
|
duplicates: [],
|
||||||
|
unavailable
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getHistory() {
|
||||||
|
automationProbe.readCalls.history++;
|
||||||
|
return Promise.resolve(automationProbe.history);
|
||||||
|
},
|
||||||
|
readOwnUploadLog() {
|
||||||
|
automationProbe.readCalls.uploadLog++;
|
||||||
|
return Promise.resolve(automationProbe.uploadLog);
|
||||||
|
},
|
||||||
|
automationGetStatus() {
|
||||||
|
automationProbe.readCalls.status++;
|
||||||
|
return Promise.resolve({ paused: automationProbe.paused });
|
||||||
|
},
|
||||||
|
folderMonitorTestScan() {
|
||||||
|
automationProbe.readCalls.testScan++;
|
||||||
|
return Promise.resolve(automationProbe.dryScan);
|
||||||
|
},
|
||||||
|
folderMonitorReconcile() {
|
||||||
|
automationProbe.readCalls.reconcile++;
|
||||||
|
return Promise.resolve(automationProbe.dryScan);
|
||||||
|
},
|
||||||
|
debugLog(value) { automationProbe.logs.push(String(value)); },
|
||||||
|
saveGlobalSettings(value) {
|
||||||
|
automationProbe.savedSettings.push(value);
|
||||||
|
automationProbe.mutationCalls.push(['settings']);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
},
|
||||||
savePendingQueue(payload) {
|
savePendingQueue(payload) {
|
||||||
folderMonitorProbeCalls.push(['save', payload?.queueJobs?.length || 0]);
|
folderMonitorProbeCalls.push(['save', payload?.queueJobs?.length || 0]);
|
||||||
|
automationProbe.mutationCalls.push(['save', payload?.queueJobs?.length || 0]);
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
},
|
},
|
||||||
addJobsToBatch(payload) {
|
addJobsToBatch(payload) {
|
||||||
folderMonitorProbeCalls.push(['inject', payload?.jobs?.length || 0]);
|
folderMonitorProbeCalls.push(['inject', payload?.jobs?.length || 0]);
|
||||||
return Promise.resolve({});
|
automationProbe.mutationCalls.push(['inject', payload?.jobs?.length || 0]);
|
||||||
|
return Promise.resolve({ added: payload?.jobs?.length || 0 });
|
||||||
|
},
|
||||||
|
startUpload(payload) {
|
||||||
|
automationProbe.mutationCalls.push(['start', payload?.jobs?.length || 0]);
|
||||||
|
return Promise.resolve({ started: true });
|
||||||
},
|
},
|
||||||
getFolderMonitorProbeCalls() { return folderMonitorProbeCalls; }
|
getFolderMonitorProbeCalls() { return folderMonitorProbeCalls; }
|
||||||
});
|
});
|
||||||
@@ -310,14 +386,14 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
rebuildJobIndex();
|
rebuildJobIndex();
|
||||||
};
|
};
|
||||||
resetQueue(false);
|
resetQueue(false);
|
||||||
handleFolderMonitorFiles(['C:\\\\folder-monitor-queue-only.mkv']);
|
await handleFolderMonitorFiles(['C:\\\\folder-monitor-queue-only.mkv']);
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
const queueOnly = {
|
const queueOnly = {
|
||||||
statuses: queueJobs.map(job => job.status),
|
statuses: queueJobs.map(job => job.status),
|
||||||
injectCalls: window.api.getFolderMonitorProbeCalls().filter(call => call[0] === 'inject').length
|
injectCalls: window.api.getFolderMonitorProbeCalls().filter(call => call[0] === 'inject').length
|
||||||
};
|
};
|
||||||
resetQueue(true);
|
resetQueue(true);
|
||||||
handleFolderMonitorFiles(['C:\\\\folder-monitor-inject.mkv']);
|
await handleFolderMonitorFiles(['C:\\\\folder-monitor-inject.mkv']);
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
const autoStart = {
|
const autoStart = {
|
||||||
statuses: queueJobs.map(job => job.status),
|
statuses: queueJobs.map(job => job.status),
|
||||||
@@ -371,6 +447,212 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
const sameBasenameResult = Object.fromEntries(queueJobs.map(job => [job.id, { status: job.status, code: job.result?.file_code || null }]));
|
const sameBasenameResult = Object.fromEntries(queueJobs.map(job => [job.id, { status: job.status, code: job.result?.file_code || null }]));
|
||||||
return { germanTooltips, englishTooltips, gridScope, actions, queueOnly, autoStart, manualQueueOnly, manualAutoStartRunning, manualAutoStartIdle, sameBasenameResult };
|
return { germanTooltips, englishTooltips, gridScope, actions, queueOnly, autoStart, manualQueueOnly, manualAutoStartRunning, manualAutoStartIdle, sameBasenameResult };
|
||||||
})()`;
|
})()`;
|
||||||
|
const automationPipelineScript = `(async () => {
|
||||||
|
const clone = value => JSON.parse(JSON.stringify(value));
|
||||||
|
const captureMutationFingerprint = async () => {
|
||||||
|
const api = await window.api.getAutomationProbeState();
|
||||||
|
return {
|
||||||
|
queueJobs: clone(queueJobs),
|
||||||
|
selectedFiles: clone(selectedFiles),
|
||||||
|
counters: { sessionDone: _sessionDoneCount, sessionError: _sessionErrorCount },
|
||||||
|
pendingFiles: clone(_pendingFiles),
|
||||||
|
pendingInspection: clone(_pendingImportInspection),
|
||||||
|
pendingInspections: _pendingImportInspections,
|
||||||
|
pendingAutoStart: clone([..._pendingFolderMonitorAutoStart]),
|
||||||
|
config: clone(config),
|
||||||
|
monitorSettings: clone(config.globalSettings?.folderMonitor || {}),
|
||||||
|
api: { mutationCalls: api.mutationCalls, logs: api.logs, savedSettings: api.savedSettings }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const hosters = ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx'];
|
||||||
|
const candidates = Array.from({ length: 500 }, (_, index) => ({
|
||||||
|
path: 'C:\\\\watch\\\\candidate-' + String(index).padStart(3, '0') + '.mkv',
|
||||||
|
name: 'candidate-' + String(index).padStart(3, '0') + '.mkv',
|
||||||
|
size: index >= 55 && index < 75 ? 2 * 1024 * 1024 : 512 * 1024,
|
||||||
|
mtimeMs: index,
|
||||||
|
filterMatched: index < 430,
|
||||||
|
unavailable: index >= 50 && index < 55
|
||||||
|
}));
|
||||||
|
const history = [{
|
||||||
|
id: 'history',
|
||||||
|
files: candidates.slice(0, 25).map(file => ({ path: file.path, name: file.name, results: [{ hoster: hosters[0], status: 'done' }] }))
|
||||||
|
}];
|
||||||
|
const uploadLog = candidates.slice(25, 50).map(file => ({ fileName: file.name, hoster: hosters[0] }));
|
||||||
|
config = {
|
||||||
|
hosters: Object.fromEntries(HOSTERS.map(hoster => [hoster, []])),
|
||||||
|
hosterSettings: {},
|
||||||
|
globalSettings: {
|
||||||
|
folderMonitor: {
|
||||||
|
enabled: true,
|
||||||
|
folderPath: 'C:\\\\watch',
|
||||||
|
hosters,
|
||||||
|
autoStart: false,
|
||||||
|
queueLimitJobs: 1500,
|
||||||
|
paused: false,
|
||||||
|
telemetry: { dateKey: '2026-08-26', detected: 7, queued: 3, skipped: 2, deferred: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
hosterSettings = { 'doodstream.com': { maxSizeMb: 1 } };
|
||||||
|
selectedUploadHosters = [];
|
||||||
|
selectedFiles = [{ path: 'C:\\\\manual\\\\selected.mkv', name: 'selected.mkv', size: 1 }];
|
||||||
|
queueJobs = Array.from({ length: 300 }, (_, index) => ({
|
||||||
|
id: 'existing-' + index,
|
||||||
|
file: 'C:\\\\queue\\\\existing-' + index + '.mkv',
|
||||||
|
fileName: 'existing-' + index + '.mkv',
|
||||||
|
hoster: hosters[index % hosters.length],
|
||||||
|
status: 'queued',
|
||||||
|
bytesTotal: 1
|
||||||
|
}));
|
||||||
|
_sessionDoneCount = 4;
|
||||||
|
_sessionErrorCount = 5;
|
||||||
|
_pendingFiles = [{ path: 'C:\\\\pending\\\\pending.mkv', name: 'pending.mkv', size: 1 }];
|
||||||
|
_pendingImportInspection = { candidateCount: 1, accepted: clone(_pendingFiles) };
|
||||||
|
_pendingImportInspections = 1;
|
||||||
|
_pendingFolderMonitorAutoStart.clear();
|
||||||
|
_pendingFolderMonitorAutoStart.set('C:\\\\pending\\\\pending.mkv', true);
|
||||||
|
rebuildJobIndex();
|
||||||
|
window.api.configureAutomationProbe({ history, uploadLog, paused: false });
|
||||||
|
const before = await captureMutationFingerprint();
|
||||||
|
const preview = await evaluateAutomationCandidates(candidates, { dryRun: true, trigger: 'test' });
|
||||||
|
const after = await captureMutationFingerprint();
|
||||||
|
const dryReads = (await window.api.getAutomationProbeState()).readCalls;
|
||||||
|
const dry = {
|
||||||
|
fingerprintEqual: JSON.stringify(after) === JSON.stringify(before),
|
||||||
|
summary: preview.summary,
|
||||||
|
frozen: Object.isFrozen(preview) && Object.isFrozen(preview.summary) && Object.isFrozen(preview.admittedFiles) && Object.isFrozen(preview.deferredFiles),
|
||||||
|
reads: dryReads
|
||||||
|
};
|
||||||
|
window.api.configureAutomationProbe({
|
||||||
|
dryScan: { files: [candidates[55]], reachable: true, trigger: 'test' },
|
||||||
|
paused: false
|
||||||
|
});
|
||||||
|
const manualTestBefore = await captureMutationFingerprint();
|
||||||
|
const manualTestPreview = await runFolderMonitorTestScan();
|
||||||
|
const manualTestAfter = await captureMutationFingerprint();
|
||||||
|
const manualTestProbe = await window.api.getAutomationProbeState();
|
||||||
|
const manualTest = {
|
||||||
|
fingerprintEqual: JSON.stringify(manualTestAfter) === JSON.stringify(manualTestBefore),
|
||||||
|
summary: manualTestPreview.summary,
|
||||||
|
reads: manualTestProbe.readCalls
|
||||||
|
};
|
||||||
|
|
||||||
|
const configureAtomicState = currentCount => {
|
||||||
|
config.globalSettings.folderMonitor = {
|
||||||
|
enabled: true,
|
||||||
|
folderPath: 'C:\\\\watch',
|
||||||
|
hosters,
|
||||||
|
autoStart: false,
|
||||||
|
queueLimitJobs: 15000,
|
||||||
|
paused: false,
|
||||||
|
telemetry: { dateKey: new Date().toLocaleDateString('en-CA'), detected: 0, queued: 0, skipped: 0, deferred: 0 }
|
||||||
|
};
|
||||||
|
hosterSettings = {
|
||||||
|
'doodstream.com': { maxSizeMb: 2 },
|
||||||
|
'voe.sx': { maxSizeMb: 2 }
|
||||||
|
};
|
||||||
|
selectedUploadHosters = [];
|
||||||
|
selectedFiles = [{ path: 'C:\\\\manual\\\\unplanned.mkv', name: 'unplanned.mkv', size: 1 }];
|
||||||
|
queueJobs = Array.from({ length: currentCount }, (_, index) => ({
|
||||||
|
id: 'capacity-' + index,
|
||||||
|
file: 'C:\\\\capacity\\\\existing-' + index + '.mkv',
|
||||||
|
fileName: 'existing-' + index + '.mkv',
|
||||||
|
hoster: hosters[index % hosters.length],
|
||||||
|
status: 'queued',
|
||||||
|
bytesTotal: 1
|
||||||
|
}));
|
||||||
|
_pendingFiles = [];
|
||||||
|
_pendingImportInspection = null;
|
||||||
|
_pendingImportInspections = 0;
|
||||||
|
_pendingFolderMonitorAutoStart.clear();
|
||||||
|
uploading = false;
|
||||||
|
rebuildJobIndex();
|
||||||
|
window.api.configureAutomationProbe({ paused: false });
|
||||||
|
};
|
||||||
|
const atomicCandidates = [
|
||||||
|
{ path: 'C:\\\\watch\\\\a.mkv', name: 'a.mkv', size: 1024 * 1024, mtimeMs: 1, filterMatched: true },
|
||||||
|
{ path: 'C:\\\\watch\\\\b.mkv', name: 'b.mkv', size: 3 * 1024 * 1024, mtimeMs: 2, filterMatched: true }
|
||||||
|
];
|
||||||
|
configureAtomicState(14998);
|
||||||
|
const atomicEvaluation = await evaluateAutomationCandidates(atomicCandidates, { dryRun: false, trigger: 'watcher' });
|
||||||
|
const atomicResult = await applyAutomationEvaluation(atomicEvaluation);
|
||||||
|
const atomic = {
|
||||||
|
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),
|
||||||
|
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
|
||||||
|
};
|
||||||
|
const statusSnapshot = createAutomationStatusSnapshot();
|
||||||
|
const status = {
|
||||||
|
state: statusSnapshot.state,
|
||||||
|
currentJobCount: statusSnapshot.currentJobCount,
|
||||||
|
availableSlots: statusSnapshot.availableSlots,
|
||||||
|
queueLimited: statusSnapshot.queueLimited,
|
||||||
|
frozen: Object.isFrozen(statusSnapshot) && Object.isFrozen(statusSnapshot.telemetry)
|
||||||
|
};
|
||||||
|
|
||||||
|
configureAtomicState(14994);
|
||||||
|
const staleEvaluation = await evaluateAutomationCandidates(atomicCandidates, { dryRun: false, trigger: 'watcher' });
|
||||||
|
queueJobs.push(...Array.from({ length: 4 }, (_, index) => ({
|
||||||
|
id: 'stale-' + index,
|
||||||
|
file: 'C:\\\\capacity\\\\stale-' + index + '.mkv',
|
||||||
|
fileName: 'stale-' + index + '.mkv',
|
||||||
|
hoster: hosters[index],
|
||||||
|
status: 'queued',
|
||||||
|
bytesTotal: 1
|
||||||
|
})));
|
||||||
|
rebuildJobIndex();
|
||||||
|
const staleResult = await applyAutomationEvaluation(staleEvaluation);
|
||||||
|
const stale = {
|
||||||
|
plannedBeforeApply: staleEvaluation.admittedFiles.map(file => file.name),
|
||||||
|
admittedAfterApply: staleResult.admittedFiles.map(file => file.name),
|
||||||
|
newQueueFiles: [...new Set(queueJobs.filter(job => job.file === atomicCandidates[0].path || job.file === atomicCandidates[1].path).map(job => job.fileName))]
|
||||||
|
};
|
||||||
|
|
||||||
|
configureAtomicState(0);
|
||||||
|
config.globalSettings.folderMonitor.paused = true;
|
||||||
|
window.api.configureAutomationProbe({ paused: true });
|
||||||
|
const pausedJob = {
|
||||||
|
id: 'paused-preview',
|
||||||
|
file: 'C:\\\\manual\\\\paused-preview.mkv',
|
||||||
|
fileName: 'paused-preview.mkv',
|
||||||
|
hoster: hosters[0],
|
||||||
|
status: 'preview',
|
||||||
|
bytesTotal: 1
|
||||||
|
};
|
||||||
|
queueJobs = [pausedJob];
|
||||||
|
selectedFiles = [{ path: pausedJob.file, name: pausedJob.fileName, size: 1 }];
|
||||||
|
selectedUploadHosters = [hosters[0]];
|
||||||
|
rebuildJobIndex();
|
||||||
|
await startUpload();
|
||||||
|
uploading = true;
|
||||||
|
await startSelectedUpload([pausedJob]);
|
||||||
|
uploading = false;
|
||||||
|
const pausedAutomaticEvaluation = await evaluateAutomationCandidates([
|
||||||
|
{ path: 'C:\\\\watch\\\\paused-auto.mkv', name: 'paused-auto.mkv', size: 1, mtimeMs: 1, filterMatched: true }
|
||||||
|
], { dryRun: false, trigger: 'watcher' });
|
||||||
|
const pausedAutomaticResult = await applyAutomationEvaluation(pausedAutomaticEvaluation);
|
||||||
|
await coordinateImportEntries([
|
||||||
|
{ path: 'C:\\\\manual\\\\allowed-preview.mkv', name: 'allowed-preview.mkv', size: 1 }
|
||||||
|
]);
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'checkbox';
|
||||||
|
input.dataset.hosterModal = hosters[0];
|
||||||
|
input.checked = true;
|
||||||
|
document.getElementById('hosterModalList').replaceChildren(input);
|
||||||
|
await applyHosterSelection();
|
||||||
|
const pausedProbe = await window.api.getAutomationProbeState();
|
||||||
|
const paused = {
|
||||||
|
uploading,
|
||||||
|
statuses: Object.fromEntries(queueJobs.map(job => [job.fileName, job.status])),
|
||||||
|
automaticApplied: pausedAutomaticResult.admittedFiles.length,
|
||||||
|
startCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'start').length,
|
||||||
|
injectCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'inject').length
|
||||||
|
};
|
||||||
|
return { dry, manualTest, atomic, status, stale, paused };
|
||||||
|
})()`;
|
||||||
const onlineBackupBehaviorScript = `(async () => {
|
const onlineBackupBehaviorScript = `(async () => {
|
||||||
const ids = {
|
const ids = {
|
||||||
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
||||||
@@ -577,6 +859,7 @@ app.whenReady().then(async () => {
|
|||||||
const onlineBackupBehavior = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupBehaviorScript)});
|
const onlineBackupBehavior = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupBehaviorScript)});
|
||||||
const settingsSearchBehavior = await window.webContents.executeJavaScript(${JSON.stringify(settingsSearchBehaviorScript)});
|
const settingsSearchBehavior = await window.webContents.executeJavaScript(${JSON.stringify(settingsSearchBehaviorScript)});
|
||||||
const folderMonitorBehavior = await window.webContents.executeJavaScript(${JSON.stringify(folderMonitorBehaviorScript)});
|
const folderMonitorBehavior = await window.webContents.executeJavaScript(${JSON.stringify(folderMonitorBehaviorScript)});
|
||||||
|
const automationPipeline = await window.webContents.executeJavaScript(${JSON.stringify(automationPipelineScript)});
|
||||||
const onlineBackupLayout = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupLayoutScript)});
|
const onlineBackupLayout = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupLayoutScript)});
|
||||||
window.setContentSize(760, Math.min(900, display.workAreaSize.height));
|
window.setContentSize(760, Math.min(900, display.workAreaSize.height));
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
@@ -595,6 +878,7 @@ app.whenReady().then(async () => {
|
|||||||
appDialogBehavior,
|
appDialogBehavior,
|
||||||
settingsSearchBehavior,
|
settingsSearchBehavior,
|
||||||
folderMonitorBehavior,
|
folderMonitorBehavior,
|
||||||
|
automationPipeline,
|
||||||
onlineBackupBehavior,
|
onlineBackupBehavior,
|
||||||
onlineBackupLayout,
|
onlineBackupLayout,
|
||||||
onlineBackupNarrowLayout
|
onlineBackupNarrowLayout
|
||||||
@@ -734,6 +1018,69 @@ app.whenReady().then(async () => {
|
|||||||
'waiting-same-name': { status: 'preview', code: null }
|
'waiting-same-name': { status: 'preview', code: null }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
assert.deepEqual(result.automationPipeline.dry, {
|
||||||
|
fingerprintEqual: true,
|
||||||
|
summary: {
|
||||||
|
found: 500,
|
||||||
|
filterMatched: 430,
|
||||||
|
alreadyProcessed: 50,
|
||||||
|
unavailable: 5,
|
||||||
|
sizeLimitedJobs: 20,
|
||||||
|
acceptedFiles: 375,
|
||||||
|
selectedTargets: 4,
|
||||||
|
resultingJobs: 1480,
|
||||||
|
availableSlots: 1200,
|
||||||
|
deferredFiles: 70
|
||||||
|
},
|
||||||
|
frozen: true,
|
||||||
|
reads: { history: 1, uploadLog: 1, inspect: 1, status: 0, testScan: 0, reconcile: 0 }
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationPipeline.manualTest, {
|
||||||
|
fingerprintEqual: true,
|
||||||
|
summary: {
|
||||||
|
found: 1,
|
||||||
|
filterMatched: 1,
|
||||||
|
alreadyProcessed: 0,
|
||||||
|
unavailable: 0,
|
||||||
|
sizeLimitedJobs: 1,
|
||||||
|
acceptedFiles: 1,
|
||||||
|
selectedTargets: 4,
|
||||||
|
resultingJobs: 3,
|
||||||
|
availableSlots: 1200,
|
||||||
|
deferredFiles: 0
|
||||||
|
},
|
||||||
|
reads: { history: 1, uploadLog: 1, inspect: 1, status: 0, testScan: 1, reconcile: 0 }
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationPipeline.atomic, {
|
||||||
|
newQueueFiles: ['b.mkv'],
|
||||||
|
admittedFiles: ['b.mkv'],
|
||||||
|
deferred: 1,
|
||||||
|
queued: 1,
|
||||||
|
currentJobCount: 15000,
|
||||||
|
unplannedJobs: 0
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationPipeline.status, {
|
||||||
|
state: 'queue-limited',
|
||||||
|
currentJobCount: 15000,
|
||||||
|
availableSlots: 0,
|
||||||
|
queueLimited: true,
|
||||||
|
frozen: true
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationPipeline.stale, {
|
||||||
|
plannedBeforeApply: ['a.mkv', 'b.mkv'],
|
||||||
|
admittedAfterApply: ['b.mkv'],
|
||||||
|
newQueueFiles: ['b.mkv']
|
||||||
|
});
|
||||||
|
assert.deepEqual(result.automationPipeline.paused, {
|
||||||
|
uploading: false,
|
||||||
|
statuses: {
|
||||||
|
'paused-preview.mkv': 'preview',
|
||||||
|
'allowed-preview.mkv': 'preview'
|
||||||
|
},
|
||||||
|
automaticApplied: 0,
|
||||||
|
startCalls: 0,
|
||||||
|
injectCalls: 0
|
||||||
|
});
|
||||||
assert.deepEqual(result.onlineBackupBehavior.initialKeys, ['MHU2-ZYXW…9876', 'MHU2-ABCD…1234']);
|
assert.deepEqual(result.onlineBackupBehavior.initialKeys, ['MHU2-ZYXW…9876', 'MHU2-ABCD…1234']);
|
||||||
assert.deepEqual(result.onlineBackupBehavior.initialWarning, {
|
assert.deepEqual(result.onlineBackupBehavior.initialWarning, {
|
||||||
hidden: false,
|
hidden: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user