334 lines
12 KiB
JavaScript
334 lines
12 KiB
JavaScript
// ── 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 apiGetAgents();
|
||
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, emptyHtml) {
|
||
cardEl.innerHTML = '';
|
||
if (!session) {
|
||
cardEl.innerHTML = emptyHtml;
|
||
return;
|
||
}
|
||
const agent = session.agentId ? agentMap[session.agentId] : null;
|
||
const inner = buildSessionCard(session, agent);
|
||
while (inner.firstChild) cardEl.appendChild(inner.firstChild);
|
||
cardEl.insertAdjacentHTML('beforeend', '<span class="session-select-change"><i class="fa fa-pencil"></i></span>');
|
||
}
|
||
|
||
const sessionEmptyHtml = '<div class="session-select-empty"><i class="fa fa-comment-o"></i> Keine Session ausgewählt</div>';
|
||
const notifyEmptyHtml = '<div class="session-select-empty"><i class="fa fa-bell-o"></i> Keine (Antwort bleibt in Arbeits-Session)</div>';
|
||
|
||
// 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, sessionEmptyHtml);
|
||
}, { 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, notifyEmptyHtml);
|
||
}, { 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, sessionEmptyHtml);
|
||
cronInput.value = job.cronExpression || '';
|
||
promptInput.value = job.prompt || '';
|
||
notifySelect.value = job.notifySessionId || '';
|
||
renderSessionSelectCard(notifyCard, job.notifySessionId ? sessionMap[job.notifySessionId] : null, notifyEmptyHtml);
|
||
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', async () => {
|
||
runBtn.disabled = true;
|
||
runBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Läuft...';
|
||
try {
|
||
const result = await apiRunCronjob(params.id);
|
||
if (result.status === 'success') {
|
||
showToast('Cronjob erfolgreich ausgeführt');
|
||
} else {
|
||
showToast('Fehler: ' + (result.error || 'Unbekannt'), 'error');
|
||
}
|
||
// Status neu laden
|
||
const updated = await apiGetCronjob(params.id);
|
||
if (updated) showCronjobStatus(statusBox, updated);
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
} finally {
|
||
runBtn.disabled = false;
|
||
runBtn.innerHTML = '<i class="fa fa-play"></i> Jetzt ausführen';
|
||
}
|
||
});
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
saveBtn.disabled = true;
|
||
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');
|
||
} finally {
|
||
saveBtn.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
function openSessionPickerModal(sessions, agentMap, currentId, onSelect, options) {
|
||
options = options || {};
|
||
const filterWithAgent = options.filterWithAgent || false;
|
||
const title = options.title || 'Session auswählen';
|
||
const noneLabel = options.noneLabel || 'Keine Session';
|
||
const noneIcon = options.noneIcon || 'fa-ban';
|
||
|
||
// Overlay
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'agent-picker-overlay';
|
||
|
||
const modal = document.createElement('div');
|
||
modal.className = 'agent-picker-modal';
|
||
|
||
// Header
|
||
const header = document.createElement('div');
|
||
header.className = 'agent-picker-header';
|
||
header.innerHTML = `<span>${esc(title)}</span><button class="icon-btn agent-picker-close"><i class="fa fa-times"></i></button>`;
|
||
modal.appendChild(header);
|
||
|
||
// Body
|
||
const body = document.createElement('div');
|
||
body.className = 'agent-picker-body';
|
||
|
||
// "Keine Session" Option
|
||
const noneCard = document.createElement('div');
|
||
noneCard.className = 'card agent-picker-item' + (!currentId ? ' selected' : '');
|
||
noneCard.innerHTML = `
|
||
<div class="card-image" style="display:flex;align-items:center;justify-content:center"><i class="fa ${esc(noneIcon)}" style="font-size:2em;opacity:var(--semi-transparent)"></i></div>
|
||
<div class="card-body">
|
||
<div class="card-title-row">
|
||
<span class="card-title">${esc(noneLabel)}</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
noneCard.addEventListener('click', () => {
|
||
onSelect(null);
|
||
overlay.remove();
|
||
});
|
||
body.appendChild(noneCard);
|
||
|
||
// Session Cards via Helper
|
||
for (const s of sessions) {
|
||
// Filter: nur Sessions mit Agent, wenn gewünscht
|
||
if (filterWithAgent && !s.agentId) continue;
|
||
|
||
const agent = s.agentId ? agentMap[s.agentId] : null;
|
||
const item = buildSessionCard(s, agent);
|
||
item.classList.add('agent-picker-item');
|
||
if (s.sessionId === currentId) item.classList.add('selected');
|
||
item.addEventListener('click', () => {
|
||
onSelect(s.sessionId);
|
||
overlay.remove();
|
||
});
|
||
body.appendChild(item);
|
||
}
|
||
|
||
modal.appendChild(body);
|
||
overlay.appendChild(modal);
|
||
document.body.appendChild(overlay);
|
||
|
||
// Schließen
|
||
header.querySelector('.agent-picker-close').addEventListener('click', () => overlay.remove());
|
||
overlay.addEventListener('click', (e) => {
|
||
if (e.target === overlay) overlay.remove();
|
||
});
|
||
}
|
||
|
||
function updateCronPreview(input, preview) {
|
||
const expr = input.value.trim();
|
||
if (!expr) {
|
||
preview.textContent = '';
|
||
return;
|
||
}
|
||
const desc = describeCron(expr);
|
||
if (desc) {
|
||
preview.textContent = desc;
|
||
preview.style.color = '';
|
||
} else {
|
||
preview.textContent = 'Ungültige Cron-Expression';
|
||
preview.style.color = 'var(--color-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('<span class="status-active"><i class="fa fa-check-circle"></i> Aktiv</span>');
|
||
} else {
|
||
parts.push('<span class="status-inactive"><i class="fa fa-pause-circle"></i> Pausiert</span>');
|
||
}
|
||
|
||
if (job.nextRunAt) {
|
||
const next = new Date(job.nextRunAt);
|
||
parts.push(`<span><i class="fa fa-clock-o"></i> Nächste Ausführung: ${next.toLocaleString('de-AT')}</span>`);
|
||
}
|
||
|
||
if (job.lastRunAt) {
|
||
const last = new Date(job.lastRunAt);
|
||
const statusIcon = job.lastRunStatus === 'success' ? '✅' : '❌';
|
||
parts.push(`<span>${statusIcon} Letzte Ausführung: ${last.toLocaleString('de-AT')}</span>`);
|
||
if (job.lastRunStatus === 'error' && job.lastRunError) {
|
||
parts.push(`<span class="status-error"><i class="fa fa-exclamation-triangle"></i> ${esc(job.lastRunError)}</span>`);
|
||
}
|
||
}
|
||
|
||
if (job.sessionName) {
|
||
parts.push(`<span><i class="fa fa-comment"></i> Session: ${esc(job.sessionName)}</span>`);
|
||
}
|
||
|
||
if (job.notifySessionName) {
|
||
parts.push(`<span><i class="fa fa-bell"></i> Benachrichtigung → ${esc(job.notifySessionName)}</span>`);
|
||
}
|
||
|
||
box.innerHTML = parts.join('');
|
||
}
|
||
|
||
|