- #claudeStatusBar aus Dashboard-Template nach body verschoben (vor #app) - loadClaudeStatus() von dashboard.js nach app.js (einmalig beim Start) - Schließen-Button (×) zum Ausblenden der Statusbar - Status-Bar CSS von dashboard.css nach main.css verschoben
319 lines
14 KiB
JavaScript
319 lines
14 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-Status parallel laden
|
|
const telegramStatusPromise = apiTelegramStatus().catch(() => ({ configured: false }));
|
|
|
|
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>`);
|
|
|
|
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="telegram" hidden><i class="fa fa-paper-plane"></i> Telegram verknüpfen</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 });
|
|
});
|
|
const telegramBtn = menuDropdown.querySelector('[data-action="telegram"]');
|
|
telegramBtn.addEventListener('click', async (e) => {
|
|
e.stopPropagation();
|
|
menuDropdown.hidden = true;
|
|
try {
|
|
const result = await apiTelegramLink(s.sessionId);
|
|
showTelegramLinkCode(result.code, s.name || 'Unbenannt');
|
|
} catch (err) {
|
|
showToast(err.message, 'error');
|
|
}
|
|
});
|
|
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Telegram-Buttons einblenden wenn konfiguriert
|
|
telegramStatusPromise.then(status => {
|
|
if (status.configured) {
|
|
grid.querySelectorAll('[data-action="telegram"]').forEach(btn => btn.hidden = false);
|
|
}
|
|
});
|
|
|
|
// 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'));
|
|
},
|
|
destroy() {
|
|
if (this._closeCardMenus) {
|
|
document.removeEventListener('click', this._closeCardMenus);
|
|
this._closeCardMenus = null;
|
|
}
|
|
}
|
|
});
|
|
|
|
function showTelegramLinkCode(code, sessionName) {
|
|
// Bestehendes Modal entfernen
|
|
const existing = document.getElementById('telegramLinkModal');
|
|
if (existing) existing.remove();
|
|
|
|
const modal = document.createElement('div');
|
|
modal.id = 'telegramLinkModal';
|
|
modal.className = 'modal-overlay';
|
|
modal.innerHTML = `
|
|
<div class="modal telegram-link-modal">
|
|
<div class="modal-header">
|
|
<h3><i class="fa fa-paper-plane"></i> Telegram verknüpfen</h3>
|
|
<button class="icon-btn modal-close"><i class="fa fa-times"></i></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Sende diesen Code an deinen Telegram-Bot um die Session <strong>${esc(sessionName)}</strong> zu verknüpfen:</p>
|
|
<div class="telegram-code-display">
|
|
<code>/link ${esc(code)}</code>
|
|
<button class="icon-btn" title="Kopieren" id="copyTelegramCode"><i class="fa fa-copy"></i></button>
|
|
</div>
|
|
<p class="telegram-code-hint"><i class="fa fa-clock-o"></i> Der Code ist 5 Minuten gültig.</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
modal.querySelector('.modal-close').addEventListener('click', () => modal.remove());
|
|
modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
|
|
|
|
document.getElementById('copyTelegramCode').addEventListener('click', () => {
|
|
navigator.clipboard.writeText('/link ' + code).then(() => {
|
|
showToast('In Zwischenablage kopiert', 'success');
|
|
});
|
|
});
|
|
}
|
|
|
|
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'
|
|
});
|
|
}
|