aktueller stand

This commit is contained in:
2025-12-15 12:14:59 +01:00
parent c63955f8a5
commit 6c83e4a7ee
6 changed files with 2544 additions and 0 deletions

View File

@@ -30,6 +30,16 @@ 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_MAX_NAME_LENGTH = 160;
const AUTOMATION_MAX_URL_LENGTH = 2000;
const AUTOMATION_MAX_BODY_LENGTH = 12000;
const AUTOMATION_MAX_HEADERS_LENGTH = 6000;
const AUTOMATION_MIN_INTERVAL_MINUTES = 5;
const AUTOMATION_MAX_INTERVAL_MINUTES = 60 * 24 * 14; // 2 Wochen
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 SPORTS_SCORING_DEFAULTS = {
enabled: 1,
threshold: 5,
@@ -1277,6 +1287,178 @@ const deleteDailyBookmarkCheckStmt = db.prepare(`
AND day_key = @dayKey
`);
db.exec(`
CREATE TABLE IF NOT EXISTS automation_requests (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
method TEXT NOT NULL DEFAULT 'GET',
url_template TEXT NOT NULL,
headers_json TEXT,
body_template TEXT,
interval_minutes INTEGER NOT NULL DEFAULT ${AUTOMATION_DEFAULT_INTERVAL_MINUTES},
jitter_minutes INTEGER DEFAULT 0,
start_at DATETIME,
run_until DATETIME,
active INTEGER NOT NULL DEFAULT 1,
last_run_at DATETIME,
last_status TEXT,
last_status_code INTEGER,
last_error TEXT,
next_run_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_automation_requests_next_run
ON automation_requests(next_run_at);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS automation_request_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
request_id TEXT NOT NULL,
trigger TEXT DEFAULT 'schedule',
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME,
status TEXT,
status_code INTEGER,
error TEXT,
response_body TEXT,
duration_ms INTEGER,
FOREIGN KEY (request_id) REFERENCES automation_requests(id) ON DELETE CASCADE
);
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_automation_request_runs_request
ON automation_request_runs(request_id, started_at DESC);
`);
const listAutomationRequestsStmt = db.prepare(`
SELECT
id,
name,
description,
method,
url_template,
headers_json,
body_template,
interval_minutes,
jitter_minutes,
start_at,
run_until,
active,
last_run_at,
last_status,
last_status_code,
last_error,
next_run_at,
created_at,
updated_at
FROM automation_requests
ORDER BY datetime(updated_at) DESC, datetime(created_at) DESC, name COLLATE NOCASE
`);
const getAutomationRequestStmt = db.prepare(`
SELECT
id,
name,
description,
method,
url_template,
headers_json,
body_template,
interval_minutes,
jitter_minutes,
start_at,
run_until,
active,
last_run_at,
last_status,
last_status_code,
last_error,
next_run_at,
created_at,
updated_at
FROM automation_requests
WHERE id = ?
`);
const insertAutomationRequestStmt = db.prepare(`
INSERT INTO automation_requests (
id, name, description, method, url_template, headers_json, body_template,
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,
@interval_minutes, @jitter_minutes, @start_at, @run_until, @active, @last_run_at,
@last_status, @last_status_code, @last_error, @next_run_at
)
`);
const updateAutomationRequestStmt = db.prepare(`
UPDATE automation_requests
SET name = @name,
description = @description,
method = @method,
url_template = @url_template,
headers_json = @headers_json,
body_template = @body_template,
interval_minutes = @interval_minutes,
jitter_minutes = @jitter_minutes,
start_at = @start_at,
run_until = @run_until,
active = @active,
last_run_at = @last_run_at,
last_status = @last_status,
last_status_code = @last_status_code,
last_error = @last_error,
next_run_at = @next_run_at,
updated_at = CURRENT_TIMESTAMP
WHERE id = @id
`);
const deleteAutomationRequestStmt = db.prepare('DELETE FROM automation_requests WHERE id = ?');
const listAutomationRunsStmt = db.prepare(`
SELECT
id,
request_id,
trigger,
started_at,
completed_at,
status,
status_code,
error,
response_body,
duration_ms
FROM automation_request_runs
WHERE request_id = @requestId
ORDER BY datetime(started_at) DESC
LIMIT @limit
`);
const insertAutomationRunStmt = db.prepare(`
INSERT INTO automation_request_runs (
request_id, trigger, started_at, completed_at, status, status_code, error, response_body, duration_ms
) VALUES (
@request_id, @trigger, @started_at, @completed_at, @status, @status_code, @error, @response_body, @duration_ms
)
`);
const listDueAutomationRequestsStmt = db.prepare(`
SELECT *
FROM automation_requests
WHERE active = 1
AND next_run_at IS NOT NULL
AND datetime(next_run_at) <= datetime(@now)
ORDER BY datetime(next_run_at) ASC
LIMIT 10
`);
ensureColumn('posts', 'checked_count', 'checked_count INTEGER DEFAULT 0');
ensureColumn('posts', 'screenshot_path', 'screenshot_path TEXT');
ensureColumn('posts', 'created_by_profile', 'created_by_profile INTEGER');
@@ -1613,6 +1795,424 @@ function determineAutoDisable(error) {
};
}
function parseAutomationDate(input) {
if (!input && input !== 0) {
return null;
}
if (input === 'now') {
return new Date().toISOString();
}
if (input instanceof Date) {
const time = input.getTime();
return Number.isNaN(time) ? null : input.toISOString();
}
const date = new Date(input);
return Number.isNaN(date.getTime()) ? null : date.toISOString();
}
function clampAutomationIntervalMinutes(value) {
const numeric = Number(value);
if (!Number.isFinite(numeric) || numeric <= 0) {
return AUTOMATION_DEFAULT_INTERVAL_MINUTES;
}
return Math.max(
AUTOMATION_MIN_INTERVAL_MINUTES,
Math.min(AUTOMATION_MAX_INTERVAL_MINUTES, Math.round(numeric))
);
}
function clampAutomationJitterMinutes(value) {
const numeric = Number(value);
if (!Number.isFinite(numeric) || numeric < 0) {
return 0;
}
return Math.min(AUTOMATION_MAX_JITTER_MINUTES, Math.round(numeric));
}
function normalizeAutomationHeaders(raw) {
if (!raw) {
return {};
}
let source = raw;
if (typeof raw === 'string') {
const trimmed = raw.trim();
if (!trimmed) {
return {};
}
try {
source = JSON.parse(trimmed);
} catch (error) {
source = trimmed.split('\n').reduce((acc, line) => {
const idx = line.indexOf(':');
if (idx === -1) {
return acc;
}
const key = line.slice(0, idx).trim();
const value = line.slice(idx + 1).trim();
if (key) {
acc[key] = value;
}
return acc;
}, {});
}
}
if (!source || typeof source !== 'object') {
return {};
}
const headers = {};
for (const [key, value] of Object.entries(source)) {
if (!key) {
continue;
}
const normalizedKey = String(key).trim();
if (!normalizedKey) {
continue;
}
let normalizedValue;
if (value === undefined || value === null) {
normalizedValue = '';
} else if (typeof value === 'string') {
normalizedValue = value.trim();
} else if (typeof value === 'number' || typeof value === 'boolean') {
normalizedValue = String(value);
} else {
try {
normalizedValue = JSON.stringify(value);
} catch (error) {
normalizedValue = '';
}
}
headers[normalizedKey] = truncateString(normalizedValue, 1000);
}
return headers;
}
function serializeAutomationHeaders(headers) {
if (!headers || typeof headers !== 'object' || !Object.keys(headers).length) {
return null;
}
try {
const serialized = JSON.stringify(headers);
return serialized.length > AUTOMATION_MAX_HEADERS_LENGTH ? null : serialized;
} catch (error) {
return null;
}
}
function renderAutomationTemplate(template, context = {}) {
if (typeof template !== 'string') {
return '';
}
const baseDate = context.now instanceof Date ? context.now : new Date();
return template.replace(/\{\{\s*([^}\s]+)\s*\}\}/g, (_, keyRaw) => {
const key = String(keyRaw || '').trim();
if (!key) {
return '';
}
if (key === 'uuid') {
return uuidv4();
}
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);
}
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 buildAutomationTemplateContext(baseDate = new Date()) {
const now = baseDate instanceof Date ? baseDate : 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 renderAutomationHeaders(headersJson, context) {
if (!headersJson) {
return {};
}
let parsed = {};
try {
parsed = JSON.parse(headersJson);
} catch (error) {
parsed = {};
}
if (!parsed || typeof parsed !== 'object') {
return {};
}
const rendered = {};
for (const [key, value] of Object.entries(parsed)) {
const renderedKey = renderAutomationTemplate(key, context).trim();
if (!renderedKey) {
continue;
}
const renderedValue = renderAutomationTemplate(
value === undefined || value === null ? '' : String(value),
context
);
rendered[renderedKey] = renderedValue;
}
return rendered;
}
function computeNextAutomationRun(request, options = {}) {
if (!request) {
return null;
}
const now = options.fromDate instanceof Date ? options.fromDate : new Date();
const intervalMinutes = clampAutomationIntervalMinutes(
request.interval_minutes || AUTOMATION_DEFAULT_INTERVAL_MINUTES
);
const jitterMinutes = clampAutomationJitterMinutes(request.jitter_minutes || 0);
const lastRun = request.last_run_at ? new Date(request.last_run_at) : null;
const startAt = request.start_at ? new Date(request.start_at) : null;
let base = lastRun
? new Date(lastRun.getTime() + intervalMinutes * 60000)
: (startAt ? new Date(startAt) : new Date(now.getTime() + intervalMinutes * 60000));
if (base < now) {
base = new Date(now.getTime() + intervalMinutes * 60000);
}
const jitterMs = jitterMinutes > 0
? Math.floor(Math.random() * ((jitterMinutes * 60000) + 1))
: 0;
const candidate = new Date(base.getTime() + jitterMs);
if (request.run_until) {
const until = new Date(request.run_until);
if (!Number.isNaN(until.getTime()) && candidate > until) {
return null;
}
}
return candidate.toISOString();
}
function ensureNextRunForRequest(request, options = {}) {
if (!request || !request.id || !request.active) {
return null;
}
const existingNext = request.next_run_at;
if (existingNext) {
return existingNext;
}
const next = computeNextAutomationRun(request, options);
if (!next) {
return null;
}
try {
updateAutomationRequestStmt.run({
...request,
next_run_at: next
});
} catch (error) {
console.warn(`Failed to persist next_run_at for ${request.id}:`, error.message);
}
return next;
}
function serializeAutomationRequest(row) {
if (!row) {
return null;
}
let headers = {};
if (row.headers_json) {
try {
const parsed = JSON.parse(row.headers_json);
if (parsed && typeof parsed === 'object') {
headers = parsed;
}
} catch (error) {
headers = {};
}
}
return {
id: row.id,
name: row.name,
description: row.description || '',
method: row.method || 'GET',
url_template: row.url_template,
headers,
body_template: row.body_template || '',
interval_minutes: clampAutomationIntervalMinutes(row.interval_minutes),
jitter_minutes: clampAutomationJitterMinutes(row.jitter_minutes || 0),
start_at: row.start_at ? ensureIsoDate(row.start_at) : null,
run_until: row.run_until ? ensureIsoDate(row.run_until) : null,
active: row.active ? 1 : 0,
last_run_at: row.last_run_at ? ensureIsoDate(row.last_run_at) : null,
last_status: row.last_status || null,
last_status_code: row.last_status_code || null,
last_error: row.last_error || null,
next_run_at: row.next_run_at ? ensureIsoDate(row.next_run_at) : null,
created_at: row.created_at ? ensureIsoDate(row.created_at) : null,
updated_at: row.updated_at ? ensureIsoDate(row.updated_at) : null
};
}
function serializeAutomationRun(row) {
if (!row) {
return null;
}
return {
id: row.id,
request_id: row.request_id,
trigger: row.trigger || 'schedule',
started_at: row.started_at ? ensureIsoDate(row.started_at) : null,
completed_at: row.completed_at ? ensureIsoDate(row.completed_at) : null,
status: row.status || null,
status_code: row.status_code || null,
error: row.error || null,
response_body: row.response_body || '',
duration_ms: row.duration_ms || null
};
}
function normalizeAutomationPayload(payload, existing = {}) {
const errors = [];
const normalized = {};
const nameSource = typeof payload.name === 'string'
? payload.name
: existing.name || '';
const name = nameSource.trim();
if (!name) {
errors.push('Name ist erforderlich');
} else {
normalized.name = truncateString(name, AUTOMATION_MAX_NAME_LENGTH);
}
const descriptionSource = typeof payload.description === 'string'
? payload.description.trim()
: (existing.description || '');
normalized.description = truncateString(descriptionSource || '', 800);
const rawMethod = typeof payload.method === 'string'
? payload.method.trim().toUpperCase()
: (existing.method || 'GET');
const allowedMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
if (!allowedMethods.includes(rawMethod)) {
errors.push('Ungültige HTTP-Methode');
} else {
normalized.method = rawMethod;
}
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');
} else {
normalized.url_template = truncateString(urlTemplate, AUTOMATION_MAX_URL_LENGTH);
}
const headersInput = payload.headers
|| payload.headers_json
|| payload.headersText
|| null;
if (headersInput) {
const parsedHeaders = normalizeAutomationHeaders(headersInput);
const serializedHeaders = serializeAutomationHeaders(parsedHeaders);
if (serializedHeaders === null && Object.keys(parsedHeaders).length) {
errors.push('Headers sind zu groß oder ungültig');
}
normalized.headers_json = serializedHeaders;
} else {
normalized.headers_json = existing.headers_json || null;
}
if (typeof payload.body_template === 'string') {
normalized.body_template = truncateString(payload.body_template, AUTOMATION_MAX_BODY_LENGTH);
} else if (typeof payload.body === 'string') {
normalized.body_template = truncateString(payload.body, AUTOMATION_MAX_BODY_LENGTH);
} else {
normalized.body_template = typeof existing.body_template === 'string'
? existing.body_template
: '';
}
const scheduleType = typeof payload.schedule_type === 'string'
? payload.schedule_type
: payload.interval_type;
let intervalMinutes;
if (scheduleType === 'daily') {
intervalMinutes = 24 * 60;
} else if (scheduleType === 'hourly') {
intervalMinutes = 60;
} else {
intervalMinutes = payload.interval_minutes
?? payload.every_minutes
?? existing.interval_minutes
?? AUTOMATION_DEFAULT_INTERVAL_MINUTES;
}
normalized.interval_minutes = clampAutomationIntervalMinutes(intervalMinutes);
normalized.jitter_minutes = clampAutomationJitterMinutes(
payload.jitter_minutes ?? payload.variance_minutes ?? existing.jitter_minutes ?? 0
);
normalized.start_at = parseAutomationDate(payload.start_at) || parseAutomationDate(existing.start_at);
normalized.run_until = parseAutomationDate(payload.run_until) || null;
normalized.active = payload.active === undefined || payload.active === null
? (existing.active ? 1 : 0)
: (payload.active ? 1 : 0);
return { data: normalized, errors };
}
function recordAIUsageEvent(credentialId, eventType, options = {}) {
if (!credentialId || !eventType) {
return;
@@ -2573,6 +3173,313 @@ function broadcastPostDeletion(postId, options = {}) {
broadcastSseEvent(payload);
}
const automationRunningRequests = new Set();
let automationWorkerTimer = null;
let automationWorkerBusy = false;
async function executeAutomationRequest(request, options = {}) {
if (!request || !request.id) {
return null;
}
const trigger = options.trigger || 'schedule';
const allowInactive = !!options.allowInactive;
const current = getAutomationRequestStmt.get(request.id);
if (!current || (!current.active && !allowInactive)) {
return null;
}
if (automationRunningRequests.has(current.id)) {
return null;
}
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;
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}`;
}
} catch (error) {
status = 'error';
errorMessage = error.message || 'Unbekannter Fehler';
}
const completedAt = new Date();
const runRecord = {
request_id: current.id,
trigger,
started_at: startedAt.toISOString(),
completed_at: completedAt.toISOString(),
status,
status_code: statusCode,
error: truncateString(errorMessage, 900),
response_body: truncateString(responseText, AUTOMATION_MAX_RESPONSE_PREVIEW),
duration_ms: Math.max(0, completedAt.getTime() - startedAt.getTime())
};
try {
insertAutomationRunStmt.run(runRecord);
} catch (error) {
console.warn('Failed to persist automation run:', error.message);
}
const nextRunAt = current.active
? computeNextAutomationRun(
{ ...current, last_run_at: runRecord.started_at },
{ fromDate: completedAt }
)
: null;
try {
updateAutomationRequestStmt.run({
...current,
last_run_at: runRecord.started_at,
last_status: status,
last_status_code: statusCode,
last_error: runRecord.error,
next_run_at: nextRunAt
});
} catch (error) {
console.warn(`Failed to update automation request ${current.id}:`, error.message);
} finally {
automationRunningRequests.delete(current.id);
}
return runRecord;
}
async function processAutomationQueue() {
if (automationWorkerBusy) {
return;
}
automationWorkerBusy = true;
try {
const nowIso = new Date().toISOString();
const dueRequests = listDueAutomationRequestsStmt.all({ now: nowIso }) || [];
for (const item of dueRequests) {
await executeAutomationRequest(item, { trigger: 'schedule' });
}
} catch (error) {
console.error('Automation worker failed:', error);
} finally {
automationWorkerBusy = false;
}
}
function startAutomationWorker() {
if (automationWorkerTimer) {
return;
}
automationWorkerTimer = setInterval(() => {
processAutomationQueue().catch((error) => {
console.error('Automation queue tick failed:', error);
});
}, AUTOMATION_WORKER_INTERVAL_MS);
processAutomationQueue().catch((error) => {
console.error('Automation initial run failed:', error);
});
}
app.get('/api/automation/requests', (req, res) => {
try {
const rows = listAutomationRequestsStmt.all();
const hydrated = rows.map((row) => {
ensureNextRunForRequest(row, { fromDate: new Date() });
return serializeAutomationRequest(row);
});
res.json(hydrated);
} catch (error) {
console.error('Failed to load automation requests:', error);
res.status(500).json({ error: 'Automationen konnten nicht geladen werden' });
}
});
app.get('/api/automation/requests/:requestId', (req, res) => {
const { requestId } = req.params;
try {
const row = getAutomationRequestStmt.get(requestId);
if (!row) {
return res.status(404).json({ error: 'Automation nicht gefunden' });
}
const payload = serializeAutomationRequest(row);
ensureNextRunForRequest(row, { fromDate: new Date() });
if (req.query && req.query.includeRuns) {
const limit = 10;
const runs = listAutomationRunsStmt.all({ requestId, limit }).map(serializeAutomationRun);
payload.runs = runs;
}
res.json(payload);
} catch (error) {
console.error('Failed to load automation request:', error);
res.status(500).json({ error: 'Automation konnte nicht geladen werden' });
}
});
app.get('/api/automation/requests/:requestId/runs', (req, res) => {
const { requestId } = req.params;
const limit = Math.max(1, Math.min(200, parseInt(req.query?.limit, 10) || 30));
try {
const rows = listAutomationRunsStmt.all({ requestId, limit });
res.json(rows.map(serializeAutomationRun));
} catch (error) {
console.error('Failed to load automation runs:', error);
res.status(500).json({ error: 'Runs konnten nicht geladen werden' });
}
});
app.post('/api/automation/requests', (req, res) => {
const payload = req.body || {};
const { data, errors } = normalizeAutomationPayload(payload, {});
if (errors.length) {
return res.status(400).json({ error: errors[0], details: errors });
}
try {
const id = uuidv4();
let nextRunAt = data.active
? computeNextAutomationRun({ ...data, last_run_at: null }, { fromDate: new Date() })
: null;
insertAutomationRequestStmt.run({
...data,
id,
last_run_at: null,
last_status: null,
last_status_code: null,
last_error: null,
next_run_at: nextRunAt
});
let saved = getAutomationRequestStmt.get(id);
if (data.active && (!saved || !saved.next_run_at)) {
const ensured = ensureNextRunForRequest(saved || data, { fromDate: new Date() });
if (ensured) {
saved = getAutomationRequestStmt.get(id);
}
}
res.status(201).json(serializeAutomationRequest(saved));
} catch (error) {
console.error('Failed to create automation:', error);
res.status(500).json({ error: 'Automation konnte nicht erstellt werden' });
}
});
app.put('/api/automation/requests/:requestId', (req, res) => {
const { requestId } = req.params;
const existing = getAutomationRequestStmt.get(requestId);
if (!existing) {
return res.status(404).json({ error: 'Automation nicht gefunden' });
}
const { data, errors } = normalizeAutomationPayload(req.body || {}, existing);
if (errors.length) {
return res.status(400).json({ error: errors[0], details: errors });
}
const merged = {
...existing,
...data,
last_run_at: existing.last_run_at,
last_status: existing.last_status,
last_status_code: existing.last_status_code,
last_error: existing.last_error
};
merged.next_run_at = merged.active
? computeNextAutomationRun(merged, { fromDate: new Date() })
: null;
try {
updateAutomationRequestStmt.run(merged);
let saved = getAutomationRequestStmt.get(requestId);
if (merged.active && (!saved || !saved.next_run_at)) {
const ensured = ensureNextRunForRequest(saved || merged, { fromDate: new Date() });
if (ensured) {
saved = getAutomationRequestStmt.get(requestId);
}
}
res.json(serializeAutomationRequest(saved));
} catch (error) {
console.error('Failed to update automation:', error);
res.status(500).json({ error: 'Automation konnte nicht aktualisiert werden' });
}
});
app.post('/api/automation/requests/:requestId/run', async (req, res) => {
const { requestId } = req.params;
const existing = getAutomationRequestStmt.get(requestId);
if (!existing) {
return res.status(404).json({ error: 'Automation nicht gefunden' });
}
try {
const run = await executeAutomationRequest(existing, {
trigger: 'manual',
allowInactive: true
});
const refreshed = getAutomationRequestStmt.get(requestId);
res.json({
request: serializeAutomationRequest(refreshed),
run: serializeAutomationRun(run)
});
} catch (error) {
console.error('Failed to trigger automation run:', error);
res.status(500).json({ error: 'Automation-Run konnte nicht gestartet werden' });
}
});
app.delete('/api/automation/requests/:requestId', (req, res) => {
const { requestId } = req.params;
try {
const result = deleteAutomationRequestStmt.run(requestId);
if (!result.changes) {
return res.status(404).json({ error: 'Automation nicht gefunden' });
}
res.status(204).send();
} catch (error) {
console.error('Failed to delete automation:', error);
res.status(500).json({ error: 'Automation konnte nicht gelöscht werden' });
}
});
app.get('/api/bookmarks', (req, res) => {
try {
const rows = listBookmarksStmt.all();
@@ -4726,6 +5633,8 @@ app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
startAutomationWorker();
app.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on port ${PORT}`);
});