1359 lines
48 KiB
JavaScript
1359 lines
48 KiB
JavaScript
(function () {
|
||
let active = false;
|
||
let initialized = false;
|
||
const API_URL = (() => {
|
||
if (window.API_URL) return window.API_URL;
|
||
try {
|
||
return `${window.location.origin}/api`;
|
||
} catch (error) {
|
||
return 'https://fb.srv.medeba-media.de/api';
|
||
}
|
||
})();
|
||
const DEFAULT_INTERVAL_MINUTES = 60;
|
||
|
||
const state = {
|
||
requests: [],
|
||
runs: [],
|
||
selectedId: null,
|
||
loading: false,
|
||
saving: false,
|
||
running: false
|
||
};
|
||
|
||
let editingId = null;
|
||
let exclusionWindows = [];
|
||
|
||
const form = document.getElementById('automationForm');
|
||
const nameInput = document.getElementById('nameInput');
|
||
const descriptionInput = document.getElementById('descriptionInput');
|
||
const urlInput = document.getElementById('urlInput');
|
||
const methodSelect = document.getElementById('methodSelect');
|
||
const headersInput = document.getElementById('headersInput');
|
||
const bodyInput = document.getElementById('bodyInput');
|
||
const intervalPreset = document.getElementById('intervalPreset');
|
||
const intervalMinutesInput = document.getElementById('intervalMinutesInput');
|
||
const jitterInput = document.getElementById('jitterInput');
|
||
const startAtInput = document.getElementById('startAtInput');
|
||
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 formStatus = document.getElementById('formStatus');
|
||
const formModeLabel = document.getElementById('formModeLabel');
|
||
const formModal = document.getElementById('formModal');
|
||
const modalBackdrop = document.getElementById('formModalBackdrop');
|
||
const modalCloseBtn = document.getElementById('modalCloseBtn');
|
||
const typeSelect = document.getElementById('typeSelect');
|
||
const emailSection = document.querySelector('[data-section="email"]');
|
||
const flowSection = document.querySelector('[data-section="flow"]');
|
||
const httpSection = document.querySelector('[data-section="http"]');
|
||
const emailToInput = document.getElementById('emailToInput');
|
||
const emailSubjectInput = document.getElementById('emailSubjectInput');
|
||
const emailBodyInput = document.getElementById('emailBodyInput');
|
||
const flowStep1Url = document.getElementById('flowStep1Url');
|
||
const flowStep1Method = document.getElementById('flowStep1Method');
|
||
const flowStep1Headers = document.getElementById('flowStep1Headers');
|
||
const flowStep1Body = document.getElementById('flowStep1Body');
|
||
const flowStep2Url = document.getElementById('flowStep2Url');
|
||
const flowStep2Method = document.getElementById('flowStep2Method');
|
||
const flowStep2Headers = document.getElementById('flowStep2Headers');
|
||
const flowStep2Body = document.getElementById('flowStep2Body');
|
||
|
||
const requestTableBody = document.getElementById('requestTableBody');
|
||
const listStatus = document.getElementById('listStatus');
|
||
const filterName = document.getElementById('filterName');
|
||
const filterNext = document.getElementById('filterNext');
|
||
const filterRunUntil = document.getElementById('filterRunUntil');
|
||
const filterLast = document.getElementById('filterLast');
|
||
const filterStatus = document.getElementById('filterStatus');
|
||
const filterRuns = document.getElementById('filterRuns');
|
||
const runsList = document.getElementById('runsList');
|
||
const runsStatus = document.getElementById('runsStatus');
|
||
const heroStats = document.getElementById('heroStats');
|
||
|
||
const saveBtn = document.getElementById('saveBtn');
|
||
const resetFormBtn = document.getElementById('resetFormBtn');
|
||
const refreshBtn = document.getElementById('refreshBtn');
|
||
const newAutomationBtn = document.getElementById('newAutomationBtn');
|
||
const modalTitle = document.getElementById('modalTitle');
|
||
const importInput = document.getElementById('importInput');
|
||
const importStatus = document.getElementById('importStatus');
|
||
const applyImportBtn = document.getElementById('applyImportBtn');
|
||
const openImportBtn = document.getElementById('openImportBtn');
|
||
const importModal = document.getElementById('importModal');
|
||
const importModalBackdrop = document.getElementById('importModalBackdrop');
|
||
const importCloseBtn = document.getElementById('importCloseBtn');
|
||
const previewUrl = document.getElementById('previewUrl');
|
||
const previewHeaders = document.getElementById('previewHeaders');
|
||
const previewBody = document.getElementById('previewBody');
|
||
const refreshPreviewBtn = document.getElementById('refreshPreviewBtn');
|
||
const placeholderTableBody = document.getElementById('placeholderTableBody');
|
||
|
||
let sortState = { key: 'next', dir: 'asc' };
|
||
let listInstance = null;
|
||
let sse = null;
|
||
let relativeTimer = null;
|
||
|
||
function handleUnauthorized(response) {
|
||
if (response && response.status === 401) {
|
||
if (typeof redirectToLogin === 'function') {
|
||
redirectToLogin();
|
||
} else {
|
||
window.location.href = 'login.html';
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function toDateTimeLocal(value) {
|
||
if (!value) return '';
|
||
const date = value instanceof Date ? value : new Date(value);
|
||
if (Number.isNaN(date.getTime())) return '';
|
||
const pad = (num) => String(num).padStart(2, '0');
|
||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||
}
|
||
|
||
function parseDateInput(value) {
|
||
if (!value) return null;
|
||
const date = new Date(value);
|
||
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
||
}
|
||
|
||
async function apiFetchJSON(path, options = {}) {
|
||
const opts = {
|
||
credentials: 'include',
|
||
...options
|
||
};
|
||
opts.headers = {
|
||
'Content-Type': 'application/json',
|
||
...(options.headers || {})
|
||
};
|
||
const response = await fetch(`${API_URL}${path}`, opts);
|
||
if (handleUnauthorized(response)) {
|
||
throw new Error('Nicht angemeldet');
|
||
}
|
||
if (!response.ok) {
|
||
let message = 'Unbekannter Fehler';
|
||
try {
|
||
const err = await response.json();
|
||
message = err.error || message;
|
||
} catch (error) {
|
||
// ignore
|
||
}
|
||
throw new Error(message);
|
||
}
|
||
if (response.status === 204) {
|
||
return null;
|
||
}
|
||
return response.json();
|
||
}
|
||
|
||
function setStatus(target, message, type = 'info') {
|
||
if (!target) return;
|
||
target.textContent = message || '';
|
||
target.classList.remove('error', 'success');
|
||
if (type === 'error') target.classList.add('error');
|
||
if (type === 'success') target.classList.add('success');
|
||
}
|
||
|
||
function formatDateTime(value) {
|
||
if (!value) return '—';
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return '—';
|
||
return date.toLocaleString('de-DE', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
}
|
||
|
||
function formatRelative(value) {
|
||
if (!value) return '—';
|
||
const date = new Date(value);
|
||
const now = new Date();
|
||
const diffMs = date.getTime() - now.getTime();
|
||
const diffMinutes = Math.round(diffMs / 60000);
|
||
if (Number.isNaN(diffMinutes)) return '—';
|
||
if (Math.abs(diffMinutes) < 1) return 'jetzt';
|
||
if (diffMinutes > 0) {
|
||
if (diffMinutes < 60) return `in ${diffMinutes} Min`;
|
||
const hours = Math.round(diffMinutes / 60);
|
||
if (hours < 48) return `in ${hours} Std`;
|
||
const days = Math.round(hours / 24);
|
||
return `in ${days} Tagen`;
|
||
} else {
|
||
const abs = Math.abs(diffMinutes);
|
||
if (abs < 60) return `vor ${abs} Min`;
|
||
const hours = Math.round(abs / 60);
|
||
if (hours < 48) return `vor ${hours} Std`;
|
||
const days = Math.round(hours / 24);
|
||
return `vor ${days} Tagen`;
|
||
}
|
||
}
|
||
|
||
function parseHeadersInput(text) {
|
||
const trimmed = (text || '').trim();
|
||
if (!trimmed) return {};
|
||
if (trimmed.startsWith('{')) {
|
||
try {
|
||
const parsed = JSON.parse(trimmed);
|
||
if (parsed && typeof parsed === 'object') {
|
||
return parsed;
|
||
}
|
||
} catch (error) {
|
||
// fall back to line parsing
|
||
}
|
||
}
|
||
const headers = {};
|
||
trimmed.split('\n').forEach((line) => {
|
||
const idx = line.indexOf(':');
|
||
if (idx === -1) return;
|
||
const key = line.slice(0, idx).trim();
|
||
const value = line.slice(idx + 1).trim();
|
||
if (key) {
|
||
headers[key] = value;
|
||
}
|
||
});
|
||
return headers;
|
||
}
|
||
|
||
function stringifyHeaders(headers) {
|
||
if (!headers || typeof headers !== 'object') return '';
|
||
return Object.entries(headers)
|
||
.map(([key, value]) => `${key}: ${value}`)
|
||
.join('\n');
|
||
}
|
||
|
||
function buildTemplateContext() {
|
||
const now = new Date();
|
||
return {
|
||
now,
|
||
date: now.toISOString().slice(0, 10),
|
||
today: now.toISOString().slice(0, 10),
|
||
iso: now.toISOString(),
|
||
datetime: now.toISOString(),
|
||
timestamp: now.getTime(),
|
||
year: now.getFullYear(),
|
||
month: String(now.getMonth() + 1).padStart(2, '0'),
|
||
day: String(now.getDate()).padStart(2, '0'),
|
||
hour: String(now.getHours()).padStart(2, '0'),
|
||
minute: String(now.getMinutes()).padStart(2, '0'),
|
||
weekday: now.toLocaleDateString('de-DE', { weekday: 'long' }),
|
||
weekday_short: now.toLocaleDateString('de-DE', { weekday: 'short' })
|
||
};
|
||
}
|
||
|
||
function renderTemplate(template, context = {}) {
|
||
if (typeof template !== 'string') return '';
|
||
const baseDate = context.now instanceof Date ? context.now : new Date();
|
||
const uuidFn = typeof crypto !== 'undefined' && crypto.randomUUID
|
||
? () => crypto.randomUUID()
|
||
: () => Math.random().toString(16).slice(2, 10);
|
||
|
||
return template.replace(/\{\{\s*([^}\s]+)\s*\}\}/g, (_, keyRaw) => {
|
||
const key = String(keyRaw || '').trim();
|
||
if (!key) return '';
|
||
|
||
if (key === 'uuid') return uuidFn();
|
||
|
||
const dateOffsetMatch = key.match(/^date([+-]\d+)?$/);
|
||
if (dateOffsetMatch) {
|
||
const offset = dateOffsetMatch[1] ? parseInt(dateOffsetMatch[1], 10) : 0;
|
||
const shifted = new Date(baseDate);
|
||
shifted.setDate(baseDate.getDate() + (Number.isNaN(offset) ? 0 : offset));
|
||
return shifted.toISOString().slice(0, 10);
|
||
}
|
||
|
||
const dayOffsetMatch = key.match(/^day([+-]\d+)?$/);
|
||
if (dayOffsetMatch) {
|
||
const offset = dayOffsetMatch[1] ? parseInt(dayOffsetMatch[1], 10) : 0;
|
||
const shifted = new Date(baseDate);
|
||
shifted.setDate(baseDate.getDate() + (Number.isNaN(offset) ? 0 : offset));
|
||
return String(shifted.getDate()).padStart(2, '0');
|
||
}
|
||
|
||
switch (key) {
|
||
case 'today':
|
||
case 'date':
|
||
return baseDate.toISOString().slice(0, 10);
|
||
case 'iso':
|
||
case 'now':
|
||
case 'datetime':
|
||
return baseDate.toISOString();
|
||
case 'timestamp':
|
||
return String(baseDate.getTime());
|
||
case 'year':
|
||
return String(baseDate.getFullYear());
|
||
case 'month':
|
||
return String(baseDate.getMonth() + 1).padStart(2, '0');
|
||
case 'day':
|
||
return String(baseDate.getDate()).padStart(2, '0');
|
||
case 'hour':
|
||
return String(baseDate.getHours()).padStart(2, '0');
|
||
case 'minute':
|
||
return String(baseDate.getMinutes()).padStart(2, '0');
|
||
case 'weekday':
|
||
return baseDate.toLocaleDateString('de-DE', { weekday: 'long' });
|
||
case 'weekday_short':
|
||
return baseDate.toLocaleDateString('de-DE', { weekday: 'short' });
|
||
default:
|
||
return context[key] !== undefined && context[key] !== null
|
||
? String(context[key])
|
||
: '';
|
||
}
|
||
});
|
||
}
|
||
|
||
function renderHeaderTemplates(rawHeaders, context) {
|
||
const parsed = parseHeadersInput(rawHeaders);
|
||
const rendered = {};
|
||
Object.entries(parsed).forEach(([key, value]) => {
|
||
const renderedKey = renderTemplate(key, context).trim();
|
||
if (!renderedKey) return;
|
||
rendered[renderedKey] = renderTemplate(
|
||
value === undefined || value === null ? '' : String(value),
|
||
context
|
||
);
|
||
});
|
||
return rendered;
|
||
}
|
||
|
||
function renderPlaceholderTable(context) {
|
||
if (!placeholderTableBody) return;
|
||
const rows = [
|
||
{ key: '{{date}}', value: context.date },
|
||
{ key: '{{date+1}}', value: renderTemplate('{{date+1}}', context) },
|
||
{ key: '{{day}}', value: context.day },
|
||
{ key: '{{day-1}}', value: renderTemplate('{{day-1}}', context) },
|
||
{ key: '{{datetime}}', value: context.datetime },
|
||
{ key: '{{iso}}', value: context.iso },
|
||
{ key: '{{timestamp}}', value: context.timestamp },
|
||
{ key: '{{uuid}}', value: renderTemplate('{{uuid}}', context) },
|
||
{ key: '{{year}}', value: context.year },
|
||
{ key: '{{month}}', value: context.month },
|
||
{ key: '{{day}}', value: context.day },
|
||
{ key: '{{hour}}', value: context.hour },
|
||
{ key: '{{minute}}', value: context.minute },
|
||
{ key: '{{weekday}}', value: context.weekday },
|
||
{ key: '{{weekday_short}}', value: context.weekday_short }
|
||
];
|
||
placeholderTableBody.innerHTML = rows.map((row) => `
|
||
<tr>
|
||
<td class="placeholder-key">${row.key}</td>
|
||
<td>${row.value}</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function buildPayloadFromForm() {
|
||
const type = typeSelect ? typeSelect.value : 'request';
|
||
const base = {
|
||
type,
|
||
name: nameInput.value.trim(),
|
||
description: descriptionInput.value.trim(),
|
||
interval_minutes: intervalPreset.value === 'custom'
|
||
? Math.max(5, parseInt(intervalMinutesInput.value, 10) || DEFAULT_INTERVAL_MINUTES)
|
||
: undefined,
|
||
schedule_type: intervalPreset.value !== 'custom' ? intervalPreset.value : undefined,
|
||
jitter_minutes: Math.max(0, parseInt(jitterInput.value, 10) || 0),
|
||
start_at: parseDateInput(startAtInput.value),
|
||
run_until: parseDateInput(runUntilInput.value),
|
||
active: activeToggle.checked,
|
||
exclusion_windows: exclusionWindows.map((win) => ({ start: win.start, end: win.end }))
|
||
};
|
||
|
||
if (type === 'email') {
|
||
return {
|
||
...base,
|
||
email_to: emailToInput?.value.trim(),
|
||
email_subject_template: emailSubjectInput?.value.trim(),
|
||
email_body_template: emailBodyInput?.value
|
||
};
|
||
}
|
||
|
||
if (type === 'flow') {
|
||
const steps = [];
|
||
const step1Url = flowStep1Url?.value.trim();
|
||
if (step1Url) {
|
||
steps.push({
|
||
method: flowStep1Method?.value || 'GET',
|
||
url: step1Url,
|
||
headers: parseHeadersInput(flowStep1Headers?.value || ''),
|
||
body: flowStep1Body?.value || ''
|
||
});
|
||
}
|
||
const step2Url = flowStep2Url?.value.trim();
|
||
if (step2Url) {
|
||
steps.push({
|
||
method: flowStep2Method?.value || 'GET',
|
||
url: step2Url,
|
||
headers: parseHeadersInput(flowStep2Headers?.value || ''),
|
||
body: flowStep2Body?.value || ''
|
||
});
|
||
}
|
||
return {
|
||
...base,
|
||
steps
|
||
};
|
||
}
|
||
|
||
return {
|
||
...base,
|
||
method: methodSelect.value,
|
||
url_template: urlInput.value.trim(),
|
||
headers: parseHeadersInput(headersInput.value),
|
||
body_template: bodyInput.value
|
||
};
|
||
}
|
||
|
||
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() {
|
||
if (intervalPreset.value === 'custom') {
|
||
intervalMinutesInput.disabled = false;
|
||
} else {
|
||
intervalMinutesInput.disabled = true;
|
||
intervalMinutesInput.value = intervalPreset.value === 'daily' ? 1440 : DEFAULT_INTERVAL_MINUTES;
|
||
}
|
||
}
|
||
|
||
function refreshPreview() {
|
||
if (!previewUrl || !previewHeaders || !previewBody) return;
|
||
const type = typeSelect ? typeSelect.value : 'request';
|
||
const context = buildTemplateContext();
|
||
renderPlaceholderTable(context);
|
||
if (type === 'email') {
|
||
const renderedTo = renderTemplate(emailToInput?.value || '', context);
|
||
const renderedSubject = renderTemplate(emailSubjectInput?.value || '', context);
|
||
const renderedEmailBody = renderTemplate(emailBodyInput?.value || '', context);
|
||
previewUrl.textContent = `To: ${renderedTo || '—'}\nSubject: ${renderedSubject || '—'}`;
|
||
previewHeaders.textContent = '—';
|
||
previewBody.textContent = renderedEmailBody || '—';
|
||
return;
|
||
}
|
||
|
||
if (type === 'flow') {
|
||
const steps = [];
|
||
if (flowStep1Url?.value) {
|
||
steps.push({
|
||
label: 'Step 1',
|
||
url: renderTemplate(flowStep1Url.value, context),
|
||
method: flowStep1Method?.value || 'GET',
|
||
headers: renderHeaderTemplates(flowStep1Headers?.value || '', context),
|
||
body: renderTemplate(flowStep1Body?.value || '', context)
|
||
});
|
||
}
|
||
if (flowStep2Url?.value) {
|
||
steps.push({
|
||
label: 'Step 2',
|
||
url: renderTemplate(flowStep2Url.value, context),
|
||
method: flowStep2Method?.value || 'GET',
|
||
headers: renderHeaderTemplates(flowStep2Headers?.value || '', context),
|
||
body: renderTemplate(flowStep2Body?.value || '', context)
|
||
});
|
||
}
|
||
previewUrl.textContent = steps.length
|
||
? steps.map((s) => `${s.label}: ${s.method} ${s.url || '—'}`).join('\n')
|
||
: '—';
|
||
previewHeaders.textContent = steps.length
|
||
? steps.map((s) => `${s.label} Headers:\n${stringifyHeaders(s.headers) || '—'}`).join('\n\n')
|
||
: '—';
|
||
previewBody.textContent = steps.length
|
||
? steps.map((s) => `${s.label} Body:\n${s.body || '—'}`).join('\n\n')
|
||
: '—';
|
||
return;
|
||
}
|
||
|
||
const renderedUrl = renderTemplate(urlInput.value || '', context);
|
||
const headersObj = renderHeaderTemplates(headersInput.value || '', context);
|
||
const renderedBody = renderTemplate(bodyInput.value || '', context);
|
||
|
||
previewUrl.textContent = renderedUrl || '—';
|
||
previewHeaders.textContent = Object.keys(headersObj).length
|
||
? stringifyHeaders(headersObj)
|
||
: '—';
|
||
previewBody.textContent = renderedBody || '—';
|
||
}
|
||
|
||
async function loadRequests() {
|
||
if (!active) return;
|
||
if (!state.loading) {
|
||
setStatus(listStatus, 'Lade Automationen...');
|
||
}
|
||
state.loading = true;
|
||
try {
|
||
const data = await apiFetchJSON('/automation/requests');
|
||
state.requests = Array.isArray(data) ? data : [];
|
||
renderRequests();
|
||
renderHero();
|
||
if (state.selectedId) {
|
||
const stillExists = state.requests.some((req) => req.id === state.selectedId);
|
||
if (!stillExists) {
|
||
state.selectedId = null;
|
||
state.runs = [];
|
||
renderRuns();
|
||
}
|
||
}
|
||
if (!state.selectedId && state.requests.length) {
|
||
selectRequest(state.requests[0].id, { focusForm: false });
|
||
}
|
||
setStatus(listStatus, state.requests.length ? '' : 'Noch keine Automationen angelegt.');
|
||
} catch (error) {
|
||
console.error(error);
|
||
setStatus(listStatus, error.message || 'Automationen konnten nicht geladen werden', 'error');
|
||
} finally {
|
||
state.loading = false;
|
||
}
|
||
}
|
||
|
||
function renderHero() {
|
||
if (!heroStats) return;
|
||
const active = state.requests.filter((req) => req.active);
|
||
const nextRun = active
|
||
.filter((req) => req.next_run_at)
|
||
.sort((a, b) => new Date(a.next_run_at) - new Date(b.next_run_at))[0];
|
||
const lastRun = [...state.requests]
|
||
.filter((req) => req.last_run_at)
|
||
.sort((a, b) => new Date(b.last_run_at) - new Date(a.last_run_at))[0];
|
||
|
||
heroStats.innerHTML = `
|
||
<div class="stat">
|
||
<div class="stat-label">Aktive Automationen</div>
|
||
<div class="stat-value">${active.length}/${state.requests.length}</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-label">Nächster Lauf</div>
|
||
<div class="stat-value">${nextRun ? formatRelative(nextRun.next_run_at) : '—'}</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-label">Letzter Status</div>
|
||
<div class="stat-value">${lastRun ? (lastRun.last_status || '—') : '—'}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function updateRelativeTimes() {
|
||
if (!active) return;
|
||
renderHero();
|
||
if (!requestTableBody || !state.requests.length) return;
|
||
const byId = new Map(state.requests.map((req) => [String(req.id), req]));
|
||
requestTableBody.querySelectorAll('tr[data-id]').forEach((row) => {
|
||
const req = byId.get(row.dataset.id);
|
||
if (!req) return;
|
||
const nextEl = row.querySelector('.next');
|
||
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');
|
||
if (lastEl) lastEl.textContent = formatRelative(req.last_run_at);
|
||
});
|
||
}
|
||
|
||
function renderRequests() {
|
||
if (!requestTableBody) return;
|
||
requestTableBody.innerHTML = '';
|
||
if (!state.requests.length) {
|
||
requestTableBody.innerHTML = '<tr><td colspan="7">Keine Automationen vorhanden.</td></tr>';
|
||
listInstance = null;
|
||
return;
|
||
}
|
||
|
||
const rows = state.requests.map((req) => {
|
||
const isSelected = state.selectedId === req.id;
|
||
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 runsCount = Array.isArray(req.runs) ? req.runs.length : (req.runs_count || req.runsCount || 0);
|
||
const statusBadge = req.last_status === 'success'
|
||
? '<span class="badge success">OK</span>'
|
||
: req.last_status === 'error'
|
||
? '<span class="badge error">Fehler</span>'
|
||
: '<span class="badge">—</span>';
|
||
let subline = '';
|
||
if (req.type === 'email') {
|
||
subline = `E-Mail → ${req.email_to || '—'}`;
|
||
} else if (req.type === 'flow') {
|
||
const stepCount = Array.isArray(req.steps) ? req.steps.length : 0;
|
||
subline = `Flow (${stepCount || '0'} Schritte)`;
|
||
} else {
|
||
subline = `${req.method || 'GET'} · ${req.url_template || '—'}`;
|
||
}
|
||
|
||
return `
|
||
<tr data-id="${req.id}" class="${isSelected ? 'is-selected' : ''}">
|
||
<td data-sort="${req.name || ''}">
|
||
<div class="row-title name">${req.name}</div>
|
||
<div class="row-sub">${subline}</div>
|
||
<span class="type hidden-value">${req.type || ''}</span>
|
||
<span class="email hidden-value">${req.email_to || ''}</span>
|
||
<span class="url hidden-value">${req.url_template || ''}</span>
|
||
<span class="steps hidden-value">${req.steps && req.steps.map((s) => s.url || s.url_template || '').join(' | ')}</span>
|
||
</td>
|
||
<td data-sort="${nextSort}">
|
||
<span class="next" data-sort="${nextSort}">${formatRelative(req.next_run_at)}</span>
|
||
<br><small>${formatDateTime(req.next_run_at)}</small>
|
||
</td>
|
||
<td data-sort="${untilSort}">
|
||
<span class="runUntil until" data-sort="${untilSort}">${req.run_until ? formatRelative(req.run_until) : '—'}</span>
|
||
<br><small>${formatDateTime(req.run_until)}</small>
|
||
</td>
|
||
<td data-sort="${lastSort}">
|
||
<span class="last" data-sort="${lastSort}">${formatRelative(req.last_run_at)}</span>
|
||
<br><small>${req.last_status_code || '—'}</small>
|
||
</td>
|
||
<td data-sort="${req.last_status || ''}"><span class="status" data-sort="${req.last_status || ''}">${statusBadge}</span></td>
|
||
<td data-sort="${runsCount}" class="runs-count">
|
||
<span class="runs hidden-value" data-sort="${runsCount}">${runsCount}</span>
|
||
${runsCount}
|
||
</td>
|
||
<td>
|
||
<div class="row-actions">
|
||
<button class="icon-btn" title="Bearbeiten" data-action="edit" data-id="${req.id}" type="button">✏️</button>
|
||
<button class="icon-btn" title="Run" data-action="run" data-id="${req.id}" type="button">▶️</button>
|
||
<button class="icon-btn" title="${req.active ? 'Pause' : 'Aktivieren'}" data-action="toggle" data-id="${req.id}" type="button">${req.active ? '⏸' : '▶'}</button>
|
||
<button class="icon-btn danger" title="Löschen" data-action="delete" data-id="${req.id}" type="button">🗑️</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
requestTableBody.innerHTML = rows.join('');
|
||
|
||
initListInstance();
|
||
}
|
||
|
||
function renderRuns() {
|
||
if (!runsList) return;
|
||
runsList.innerHTML = '';
|
||
if (!state.runs.length) {
|
||
runsList.innerHTML = '<li class="run-item">Noch keine Läufe.</li>';
|
||
return;
|
||
}
|
||
|
||
runsList.innerHTML = state.runs.map((run) => {
|
||
const badge = run.status === 'success'
|
||
? '<span class="badge success">OK</span>'
|
||
: run.status === 'error'
|
||
? '<span class="badge error">Fehler</span>'
|
||
: '<span class="badge">Offen</span>';
|
||
return `
|
||
<li class="run-item">
|
||
<div class="run-top">
|
||
<div class="run-meta">
|
||
${badge}
|
||
<span>${formatDateTime(run.started_at)}</span>
|
||
<span>Code: ${run.status_code ?? '—'}</span>
|
||
<span>Dauer: ${run.duration_ms ? `${run.duration_ms} ms` : '—'}</span>
|
||
</div>
|
||
</div>
|
||
${run.error ? `<div class="run-body">Fehler: ${run.error}</div>` : ''}
|
||
${run.response_body ? `<div class="run-body">${run.response_body}</div>` : ''}
|
||
</li>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function resetForm() {
|
||
form.reset();
|
||
editingId = null;
|
||
if (typeSelect) typeSelect.value = 'request';
|
||
intervalPreset.value = 'hourly';
|
||
intervalMinutesInput.value = DEFAULT_INTERVAL_MINUTES;
|
||
applyPresetDisabling();
|
||
const now = new Date();
|
||
now.setMinutes(now.getMinutes() + 5);
|
||
now.setSeconds(0, 0);
|
||
startAtInput.value = toDateTimeLocal(now);
|
||
runUntilInput.value = '';
|
||
exclusionWindows = [];
|
||
renderExclusionChips();
|
||
formModeLabel.textContent = 'Neue Automation';
|
||
setStatus(formStatus, '');
|
||
if (emailToInput) emailToInput.value = '';
|
||
if (emailSubjectInput) emailSubjectInput.value = '';
|
||
if (emailBodyInput) emailBodyInput.value = '';
|
||
[flowStep1Url, flowStep1Headers, flowStep1Body, flowStep2Url, flowStep2Headers, flowStep2Body]
|
||
.forEach((el) => { if (el) el.value = ''; });
|
||
if (flowStep1Method) flowStep1Method.value = 'GET';
|
||
if (flowStep2Method) flowStep2Method.value = 'GET';
|
||
applyTypeVisibility();
|
||
refreshPreview();
|
||
}
|
||
|
||
function fillForm(request) {
|
||
if (!request) return;
|
||
editingId = request.id;
|
||
formModeLabel.textContent = `Automation bearbeiten`;
|
||
nameInput.value = request.name || '';
|
||
descriptionInput.value = request.description || '';
|
||
urlInput.value = request.url_template || '';
|
||
methodSelect.value = request.method || 'GET';
|
||
headersInput.value = stringifyHeaders(request.headers);
|
||
bodyInput.value = request.body_template || '';
|
||
activeToggle.checked = !!request.active;
|
||
if (typeSelect) {
|
||
typeSelect.value = request.type || 'request';
|
||
applyTypeVisibility();
|
||
}
|
||
if (request.type === 'email') {
|
||
emailToInput.value = request.email_to || '';
|
||
emailSubjectInput.value = request.email_subject_template || '';
|
||
emailBodyInput.value = request.email_body_template || '';
|
||
} else if (request.type === 'flow') {
|
||
const steps = Array.isArray(request.steps) ? request.steps : [];
|
||
const [s1, s2] = steps;
|
||
if (s1) {
|
||
flowStep1Url.value = s1.url || s1.url_template || '';
|
||
flowStep1Method.value = s1.method || s1.http_method || 'GET';
|
||
flowStep1Headers.value = stringifyHeaders(s1.headers || {});
|
||
flowStep1Body.value = s1.body || s1.body_template || '';
|
||
}
|
||
if (s2) {
|
||
flowStep2Url.value = s2.url || s2.url_template || '';
|
||
flowStep2Method.value = s2.method || s2.http_method || 'GET';
|
||
flowStep2Headers.value = stringifyHeaders(s2.headers || {});
|
||
flowStep2Body.value = s2.body || s2.body_template || '';
|
||
}
|
||
}
|
||
|
||
if (request.interval_minutes === 60) {
|
||
intervalPreset.value = 'hourly';
|
||
} else if (request.interval_minutes === 1440) {
|
||
intervalPreset.value = 'daily';
|
||
} else {
|
||
intervalPreset.value = 'custom';
|
||
}
|
||
intervalMinutesInput.value = request.interval_minutes || DEFAULT_INTERVAL_MINUTES;
|
||
jitterInput.value = request.jitter_minutes || 0;
|
||
startAtInput.value = toDateTimeLocal(request.start_at);
|
||
runUntilInput.value = toDateTimeLocal(request.run_until);
|
||
exclusionWindows = Array.isArray(request.exclusion_windows) ? [...request.exclusion_windows] : [];
|
||
renderExclusionChips();
|
||
applyPresetDisabling();
|
||
refreshPreview();
|
||
}
|
||
|
||
async function handleSubmit(event) {
|
||
event.preventDefault();
|
||
if (state.saving) return;
|
||
const payload = buildPayloadFromForm();
|
||
if (!payload.name) {
|
||
setStatus(formStatus, 'Name ist ein Pflichtfeld.', 'error');
|
||
return;
|
||
}
|
||
if (payload.type === 'request' && !payload.url_template) {
|
||
setStatus(formStatus, 'URL ist ein Pflichtfeld für HTTP-Requests.', 'error');
|
||
return;
|
||
}
|
||
if (payload.type === 'email') {
|
||
if (!payload.email_to || !payload.email_subject_template || !payload.email_body_template) {
|
||
setStatus(formStatus, 'E-Mail: Empfänger, Betreff und Body sind Pflichtfelder.', 'error');
|
||
return;
|
||
}
|
||
}
|
||
if (payload.type === 'flow') {
|
||
const hasStep = Array.isArray(payload.steps) && payload.steps.length > 0 && payload.steps[0].url;
|
||
if (!hasStep) {
|
||
setStatus(formStatus, 'Flow: Mindestens ein Schritt mit URL ist erforderlich.', 'error');
|
||
return;
|
||
}
|
||
}
|
||
|
||
state.saving = true;
|
||
setStatus(formStatus, 'Speichere...');
|
||
saveBtn.disabled = true;
|
||
|
||
try {
|
||
if (editingId) {
|
||
await apiFetchJSON(`/automation/requests/${editingId}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify(payload)
|
||
});
|
||
} else {
|
||
await apiFetchJSON('/automation/requests', {
|
||
method: 'POST',
|
||
body: JSON.stringify(payload)
|
||
});
|
||
}
|
||
setStatus(formStatus, 'Gespeichert.', 'success');
|
||
closeFormModal();
|
||
await loadRequests();
|
||
} catch (error) {
|
||
console.error(error);
|
||
setStatus(formStatus, error.message || 'Konnte nicht speichern', 'error');
|
||
} finally {
|
||
state.saving = false;
|
||
saveBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function loadRuns(requestId) {
|
||
if (!requestId) {
|
||
state.runs = [];
|
||
renderRuns();
|
||
return;
|
||
}
|
||
setStatus(runsStatus, 'Lade Runs...');
|
||
try {
|
||
const data = await apiFetchJSON(`/automation/requests/${requestId}/runs?limit=50`);
|
||
state.runs = Array.isArray(data) ? data : [];
|
||
renderRuns();
|
||
setStatus(runsStatus, state.runs.length ? '' : 'Keine Läufe vorhanden.');
|
||
} catch (error) {
|
||
console.error(error);
|
||
setStatus(runsStatus, error.message || 'Runs konnten nicht geladen werden', 'error');
|
||
}
|
||
}
|
||
|
||
function selectRequest(id, options = {}) {
|
||
state.selectedId = id;
|
||
const request = state.requests.find((item) => item.id === id);
|
||
if (!request) {
|
||
state.runs = [];
|
||
renderRuns();
|
||
return;
|
||
}
|
||
renderRequests();
|
||
loadRuns(id);
|
||
if (options.focusForm) {
|
||
nameInput.focus();
|
||
}
|
||
}
|
||
|
||
async function runAutomation(id) {
|
||
if (!id) {
|
||
setStatus(formStatus, 'Keine Automation ausgewählt.', 'error');
|
||
return;
|
||
}
|
||
if (state.running) return;
|
||
state.running = true;
|
||
setStatus(formStatus, 'Starte manuellen Run...');
|
||
try {
|
||
await apiFetchJSON(`/automation/requests/${id}/run`, { method: 'POST' });
|
||
await loadRequests();
|
||
await loadRuns(id);
|
||
setStatus(formStatus, 'Run gestartet.', 'success');
|
||
} catch (error) {
|
||
console.error(error);
|
||
setStatus(formStatus, error.message || 'Run fehlgeschlagen', 'error');
|
||
} finally {
|
||
state.running = false;
|
||
}
|
||
}
|
||
|
||
async function toggleActive(id) {
|
||
const request = state.requests.find((item) => item.id === id);
|
||
if (!request) return;
|
||
try {
|
||
await apiFetchJSON(`/automation/requests/${id}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify({ active: !request.active })
|
||
});
|
||
await loadRequests();
|
||
} catch (error) {
|
||
console.error(error);
|
||
setStatus(listStatus, error.message || 'Konnte Status nicht ändern', 'error');
|
||
}
|
||
}
|
||
|
||
async function deleteAutomation(id) {
|
||
const request = state.requests.find((item) => item.id === id);
|
||
if (!request) return;
|
||
if (!confirm(`Automation "${request.name}" wirklich löschen?`)) {
|
||
return;
|
||
}
|
||
try {
|
||
await apiFetchJSON(`/automation/requests/${id}`, { method: 'DELETE' });
|
||
if (state.selectedId === id) {
|
||
state.selectedId = null;
|
||
state.runs = [];
|
||
renderRuns();
|
||
}
|
||
await loadRequests();
|
||
} catch (error) {
|
||
console.error(error);
|
||
setStatus(listStatus, error.message || 'Konnte nicht löschen', 'error');
|
||
}
|
||
}
|
||
|
||
function parseCurlTemplate(text) {
|
||
const result = {};
|
||
const urlMatch = text.match(/curl\s+(['"]?)([^'"\s]+)\1/);
|
||
if (urlMatch) result.url = urlMatch[2];
|
||
const methodMatch = text.match(/-X\s+([A-Z]+)/i) || text.match(/--request\s+([A-Z]+)/i);
|
||
if (methodMatch) result.method = methodMatch[1].toUpperCase();
|
||
const headers = {};
|
||
const headerRegex = /-H\s+(?:(?:"([^"]+)")|(?:'([^']+)'))/gi;
|
||
let headerMatch;
|
||
while ((headerMatch = headerRegex.exec(text)) !== null) {
|
||
const raw = headerMatch[1] || headerMatch[2] || '';
|
||
const idx = raw.indexOf(':');
|
||
if (idx > -1) {
|
||
const key = raw.slice(0, idx).trim();
|
||
const value = raw.slice(idx + 1).trim();
|
||
if (key) headers[key] = value;
|
||
}
|
||
}
|
||
if (Object.keys(headers).length) result.headers = headers;
|
||
const dataMatch = text.match(/--data(?:-raw)?\s+(?:"([^"]*)"|'([^']*)')/i);
|
||
if (dataMatch) {
|
||
result.body = dataMatch[1] || dataMatch[2] || '';
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function parseFetchTemplate(text) {
|
||
const result = {};
|
||
const urlMatch = text.match(/fetch\(\s*['"]([^'"]+)['"]/i);
|
||
if (urlMatch) result.url = urlMatch[1];
|
||
const optionsMatch = text.match(/fetch\(\s*['"][^'"]+['"]\s*,\s*({[\s\S]*?})\s*\)/i);
|
||
if (optionsMatch) {
|
||
try {
|
||
let optionsText = optionsMatch[1]
|
||
.replace(/,\s*\}\s*$/, '}')
|
||
.replace(/,\s*\]/g, ']');
|
||
const options = JSON.parse(optionsText);
|
||
if (options.method) result.method = options.method.toUpperCase();
|
||
if (options.headers && typeof options.headers === 'object') result.headers = options.headers;
|
||
if (options.body) result.body = options.body;
|
||
} catch (error) {
|
||
// ignore parse errors
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function parsePowerShellTemplate(text) {
|
||
const result = {};
|
||
const urlMatch = text.match(/-Uri\s+['"]([^'"]+)['"]/i);
|
||
if (urlMatch) result.url = urlMatch[1];
|
||
const methodMatch = text.match(/-Method\s+['"]?([A-Z]+)['"]?/i);
|
||
if (methodMatch) result.method = methodMatch[1].toUpperCase();
|
||
const headersMatch = text.match(/-Headers\s+@?\{([\s\S]*?)\}/i);
|
||
if (headersMatch) {
|
||
const headersText = headersMatch[1];
|
||
const headers = {};
|
||
headersText.split(';').forEach((pair) => {
|
||
const idx = pair.indexOf('=');
|
||
if (idx === -1) return;
|
||
const key = pair.slice(0, idx).replace(/['\s]/g, '').trim();
|
||
const value = pair.slice(idx + 1).replace(/['\s]/g, '').trim();
|
||
if (key) headers[key] = value;
|
||
});
|
||
if (Object.keys(headers).length) result.headers = headers;
|
||
}
|
||
const bodyMatch = text.match(/-Body\s+['"]([\s\S]*?)['"]/i);
|
||
if (bodyMatch) result.body = bodyMatch[1];
|
||
return result;
|
||
}
|
||
|
||
function parseTemplate(raw) {
|
||
if (!raw) return null;
|
||
const text = raw.trim();
|
||
if (text.startsWith('curl')) return parseCurlTemplate(text);
|
||
if (text.includes('fetch(')) return parseFetchTemplate(text);
|
||
if (/Invoke-WebRequest|Invoke-RestMethod/i.test(text)) return parsePowerShellTemplate(text);
|
||
return null;
|
||
}
|
||
|
||
function applyImport() {
|
||
if (!importInput) return;
|
||
const raw = importInput.value;
|
||
if (!raw || !raw.trim()) {
|
||
setStatus(importStatus, 'Keine Vorlage eingegeben.', 'error');
|
||
return;
|
||
}
|
||
const parsed = parseTemplate(raw);
|
||
if (!parsed || (!parsed.url && !parsed.method && !parsed.headers && !parsed.body)) {
|
||
setStatus(importStatus, 'Konnte die Vorlage nicht erkennen.', 'error');
|
||
return;
|
||
}
|
||
// Neues Formular öffnen und mit der Vorlage befüllen
|
||
openFormModal('create');
|
||
if (parsed.url) urlInput.value = parsed.url;
|
||
if (parsed.method) methodSelect.value = parsed.method.toUpperCase();
|
||
if (parsed.headers) headersInput.value = stringifyHeaders(parsed.headers);
|
||
if (parsed.body) bodyInput.value = parsed.body;
|
||
setStatus(importStatus, 'Vorlage übernommen.', 'success');
|
||
setStatus(formStatus, 'Vorlage importiert. Prüfen & speichern.', 'success');
|
||
refreshPreview();
|
||
closeImportModal();
|
||
}
|
||
|
||
function updateSortIndicators() {
|
||
const headers = document.querySelectorAll('[data-sort-column]');
|
||
headers.forEach((th) => {
|
||
th.classList.remove('sort-asc', 'sort-desc');
|
||
const col = th.getAttribute('data-sort-column');
|
||
if (col === sortState.key) {
|
||
th.classList.add(sortState.dir === 'desc' ? 'sort-desc' : 'sort-asc');
|
||
}
|
||
});
|
||
}
|
||
|
||
function initListInstance() {
|
||
const container = document.getElementById('automationTable');
|
||
if (!container) return;
|
||
listInstance = null;
|
||
const options = {
|
||
listClass: 'list',
|
||
valueNames: [
|
||
'name',
|
||
'type',
|
||
'email',
|
||
'url',
|
||
'steps',
|
||
{ name: 'next', attr: 'data-sort' },
|
||
{ name: 'runUntil', attr: 'data-sort' },
|
||
{ name: 'last', attr: 'data-sort' },
|
||
{ name: 'status', attr: 'data-sort' },
|
||
{ name: 'runs', attr: 'data-sort' }
|
||
]
|
||
};
|
||
listInstance = new List(container, options);
|
||
applyFilters();
|
||
listInstance.sort(sortState.key, { order: sortState.dir === 'desc' ? 'desc' : 'asc' });
|
||
updateSortIndicators();
|
||
}
|
||
|
||
function applyFilters() {
|
||
if (!listInstance) return;
|
||
const searchName = (filterName?.value || '').toLowerCase().trim();
|
||
const searchNext = (filterNext?.value || '').toLowerCase().trim();
|
||
const searchRunUntil = (filterRunUntil?.value || '').toLowerCase().trim();
|
||
const searchLast = (filterLast?.value || '').toLowerCase().trim();
|
||
const statusValue = (filterStatus?.value || '').toLowerCase().trim();
|
||
const runsMin = filterRuns?.value ? Number(filterRuns.value) : null;
|
||
|
||
listInstance.filter((item) => {
|
||
const v = item.values();
|
||
const matchName = !searchName || [
|
||
v.name || '',
|
||
v.type || '',
|
||
v.email || '',
|
||
v.url || '',
|
||
v.steps || ''
|
||
].some((p) => String(p).toLowerCase().includes(searchName));
|
||
|
||
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 matchStatus = !statusValue || String(v.status || '').toLowerCase().includes(statusValue);
|
||
const matchRuns = runsMin === null || Number(v.runs || 0) >= runsMin;
|
||
|
||
return matchName && matchNext && matchRunUntil && matchLast && matchStatus && matchRuns;
|
||
});
|
||
updateSortIndicators();
|
||
}
|
||
|
||
function getSelectedRequest() {
|
||
if (!state.selectedId) return null;
|
||
return state.requests.find((item) => item.id === state.selectedId) || null;
|
||
}
|
||
|
||
function openFormModal(mode = 'create', request = null, options = {}) {
|
||
const skipReset = !!options.skipReset;
|
||
if (!skipReset) {
|
||
resetForm();
|
||
}
|
||
if (mode === 'edit' && request) {
|
||
fillForm(request);
|
||
formModeLabel.textContent = 'Automation bearbeiten';
|
||
modalTitle.textContent = 'Automation bearbeiten';
|
||
} else {
|
||
formModeLabel.textContent = 'Neue Automation';
|
||
modalTitle.textContent = 'Neue Automation';
|
||
}
|
||
formModal.hidden = false;
|
||
setTimeout(() => {
|
||
nameInput.focus();
|
||
}, 10);
|
||
refreshPreview();
|
||
}
|
||
|
||
function closeFormModal() {
|
||
formModal.hidden = true;
|
||
}
|
||
|
||
function openImportModal() {
|
||
if (!importModal) return;
|
||
if (importStatus) setStatus(importStatus, '');
|
||
importModal.hidden = false;
|
||
setTimeout(() => {
|
||
importInput?.focus();
|
||
}, 10);
|
||
}
|
||
|
||
function closeImportModal() {
|
||
if (!importModal) return;
|
||
importModal.hidden = true;
|
||
}
|
||
|
||
function applyTypeVisibility() {
|
||
const type = typeSelect ? typeSelect.value : 'request';
|
||
document.querySelectorAll('[data-section="http"]').forEach((el) => {
|
||
el.style.display = type === 'request' ? 'grid' : 'none';
|
||
});
|
||
document.querySelectorAll('[data-section="email"]').forEach((el) => {
|
||
el.style.display = type === 'email' ? 'grid' : 'none';
|
||
});
|
||
document.querySelectorAll('[data-section="flow"]').forEach((el) => {
|
||
el.style.display = type === 'flow' ? 'grid' : 'none';
|
||
});
|
||
// required toggles
|
||
urlInput.required = type === 'request';
|
||
methodSelect.required = type === 'request';
|
||
emailToInput && (emailToInput.required = type === 'email');
|
||
emailSubjectInput && (emailSubjectInput.required = type === 'email');
|
||
emailBodyInput && (emailBodyInput.required = type === 'email');
|
||
flowStep1Url && (flowStep1Url.required = type === 'flow');
|
||
}
|
||
|
||
function handleTableClick(event) {
|
||
const button = event.target.closest('[data-action]');
|
||
if (button) {
|
||
const { action, id } = button.dataset;
|
||
if (!id) return;
|
||
switch (action) {
|
||
case 'edit':
|
||
selectRequest(id);
|
||
openFormModal('edit', state.requests.find((item) => item.id === id));
|
||
break;
|
||
case 'run':
|
||
runAutomation(id);
|
||
break;
|
||
case 'toggle':
|
||
toggleActive(id);
|
||
break;
|
||
case 'delete':
|
||
deleteAutomation(id);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
return;
|
||
}
|
||
|
||
const row = event.target.closest('tr[data-id]');
|
||
if (row && row.dataset.id) {
|
||
selectRequest(row.dataset.id);
|
||
}
|
||
}
|
||
|
||
function handleSseMessage(payload) {
|
||
if (!active) return;
|
||
if (!payload || !payload.type) return;
|
||
const { type, request_id: requestId } = payload;
|
||
switch (payload.type) {
|
||
case 'automation-run':
|
||
loadRequests();
|
||
if (requestId && state.selectedId && String(requestId) === String(state.selectedId)) {
|
||
loadRuns(state.selectedId);
|
||
}
|
||
break;
|
||
case 'automation-upsert':
|
||
loadRequests();
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
function startSse() {
|
||
if (!active) return;
|
||
if (sse || typeof EventSource === 'undefined') return;
|
||
try {
|
||
sse = new EventSource(`${API_URL}/events`, { withCredentials: true });
|
||
} catch (error) {
|
||
console.warn('Konnte SSE nicht starten:', error);
|
||
sse = null;
|
||
return;
|
||
}
|
||
sse.addEventListener('message', (event) => {
|
||
if (!event || !event.data) return;
|
||
try {
|
||
const payload = JSON.parse(event.data);
|
||
handleSseMessage(payload);
|
||
} catch (error) {
|
||
// ignore parse errors
|
||
}
|
||
});
|
||
sse.addEventListener('error', () => {
|
||
if (sse) {
|
||
sse.close();
|
||
sse = null;
|
||
}
|
||
setTimeout(startSse, 5000);
|
||
});
|
||
}
|
||
|
||
function ensureRelativeTimer() {
|
||
if (!relativeTimer) {
|
||
updateRelativeTimes();
|
||
relativeTimer = setInterval(updateRelativeTimes, 60000);
|
||
}
|
||
}
|
||
|
||
function cleanup(options = {}) {
|
||
const { keepActive = false } = options;
|
||
if (!keepActive) {
|
||
active = false;
|
||
}
|
||
if (relativeTimer) {
|
||
clearInterval(relativeTimer);
|
||
relativeTimer = null;
|
||
}
|
||
if (sse) {
|
||
sse.close();
|
||
sse = null;
|
||
}
|
||
}
|
||
|
||
function init() {
|
||
if (initialized) return;
|
||
applyPresetDisabling();
|
||
resetForm();
|
||
ensureRelativeTimer();
|
||
|
||
form.addEventListener('submit', handleSubmit);
|
||
intervalPreset.addEventListener('change', applyPresetDisabling);
|
||
resetFormBtn.addEventListener('click', () => {
|
||
resetForm();
|
||
nameInput.focus();
|
||
});
|
||
newAutomationBtn.addEventListener('click', () => openFormModal('create'));
|
||
refreshBtn.addEventListener('click', loadRequests);
|
||
applyImportBtn?.addEventListener('click', applyImport);
|
||
refreshPreviewBtn?.addEventListener('click', refreshPreview);
|
||
[urlInput, headersInput, bodyInput].forEach((el) => el?.addEventListener('input', refreshPreview));
|
||
[emailToInput, emailSubjectInput, emailBodyInput].forEach((el) => el?.addEventListener('input', refreshPreview));
|
||
[flowStep1Url, flowStep1Headers, flowStep1Body, flowStep1Method,
|
||
flowStep2Url, flowStep2Headers, flowStep2Body, flowStep2Method].forEach((el) => {
|
||
el?.addEventListener('input', refreshPreview);
|
||
el?.addEventListener('change', refreshPreview);
|
||
});
|
||
if (addExclusionBtn) {
|
||
addExclusionBtn.addEventListener('click', addExclusion);
|
||
}
|
||
typeSelect?.addEventListener('change', () => {
|
||
applyTypeVisibility();
|
||
refreshPreview();
|
||
});
|
||
[filterName, filterNext, filterRunUntil, filterLast, filterStatus].forEach((el) => {
|
||
el?.addEventListener('input', applyFilters);
|
||
el?.addEventListener('change', applyFilters);
|
||
});
|
||
filterRuns?.addEventListener('input', applyFilters);
|
||
filterRuns?.addEventListener('change', applyFilters);
|
||
document.querySelectorAll('[data-sort-column]').forEach((th) => {
|
||
th.addEventListener('click', () => {
|
||
const col = th.getAttribute('data-sort-column');
|
||
if (!col) return;
|
||
if (sortState.key === col) {
|
||
sortState.dir = sortState.dir === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
sortState.key = col;
|
||
sortState.dir = 'asc';
|
||
}
|
||
if (listInstance) {
|
||
listInstance.sort(col, { order: sortState.dir });
|
||
}
|
||
updateSortIndicators();
|
||
});
|
||
});
|
||
applyTypeVisibility();
|
||
openImportBtn?.addEventListener('click', openImportModal);
|
||
importCloseBtn?.addEventListener('click', closeImportModal);
|
||
importModalBackdrop?.addEventListener('click', closeImportModal);
|
||
requestTableBody.addEventListener('click', handleTableClick);
|
||
modalCloseBtn.addEventListener('click', closeFormModal);
|
||
modalBackdrop.addEventListener('click', closeFormModal);
|
||
document.addEventListener('keydown', (event) => {
|
||
if (event.key === 'Escape') {
|
||
if (!formModal.hidden) {
|
||
closeFormModal();
|
||
}
|
||
if (importModal && !importModal.hidden) {
|
||
closeImportModal();
|
||
}
|
||
}
|
||
});
|
||
document.addEventListener('visibilitychange', () => {
|
||
if (document.hidden) {
|
||
cleanup({ keepActive: true });
|
||
} else {
|
||
if (active) {
|
||
ensureRelativeTimer();
|
||
startSse();
|
||
}
|
||
}
|
||
});
|
||
window.addEventListener('beforeunload', cleanup);
|
||
window.addEventListener('pagehide', cleanup);
|
||
window.addEventListener('unload', cleanup);
|
||
initialized = true;
|
||
}
|
||
|
||
function activate() {
|
||
init();
|
||
active = true;
|
||
ensureRelativeTimer();
|
||
startSse();
|
||
loadRequests();
|
||
}
|
||
|
||
window.AutomationPage = {
|
||
activate,
|
||
deactivate: cleanup
|
||
};
|
||
|
||
const automationSection = document.querySelector('[data-view="automation"]');
|
||
if (automationSection && automationSection.classList.contains('app-view--active')) {
|
||
activate();
|
||
}
|
||
})();
|