Files
gott/wwwroot/js/pages/dashboard.js
T
Meleki b645625b56 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
2026-03-03 22:44:46 +01:00

356 lines
16 KiB
JavaScript

// ── Dashboard Page ──
registerPage('dashboard', {
async init() {
// Einstellungen-Link
document.getElementById('dashboardSettingsLink').addEventListener('click', (e) => {
e.preventDefault();
navigateTo('settings');
});
// Profil-Link
document.getElementById('dashboardProfileLink').addEventListener('click', (e) => {
e.preventDefault();
navigateTo('profile');
});
// Globaler Click-Handler zum Schließen aller Card-Menüs
document.addEventListener('click', this._closeCardMenus = () => closeAllCardMenus());
// Telegram-Config parallel pro Session laden (sessions werden gleich geladen)
const agents = await apiGetAgents();
const agentMap = {};
for (const a of agents) agentMap[a.agentId] = a;
// Sessions
const sessions = await apiGetSessions();
const grid = document.getElementById('sessionCards');
grid.innerHTML = '';
for (const s of sessions) {
const agent = s.agentId ? agentMap[s.agentId] : null;
const card = document.createElement('div');
card.className = 'card';
const msgLabel = s.messageCount !== undefined
? `${s.messageCount}${s.totalMessageCount !== s.messageCount ? ` (${s.totalMessageCount})` : ''} Nachrichten`
: '';
const workspaceLabel = s.workspace ? s.workspace : '';
const sessionBadges = [];
if (agent) sessionBadges.push(`<span class="badge">${esc(agent.name)}</span>`);
if (s.context) sessionBadges.push(`<span class="badge badge-feature" title="${esc(s.context)}"><i class="fa fa-file-text-o"></i></span>`);
if (s.telegramConfigured) sessionBadges.push(`<span class="badge ${s.telegramLinked ? 'badge-feature' : 'badge-off'}" title="Telegram ${s.telegramLinked ? 'verbunden' : 'konfiguriert'}"><i class="fa fa-paper-plane"></i></span>`);
card.innerHTML = `
<div class="card-image">${s.image ? `<img src="${s.image}" alt="">` : ''}</div>
<div class="card-body">
<div class="card-title-row">
<span class="card-title">${esc(s.name || 'Unbenannt')}</span>
${sessionBadges.join('')}
<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="chat"><i class="fa fa-comment"></i> Chat öffnen</button>
<button class="card-menu-item" data-action="edit"><i class="fa fa-pencil"></i> Bearbeiten</button>
</div>
</div>
</div>
<div class="card-meta">
${msgLabel ? `<span class="card-meta-item"><i class="fa fa-comment"></i> ${esc(msgLabel)}</span>` : ''}
${workspaceLabel ? `<span class="card-meta-item card-workspace" title="${esc(workspaceLabel)}"><i class="fa fa-folder"></i> ${esc(workspaceLabel)}</span>` : ''}
</div>
</div>
`;
// Chat deaktivieren wenn kein Agent gesetzt
const chatBtn = card.querySelector('[data-action="chat"]');
if (!agent) {
chatBtn.disabled = true;
chatBtn.title = 'Kein Agent zugewiesen';
}
const menuTrigger = card.querySelector('.card-menu-trigger');
const menuDropdown = card.querySelector('.card-menu-dropdown');
menuTrigger.addEventListener('click', (e) => {
e.stopPropagation();
closeAllCardMenus();
// Position fixed dropdown below trigger, aligned to the left
const rect = menuTrigger.getBoundingClientRect();
menuDropdown.hidden = false;
const dropRect = menuDropdown.getBoundingClientRect();
// Position: below trigger, right edge aligned with trigger's right edge
menuDropdown.style.top = `${rect.bottom + 4}px`;
menuDropdown.style.left = `${rect.right - dropRect.width}px`;
// Adjust if off-screen bottom
if (rect.bottom + 4 + dropRect.height > window.innerHeight) {
menuDropdown.style.top = `${rect.top - dropRect.height - 4}px`;
}
// Safety: if off-screen left
const finalRect = menuDropdown.getBoundingClientRect();
if (finalRect.left < 0) {
menuDropdown.style.left = '8px';
}
});
chatBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (chatBtn.disabled) return;
menuDropdown.hidden = true;
navigateTo('chat', { id: s.sessionId });
});
menuDropdown.querySelector('[data-action="edit"]').addEventListener('click', (e) => {
e.stopPropagation();
menuDropdown.hidden = true;
navigateTo('session', { id: s.sessionId });
});
grid.appendChild(card);
// Git-Status asynchron laden
if (s.workspace) {
apiGitStatus(s.sessionId).then(status => {
if (!status || status.error) return;
const addCount = (status.add || []).length;
const editCount = (status.edit || []).length;
const deleteCount = (status.delete || []).length;
const outgoing = status.outgoing || 0;
if (addCount + editCount + deleteCount + outgoing === 0) return;
const metaEl = card.querySelector('.card-meta');
if (!metaEl) return;
const parts = [];
if (addCount > 0) {
const tip = `${addCount} ${addCount === 1 ? 'Datei' : 'Dateien'} hinzugefügt`;
parts.push(`<span class="git-badge git-add" title="${esc(tip)}">+${addCount}</span>`);
}
if (editCount > 0) {
const tip = `${editCount} ${editCount === 1 ? 'Datei' : 'Dateien'} geändert`;
parts.push(`<span class="git-badge git-edit" title="${esc(tip)}">~${editCount}</span>`);
}
if (deleteCount > 0) {
const tip = `${deleteCount} ${deleteCount === 1 ? 'Datei' : 'Dateien'} gelöscht`;
parts.push(`<span class="git-badge git-delete" title="${esc(tip)}">-${deleteCount}</span>`);
}
if (outgoing > 0) {
const tip = `${outgoing} ${outgoing === 1 ? 'Commit muss' : 'Commits müssen'} noch gepusht werden`;
parts.push(`<span class="git-badge git-outgoing" title="${esc(tip)}"><i class="fa fa-arrow-up"></i> ${outgoing}</span>`);
}
const gitEl = document.createElement('span');
gitEl.className = 'card-meta-item card-git-status';
gitEl.innerHTML = parts.join(' ');
metaEl.appendChild(gitEl);
});
}
}
// Neue Session Button
document.getElementById('newSessionBtn').addEventListener('click', () => navigateTo('session'));
// Agents
const agentGrid = document.getElementById('agentCards');
agentGrid.innerHTML = '';
// Skills pro Agent parallel laden
const agentSkillsMap = {};
await Promise.all(agents.map(async a => {
agentSkillsMap[a.agentId] = await apiGetAgentSkills(a.agentId);
}));
for (const a of agents) {
const card = document.createElement('div');
card.className = 'card';
const badges = [];
if (a.toolChoice === 'any') badges.push('<span class="badge badge-forced-feature" title="Tool-Nutzung erzwingen"><i class="fa fa-wrench"></i> Erzwungen</span>');
if (a.autoCommit === 'mention') badges.push('<span class="badge badge-feature" title="Git-Commits nur auf Anweisung"><i class="fa fa-code-fork"></i> Anmerken</span>');
if (a.autoCommit === 'enforce') badges.push('<span class="badge badge-forced-feature" title="Git-Commits nach jeder Änderung erzwungen"><i class="fa fa-code-fork"></i> Erzwungen</span>');
if (a.autoCommit === 'forbid') badges.push('<span class="badge badge-forced-feature" title="Git-Commits strikt verboten"><i class="fa fa-code-fork"></i> Verboten</span>');
if (a.useSoul) badges.push('<span class="badge badge-feature" title="Hat eine Seele (SOUL.md)"><i class="fa fa-heart"></i> Seele</span>');
if (a.useIdentity) badges.push('<span class="badge badge-feature" title="Hat eine Identität (IDENTITY.md)"><i class="fa fa-id-badge"></i> Ich</span>');
if (a.useUserInfo) badges.push('<span class="badge badge-feature" title="Kennt sein Gegenüber (Benutzer-Persona)"><i class="fa fa-user"></i> Du</span>');
if (a.useMemory) badges.push('<span class="badge badge-feature" title="Zugriff auf Erinnerungen (Memory-Tools)"><i class="fa fa-database"></i> Erinnerungen</span>');
if (a.webSearchVersion) {
const wsYear = a.webSearchVersion.includes('2025') ? 'Version 2025' : 'v2026';
badges.push(`<span class="badge badge-feature" title="Web-Suche - ${wsYear}"><i class="fa fa-search"></i> Web-Suche</span>`);
}
const skills = agentSkillsMap[a.agentId] || [];
for (const skill of skills) {
badges.push(`<span class="badge badge-skill" title="${esc(skill.description || skill.slug)}"><i class="fa fa-puzzle-piece"></i> ${esc(skill.name)}</span>`);
}
card.innerHTML = `
<div class="card-image">${a.avatar ? `<img src="${a.avatar}" alt="">` : ''}</div>
<div class="card-body">
<div class="card-title-row">
<span class="card-title">${esc(a.name)}</span>
<span class="badge">${prettyModelName(a.model)}</span>
<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="files"><i class="fa fa-folder"></i> Dateien verwalten</button>
</div>
</div>
</div>
<div class="card-badges">${badges.join('')}</div>
</div>
`;
const menuTrigger = card.querySelector('.card-menu-trigger');
const menuDropdown = card.querySelector('.card-menu-dropdown');
menuTrigger.addEventListener('click', (e) => {
e.stopPropagation();
closeAllCardMenus();
// Position fixed dropdown below trigger, aligned to the left
const rect = menuTrigger.getBoundingClientRect();
menuDropdown.hidden = false;
const dropRect = menuDropdown.getBoundingClientRect();
// Position: below trigger, right edge aligned with trigger's right edge
menuDropdown.style.top = `${rect.bottom + 4}px`;
menuDropdown.style.left = `${rect.right - dropRect.width}px`;
// Adjust if off-screen bottom
if (rect.bottom + 4 + dropRect.height > window.innerHeight) {
menuDropdown.style.top = `${rect.top - dropRect.height - 4}px`;
}
// Safety: if off-screen left
const finalRect = menuDropdown.getBoundingClientRect();
if (finalRect.left < 0) {
menuDropdown.style.left = '8px';
}
});
menuDropdown.querySelector('[data-action="files"]').addEventListener('click', (e) => {
e.stopPropagation();
menuDropdown.hidden = true;
openFileExplorer({ agentId: a.agentId });
});
menuDropdown.querySelector('[data-action="edit"]').addEventListener('click', (e) => {
e.stopPropagation();
menuDropdown.hidden = true;
navigateTo('agent', { id: a.agentId });
});
agentGrid.appendChild(card);
}
// 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) {
document.removeEventListener('click', this._closeCardMenus);
this._closeCardMenus = null;
}
}
});
function closeAllCardMenus() {
document.querySelectorAll('.card-menu-dropdown').forEach(d => d.hidden = true);
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function formatDate(iso) {
if (!iso) return '';
return new Date(iso).toLocaleString('de-AT', {
day: '2-digit', month: '2-digit', year: '2-digit',
hour: '2-digit', minute: '2-digit'
});
}