diff --git a/backend/package.json b/backend/package.json index 1ba2b61..4459b4c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,9 +11,10 @@ "express": "^4.18.2", "cors": "^2.8.5", "better-sqlite3": "^9.2.2", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "nodemailer": "^6.9.14" }, "devDependencies": { "nodemon": "^3.0.2" } -} \ No newline at end of file +} diff --git a/backend/server.js b/backend/server.js index 1f5684a..2c4b588 100644 --- a/backend/server.js +++ b/backend/server.js @@ -30,6 +30,9 @@ const DAILY_BOOKMARK_TITLE_MAX_LENGTH = 160; const DAILY_BOOKMARK_URL_MAX_LENGTH = 800; const DAILY_BOOKMARK_NOTES_MAX_LENGTH = 800; const DAILY_BOOKMARK_MARKER_MAX_LENGTH = 120; +const AUTOMATION_TYPE_REQUEST = 'request'; +const AUTOMATION_TYPE_EMAIL = 'email'; +const AUTOMATION_TYPE_FLOW = 'flow'; const AUTOMATION_MAX_NAME_LENGTH = 160; const AUTOMATION_MAX_URL_LENGTH = 2000; const AUTOMATION_MAX_BODY_LENGTH = 12000; @@ -40,6 +43,9 @@ const AUTOMATION_DEFAULT_INTERVAL_MINUTES = 60; const AUTOMATION_MAX_JITTER_MINUTES = 120; const AUTOMATION_MAX_RESPONSE_PREVIEW = 4000; const AUTOMATION_WORKER_INTERVAL_MS = 30000; +const AUTOMATION_MAX_STEPS = 3; +const AUTOMATION_MAX_EMAIL_TO_LENGTH = 320; +const AUTOMATION_MAX_EMAIL_SUBJECT_LENGTH = 320; const SPORTS_SCORING_DEFAULTS = { enabled: 1, threshold: 5, @@ -90,6 +96,54 @@ if (!fs.existsSync(screenshotDir)) { fs.mkdirSync(screenshotDir, { recursive: true }); } +const automationConfigPath = path.join(__dirname, 'data', 'automation-config.json'); +const defaultAutomationConfig = { + smtp: { + host: '', + port: 587, + secure: false, + user: '', + pass: '', + from: '' + } +}; + +function ensureAutomationConfigFile() { + try { + if (!fs.existsSync(automationConfigPath)) { + const dir = path.dirname(automationConfigPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(automationConfigPath, JSON.stringify(defaultAutomationConfig, null, 2), 'utf8'); + console.log('Automation-Config angelegt unter', automationConfigPath); + } + } catch (error) { + console.warn('Konnte Automation-Config nicht erstellen:', error.message); + } +} + +ensureAutomationConfigFile(); + +function loadAutomationConfig() { + try { + const raw = fs.readFileSync(automationConfigPath, 'utf8'); + const parsed = JSON.parse(raw); + return parsed && typeof parsed === 'object' ? parsed : { ...defaultAutomationConfig }; + } catch (error) { + return { ...defaultAutomationConfig }; + } +} + +const automationConfig = loadAutomationConfig(); + +let nodemailer = null; +try { + nodemailer = require('nodemailer'); +} catch (error) { + nodemailer = null; +} + // Middleware - Enhanced CORS for extension app.use(cors({ origin: (origin, callback) => { @@ -1292,10 +1346,15 @@ db.exec(` id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, + type TEXT NOT NULL DEFAULT 'request', method TEXT NOT NULL DEFAULT 'GET', - url_template TEXT NOT NULL, + url_template TEXT, headers_json TEXT, body_template TEXT, + email_to TEXT, + email_subject_template TEXT, + email_body_template TEXT, + steps_json TEXT, interval_minutes INTEGER NOT NULL DEFAULT ${AUTOMATION_DEFAULT_INTERVAL_MINUTES}, jitter_minutes INTEGER DEFAULT 0, start_at DATETIME, @@ -1311,6 +1370,12 @@ db.exec(` ); `); +ensureColumn('automation_requests', 'type', 'type TEXT NOT NULL DEFAULT \'request\''); +ensureColumn('automation_requests', 'email_to', 'email_to TEXT'); +ensureColumn('automation_requests', 'email_subject_template', 'email_subject_template TEXT'); +ensureColumn('automation_requests', 'email_body_template', 'email_body_template TEXT'); +ensureColumn('automation_requests', 'steps_json', 'steps_json TEXT'); + db.exec(` CREATE INDEX IF NOT EXISTS idx_automation_requests_next_run ON automation_requests(next_run_at); @@ -1342,10 +1407,15 @@ const listAutomationRequestsStmt = db.prepare(` id, name, description, + type, method, url_template, headers_json, body_template, + email_to, + email_subject_template, + email_body_template, + steps_json, interval_minutes, jitter_minutes, start_at, @@ -1356,6 +1426,11 @@ const listAutomationRequestsStmt = db.prepare(` last_status_code, last_error, next_run_at, + ( + SELECT COUNT(1) + FROM automation_request_runs r + WHERE r.request_id = automation_requests.id + ) AS runs_count, created_at, updated_at FROM automation_requests @@ -1367,10 +1442,15 @@ const getAutomationRequestStmt = db.prepare(` id, name, description, + type, method, url_template, headers_json, body_template, + email_to, + email_subject_template, + email_body_template, + steps_json, interval_minutes, jitter_minutes, start_at, @@ -1389,11 +1469,13 @@ const getAutomationRequestStmt = db.prepare(` const insertAutomationRequestStmt = db.prepare(` INSERT INTO automation_requests ( - id, name, description, 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, interval_minutes, jitter_minutes, start_at, run_until, active, last_run_at, last_status, last_status_code, last_error, next_run_at ) VALUES ( - @id, @name, @description, @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, @interval_minutes, @jitter_minutes, @start_at, @run_until, @active, @last_run_at, @last_status, @last_status_code, @last_error, @next_run_at ) @@ -1403,10 +1485,15 @@ const updateAutomationRequestStmt = db.prepare(` UPDATE automation_requests SET name = @name, description = @description, + type = @type, method = @method, url_template = @url_template, headers_json = @headers_json, body_template = @body_template, + email_to = @email_to, + email_subject_template = @email_subject_template, + email_body_template = @email_body_template, + steps_json = @steps_json, interval_minutes = @interval_minutes, jitter_minutes = @jitter_minutes, start_at = @start_at, @@ -1486,7 +1573,6 @@ ensureColumn('ai_credentials', 'usage_24h_count', 'usage_24h_count INTEGER DEFAU ensureColumn('ai_credentials', 'usage_24h_reset_at', 'usage_24h_reset_at DATETIME'); ensureColumn('search_seen_posts', 'manually_hidden', 'manually_hidden INTEGER NOT NULL DEFAULT 0'); ensureColumn('search_seen_posts', 'sports_auto_hidden', 'sports_auto_hidden INTEGER NOT NULL DEFAULT 0'); - db.exec(` CREATE TABLE IF NOT EXISTS ai_usage_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -1927,6 +2013,14 @@ function renderAutomationTemplate(template, context = {}) { 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': @@ -2082,11 +2176,24 @@ function serializeAutomationRequest(row) { return { id: row.id, name: row.name, + type: row.type || AUTOMATION_TYPE_REQUEST, description: row.description || '', method: row.method || 'GET', url_template: row.url_template, headers, body_template: row.body_template || '', + email_to: row.email_to || '', + email_subject_template: row.email_subject_template || '', + email_body_template: row.email_body_template || '', + steps: row.steps_json ? (() => { + try { + const parsed = JSON.parse(row.steps_json); + return Array.isArray(parsed) ? parsed : []; + } catch (error) { + return []; + } + })() : [], + runs_count: row.runs_count || 0, interval_minutes: clampAutomationIntervalMinutes(row.interval_minutes), jitter_minutes: clampAutomationJitterMinutes(row.jitter_minutes || 0), start_at: row.start_at ? ensureIsoDate(row.start_at) : null, @@ -2124,6 +2231,14 @@ function normalizeAutomationPayload(payload, existing = {}) { const errors = []; const normalized = {}; + const rawType = typeof payload.type === 'string' + ? payload.type.trim().toLowerCase() + : (existing.type || AUTOMATION_TYPE_REQUEST); + const type = [AUTOMATION_TYPE_REQUEST, AUTOMATION_TYPE_EMAIL, AUTOMATION_TYPE_FLOW].includes(rawType) + ? rawType + : AUTOMATION_TYPE_REQUEST; + normalized.type = type; + const nameSource = typeof payload.name === 'string' ? payload.name : existing.name || ''; @@ -2152,11 +2267,17 @@ function normalizeAutomationPayload(payload, existing = {}) { const rawUrl = typeof payload.url_template === 'string' ? payload.url_template : (typeof payload.url === 'string' ? payload.url : existing.url_template || ''); - const urlTemplate = rawUrl.trim(); - if (!urlTemplate) { - errors.push('URL-Template fehlt'); + const urlTemplate = (rawUrl || '').trim(); + + if (type === AUTOMATION_TYPE_REQUEST || type === AUTOMATION_TYPE_FLOW) { + if (!urlTemplate && type === AUTOMATION_TYPE_REQUEST) { + errors.push('URL-Template fehlt'); + } + normalized.url_template = urlTemplate + ? truncateString(urlTemplate, AUTOMATION_MAX_URL_LENGTH) + : null; } else { - normalized.url_template = truncateString(urlTemplate, AUTOMATION_MAX_URL_LENGTH); + normalized.url_template = null; } const headersInput = payload.headers @@ -2184,6 +2305,82 @@ function normalizeAutomationPayload(payload, existing = {}) { : ''; } + if (type === AUTOMATION_TYPE_EMAIL) { + const toValue = (payload.email_to || payload.to || existing.email_to || '').trim(); + if (!toValue) { + errors.push('E-Mail Empfänger fehlt'); + } else { + normalized.email_to = truncateString(toValue, AUTOMATION_MAX_EMAIL_TO_LENGTH); + } + const subjectValue = (payload.email_subject_template || payload.subject || existing.email_subject_template || '').trim(); + if (!subjectValue) { + errors.push('E-Mail Betreff fehlt'); + } else { + normalized.email_subject_template = truncateString(subjectValue, AUTOMATION_MAX_EMAIL_SUBJECT_LENGTH); + } + const bodyValue = typeof payload.email_body_template === 'string' + ? payload.email_body_template + : (typeof payload.body_template === 'string' ? payload.body_template : existing.email_body_template || ''); + if (!bodyValue || !String(bodyValue).trim()) { + errors.push('E-Mail Body fehlt'); + } else { + normalized.email_body_template = truncateString(String(bodyValue), AUTOMATION_MAX_BODY_LENGTH); + } + normalized.steps_json = null; + normalized.url_template = ''; + normalized.headers_json = null; + normalized.body_template = null; + } else if (type === AUTOMATION_TYPE_FLOW) { + const stepsInput = Array.isArray(payload.steps) + ? payload.steps + : (typeof payload.steps_json === 'string' ? (() => { + try { return JSON.parse(payload.steps_json); } catch (err) { return []; } + })() : []); + + const steps = []; + for (const rawStep of stepsInput) { + if (!rawStep || typeof rawStep !== 'object') continue; + const stepUrl = typeof rawStep.url === 'string' ? rawStep.url.trim() : ''; + if (!stepUrl) continue; + const stepMethod = typeof rawStep.method === 'string' + ? rawStep.method.toUpperCase() + : 'GET'; + const allowed = allowedMethods.includes(stepMethod) ? stepMethod : 'GET'; + const stepHeaders = normalizeAutomationHeaders(rawStep.headers || rawStep.headers_json || {}); + const stepHeadersSerialized = serializeAutomationHeaders(stepHeaders); + steps.push({ + method: allowed, + url: truncateString(stepUrl, AUTOMATION_MAX_URL_LENGTH), + headers: stepHeadersSerialized ? JSON.parse(stepHeadersSerialized) : {}, + body: typeof rawStep.body === 'string' + ? truncateString(rawStep.body, AUTOMATION_MAX_BODY_LENGTH) + : '' + }); + if (steps.length >= AUTOMATION_MAX_STEPS) break; + } + + if (!steps.length) { + errors.push('Mindestens ein Schritt mit URL ist erforderlich'); + } else { + try { + const serializedSteps = JSON.stringify(steps); + normalized.steps_json = serializedSteps; + } catch (error) { + errors.push('Schritte konnten nicht gespeichert werden'); + } + } + + normalized.email_to = null; + normalized.email_subject_template = null; + normalized.email_body_template = null; + normalized.url_template = normalized.url_template || ''; + } else { + normalized.email_to = null; + normalized.email_subject_template = null; + normalized.email_body_template = null; + normalized.steps_json = null; + } + const scheduleType = typeof payload.schedule_type === 'string' ? payload.schedule_type : payload.interval_type; @@ -3173,6 +3370,14 @@ function broadcastPostDeletion(postId, options = {}) { broadcastSseEvent(payload); } +function broadcastAutomationEvent(eventType, payload = {}) { + if (!eventType) { + return; + } + const data = { type: eventType, ...payload }; + broadcastSseEvent(data); +} + const automationRunningRequests = new Set(); let automationWorkerTimer = null; let automationWorkerBusy = false; @@ -3196,36 +3401,187 @@ async function executeAutomationRequest(request, options = {}) { automationRunningRequests.add(current.id); const context = buildAutomationTemplateContext(new Date()); - const method = (current.method || 'GET').toUpperCase(); - const url = renderAutomationTemplate(current.url_template, context); - const headers = renderAutomationHeaders(current.headers_json, context); - const shouldSendBody = !['GET', 'HEAD'].includes(method); - const body = shouldSendBody && current.body_template - ? renderAutomationTemplate(current.body_template, context) - : null; - const startedAt = new Date(); let status = 'success'; let statusCode = null; let responseText = ''; let errorMessage = null; + const stepResults = []; + + async function executeHttpStep(step, stepIndex, inheritedContext) { + const method = (step.method || 'GET').toUpperCase(); + const url = renderAutomationTemplate(step.url_template || step.url, inheritedContext); + const headers = renderAutomationHeaders(step.headers_json || serializeAutomationHeaders(step.headers || {}), inheritedContext); + const shouldSendBody = !['GET', 'HEAD'].includes(method); + const body = shouldSendBody && (step.body_template || step.body) + ? renderAutomationTemplate(step.body_template || step.body, inheritedContext) + : null; + + let localStatus = 'success'; + let localStatusCode = null; + let localResponseText = ''; + let localError = null; + + try { + const response = await fetch(url, { + method, + headers, + body: shouldSendBody && body !== null ? body : undefined, + redirect: 'follow' + }); + localStatusCode = response.status; + try { + localResponseText = await response.text(); + } catch (error) { + localResponseText = ''; + } + if (!response.ok) { + localStatus = 'error'; + localError = `HTTP ${response.status}`; + } + } catch (error) { + localStatus = 'error'; + localError = error.message || 'Unbekannter Fehler'; + } + + let parsedJson = null; + if (localResponseText) { + try { + parsedJson = JSON.parse(localResponseText); + } catch (error) { + parsedJson = null; + } + } + + const resultContext = { + ...inheritedContext, + [`step${stepIndex}_text`]: localResponseText, + [`step${stepIndex}_status`]: localStatus, + [`step${stepIndex}_status_code`]: localStatusCode, + [`step${stepIndex}_json`]: parsedJson || null + }; + + return { + status: localStatus, + statusCode: localStatusCode, + responseText: localResponseText, + error: localError, + context: resultContext + }; + } + + async function executeEmailStep(stepContext) { + if (!nodemailer) { + throw new Error('E-Mail Versand nicht verfügbar (nodemailer fehlt)'); + } + const { + SMTP_HOST, + SMTP_PORT, + SMTP_USER, + SMTP_PASS, + SMTP_SECURE, + SMTP_FROM + } = process.env; + + const configSmtp = (automationConfig && automationConfig.smtp) || {}; + + const host = SMTP_HOST || configSmtp.host; + if (!host) { + throw new Error('SMTP_HOST nicht gesetzt und keine Konfiguration in automation-config.json'); + } + + const port = SMTP_PORT ? Number(SMTP_PORT) : (configSmtp.port || 587); + const secure = SMTP_SECURE === 'true' || SMTP_SECURE === '1' + ? true + : (typeof configSmtp.secure === 'boolean' ? configSmtp.secure : port === 465); + const user = SMTP_USER || configSmtp.user || ''; + const pass = SMTP_PASS || configSmtp.pass || ''; + const from = SMTP_FROM || configSmtp.from || user || 'automation@example.com'; + + const transporter = nodemailer.createTransport({ + host, + port, + secure, + auth: user ? { user, pass } : undefined + }); + + const to = renderAutomationTemplate(current.email_to || '', stepContext); + const subject = renderAutomationTemplate(current.email_subject_template || '', stepContext); + const body = renderAutomationTemplate(current.email_body_template || '', stepContext); + const bodyHtml = body ? body.replace(/\n/g, '
') : ''; + + if (!to || !subject || !body) { + throw new Error('E-Mail Felder unvollständig'); + } + + const info = await transporter.sendMail({ + from, + to, + subject, + text: body, + html: bodyHtml || body + }); + + return { + status: 'success', + statusCode: info && info.accepted ? 200 : null, + responseText: info ? JSON.stringify({ messageId: info.messageId }) : '', + error: null, + context: { ...stepContext } + }; + } try { - const response = await fetch(url, { - method, - headers, - body: shouldSendBody && body !== null ? body : undefined, - redirect: 'follow' - }); - statusCode = response.status; - try { - responseText = await response.text(); - } catch (error) { - responseText = ''; - } - if (!response.ok) { - status = 'error'; - errorMessage = `HTTP ${response.status}`; + if (current.type === AUTOMATION_TYPE_EMAIL) { + const emailResult = await executeEmailStep(context); + status = emailResult.status; + statusCode = emailResult.statusCode; + responseText = emailResult.responseText; + errorMessage = emailResult.error; + } else if (current.type === AUTOMATION_TYPE_FLOW) { + let flowContext = { ...context }; + const steps = current.steps_json ? (() => { + try { + const parsed = JSON.parse(current.steps_json); + return Array.isArray(parsed) ? parsed : []; + } catch (error) { + return []; + } + })() : []; + let stepIndex = 1; + for (const step of steps) { + const stepResult = await executeHttpStep({ + method: step.method || step.http_method || 'GET', + url_template: step.url_template || step.url, + headers_json: serializeAutomationHeaders(step.headers || {}), + body_template: step.body_template || step.body + }, stepIndex, flowContext); + stepResults.push(stepResult); + flowContext = { ...flowContext, ...stepResult.context }; + if (stepResult.status === 'error') { + status = 'error'; + errorMessage = stepResult.error; + statusCode = stepResult.statusCode; + responseText = stepResult.responseText; + break; + } else { + statusCode = stepResult.statusCode; + responseText = stepResult.responseText; + } + stepIndex += 1; + } + } else { + const result = await executeHttpStep({ + method: current.method, + url_template: current.url_template, + headers_json: current.headers_json, + body_template: current.body_template + }, 1, context); + status = result.status; + statusCode = result.statusCode; + responseText = result.responseText; + errorMessage = result.error; + stepResults.push(result); } } catch (error) { status = 'error'; @@ -3259,6 +3615,15 @@ async function executeAutomationRequest(request, options = {}) { ) : null; + broadcastAutomationEvent('automation-run', { + request_id: current.id, + status, + status_code: statusCode, + next_run_at: nextRunAt, + last_run_at: runRecord.started_at, + runs_count: (current.runs_count || 0) + 1 + }); + try { updateAutomationRequestStmt.run({ ...current, diff --git a/web/Dockerfile b/web/Dockerfile index f63986d..7c869cb 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -17,6 +17,7 @@ COPY dashboard.js /usr/share/nginx/html/ COPY settings.js /usr/share/nginx/html/ COPY daily-bookmarks.js /usr/share/nginx/html/ COPY automation.js /usr/share/nginx/html/ +COPY vendor /usr/share/nginx/html/vendor/ COPY assets /usr/share/nginx/html/assets/ EXPOSE 80 diff --git a/web/automation.css b/web/automation.css index d9b4bf2..e0ed921 100644 --- a/web/automation.css +++ b/web/automation.css @@ -28,7 +28,7 @@ body { } .auto-shell { - max-width: 1300px; + max-width: 1500px; margin: 0 auto; display: flex; flex-direction: column; @@ -177,6 +177,9 @@ body { .field.inline { min-width: 0; } +.field[data-section] { + display: grid; +} label { font-weight: 600; @@ -257,6 +260,7 @@ small { text-align: left; border-bottom: 1px solid var(--border); font-size: 14px; + white-space: nowrap; } .auto-table th { @@ -264,6 +268,48 @@ small { font-weight: 600; } +.auto-table th[data-sort-column="runs"], +.auto-table td.runs-count { + width: 1%; + white-space: nowrap; + text-align: right; +} + +.auto-table .sort-indicator { + display: inline-block; + margin-left: 6px; + width: 10px; + height: 10px; + border: 5px solid transparent; + border-bottom: 0; + border-left: 0; + transform: rotate(45deg); + opacity: 0.35; +} + +.auto-table th.sort-asc .sort-indicator { + border-top: 6px solid var(--accent-2); + transform: rotate(225deg); + opacity: 0.9; +} + +.auto-table th.sort-desc .sort-indicator { + border-top: 6px solid var(--accent-2); + transform: rotate(45deg); + opacity: 0.9; +} + +.table-filter-row input, +.table-filter-row select { + width: 100%; + border-radius: 8px; + border: 1px solid var(--border); + padding: 8px 10px; + background: #0c1427; + color: var(--text); + font-size: 13px; +} + .auto-table tr.is-selected { background: rgba(14, 165, 233, 0.08); } @@ -307,7 +353,34 @@ small { .row-actions { display: flex; gap: 6px; - flex-wrap: wrap; + flex-wrap: nowrap; +} + +.hidden-value { + display: none; +} + +.icon-btn { + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.04); + color: var(--text); + border-radius: 8px; + padding: 6px 8px; + cursor: pointer; + font-size: 14px; + line-height: 1; + transition: transform 0.15s ease, opacity 0.15s ease, border-color 0.15s ease; +} + +.icon-btn:hover { + transform: translateY(-1px); + opacity: 0.95; + border-color: rgba(255, 255, 255, 0.2); +} + +.icon-btn.danger { + color: #ef4444; + border-color: rgba(239, 68, 68, 0.35); } .primary-btn, @@ -355,6 +428,15 @@ small { margin-bottom: 8px; } +.filter-input { + border-radius: 10px; + border: 1px solid var(--border); + padding: 8px 10px; + background: #0c1427; + color: var(--text); + min-width: 200px; +} + .runs-list { list-style: none; padding: 0; @@ -551,6 +633,32 @@ small { font-size: 13px; } +.placeholder-table { + width: 100%; + border-collapse: collapse; + margin-top: 6px; +} + +.placeholder-table td { + padding: 6px 8px; + border-bottom: 1px solid var(--border); + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + color: var(--text); + vertical-align: top; +} + +.placeholder-table td.placeholder-key { + width: 30%; + color: var(--muted); +} + +.placeholder-hint { + color: var(--muted); + font-size: 12px; + margin: 6px 0 0; +} + @media (max-width: 1050px) { .auto-grid { grid-template-columns: 1fr; diff --git a/web/automation.html b/web/automation.html index ae1a535..484efd1 100644 --- a/web/automation.html +++ b/web/automation.html @@ -18,15 +18,8 @@ ← Zurück zur App

Automatisierte Requests

Request Automationen

-

Plane HTTP-Requests mit Zeitplan, Varianz und Variablen-Templates.

-
- Jitter & Intervalle - Import aus cURL / fetch - Platzhalter {{date}} {{uuid}} -
-
@@ -42,22 +35,38 @@

Automationen

+
-
+
- - - - + + + + + + + + + + + + + - +
NameNächster LaufLetzter LaufStatusNameNächster LaufLetzter LaufStatus#Läufe Aktionen
+ +
@@ -87,6 +96,14 @@
+
+ + +
@@ -95,11 +112,11 @@
-
+
-
+
-
+
+
+ + +
+
+ + +
+
+ + +
+
+
+

Flow Schritte

+

Max. 3 Schritte, Kontext steht als {{step1_json}}, {{step1_text}}, {{step1_status_code}} usw. im nächsten Schritt zur Verfügung.

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +