Cronjobs + AnthropicService Singleton Refactoring
Cronjobs: - CronjobService (BackgroundService) prüft jede Minute auf fällige Jobs - Cron-Expression in Server-Lokalzeit via Cronos (CronFormat.Standard) - Cronjob-Ausführung: Prompt → Session → Agent-Antwort in Chat-History - Optionale Benachrichtigungs-Session (notify_session_id): Zusammenfassung wird in separate Session geschrieben (z.B. mit Telegram) - Telegram-Benachrichtigung mit Cronjob-Name + Ursprungs-Session - REST API: CRUD + toggle + manuelles Ausführen (/run) - Frontend: Dashboard-Cards, Cronjob-Formular mit Cron-Vorschau, Session-Dropdown, Notify-Session-Dropdown, Status-Box Refactoring: - AnthropicService als Singleton in DI registriert - ChatEndpoint + TelegramService nutzen injizierte Instanz - Redundante Dependencies (Token, ToolService, MemoryService) entfernt Neue Dateien: - Models/CronjobRecord.cs - Services/CronjobService.cs - Endpoints/CronjobEndpoint.cs - wwwroot/js/pages/cronjob-page.js - PLAN-Cronjob.md
This commit is contained in:
1 parent
0120bb8f73
commit
b645625b56
16 files changed
+1132
-28
No files matched your search
@@ -321,6 +321,61 @@ async function apiSaveSettings(settings) {
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
// ── Cronjob API ──
|
||||
|
||||
async function apiGetCronjobs() {
|
||||
const resp = await fetch('/api/cronjobs');
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiGetCronjob(id) {
|
||||
const resp = await fetch(`/api/cronjobs/${id}`);
|
||||
if (!resp.ok) return null;
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiCreateCronjob(data) {
|
||||
const resp = await fetch('/api/cronjobs', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.error || 'Cronjob erstellen fehlgeschlagen');
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiUpdateCronjob(id, data) {
|
||||
const resp = await fetch(`/api/cronjobs/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.error || 'Cronjob speichern fehlgeschlagen');
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiDeleteCronjob(id) {
|
||||
return fetch(`/api/cronjobs/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
async function apiToggleCronjob(id) {
|
||||
const resp = await fetch(`/api/cronjobs/${id}/toggle`, { method: 'POST' });
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiRunCronjob(id) {
|
||||
const resp = await fetch(`/api/cronjobs/${id}/run`, { method: 'POST' });
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
// ── Backup API ──
|
||||
|
||||
async function apiBackupDb() {
|
||||
const resp = await fetch('/api/backup', { method: 'POST', credentials: 'include' });
|
||||
if (!resp.ok) throw new Error('Backup fehlgeschlagen');
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
// ── 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 cronInput = document.getElementById('cronjobCron');
|
||||
const cronPreview = document.getElementById('cronjobCronPreview');
|
||||
const promptInput = document.getElementById('cronjobPrompt');
|
||||
const notifySelect = document.getElementById('cronjobNotifySession');
|
||||
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 laden für Dropdown
|
||||
const sessions = await apiGetSessions();
|
||||
sessionSelect.innerHTML = '<option value="">– Session wählen –</option>';
|
||||
notifySelect.innerHTML = '<option value="">– Keine (Antwort bleibt in Arbeits-Session) –</option>';
|
||||
for (const s of sessions) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.sessionId;
|
||||
opt.textContent = s.name || 'Unbenannt';
|
||||
if (!s.agentId) {
|
||||
opt.textContent += ' (kein Agent)';
|
||||
opt.disabled = true;
|
||||
}
|
||||
sessionSelect.appendChild(opt);
|
||||
|
||||
// Notify-Dropdown: alle Sessions (auch ohne Agent), Telegram-Sessions markieren
|
||||
const nOpt = document.createElement('option');
|
||||
nOpt.value = s.sessionId;
|
||||
nOpt.textContent = s.name || 'Unbenannt';
|
||||
if (s.telegramChatId) nOpt.textContent += ' 📱';
|
||||
notifySelect.appendChild(nOpt);
|
||||
}
|
||||
|
||||
// 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 || '';
|
||||
cronInput.value = job.cronExpression || '';
|
||||
promptInput.value = job.prompt || '';
|
||||
notifySelect.value = job.notifySessionId || '';
|
||||
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 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> ${escapeHtml(job.lastRunError)}</span>`);
|
||||
}
|
||||
}
|
||||
|
||||
if (job.sessionName) {
|
||||
parts.push(`<span><i class="fa fa-comment"></i> Session: ${escapeHtml(job.sessionName)}</span>`);
|
||||
}
|
||||
|
||||
if (job.notifySessionName) {
|
||||
parts.push(`<span><i class="fa fa-bell"></i> Benachrichtigung → ${escapeHtml(job.notifySessionName)}</span>`);
|
||||
}
|
||||
|
||||
box.innerHTML = parts.join('<br>');
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s || '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
@@ -235,6 +235,96 @@ registerPage('dashboard', {
|
||||
|
||||
// Neuer Agent Button
|
||||
document.getElementById('newAgentBtn').addEventListener('click', () => navigateTo('agent'));
|
||||
|
||||
// Cronjobs
|
||||
const cronjobs = await apiGetCronjobs();
|
||||
const cronjobGrid = document.getElementById('cronjobCards');
|
||||
cronjobGrid.innerHTML = '';
|
||||
|
||||
for (const cj of cronjobs) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card card-compact';
|
||||
|
||||
const statusBadge = cj.isEnabled
|
||||
? '<span class="badge badge-feature"><i class="fa fa-check-circle"></i> Aktiv</span>'
|
||||
: '<span class="badge badge-off"><i class="fa fa-pause-circle"></i> Pausiert</span>';
|
||||
|
||||
const lastRunIcon = cj.lastRunStatus === 'success' ? '✅'
|
||||
: cj.lastRunStatus === 'error' ? '❌' : '';
|
||||
|
||||
const cronDesc = describeCron(cj.cronExpression) || cj.cronExpression;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="card-body">
|
||||
<div class="card-title-row">
|
||||
<span class="card-title">${esc(cj.name)}</span>
|
||||
${statusBadge}
|
||||
<div class="card-menu">
|
||||
<button class="card-menu-trigger icon-btn"><i class="fa fa-ellipsis-v"></i></button>
|
||||
<div class="card-menu-dropdown" hidden>
|
||||
<button class="card-menu-item" data-action="edit"><i class="fa fa-pencil"></i> Bearbeiten</button>
|
||||
<button class="card-menu-item" data-action="toggle"><i class="fa fa-${cj.isEnabled ? 'pause' : 'play'}"></i> ${cj.isEnabled ? 'Pausieren' : 'Aktivieren'}</button>
|
||||
<button class="card-menu-item" data-action="run"><i class="fa fa-play-circle"></i> Jetzt ausführen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-meta">
|
||||
<span class="card-meta-item"><i class="fa fa-clock-o"></i> ${esc(cronDesc)}</span>
|
||||
${cj.sessionName ? `<span class="card-meta-item"><i class="fa fa-comment"></i> ${esc(cj.sessionName)}</span>` : ''}
|
||||
${lastRunIcon ? `<span class="card-meta-item">${lastRunIcon} ${cj.lastRunAt ? formatDate(cj.lastRunAt) : ''}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const menuTrigger = card.querySelector('.card-menu-trigger');
|
||||
const menuDropdown = card.querySelector('.card-menu-dropdown');
|
||||
menuTrigger.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
closeAllCardMenus();
|
||||
const rect = menuTrigger.getBoundingClientRect();
|
||||
menuDropdown.hidden = false;
|
||||
const dropRect = menuDropdown.getBoundingClientRect();
|
||||
menuDropdown.style.top = `${rect.bottom + 4}px`;
|
||||
menuDropdown.style.left = `${rect.right - dropRect.width}px`;
|
||||
if (rect.bottom + 4 + dropRect.height > window.innerHeight) {
|
||||
menuDropdown.style.top = `${rect.top - dropRect.height - 4}px`;
|
||||
}
|
||||
const finalRect = menuDropdown.getBoundingClientRect();
|
||||
if (finalRect.left < 0) menuDropdown.style.left = '8px';
|
||||
});
|
||||
|
||||
menuDropdown.querySelector('[data-action="edit"]').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
menuDropdown.hidden = true;
|
||||
navigateTo('cronjob', { id: cj.cronjobId });
|
||||
});
|
||||
|
||||
menuDropdown.querySelector('[data-action="toggle"]').addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
menuDropdown.hidden = true;
|
||||
await apiToggleCronjob(cj.cronjobId);
|
||||
showToast(cj.isEnabled ? 'Cronjob pausiert' : 'Cronjob aktiviert');
|
||||
navigateTo('dashboard', {}, { replace: true });
|
||||
});
|
||||
|
||||
menuDropdown.querySelector('[data-action="run"]').addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
menuDropdown.hidden = true;
|
||||
showToast('Cronjob wird ausgeführt...');
|
||||
const result = await apiRunCronjob(cj.cronjobId);
|
||||
if (result.status === 'success') {
|
||||
showToast('Cronjob erfolgreich ausgeführt');
|
||||
} else {
|
||||
showToast('Fehler: ' + (result.error || 'Unbekannt'), 'error');
|
||||
}
|
||||
navigateTo('dashboard', {}, { replace: true });
|
||||
});
|
||||
|
||||
cronjobGrid.appendChild(card);
|
||||
}
|
||||
|
||||
// Neuer Cronjob Button
|
||||
document.getElementById('newCronjobBtn').addEventListener('click', () => navigateTo('cronjob'));
|
||||
},
|
||||
destroy() {
|
||||
if (this._closeCardMenus) {
|
||||
|
||||
Reference in new issue
Block a user