// ── Cronjob Page ── registerPage('cronjob', { async init(params) { const isEdit = params && params.id; const title = document.getElementById('cronjobPageTitle'); const nameInput = document.getElementById('cronjobName'); const sessionSelect = document.getElementById('cronjobSession'); const sessionCard = document.getElementById('cronjobSessionCard'); const cronInput = document.getElementById('cronjobCron'); const cronPreview = document.getElementById('cronjobCronPreview'); const promptInput = document.getElementById('cronjobPrompt'); const notifySelect = document.getElementById('cronjobNotifySession'); const notifyCard = document.getElementById('cronjobNotifySessionCard'); const enabledCheckbox = document.getElementById('cronjobEnabled'); const statusBox = document.getElementById('cronjobStatusBox'); const saveBtn = document.getElementById('cronjobSaveBtn'); const runBtn = document.getElementById('cronjobRunBtn'); const deleteBtn = document.getElementById('cronjobDeleteBtn'); // Sessions + Agents laden const sessions = await apiGetSessions(); const agents = await apiGetAvailableAgents(); const agentMap = {}; for (const a of agents) agentMap[a.agentId] = a; const sessionMap = {}; for (const s of sessions) sessionMap[s.sessionId] = s; // Session-Card rendern function renderSessionSelectCard(cardEl, session, emptyIcon, emptyText) { const inner = session ? buildSessionCard(session, session.agentId ? agentMap[session.agentId] : null) : null; _renderCardPreview(cardEl, inner, emptyIcon, emptyText, 'session-select-change'); } // Session-Card click → Picker (nur Sessions mit Agent) sessionCard.addEventListener('click', () => { openSessionPickerModal(sessions, agentMap, sessionSelect.value, (selectedId) => { sessionSelect.value = selectedId || ''; renderSessionSelectCard(sessionCard, selectedId ? sessionMap[selectedId] : null, 'fa-comment-o', 'Keine Session ausgewählt'); }, { filterWithAgent: true, title: 'Session auswählen' }); }); // Notify-Session-Card click → Picker (alle Sessions) notifyCard.addEventListener('click', () => { openSessionPickerModal(sessions, agentMap, notifySelect.value, (selectedId) => { notifySelect.value = selectedId || ''; renderSessionSelectCard(notifyCard, selectedId ? sessionMap[selectedId] : null, 'fa-bell-o', 'Keine (Antwort bleibt in Arbeits-Session)'); }, { filterWithAgent: false, title: 'Benachrichtigungs-Session auswählen', noneLabel: 'Keine Benachrichtigung', noneIcon: 'fa-bell-slash' }); }); // Cron-Expression Vorschau cronInput.addEventListener('input', () => updateCronPreview(cronInput, cronPreview)); if (isEdit) { title.textContent = 'Cronjob bearbeiten'; runBtn.hidden = false; deleteBtn.hidden = false; const job = await apiGetCronjob(params.id); if (!job) { showToast('Cronjob nicht gefunden', 'error'); navigateTo('dashboard'); return; } nameInput.value = job.name || ''; sessionSelect.value = job.sessionId || ''; renderSessionSelectCard(sessionCard, job.sessionId ? sessionMap[job.sessionId] : null, 'fa-comment-o', 'Keine Session ausgewählt'); cronInput.value = job.cronExpression || ''; promptInput.value = job.prompt || ''; notifySelect.value = job.notifySessionId || ''; renderSessionSelectCard(notifyCard, job.notifySessionId ? sessionMap[job.notifySessionId] : null, 'fa-bell-o', 'Keine (Antwort bleibt in Arbeits-Session)'); enabledCheckbox.checked = job.isEnabled; updateCronPreview(cronInput, cronPreview); showCronjobStatus(statusBox, job); // Löschen deleteBtn.addEventListener('click', async () => { if (!confirm('Cronjob wirklich löschen?')) return; await apiDeleteCronjob(params.id); showToast('Cronjob gelöscht'); navigateTo('dashboard'); }); // Jetzt ausführen runBtn.addEventListener('click', () => withButtonLoading(runBtn, 'Läuft...', async () => { try { const result = await apiRunCronjob(params.id); if (result.status === 'success') { showToast('Cronjob erfolgreich ausgeführt'); } else { showToast('Fehler: ' + (result.error || 'Unbekannt'), 'error'); } const updated = await apiGetCronjob(params.id); if (updated) showCronjobStatus(statusBox, updated); } catch (e) { showToast('Fehler: ' + e.message, 'error'); } })); } // Speichern saveBtn.addEventListener('click', async () => { const data = { name: nameInput.value.trim(), sessionId: sessionSelect.value, cronExpression: cronInput.value.trim(), prompt: promptInput.value.trim(), notifySessionId: notifySelect.value, isEnabled: enabledCheckbox.checked }; if (!data.name || !data.sessionId || !data.cronExpression || !data.prompt) { showToast('Bitte alle Felder ausfüllen', 'error'); return; } await withButtonLoading(saveBtn, 'Speichern...', async () => { try { if (isEdit) { await apiUpdateCronjob(params.id, data); showToast('Cronjob gespeichert'); const updated = await apiGetCronjob(params.id); if (updated) showCronjobStatus(statusBox, updated); } else { const created = await apiCreateCronjob(data); showToast('Cronjob erstellt'); navigateTo('cronjob', { id: created.cronjobId }); } } catch (e) { showToast('Fehler: ' + e.message, 'error'); } }); }); } }); function openSessionPickerModal(sessions, agentMap, currentId, onSelect, options) { options = options || {}; const filterWithAgent = options.filterWithAgent || false; const filtered = filterWithAgent ? sessions.filter(s => s.agentId) : sessions; const items = filtered.map(s => { s._pickerId = s.sessionId; return s; }); _openPickerModal(options.title || 'Session auswählen', items, currentId, onSelect, { noneLabel: options.noneLabel || 'Keine Session', noneIcon: options.noneIcon || 'fa-ban', buildCard: (s) => buildSessionCard(s, s.agentId ? agentMap[s.agentId] : null) }); } function updateCronPreview(input, preview) { const expr = input.value.trim(); if (!expr) { preview.textContent = ''; return; } const desc = describeCron(expr); if (desc) { preview.textContent = desc; preview.classList.remove('text-error'); } else { preview.textContent = 'Ungültige Cron-Expression'; preview.classList.add('text-error'); } } function describeCron(expr) { try { const parts = expr.split(/\s+/); if (parts.length < 5 || parts.length > 5) return null; const [min, hour, dom, month, dow] = parts; // Einfache Fälle beschreiben const dowNames = { '0': 'So', '1': 'Mo', '2': 'Di', '3': 'Mi', '4': 'Do', '5': 'Fr', '6': 'Sa', '7': 'So' }; let desc = ''; const tz = 'Europe/Berlin'; if (dom === '*' && month === '*' && dow === '*') { if (min !== '*' && hour !== '*') { desc = `Täglich um ${hour.padStart(2, '0')}:${min.padStart(2, '0')} Uhr`; } else if (hour === '*' && min === '0') { desc = 'Jede Stunde'; } else if (hour === '*' && min === '*') { desc = 'Jede Minute'; } else if (hour !== '*') { desc = `Täglich, stündlich um ${hour} Uhr`; } } else if (dom === '*' && month === '*' && dow !== '*') { // Ranges wie 1-5 auflösen let dayParts = []; for (const part of dow.split(',')) { if (part.includes('-')) { const [from, to] = part.split('-'); dayParts.push(`${dowNames[from] || from}–${dowNames[to] || to}`); } else { dayParts.push(dowNames[part] || part); } } const days = dayParts.join(', '); if (min !== '*' && hour !== '*') { desc = `${days} um ${hour.padStart(2, '0')}:${min.padStart(2, '0')} Uhr`; } else { desc = `An: ${days}`; } } else if (dow === '*' && month === '*' && dom !== '*') { desc = `Am ${dom}. jedes Monats`; if (min !== '*' && hour !== '*') { desc += ` um ${hour.padStart(2, '0')}:${min.padStart(2, '0')} Uhr`; } } if (!desc) { desc = `${expr} (5-Feld Cron)`; } return desc; } catch { return null; } } function showCronjobStatus(box, job) { box.hidden = false; const parts = []; if (job.isEnabled) { parts.push(' Aktiv'); } else { parts.push(' Pausiert'); } if (job.nextRunAt) { const next = new Date(job.nextRunAt); parts.push(` Nächste Ausführung: ${next.toLocaleString('de-AT')}`); } if (job.lastRunAt) { const last = new Date(job.lastRunAt); const statusIcon = job.lastRunStatus === 'success' ? '✅' : '❌'; parts.push(`${statusIcon} Letzte Ausführung: ${last.toLocaleString('de-AT')}`); if (job.lastRunStatus === 'error' && job.lastRunError) { parts.push(` ${esc(job.lastRunError)}`); } } if (job.sessionName) { parts.push(` Session: ${esc(job.sessionName)}`); } if (job.notifySessionName) { parts.push(` Benachrichtigung → ${esc(job.notifySessionName)}`); } box.innerHTML = parts.join(''); }