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
@@ -255,3 +255,17 @@
|
||||
background: rgb(from var(--text) r g b / .15);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Compact cards (no image) */
|
||||
.card-compact {
|
||||
padding: var(--box-padding);
|
||||
}
|
||||
|
||||
.card-compact .card-meta {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.card-compact .card-meta-item {
|
||||
margin-right: var(--box-spacing);
|
||||
}
|
||||
@@ -291,3 +291,66 @@ label.skill-item {
|
||||
.telegram-actions {
|
||||
margin-top: var(--badge-spacing);
|
||||
}
|
||||
|
||||
/* ── Cronjob Status ── */
|
||||
|
||||
.form-status-box {
|
||||
padding: var(--box-padding);
|
||||
background: var(--f3-bg);
|
||||
border: var(--border-width) solid var(--f2-border);
|
||||
border-radius: var(--border-radius);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--badge-spacing);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.form-status-box .status-active {
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
.form-status-box .status-inactive {
|
||||
opacity: var(--semi-transparent);
|
||||
}
|
||||
|
||||
.form-status-box .status-error {
|
||||
color: var(--dangerous);
|
||||
}
|
||||
|
||||
.form-check-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--badge-spacing);
|
||||
}
|
||||
|
||||
.form-check-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--badge-spacing);
|
||||
cursor: pointer;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.form-check-group input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: var(--font-size);
|
||||
opacity: var(--semi-transparent);
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
color: var(--secondary);
|
||||
border-color: var(--secondary);
|
||||
}
|
||||
|
||||
.secondary-btn:hover {
|
||||
background: var(--secondary);
|
||||
color: var(--f1-bg);
|
||||
}
|
||||
@@ -73,6 +73,13 @@
|
||||
</div>
|
||||
<div class="card-grid" id="agentCards"></div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="section-header">
|
||||
<h2>Cronjobs</h2>
|
||||
<button id="newCronjobBtn" class="section-add-btn"><i class="fa fa-plus"></i> Neuer Cronjob</button>
|
||||
</div>
|
||||
<div class="card-grid" id="cronjobCards"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -208,6 +215,55 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-cronjob">
|
||||
<div class="page form-page">
|
||||
<header>
|
||||
<button class="back-btn icon-btn" data-page="dashboard"><i class="fa fa-arrow-left"></i></button>
|
||||
<h1 id="cronjobPageTitle">Neuer Cronjob</h1>
|
||||
</header>
|
||||
<div class="page-content">
|
||||
<div class="form-container">
|
||||
<div class="form-group">
|
||||
<label for="cronjobName">Name</label>
|
||||
<input type="text" id="cronjobName" placeholder="z.B. Täglicher Report">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cronjobSession">Session</label>
|
||||
<select id="cronjobSession"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cronjobCron">Cron-Expression</label>
|
||||
<input type="text" id="cronjobCron" placeholder="z.B. 0 9 * * *">
|
||||
<small id="cronjobCronPreview" class="form-hint"></small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cronjobPrompt">Prompt</label>
|
||||
<textarea id="cronjobPrompt" rows="6" placeholder="Die Nachricht, die an den Agent gesendet wird..."></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cronjobNotifySession">Benachrichtigungs-Session <small>(optional)</small></label>
|
||||
<select id="cronjobNotifySession"></select>
|
||||
<small class="form-hint">Wenn gesetzt, wird eine Zusammenfassung in diese Session geschrieben (z.B. eine mit Telegram).</small>
|
||||
</div>
|
||||
<div class="form-group form-check-group">
|
||||
<label>
|
||||
<input type="checkbox" id="cronjobEnabled" checked>
|
||||
Aktiv
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="cronjobStatusBox" class="form-status-box" hidden></div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button id="cronjobSaveBtn" class="primary-btn"><i class="fa fa-save"></i> Speichern</button>
|
||||
<button id="cronjobRunBtn" class="secondary-btn" hidden><i class="fa fa-play"></i> Jetzt ausführen</button>
|
||||
<button id="cronjobDeleteBtn" class="danger-btn" hidden><i class="fa fa-trash"></i> Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-chat">
|
||||
<div class="page chat-page">
|
||||
<header>
|
||||
@@ -390,6 +446,7 @@
|
||||
<script src="js/pages/chat-page.js"></script>
|
||||
<script src="js/pages/login-page.js"></script>
|
||||
<script src="js/pages/settings-page.js"></script>
|
||||
<script src="js/pages/cronjob-page.js"></script>
|
||||
<script src="js/pages/profile-page.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -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