(function () { 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; 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 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 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 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 (!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) => ` ${row.key} ${row.value} `).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 }; 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 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 (!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 = `
Aktive Automationen
${active.length}/${state.requests.length}
Nächster Lauf
${nextRun ? formatRelative(nextRun.next_run_at) : '—'}
Letzter Status
${lastRun ? (lastRun.last_status || '—') : '—'}
`; } function updateRelativeTimes() { 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 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 = 'Keine Automationen vorhanden.'; 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 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' ? 'OK' : req.last_status === 'error' ? 'Fehler' : ''; 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 `
${req.name}
${subline}
${req.type || ''} ${req.email_to || ''} ${req.url_template || ''} ${req.steps && req.steps.map((s) => s.url || s.url_template || '').join(' | ')} ${formatRelative(req.next_run_at)}
${formatDateTime(req.next_run_at)} ${formatRelative(req.last_run_at)}
${req.last_status_code || '—'} ${statusBadge} ${runsCount} ${runsCount}
`; }); requestTableBody.innerHTML = rows.join(''); initListInstance(); } function renderRuns() { if (!runsList) return; runsList.innerHTML = ''; if (!state.runs.length) { runsList.innerHTML = '
  • Noch keine Läufe.
  • '; return; } runsList.innerHTML = state.runs.map((run) => { const badge = run.status === 'success' ? 'OK' : run.status === 'error' ? 'Fehler' : 'Offen'; return `
  • ${badge} ${formatDateTime(run.started_at)} Code: ${run.status_code ?? '—'} Dauer: ${run.duration_ms ? `${run.duration_ms} ms` : '—'}
    ${run.error ? `
    Fehler: ${run.error}
    ` : ''} ${run.response_body ? `
    ${run.response_body}
    ` : ''}
  • `; }).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 = ''; 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); 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: '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 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 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 && 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 (!payload || !payload.type) return; switch (payload.type) { case 'automation-run': case 'automation-upsert': loadRequests(); break; default: break; } } function startSse() { 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 init() { applyPresetDisabling(); resetForm(); loadRequests(); if (!relativeTimer) { updateRelativeTimes(); relativeTimer = setInterval(updateRelativeTimes, 60000); document.addEventListener('visibilitychange', () => { if (!document.hidden) { updateRelativeTimes(); } }); } 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); }); typeSelect?.addEventListener('change', () => { applyTypeVisibility(); refreshPreview(); }); [filterName, filterNext, 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(); } } }); startSse(); } init(); })();