Files
Multi-Hoster-Upload/lib/automation-control.js
T
Sucukdeluxe afd3b70cf3 fix: harden automation reconciliation and pause lifecycle
Keep full folder scans recoverable under duplicate protection and classify every discovered file once before automatic admission.

Separate manual previews from automatic capacity limits, serialize renderer intake, normalize automation counters and intervals, and make Main authoritative for runtime timestamps.

Enforce persistent pause across startup, import, close recovery, resume failures, and reconciliation while preserving read-only paused scans and exactly one activation reconciliation.

Restore failed asynchronous rotation chunks, complete automation localization and unlimited queue accessibility, and tighten hidden integration cleanup coverage.
2026-08-26 15:47:31 +02:00

197 lines
7.2 KiB
JavaScript

(function initAutomationControl(root, factory) {
const api = factory();
if (typeof module === 'object' && module.exports) module.exports = api;
if (root) root.AutomationControl = api;
})(typeof window !== 'undefined' ? window : globalThis, function createAutomationControl() {
const capacityStatuses = new Set(['preview', 'queued', 'getting-server', 'uploading', 'retrying']);
const allowedIntervals = new Set([1, 5, 15, 30, 60]);
function asObject(value) {
return value && typeof value === 'object' ? value : {};
}
function asArray(value) {
return Array.isArray(value) ? value : [];
}
function normalizeQueueLimit(value) {
const isNumber = typeof value === 'number';
const isString = typeof value === 'string';
if (!isNumber && !isString) return 15000;
if (isString && value.trim() === '') return 15000;
const number = Number(value);
if (!Number.isFinite(number) || number < 0) return 15000;
if (number === 0) return isString && value.trim() !== '0' ? 15000 : 0;
return Math.max(1, Math.floor(number));
}
function normalizeReconcileInterval(value) {
return typeof value === 'number' && Number.isFinite(value) && allowedIntervals.has(value) ? value : 5;
}
function normalizeCounter(value) {
const number = Number(value);
if (!Number.isFinite(number) || number <= 0) return 0;
return Math.min(Number.MAX_SAFE_INTEGER, Math.floor(number));
}
function normalizeAutomationSettings(value = {}) {
const settings = asObject(value);
const queueLimitJobs = normalizeQueueLimit(settings.queueLimitJobs);
return {
queueLimitJobs,
reconcileIntervalMinutes: normalizeReconcileInterval(settings.reconcileIntervalMinutes),
paused: settings.paused === true,
pausedAt: settings.paused === true && Number.isFinite(Number(settings.pausedAt)) ? Number(settings.pausedAt) : null
};
}
function countAutomaticQueueJobs(queueJobs) {
return asArray(queueJobs).reduce((count, job) => count + (capacityStatuses.has(job?.status) ? 1 : 0), 0);
}
function planAtomicAdmissions(input = {}) {
const value = asObject(input);
const ordered = [...asArray(value.candidates)].sort((left, right) =>
(Number(left?.mtimeMs) || 0) - (Number(right?.mtimeMs) || 0)
|| String(left?.path).localeCompare(String(right?.path))
);
const rawCurrentJobCount = Number(value.currentJobCount);
const currentJobCount = Number.isFinite(rawCurrentJobCount) ? Math.max(0, Math.floor(rawCurrentJobCount)) : 0;
const queueLimitJobs = normalizeQueueLimit(value.queueLimitJobs);
let available = queueLimitJobs === 0 ? Number.POSITIVE_INFINITY : Math.max(0, queueLimitJobs - currentJobCount);
const admittedPaths = [];
const deferredPaths = [];
let plannedJobs = 0;
for (const candidate of ordered) {
const required = Math.max(0, Math.floor(Number(candidate?.eligibleJobCount) || 0));
if (required > 0 && required <= available) {
admittedPaths.push(candidate?.path);
available -= required;
plannedJobs += required;
} else if (required > 0) {
deferredPaths.push(candidate?.path);
}
}
return {
admittedPaths,
deferredPaths,
currentJobCount,
plannedJobs,
availableSlots: Number.isFinite(available) ? available : null
};
}
function localDateKey(nowMs) {
const date = new Date(nowMs);
return [date.getFullYear(), String(date.getMonth() + 1).padStart(2, '0'), String(date.getDate()).padStart(2, '0')].join('-');
}
function emptyTelemetry(dateKey) {
return {
dateKey,
detected: 0,
queued: 0,
skipped: 0,
deferred: 0,
lastDetectedName: '',
lastDetectedAt: null,
lastError: '',
lastErrorAt: null
};
}
function rollDailyTelemetry(value = {}, nowMs = Date.now()) {
const telemetry = asObject(value);
const dateKey = localDateKey(nowMs);
if (telemetry.dateKey !== dateKey) return emptyTelemetry(dateKey);
return {
...emptyTelemetry(dateKey),
...telemetry,
dateKey,
detected: normalizeCounter(telemetry.detected),
queued: normalizeCounter(telemetry.queued),
skipped: normalizeCounter(telemetry.skipped),
deferred: normalizeCounter(telemetry.deferred)
};
}
function applyTelemetryDelta(value, delta = {}, nowMs = Date.now()) {
const changes = asObject(delta);
const next = rollDailyTelemetry(value, nowMs);
for (const key of ['detected', 'queued', 'skipped', 'deferred']) {
next[key] = Math.min(Number.MAX_SAFE_INTEGER, next[key] + normalizeCounter(changes[key]));
}
if (changes.lastDetectedName) {
next.lastDetectedName = String(changes.lastDetectedName);
next.lastDetectedAt = nowMs;
}
if (Object.prototype.hasOwnProperty.call(changes, 'lastError')) {
next.lastError = String(changes.lastError || '');
next.lastErrorAt = next.lastError ? nowMs : null;
}
return next;
}
function deriveAutomationState(value = {}) {
const state = asObject(value);
if (state.paused === true) return 'paused';
if (state.enabled !== true || !state.folderPath) return 'inactive';
if (state.reachable === false) return 'disconnected';
if (state.error) return 'error';
if (state.queueLimited === true) return 'queue-limited';
return 'active';
}
function normalizePath(value) {
return String(value || '').replace(/\\/g, '/').toLowerCase();
}
function baseName(value) {
return String(value || '').split(/[\\/]/).pop().toLowerCase();
}
function classifyProcessedCandidates(input = {}) {
const value = asObject(input);
const candidates = asArray(value.candidates);
const historyRows = asArray(value.historyRows);
const uploadLogRows = asArray(value.uploadLogRows);
const exactPaths = new Set(asArray(value.queuePaths).map(normalizePath).filter(Boolean));
for (const row of historyRows) {
const exact = normalizePath(row?.path || row?.file);
if (exact) exactPaths.add(exact);
}
const evidenceNames = new Set();
for (const row of [...historyRows, ...uploadLogRows]) {
const name = baseName(row?.fileName || row?.filename || row?.name || row?.path || row?.file);
if (name) evidenceNames.add(name);
}
const nameCounts = new Map();
for (const candidate of candidates) {
const name = baseName(candidate?.name || candidate?.path);
nameCounts.set(name, (nameCounts.get(name) || 0) + 1);
}
const processedPaths = [];
const ambiguousPaths = [];
const unprocessedPaths = [];
for (const candidate of candidates) {
const exact = normalizePath(candidate?.path);
const name = baseName(candidate?.name || candidate?.path);
if ((exact && exactPaths.has(exact)) || (evidenceNames.has(name) && nameCounts.get(name) === 1)) processedPaths.push(candidate?.path);
else if (evidenceNames.has(name) && nameCounts.get(name) > 1) ambiguousPaths.push(candidate?.path);
else unprocessedPaths.push(candidate?.path);
}
return { processedPaths, ambiguousPaths, unprocessedPaths };
}
return {
normalizeAutomationSettings,
countAutomaticQueueJobs,
planAtomicAdmissions,
rollDailyTelemetry,
applyTelemetryDelta,
deriveAutomationState,
classifyProcessedCandidates
};
});