188 lines
6.9 KiB
JavaScript
188 lines
6.9 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 normalizeAutomationSettings(value = {}) {
|
|
const settings = asObject(value);
|
|
const queueLimitJobs = normalizeQueueLimit(settings.queueLimitJobs);
|
|
const rawInterval = Number(settings.reconcileIntervalMinutes);
|
|
return {
|
|
queueLimitJobs,
|
|
reconcileIntervalMinutes: allowedIntervals.has(rawInterval) ? rawInterval : 5,
|
|
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: Math.max(0, Number(telemetry.detected) || 0),
|
|
queued: Math.max(0, Number(telemetry.queued) || 0),
|
|
skipped: Math.max(0, Number(telemetry.skipped) || 0),
|
|
deferred: Math.max(0, Number(telemetry.deferred) || 0)
|
|
};
|
|
}
|
|
|
|
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.max(0, Number(changes[key]) || 0);
|
|
}
|
|
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
|
|
};
|
|
});
|