letzter stand
This commit is contained in:
@@ -1395,11 +1395,12 @@ db.exec(`
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
ensureColumn('automation_requests', 'type', 'type TEXT NOT NULL DEFAULT \'request\'');
|
ensureColumn('automation_requests', 'type', 'type TEXT NOT NULL DEFAULT \'request\'');
|
||||||
ensureColumn('automation_requests', 'email_to', 'email_to TEXT');
|
ensureColumn('automation_requests', 'email_to', 'email_to TEXT');
|
||||||
ensureColumn('automation_requests', 'email_subject_template', 'email_subject_template TEXT');
|
ensureColumn('automation_requests', 'email_subject_template', 'email_subject_template TEXT');
|
||||||
ensureColumn('automation_requests', 'email_body_template', 'email_body_template TEXT');
|
ensureColumn('automation_requests', 'email_body_template', 'email_body_template TEXT');
|
||||||
ensureColumn('automation_requests', 'steps_json', 'steps_json TEXT');
|
ensureColumn('automation_requests', 'steps_json', 'steps_json TEXT');
|
||||||
|
ensureColumn('automation_requests', 'exclusion_windows_json', 'exclusion_windows_json TEXT');
|
||||||
|
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE INDEX IF NOT EXISTS idx_automation_requests_next_run
|
CREATE INDEX IF NOT EXISTS idx_automation_requests_next_run
|
||||||
@@ -1445,6 +1446,7 @@ const listAutomationRequestsStmt = db.prepare(`
|
|||||||
jitter_minutes,
|
jitter_minutes,
|
||||||
start_at,
|
start_at,
|
||||||
run_until,
|
run_until,
|
||||||
|
exclusion_windows_json,
|
||||||
active,
|
active,
|
||||||
last_run_at,
|
last_run_at,
|
||||||
last_status,
|
last_status,
|
||||||
@@ -1480,6 +1482,7 @@ const getAutomationRequestStmt = db.prepare(`
|
|||||||
jitter_minutes,
|
jitter_minutes,
|
||||||
start_at,
|
start_at,
|
||||||
run_until,
|
run_until,
|
||||||
|
exclusion_windows_json,
|
||||||
active,
|
active,
|
||||||
last_run_at,
|
last_run_at,
|
||||||
last_status,
|
last_status,
|
||||||
@@ -1496,12 +1499,12 @@ const insertAutomationRequestStmt = db.prepare(`
|
|||||||
INSERT INTO automation_requests (
|
INSERT INTO automation_requests (
|
||||||
id, name, description, type, method, url_template, headers_json, body_template,
|
id, name, description, type, method, url_template, headers_json, body_template,
|
||||||
email_to, email_subject_template, email_body_template, steps_json,
|
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
|
last_status, last_status_code, last_error, next_run_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
@id, @name, @description, @type, @method, @url_template, @headers_json, @body_template,
|
@id, @name, @description, @type, @method, @url_template, @headers_json, @body_template,
|
||||||
@email_to, @email_subject_template, @email_body_template, @steps_json,
|
@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
|
@last_status, @last_status_code, @last_error, @next_run_at
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
@@ -1523,6 +1526,7 @@ const updateAutomationRequestStmt = db.prepare(`
|
|||||||
jitter_minutes = @jitter_minutes,
|
jitter_minutes = @jitter_minutes,
|
||||||
start_at = @start_at,
|
start_at = @start_at,
|
||||||
run_until = @run_until,
|
run_until = @run_until,
|
||||||
|
exclusion_windows_json = @exclusion_windows_json,
|
||||||
active = @active,
|
active = @active,
|
||||||
last_run_at = @last_run_at,
|
last_run_at = @last_run_at,
|
||||||
last_status = @last_status,
|
last_status = @last_status,
|
||||||
@@ -1940,6 +1944,78 @@ function clampAutomationJitterMinutes(value) {
|
|||||||
return Math.min(AUTOMATION_MAX_JITTER_MINUTES, Math.round(numeric));
|
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) {
|
function normalizeAutomationHeaders(raw) {
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return {};
|
return {};
|
||||||
@@ -2136,6 +2212,7 @@ function computeNextAutomationRun(request, options = {}) {
|
|||||||
const jitterMinutes = clampAutomationJitterMinutes(request.jitter_minutes || 0);
|
const jitterMinutes = clampAutomationJitterMinutes(request.jitter_minutes || 0);
|
||||||
const lastRun = request.last_run_at ? new Date(request.last_run_at) : null;
|
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 startAt = request.start_at ? new Date(request.start_at) : null;
|
||||||
|
const exclusionWindows = parseExclusionWindows(request.exclusion_windows_json || request.exclusion_windows || []);
|
||||||
|
|
||||||
let base = lastRun
|
let base = lastRun
|
||||||
? new Date(lastRun.getTime() + intervalMinutes * 60000)
|
? new Date(lastRun.getTime() + intervalMinutes * 60000)
|
||||||
@@ -2148,13 +2225,34 @@ function computeNextAutomationRun(request, options = {}) {
|
|||||||
const jitterMs = jitterMinutes > 0
|
const jitterMs = jitterMinutes > 0
|
||||||
? Math.floor(Math.random() * ((jitterMinutes * 60000) + 1))
|
? Math.floor(Math.random() * ((jitterMinutes * 60000) + 1))
|
||||||
: 0;
|
: 0;
|
||||||
const candidate = new Date(base.getTime() + jitterMs);
|
let candidate = new Date(base.getTime() + jitterMs);
|
||||||
|
|
||||||
if (request.run_until) {
|
const until = request.run_until ? new Date(request.run_until) : null;
|
||||||
const until = new Date(request.run_until);
|
const MAX_SHIFT_ITERATIONS = 50;
|
||||||
if (!Number.isNaN(until.getTime()) && candidate > until) {
|
let shifts = 0;
|
||||||
return null;
|
|
||||||
|
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();
|
return candidate.toISOString();
|
||||||
@@ -2223,6 +2321,7 @@ function serializeAutomationRequest(row) {
|
|||||||
jitter_minutes: clampAutomationJitterMinutes(row.jitter_minutes || 0),
|
jitter_minutes: clampAutomationJitterMinutes(row.jitter_minutes || 0),
|
||||||
start_at: row.start_at ? ensureIsoDate(row.start_at) : null,
|
start_at: row.start_at ? ensureIsoDate(row.start_at) : null,
|
||||||
run_until: row.run_until ? ensureIsoDate(row.run_until) : 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,
|
active: row.active ? 1 : 0,
|
||||||
last_run_at: row.last_run_at ? ensureIsoDate(row.last_run_at) : null,
|
last_run_at: row.last_run_at ? ensureIsoDate(row.last_run_at) : null,
|
||||||
last_status: row.last_status || 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.start_at = parseAutomationDate(payload.start_at) || parseAutomationDate(existing.start_at);
|
||||||
normalized.run_until = parseAutomationDate(payload.run_until) || null;
|
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
|
normalized.active = payload.active === undefined || payload.active === null
|
||||||
? (existing.active ? 1 : 0)
|
? (existing.active ? 1 : 0)
|
||||||
: (payload.active ? 1 : 0);
|
: (payload.active ? 1 : 0);
|
||||||
|
|||||||
@@ -546,6 +546,54 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.automation-view .exclusion-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-view .exclusion-controls input[type="time"] {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-view .exclusion-sep {
|
||||||
|
color: var(--automation-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-view .exclusion-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-view .exclusion-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid var(--automation-border);
|
||||||
|
color: var(--automation-text);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-view .exclusion-chip button {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--automation-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-view .exclusion-chip button:hover {
|
||||||
|
color: var(--automation-danger);
|
||||||
|
}
|
||||||
|
|
||||||
.automation-view .modal {
|
.automation-view .modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
let editingId = null;
|
let editingId = null;
|
||||||
|
let exclusionWindows = [];
|
||||||
|
|
||||||
const form = document.getElementById('automationForm');
|
const form = document.getElementById('automationForm');
|
||||||
const nameInput = document.getElementById('nameInput');
|
const nameInput = document.getElementById('nameInput');
|
||||||
@@ -34,6 +35,10 @@
|
|||||||
const jitterInput = document.getElementById('jitterInput');
|
const jitterInput = document.getElementById('jitterInput');
|
||||||
const startAtInput = document.getElementById('startAtInput');
|
const startAtInput = document.getElementById('startAtInput');
|
||||||
const runUntilInput = document.getElementById('runUntilInput');
|
const runUntilInput = document.getElementById('runUntilInput');
|
||||||
|
const excludeStartInput = document.getElementById('excludeStartInput');
|
||||||
|
const excludeEndInput = document.getElementById('excludeEndInput');
|
||||||
|
const addExclusionBtn = document.getElementById('addExclusionBtn');
|
||||||
|
const exclusionList = document.getElementById('exclusionList');
|
||||||
const activeToggle = document.getElementById('activeToggle');
|
const activeToggle = document.getElementById('activeToggle');
|
||||||
const formStatus = document.getElementById('formStatus');
|
const formStatus = document.getElementById('formStatus');
|
||||||
const formModeLabel = document.getElementById('formModeLabel');
|
const formModeLabel = document.getElementById('formModeLabel');
|
||||||
@@ -60,6 +65,7 @@
|
|||||||
const listStatus = document.getElementById('listStatus');
|
const listStatus = document.getElementById('listStatus');
|
||||||
const filterName = document.getElementById('filterName');
|
const filterName = document.getElementById('filterName');
|
||||||
const filterNext = document.getElementById('filterNext');
|
const filterNext = document.getElementById('filterNext');
|
||||||
|
const filterRunUntil = document.getElementById('filterRunUntil');
|
||||||
const filterLast = document.getElementById('filterLast');
|
const filterLast = document.getElementById('filterLast');
|
||||||
const filterStatus = document.getElementById('filterStatus');
|
const filterStatus = document.getElementById('filterStatus');
|
||||||
const filterRuns = document.getElementById('filterRuns');
|
const filterRuns = document.getElementById('filterRuns');
|
||||||
@@ -342,7 +348,8 @@
|
|||||||
jitter_minutes: Math.max(0, parseInt(jitterInput.value, 10) || 0),
|
jitter_minutes: Math.max(0, parseInt(jitterInput.value, 10) || 0),
|
||||||
start_at: parseDateInput(startAtInput.value),
|
start_at: parseDateInput(startAtInput.value),
|
||||||
run_until: parseDateInput(runUntilInput.value),
|
run_until: parseDateInput(runUntilInput.value),
|
||||||
active: activeToggle.checked
|
active: activeToggle.checked,
|
||||||
|
exclusion_windows: exclusionWindows.map((win) => ({ start: win.start, end: win.end }))
|
||||||
};
|
};
|
||||||
|
|
||||||
if (type === 'email') {
|
if (type === 'email') {
|
||||||
@@ -389,6 +396,46 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderExclusionChips() {
|
||||||
|
if (!exclusionList) return;
|
||||||
|
if (!exclusionWindows.length) {
|
||||||
|
exclusionList.innerHTML = '<span class="placeholder-hint">Keine Ausschlusszeiten gesetzt.</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
exclusionList.innerHTML = exclusionWindows.map((win, idx) => `
|
||||||
|
<span class="exclusion-chip">
|
||||||
|
${win.start} – ${win.end}
|
||||||
|
<button type="button" aria-label="Entfernen" data-remove-exclusion="${idx}">×</button>
|
||||||
|
</span>
|
||||||
|
`).join('');
|
||||||
|
exclusionList.querySelectorAll('[data-remove-exclusion]').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const index = Number(btn.getAttribute('data-remove-exclusion'));
|
||||||
|
exclusionWindows.splice(index, 1);
|
||||||
|
renderExclusionChips();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addExclusion() {
|
||||||
|
const start = excludeStartInput?.value || '';
|
||||||
|
const end = excludeEndInput?.value || '';
|
||||||
|
if (!start || !end) {
|
||||||
|
setStatus(formStatus, 'Bitte Start- und Endzeit angeben.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (start >= end) {
|
||||||
|
setStatus(formStatus, 'Endzeit muss nach der Startzeit liegen.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
exclusionWindows.push({ start, end });
|
||||||
|
exclusionWindows.sort((a, b) => a.start.localeCompare(b.start));
|
||||||
|
renderExclusionChips();
|
||||||
|
setStatus(formStatus, '', 'info');
|
||||||
|
if (excludeStartInput) excludeStartInput.value = '';
|
||||||
|
if (excludeEndInput) excludeEndInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
function applyPresetDisabling() {
|
function applyPresetDisabling() {
|
||||||
if (intervalPreset.value === 'custom') {
|
if (intervalPreset.value === 'custom') {
|
||||||
intervalMinutesInput.disabled = false;
|
intervalMinutesInput.disabled = false;
|
||||||
@@ -523,6 +570,8 @@
|
|||||||
if (!req) return;
|
if (!req) return;
|
||||||
const nextEl = row.querySelector('.next');
|
const nextEl = row.querySelector('.next');
|
||||||
if (nextEl) nextEl.textContent = formatRelative(req.next_run_at);
|
if (nextEl) nextEl.textContent = formatRelative(req.next_run_at);
|
||||||
|
const untilEl = row.querySelector('.until');
|
||||||
|
if (untilEl) untilEl.textContent = req.run_until ? formatRelative(req.run_until) : '—';
|
||||||
const lastEl = row.querySelector('.last');
|
const lastEl = row.querySelector('.last');
|
||||||
if (lastEl) lastEl.textContent = formatRelative(req.last_run_at);
|
if (lastEl) lastEl.textContent = formatRelative(req.last_run_at);
|
||||||
});
|
});
|
||||||
@@ -532,7 +581,7 @@
|
|||||||
if (!requestTableBody) return;
|
if (!requestTableBody) return;
|
||||||
requestTableBody.innerHTML = '';
|
requestTableBody.innerHTML = '';
|
||||||
if (!state.requests.length) {
|
if (!state.requests.length) {
|
||||||
requestTableBody.innerHTML = '<tr><td colspan="6">Keine Automationen vorhanden.</td></tr>';
|
requestTableBody.innerHTML = '<tr><td colspan="7">Keine Automationen vorhanden.</td></tr>';
|
||||||
listInstance = null;
|
listInstance = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -540,6 +589,7 @@
|
|||||||
const rows = state.requests.map((req) => {
|
const rows = state.requests.map((req) => {
|
||||||
const isSelected = state.selectedId === req.id;
|
const isSelected = state.selectedId === req.id;
|
||||||
const nextSort = req.next_run_at ? new Date(req.next_run_at).getTime() : Number.MAX_SAFE_INTEGER;
|
const nextSort = req.next_run_at ? new Date(req.next_run_at).getTime() : Number.MAX_SAFE_INTEGER;
|
||||||
|
const untilSort = req.run_until ? new Date(req.run_until).getTime() : Number.MAX_SAFE_INTEGER;
|
||||||
const lastSort = req.last_run_at ? new Date(req.last_run_at).getTime() : -Number.MAX_SAFE_INTEGER;
|
const lastSort = req.last_run_at ? new Date(req.last_run_at).getTime() : -Number.MAX_SAFE_INTEGER;
|
||||||
const runsCount = Array.isArray(req.runs) ? req.runs.length : (req.runs_count || req.runsCount || 0);
|
const runsCount = Array.isArray(req.runs) ? req.runs.length : (req.runs_count || req.runsCount || 0);
|
||||||
const statusBadge = req.last_status === 'success'
|
const statusBadge = req.last_status === 'success'
|
||||||
@@ -571,6 +621,10 @@
|
|||||||
<span class="next" data-sort="${nextSort}">${formatRelative(req.next_run_at)}</span>
|
<span class="next" data-sort="${nextSort}">${formatRelative(req.next_run_at)}</span>
|
||||||
<br><small>${formatDateTime(req.next_run_at)}</small>
|
<br><small>${formatDateTime(req.next_run_at)}</small>
|
||||||
</td>
|
</td>
|
||||||
|
<td data-sort="${untilSort}">
|
||||||
|
<span class="until" data-sort="${untilSort}">${req.run_until ? formatRelative(req.run_until) : '—'}</span>
|
||||||
|
<br><small>${formatDateTime(req.run_until)}</small>
|
||||||
|
</td>
|
||||||
<td data-sort="${lastSort}">
|
<td data-sort="${lastSort}">
|
||||||
<span class="last" data-sort="${lastSort}">${formatRelative(req.last_run_at)}</span>
|
<span class="last" data-sort="${lastSort}">${formatRelative(req.last_run_at)}</span>
|
||||||
<br><small>${req.last_status_code || '—'}</small>
|
<br><small>${req.last_status_code || '—'}</small>
|
||||||
@@ -640,6 +694,8 @@
|
|||||||
now.setSeconds(0, 0);
|
now.setSeconds(0, 0);
|
||||||
startAtInput.value = toDateTimeLocal(now);
|
startAtInput.value = toDateTimeLocal(now);
|
||||||
runUntilInput.value = '';
|
runUntilInput.value = '';
|
||||||
|
exclusionWindows = [];
|
||||||
|
renderExclusionChips();
|
||||||
formModeLabel.textContent = 'Neue Automation';
|
formModeLabel.textContent = 'Neue Automation';
|
||||||
setStatus(formStatus, '');
|
setStatus(formStatus, '');
|
||||||
if (emailToInput) emailToInput.value = '';
|
if (emailToInput) emailToInput.value = '';
|
||||||
@@ -700,6 +756,8 @@
|
|||||||
jitterInput.value = request.jitter_minutes || 0;
|
jitterInput.value = request.jitter_minutes || 0;
|
||||||
startAtInput.value = toDateTimeLocal(request.start_at);
|
startAtInput.value = toDateTimeLocal(request.start_at);
|
||||||
runUntilInput.value = toDateTimeLocal(request.run_until);
|
runUntilInput.value = toDateTimeLocal(request.run_until);
|
||||||
|
exclusionWindows = Array.isArray(request.exclusion_windows) ? [...request.exclusion_windows] : [];
|
||||||
|
renderExclusionChips();
|
||||||
applyPresetDisabling();
|
applyPresetDisabling();
|
||||||
refreshPreview();
|
refreshPreview();
|
||||||
}
|
}
|
||||||
@@ -975,6 +1033,7 @@
|
|||||||
'url',
|
'url',
|
||||||
'steps',
|
'steps',
|
||||||
{ name: 'next', attr: 'data-sort' },
|
{ name: 'next', attr: 'data-sort' },
|
||||||
|
{ name: 'runUntil', attr: 'data-sort' },
|
||||||
{ name: 'last', attr: 'data-sort' },
|
{ name: 'last', attr: 'data-sort' },
|
||||||
{ name: 'status', attr: 'data-sort' },
|
{ name: 'status', attr: 'data-sort' },
|
||||||
{ name: 'runs', attr: 'data-sort' }
|
{ name: 'runs', attr: 'data-sort' }
|
||||||
@@ -990,6 +1049,7 @@
|
|||||||
if (!listInstance) return;
|
if (!listInstance) return;
|
||||||
const searchName = (filterName?.value || '').toLowerCase().trim();
|
const searchName = (filterName?.value || '').toLowerCase().trim();
|
||||||
const searchNext = (filterNext?.value || '').toLowerCase().trim();
|
const searchNext = (filterNext?.value || '').toLowerCase().trim();
|
||||||
|
const searchRunUntil = (filterRunUntil?.value || '').toLowerCase().trim();
|
||||||
const searchLast = (filterLast?.value || '').toLowerCase().trim();
|
const searchLast = (filterLast?.value || '').toLowerCase().trim();
|
||||||
const statusValue = (filterStatus?.value || '').toLowerCase().trim();
|
const statusValue = (filterStatus?.value || '').toLowerCase().trim();
|
||||||
const runsMin = filterRuns?.value ? Number(filterRuns.value) : null;
|
const runsMin = filterRuns?.value ? Number(filterRuns.value) : null;
|
||||||
@@ -1005,11 +1065,12 @@
|
|||||||
].some((p) => String(p).toLowerCase().includes(searchName));
|
].some((p) => String(p).toLowerCase().includes(searchName));
|
||||||
|
|
||||||
const matchNext = !searchNext || String(v.next || '').toLowerCase().includes(searchNext);
|
const matchNext = !searchNext || String(v.next || '').toLowerCase().includes(searchNext);
|
||||||
|
const matchRunUntil = !searchRunUntil || String(v.runUntil || '').toLowerCase().includes(searchRunUntil);
|
||||||
const matchLast = !searchLast || `${v.last || ''}`.toLowerCase().includes(searchLast);
|
const matchLast = !searchLast || `${v.last || ''}`.toLowerCase().includes(searchLast);
|
||||||
const matchStatus = !statusValue || String(v.status || '').toLowerCase().includes(statusValue);
|
const matchStatus = !statusValue || String(v.status || '').toLowerCase().includes(statusValue);
|
||||||
const matchRuns = runsMin === null || Number(v.runs || 0) >= runsMin;
|
const matchRuns = runsMin === null || Number(v.runs || 0) >= runsMin;
|
||||||
|
|
||||||
return matchName && matchNext && matchLast && matchStatus && matchRuns;
|
return matchName && matchNext && matchRunUntil && matchLast && matchStatus && matchRuns;
|
||||||
});
|
});
|
||||||
updateSortIndicators();
|
updateSortIndicators();
|
||||||
}
|
}
|
||||||
@@ -1200,11 +1261,14 @@
|
|||||||
el?.addEventListener('input', refreshPreview);
|
el?.addEventListener('input', refreshPreview);
|
||||||
el?.addEventListener('change', refreshPreview);
|
el?.addEventListener('change', refreshPreview);
|
||||||
});
|
});
|
||||||
|
if (addExclusionBtn) {
|
||||||
|
addExclusionBtn.addEventListener('click', addExclusion);
|
||||||
|
}
|
||||||
typeSelect?.addEventListener('change', () => {
|
typeSelect?.addEventListener('change', () => {
|
||||||
applyTypeVisibility();
|
applyTypeVisibility();
|
||||||
refreshPreview();
|
refreshPreview();
|
||||||
});
|
});
|
||||||
[filterName, filterNext, filterLast, filterStatus].forEach((el) => {
|
[filterName, filterNext, filterRunUntil, filterLast, filterStatus].forEach((el) => {
|
||||||
el?.addEventListener('input', applyFilters);
|
el?.addEventListener('input', applyFilters);
|
||||||
el?.addEventListener('change', applyFilters);
|
el?.addEventListener('change', applyFilters);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -193,20 +193,22 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-sort-column="name">Name<span class="sort-indicator"></span></th>
|
<th data-sort-column="name">Name<span class="sort-indicator"></span></th>
|
||||||
<th data-sort-column="next">Nächster Lauf<span class="sort-indicator"></span></th>
|
<th data-sort-column="next">Nächster Lauf<span class="sort-indicator"></span></th>
|
||||||
<th data-sort-column="last">Letzter Lauf<span class="sort-indicator"></span></th>
|
<th data-sort-column="runUntil">Läuft bis<span class="sort-indicator"></span></th>
|
||||||
<th data-sort-column="status">Status<span class="sort-indicator"></span></th>
|
<th data-sort-column="last">Letzter Lauf<span class="sort-indicator"></span></th>
|
||||||
<th data-sort-column="runs">#Läufe<span class="sort-indicator"></span></th>
|
<th data-sort-column="status">Status<span class="sort-indicator"></span></th>
|
||||||
<th>Aktionen</th>
|
<th data-sort-column="runs">#Läufe<span class="sort-indicator"></span></th>
|
||||||
</tr>
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
<tr class="table-filter-row">
|
<tr class="table-filter-row">
|
||||||
<th><input id="filterName" type="search" placeholder="Name/Typ/E-Mail/URL"></th>
|
<th><input id="filterName" type="search" placeholder="Name/Typ/E-Mail/URL"></th>
|
||||||
<th><input id="filterNext" type="search" placeholder="z.B. heute"></th>
|
<th><input id="filterNext" type="search" placeholder="z.B. heute"></th>
|
||||||
<th><input id="filterLast" type="search" placeholder="z.B. HTTP 200"></th>
|
<th><input id="filterRunUntil" type="search" placeholder="z.B. morgen"></th>
|
||||||
<th>
|
<th><input id="filterLast" type="search" placeholder="z.B. HTTP 200"></th>
|
||||||
<select id="filterStatus">
|
<th>
|
||||||
<option value="">Alle</option>
|
<select id="filterStatus">
|
||||||
<option value="success">OK</option>
|
<option value="">Alle</option>
|
||||||
|
<option value="success">OK</option>
|
||||||
<option value="error">Fehler</option>
|
<option value="error">Fehler</option>
|
||||||
</select>
|
</select>
|
||||||
</th>
|
</th>
|
||||||
@@ -377,6 +379,17 @@
|
|||||||
<label for="runUntilInput">Läuft bis</label>
|
<label for="runUntilInput">Läuft bis</label>
|
||||||
<input id="runUntilInput" type="datetime-local">
|
<input id="runUntilInput" type="datetime-local">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field full">
|
||||||
|
<label>Ausschlusszeiten (täglich, Serverzeit)</label>
|
||||||
|
<div class="exclusion-controls">
|
||||||
|
<input id="excludeStartInput" type="time" aria-label="Ausschluss von">
|
||||||
|
<span class="exclusion-sep">bis</span>
|
||||||
|
<input id="excludeEndInput" type="time" aria-label="Ausschluss bis">
|
||||||
|
<button class="secondary-btn" id="addExclusionBtn" type="button">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
<div id="exclusionList" class="exclusion-list"></div>
|
||||||
|
<p class="placeholder-hint">Beispiel: 00:00–07:00, um nächtliche Ausführungen zu vermeiden.</p>
|
||||||
|
</div>
|
||||||
<div class="field full">
|
<div class="field full">
|
||||||
<div class="template-hint">
|
<div class="template-hint">
|
||||||
<p class="template-title">Platzhalter</p>
|
<p class="template-title">Platzhalter</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user