letzter stand

This commit is contained in:
2025-12-16 20:27:37 +01:00
parent 27a8240d3f
commit ffcfce2b31
4 changed files with 256 additions and 28 deletions

View File

@@ -1395,11 +1395,12 @@ db.exec(`
);
`);
ensureColumn('automation_requests', 'type', 'type TEXT NOT NULL DEFAULT \'request\'');
ensureColumn('automation_requests', 'email_to', 'email_to TEXT');
ensureColumn('automation_requests', 'email_subject_template', 'email_subject_template TEXT');
ensureColumn('automation_requests', 'email_body_template', 'email_body_template TEXT');
ensureColumn('automation_requests', 'steps_json', 'steps_json TEXT');
ensureColumn('automation_requests', 'type', 'type TEXT NOT NULL DEFAULT \'request\'');
ensureColumn('automation_requests', 'email_to', 'email_to TEXT');
ensureColumn('automation_requests', 'email_subject_template', 'email_subject_template TEXT');
ensureColumn('automation_requests', 'email_body_template', 'email_body_template TEXT');
ensureColumn('automation_requests', 'steps_json', 'steps_json TEXT');
ensureColumn('automation_requests', 'exclusion_windows_json', 'exclusion_windows_json TEXT');
db.exec(`
CREATE INDEX IF NOT EXISTS idx_automation_requests_next_run
@@ -1445,6 +1446,7 @@ const listAutomationRequestsStmt = db.prepare(`
jitter_minutes,
start_at,
run_until,
exclusion_windows_json,
active,
last_run_at,
last_status,
@@ -1480,6 +1482,7 @@ const getAutomationRequestStmt = db.prepare(`
jitter_minutes,
start_at,
run_until,
exclusion_windows_json,
active,
last_run_at,
last_status,
@@ -1496,12 +1499,12 @@ const insertAutomationRequestStmt = db.prepare(`
INSERT INTO automation_requests (
id, name, description, type, method, url_template, headers_json, body_template,
email_to, email_subject_template, email_body_template, steps_json,
interval_minutes, jitter_minutes, start_at, run_until, active, last_run_at,
interval_minutes, jitter_minutes, start_at, run_until, exclusion_windows_json, active, last_run_at,
last_status, last_status_code, last_error, next_run_at
) VALUES (
@id, @name, @description, @type, @method, @url_template, @headers_json, @body_template,
@email_to, @email_subject_template, @email_body_template, @steps_json,
@interval_minutes, @jitter_minutes, @start_at, @run_until, @active, @last_run_at,
@interval_minutes, @jitter_minutes, @start_at, @run_until, @exclusion_windows_json, @active, @last_run_at,
@last_status, @last_status_code, @last_error, @next_run_at
)
`);
@@ -1523,6 +1526,7 @@ const updateAutomationRequestStmt = db.prepare(`
jitter_minutes = @jitter_minutes,
start_at = @start_at,
run_until = @run_until,
exclusion_windows_json = @exclusion_windows_json,
active = @active,
last_run_at = @last_run_at,
last_status = @last_status,
@@ -1940,6 +1944,78 @@ function clampAutomationJitterMinutes(value) {
return Math.min(AUTOMATION_MAX_JITTER_MINUTES, Math.round(numeric));
}
function toMinutesOfDay(value) {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
const match = /^(\d{1,2}):(\d{2})$/.exec(trimmed);
if (!match) return null;
const hours = Number(match[1]);
const minutes = Number(match[2]);
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;
return hours * 60 + minutes;
}
function formatMinutesOfDay(totalMinutes) {
if (typeof totalMinutes !== 'number' || Number.isNaN(totalMinutes)) return null;
const minutes = Math.max(0, Math.min(1439, Math.floor(totalMinutes)));
const h = String(Math.floor(minutes / 60)).padStart(2, '0');
const m = String(minutes % 60).padStart(2, '0');
return `${h}:${m}`;
}
function parseExclusionWindows(raw) {
let source = raw;
if (typeof source === 'string') {
try {
source = JSON.parse(source);
} catch (error) {
source = [];
}
}
if (!Array.isArray(source)) {
return [];
}
const windows = [];
for (const item of source) {
if (!item || typeof item !== 'object') continue;
const startStr = typeof item.start === 'string'
? item.start
: (typeof item.from === 'string' ? item.from : '');
const endStr = typeof item.end === 'string'
? item.end
: (typeof item.to === 'string' ? item.to : '');
const startMinutes = toMinutesOfDay(startStr);
const endMinutes = toMinutesOfDay(endStr);
if (startMinutes === null || endMinutes === null) continue;
if (startMinutes >= endMinutes) continue;
windows.push({
start: formatMinutesOfDay(startMinutes),
end: formatMinutesOfDay(endMinutes),
startMinutes,
endMinutes
});
}
windows.sort((a, b) => a.startMinutes - b.startMinutes);
return windows;
}
function serializeExclusionWindows(raw, existingJson = null) {
if (raw === undefined) {
return existingJson || null;
}
const parsed = parseExclusionWindows(raw);
if (!parsed.length) {
return null;
}
try {
return JSON.stringify(parsed.map((item) => ({ start: item.start, end: item.end })));
} catch (error) {
return null;
}
}
function normalizeAutomationHeaders(raw) {
if (!raw) {
return {};
@@ -2136,6 +2212,7 @@ function computeNextAutomationRun(request, options = {}) {
const jitterMinutes = clampAutomationJitterMinutes(request.jitter_minutes || 0);
const lastRun = request.last_run_at ? new Date(request.last_run_at) : null;
const startAt = request.start_at ? new Date(request.start_at) : null;
const exclusionWindows = parseExclusionWindows(request.exclusion_windows_json || request.exclusion_windows || []);
let base = lastRun
? new Date(lastRun.getTime() + intervalMinutes * 60000)
@@ -2148,13 +2225,34 @@ function computeNextAutomationRun(request, options = {}) {
const jitterMs = jitterMinutes > 0
? Math.floor(Math.random() * ((jitterMinutes * 60000) + 1))
: 0;
const candidate = new Date(base.getTime() + jitterMs);
let candidate = new Date(base.getTime() + jitterMs);
if (request.run_until) {
const until = new Date(request.run_until);
if (!Number.isNaN(until.getTime()) && candidate > until) {
return null;
const until = request.run_until ? new Date(request.run_until) : null;
const MAX_SHIFT_ITERATIONS = 50;
let shifts = 0;
while (shifts < MAX_SHIFT_ITERATIONS) {
const minutes = candidate.getHours() * 60 + candidate.getMinutes();
const hit = exclusionWindows.find((w) => minutes >= w.startMinutes && minutes < w.endMinutes);
if (!hit) {
break;
}
const nextAllowed = new Date(candidate);
nextAllowed.setHours(0, 0, 0, 0);
nextAllowed.setMinutes(hit.endMinutes);
if (nextAllowed <= candidate) {
nextAllowed.setDate(nextAllowed.getDate() + 1);
}
candidate = nextAllowed;
shifts += 1;
}
if (shifts >= MAX_SHIFT_ITERATIONS) {
return null;
}
if (until && !Number.isNaN(until.getTime()) && candidate > until) {
return null;
}
return candidate.toISOString();
@@ -2223,6 +2321,7 @@ function serializeAutomationRequest(row) {
jitter_minutes: clampAutomationJitterMinutes(row.jitter_minutes || 0),
start_at: row.start_at ? ensureIsoDate(row.start_at) : null,
run_until: row.run_until ? ensureIsoDate(row.run_until) : null,
exclusion_windows: parseExclusionWindows(row.exclusion_windows_json || row.exclusion_windows || []),
active: row.active ? 1 : 0,
last_run_at: row.last_run_at ? ensureIsoDate(row.last_run_at) : null,
last_status: row.last_status || null,
@@ -2428,6 +2527,10 @@ function normalizeAutomationPayload(payload, existing = {}) {
normalized.start_at = parseAutomationDate(payload.start_at) || parseAutomationDate(existing.start_at);
normalized.run_until = parseAutomationDate(payload.run_until) || null;
normalized.exclusion_windows_json = serializeExclusionWindows(
payload.exclusion_windows ?? payload.exclusions ?? payload.exclude_windows,
existing.exclusion_windows_json || null
);
normalized.active = payload.active === undefined || payload.active === null
? (existing.active ? 1 : 0)
: (payload.active ? 1 : 0);