- AgentRecord: neues IsPublic-Feld mit DB-Migration - GET /api/agents/available: eigene + public Agents (alle Properties + isOwner) - GET /api/telegram/bots/available: alle Properties außer Token/Secret + sessionCount - Agent-Page: Public-Checkbox (andere User können Agent in Sessions verwenden) - Dashboard: agentMap aus availableAgents gebaut (public Agents fremder User auflösbar) - Session-Page + Cronjob-Page: Agent-Picker nutzt /available statt /agents - Agent-Cards: Public- und Fremd-Badges - Fix: Chat-Button bei Sessions mit public Agent eines anderen Users nicht mehr disabled
368 lines
15 KiB
JavaScript
368 lines
15 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');
|
|
});
|
|
|
|
// Admin-Links (nur für Admins sichtbar)
|
|
var adminLink = document.getElementById('dashboardAdminLink');
|
|
var serverSettingsLink = document.getElementById('dashboardServerSettingsLink');
|
|
if (currentUser && currentUser.isAdmin) {
|
|
adminLink.hidden = false;
|
|
adminLink.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
navigateTo('admin');
|
|
});
|
|
serverSettingsLink.hidden = false;
|
|
serverSettingsLink.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
navigateTo('server-settings');
|
|
});
|
|
}
|
|
|
|
// 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, availableAgents, sessions, cronjobs, shares, telegramBots] = await Promise.all([
|
|
apiGetAgents(), apiGetAvailableAgents(), apiGetSessions(), apiGetCronjobs(), apiListShares(), apiGetTelegramBots()
|
|
]);
|
|
const agentMap = {};
|
|
for (const a of availableAgents) agentMap[a.agentId] = a;
|
|
|
|
// Sessions
|
|
const grid = document.getElementById('sessionCards');
|
|
grid.innerHTML = '';
|
|
|
|
for (const s of sessions) {
|
|
const agent = s.agentId ? agentMap[s.agentId] : null;
|
|
const card = buildSessionCard(s, agent);
|
|
|
|
// Git-Status aus der Session-Response rendern (inline vom Backend)
|
|
if (s.gitStatus) {
|
|
const gs = s.gitStatus;
|
|
const metaEl = card.querySelector('.card-meta');
|
|
if (metaEl) {
|
|
const parts = [];
|
|
if (gs.add > 0) {
|
|
const tip = `${gs.add} ${gs.add === 1 ? 'Datei' : 'Dateien'} hinzugefügt`;
|
|
parts.push(`<span class="git-badge git-add" title="${esc(tip)}">+${gs.add}</span>`);
|
|
}
|
|
if (gs.edit > 0) {
|
|
const tip = `${gs.edit} ${gs.edit === 1 ? 'Datei' : 'Dateien'} geändert`;
|
|
parts.push(`<span class="git-badge git-edit" title="${esc(tip)}">~${gs.edit}</span>`);
|
|
}
|
|
if (gs.delete > 0) {
|
|
const tip = `${gs.delete} ${gs.delete === 1 ? 'Datei' : 'Dateien'} gelöscht`;
|
|
parts.push(`<span class="git-badge git-delete" title="${esc(tip)}">-${gs.delete}</span>`);
|
|
}
|
|
if (gs.outgoing > 0) {
|
|
const tip = `${gs.outgoing} ${gs.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> ${gs.outgoing}</span>`);
|
|
}
|
|
if (parts.length) {
|
|
const gitEl = document.createElement('span');
|
|
gitEl.className = 'card-meta-item card-git-status';
|
|
gitEl.innerHTML = parts.join(' ');
|
|
metaEl.appendChild(gitEl);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Menü zum title-row hinzufügen
|
|
const titleRow = card.querySelector('.card-title-row');
|
|
titleRow.insertAdjacentHTML('beforeend', `<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="chat"><i class="fa fa-comment"></i> Chat öffnen</button>
|
|
<button class="card-menu-item" data-action="openclaw-import"><i class="fa fa-download"></i> OpenClaw Import</button>
|
|
</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';
|
|
}
|
|
|
|
// Card selbst klickbar wenn Agent zugewiesen
|
|
if (agent) {
|
|
card.classList.add('clickable');
|
|
card.addEventListener('click', () => navigateTo('chat', { id: s.sessionId }));
|
|
}
|
|
|
|
const menuTrigger = card.querySelector('.card-menu-trigger');
|
|
const menuDropdown = card.querySelector('.card-menu-dropdown');
|
|
menuTrigger.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
_openCardMenu(menuTrigger, menuDropdown);
|
|
});
|
|
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 });
|
|
});
|
|
menuDropdown.querySelector('[data-action="openclaw-import"]').addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
menuDropdown.hidden = true;
|
|
showOpenClawImportModal(s.sessionId);
|
|
});
|
|
grid.appendChild(card);
|
|
}
|
|
|
|
|
|
|
|
// Neue Session Button
|
|
document.getElementById('newSessionBtn').addEventListener('click', () => navigateTo('session'));
|
|
|
|
// Agents
|
|
const agentGrid = document.getElementById('agentCards');
|
|
agentGrid.innerHTML = '';
|
|
|
|
for (const a of agents) {
|
|
const skills = a.skills || [];
|
|
const card = buildAgentCard(a, skills);
|
|
|
|
// Menü zum title-row hinzufügen
|
|
const titleRow = card.querySelector('.card-title-row');
|
|
const menuHtml = `<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>`;
|
|
titleRow.insertAdjacentHTML('beforeend', menuHtml);
|
|
const menuTrigger = card.querySelector('.card-menu-trigger');
|
|
const menuDropdown = card.querySelector('.card-menu-dropdown');
|
|
menuTrigger.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
_openCardMenu(menuTrigger, menuDropdown);
|
|
});
|
|
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 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();
|
|
_openCardMenu(menuTrigger, menuDropdown);
|
|
});
|
|
|
|
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'));
|
|
|
|
// Telegram Bots
|
|
const tgGrid = document.getElementById('telegramBotCards');
|
|
tgGrid.innerHTML = '';
|
|
|
|
for (const bot of telegramBots) {
|
|
const card = buildTelegramBotCard(bot);
|
|
|
|
// Card klickbar → direkt zur Edit-Seite
|
|
card.classList.add('clickable');
|
|
card.addEventListener('click', () => navigateTo('telegram-bot', { id: bot.botId }));
|
|
|
|
tgGrid.appendChild(card);
|
|
}
|
|
|
|
document.getElementById('newTelegramBotBtn').addEventListener('click', () => {
|
|
navigateTo('telegram-bot');
|
|
});
|
|
|
|
// Shared Links
|
|
const sharedSection = document.getElementById('sharedLinksSection');
|
|
const sharedGrid = document.getElementById('sharedLinksCards');
|
|
if (shares.length > 0) {
|
|
sharedSection.hidden = false;
|
|
sharedGrid.innerHTML = '';
|
|
for (const s of shares) {
|
|
const card = document.createElement('div');
|
|
card.className = 'card card-compact';
|
|
const shareUrl = `${location.origin}/#shared/${s.shareToken}`;
|
|
const modeLabel = s.includeContext ? 'Thread' : 'Einzeln';
|
|
const roleIcon = s.role === 'user' ? 'fa-user' : 'fa-cog';
|
|
|
|
card.innerHTML = `
|
|
<div class="card-body">
|
|
<div class="card-title-row">
|
|
<span class="card-title"><i class="fa ${roleIcon}"></i> ${esc(s.preview || '(leer)')}</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="copy"><i class="fa fa-clipboard"></i> Link kopieren</button>
|
|
<button class="card-menu-item" data-action="open"><i class="fa fa-external-link"></i> Öffnen</button>
|
|
<button class="card-menu-item" data-action="delete"><i class="fa fa-trash"></i> Löschen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-meta">
|
|
${s.sessionName ? `<span class="card-meta-item"><i class="fa fa-comment"></i> ${esc(s.sessionName)}</span>` : ''}
|
|
<span class="card-meta-item"><i class="fa fa-${s.includeContext ? 'comments' : 'comment-o'}"></i> ${modeLabel}</span>
|
|
<span class="card-meta-item"><i class="fa fa-clock-o"></i> ${formatDate(s.createdAt)}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const menuTrigger = card.querySelector('.card-menu-trigger');
|
|
const menuDropdown = card.querySelector('.card-menu-dropdown');
|
|
menuTrigger.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
_openCardMenu(menuTrigger, menuDropdown);
|
|
});
|
|
|
|
menuDropdown.querySelector('[data-action="copy"]').addEventListener('click', async (e) => {
|
|
e.stopPropagation();
|
|
menuDropdown.hidden = true;
|
|
await navigator.clipboard.writeText(shareUrl);
|
|
showToast('Link kopiert!');
|
|
});
|
|
menuDropdown.querySelector('[data-action="open"]').addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
menuDropdown.hidden = true;
|
|
window.open(shareUrl, '_blank');
|
|
});
|
|
menuDropdown.querySelector('[data-action="delete"]').addEventListener('click', async (e) => {
|
|
e.stopPropagation();
|
|
menuDropdown.hidden = true;
|
|
await apiDeleteShare(s.shareToken);
|
|
card.remove();
|
|
if (sharedGrid.children.length === 0) sharedSection.hidden = true;
|
|
showToast('Link gelöscht');
|
|
});
|
|
|
|
sharedGrid.appendChild(card);
|
|
}
|
|
}
|
|
},
|
|
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 _openCardMenu(trigger, dropdown) {
|
|
closeAllCardMenus();
|
|
const rect = trigger.getBoundingClientRect();
|
|
dropdown.hidden = false;
|
|
const dropRect = dropdown.getBoundingClientRect();
|
|
dropdown.style.top = `${rect.bottom + 4}px`;
|
|
dropdown.style.left = `${rect.right - dropRect.width}px`;
|
|
if (rect.bottom + 4 + dropRect.height > window.innerHeight) {
|
|
dropdown.style.top = `${rect.top - dropRect.height - 4}px`;
|
|
}
|
|
const finalRect = dropdown.getBoundingClientRect();
|
|
if (finalRect.left < 0) dropdown.style.left = '8px';
|
|
}
|
|
|
|
|