// ============ STATE ============ const CURRENT_PAGE = document.body?.dataset?.page || 'tasks'; const COLUMNS = { Backlog: { title: '📋 Backlog', tasks: [] }, Todo: { title: '📝 Todo', tasks: [] }, 'In Progress': { title: '🔄 In Progress', tasks: [] }, Review: { title: '👀 Review', tasks: [] }, Done: { title: '✅ Done', tasks: [] }, }; let wikiPages = []; let currentWikiPage = null; let allAgents = []; let usageStats = null; let providerChart = null; let agentChart = null; // ============ TASK DASHBOARD ============ async function loadTasks() { const res = await fetch('/api/tasks'); const tasks = await res.json(); Object.keys(COLUMNS).forEach((status) => { COLUMNS[status].tasks = []; }); tasks.forEach((task) => { if (COLUMNS[task.status]) { COLUMNS[task.status].tasks.push(task); } }); renderBoard(); } function renderBoard() { const board = document.getElementById('board'); if (!board) return; board.innerHTML = ''; Object.entries(COLUMNS).forEach(([status, column]) => { const columnEl = document.createElement('div'); columnEl.className = 'column'; columnEl.innerHTML = `

${column.title}

${column.tasks.length}
`; const cardsEl = columnEl.querySelector('.cards'); column.tasks.forEach((task) => { const cardEl = document.createElement('div'); cardEl.className = 'card'; cardEl.innerHTML = `

${escapeHtml(task.title)}

${task.priority}

${escapeHtml(task.description || '')}

${task.assignee || 'Unassigned'}

${task.tags.map((t) => `${escapeHtml(t)}`).join(' ')}

`; const checkbox = cardEl.querySelector('.card-check'); checkbox.addEventListener('change', async () => { await fetch(`/api/tasks/${task.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'Done' }), }); loadTasks(); }); cardsEl.appendChild(cardEl); }); board.appendChild(columnEl); }); } async function populateAgentDropdown() { try { const res = await fetch('/api/agents'); const agents = await res.json(); const select = document.getElementById('assignee'); if (!select) return; const firstOption = select.options[0]; select.innerHTML = ''; select.appendChild(firstOption); agents.forEach((agent) => { const option = document.createElement('option'); option.value = agent.name; option.textContent = agent.name; select.appendChild(option); }); } catch (err) { console.error('Failed to load agents for dropdown:', err); } } function initTasksPage() { const taskForm = document.getElementById('task-form'); if (!taskForm) return; taskForm.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const tagsValue = formData.get('tags'); const task = { title: formData.get('title'), description: formData.get('description'), assignee: formData.get('assignee'), priority: formData.get('priority'), status: formData.get('status') || 'Backlog', tags: (tagsValue || '').split(',').map((t) => t.trim()).filter((t) => t), }; await fetch('/api/tasks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(task), }); e.target.reset(); loadTasks(); }); populateAgentDropdown(); loadTasks(); } // ============ WIKI ============ async function loadWiki() { try { const res = await fetch('/api/wiki'); wikiPages = await res.json(); renderWikiList(); } catch (err) { console.error('Failed to load wiki:', err); } } function renderWikiList(filter = '') { const wikiList = document.getElementById('wiki-list'); if (!wikiList) return; wikiList.innerHTML = ''; const filtered = filter ? wikiPages.filter((p) => p.title.toLowerCase().includes(filter.toLowerCase()) || p.filename.toLowerCase().includes(filter.toLowerCase())) : wikiPages; filtered.forEach((page) => { const itemEl = document.createElement('div'); itemEl.className = `wiki-item${currentWikiPage && currentWikiPage.filename === page.filename ? ' active' : ''}`; itemEl.innerHTML = `

${escapeHtml(page.title)}

${new Date(page.modified).toLocaleDateString()}

`; itemEl.addEventListener('click', () => selectWikiPage(page.filename)); wikiList.appendChild(itemEl); }); } async function selectWikiPage(filename) { try { const res = await fetch(`/api/wiki/${filename}`); if (!res.ok) throw new Error('Page not found'); currentWikiPage = await res.json(); const titleEl = document.getElementById('wiki-page-title'); const actionsEl = document.getElementById('wiki-page-actions'); const contentEl = document.getElementById('wiki-content'); const editorEl = document.getElementById('wiki-editor'); const searchEl = document.getElementById('wiki-search'); if (!titleEl || !actionsEl || !contentEl || !editorEl || !searchEl) return; titleEl.textContent = currentWikiPage.metadata.title || filename; actionsEl.style.display = 'flex'; contentEl.style.display = 'block'; editorEl.style.display = 'none'; if (typeof marked !== 'undefined') { contentEl.innerHTML = marked.parse(currentWikiPage.content); } else { contentEl.innerHTML = `
${escapeHtml(currentWikiPage.content)}
`; } renderWikiList(searchEl.value); } catch (err) { console.error('Failed to load wiki page:', err); } } function initWikiPage() { const searchInput = document.getElementById('wiki-search'); const newBtn = document.getElementById('wiki-new-btn'); const editBtn = document.getElementById('wiki-edit-btn'); const saveBtn = document.getElementById('wiki-save-btn'); const cancelBtn = document.getElementById('wiki-cancel-btn'); const deleteBtn = document.getElementById('wiki-delete-btn'); if (!searchInput || !newBtn || !editBtn || !saveBtn || !cancelBtn || !deleteBtn) return; searchInput.addEventListener('input', (e) => { renderWikiList(e.target.value); }); newBtn.addEventListener('click', async () => { const title = prompt('Enter page title:'); if (!title) return; try { const res = await fetch('/api/wiki', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }), }); if (res.ok) { const data = await res.json(); await loadWiki(); selectWikiPage(data.filename); } } catch (err) { console.error('Failed to create wiki page:', err); } }); editBtn.addEventListener('click', () => { if (!currentWikiPage) return; const contentEl = document.getElementById('wiki-content'); const editorEl = document.getElementById('wiki-editor'); const titleEl = document.getElementById('wiki-edit-title'); const editContentEl = document.getElementById('wiki-edit-content'); if (!contentEl || !editorEl || !titleEl || !editContentEl) return; contentEl.style.display = 'none'; editorEl.style.display = 'block'; titleEl.value = currentWikiPage.metadata.title || ''; editContentEl.value = currentWikiPage.content; }); saveBtn.addEventListener('click', async () => { if (!currentWikiPage) return; const editContentEl = document.getElementById('wiki-edit-content'); if (!editContentEl) return; try { const res = await fetch(`/api/wiki/${currentWikiPage.filename}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: editContentEl.value }), }); if (res.ok) { await selectWikiPage(currentWikiPage.filename); } } catch (err) { console.error('Failed to save wiki page:', err); } }); cancelBtn.addEventListener('click', () => { const editorEl = document.getElementById('wiki-editor'); const contentEl = document.getElementById('wiki-content'); if (!editorEl || !contentEl) return; editorEl.style.display = 'none'; contentEl.style.display = 'block'; }); deleteBtn.addEventListener('click', async () => { if (!currentWikiPage) return; if (!confirm(`Delete "${currentWikiPage.metadata.title || currentWikiPage.filename}"?`)) return; try { const res = await fetch(`/api/wiki/${currentWikiPage.filename}`, { method: 'DELETE', }); if (res.ok) { currentWikiPage = null; const pageTitle = document.getElementById('wiki-page-title'); const pageActions = document.getElementById('wiki-page-actions'); const wikiContent = document.getElementById('wiki-content'); if (pageTitle) pageTitle.textContent = 'Select a page'; if (pageActions) pageActions.style.display = 'none'; if (wikiContent) { wikiContent.innerHTML = '

📚 Select a wiki page from the sidebar or create a new one.

'; } await loadWiki(); } } catch (err) { console.error('Failed to delete wiki page:', err); } }); loadWiki(); } // ============ AGENTS ============ async function loadAgents() { try { const res = await fetch('/api/agents'); allAgents = await res.json(); renderAgents(); } catch (err) { console.error('Failed to load agents:', err); } } function renderAgents(filter = '', statusFilter = '') { const grid = document.getElementById('agents-grid'); if (!grid) return; grid.innerHTML = ''; let filtered = allAgents; if (filter) { filtered = filtered.filter((a) => a.name.toLowerCase().includes(filter.toLowerCase()) || (a.currentTask && a.currentTask.toLowerCase().includes(filter.toLowerCase())) ); } if (statusFilter) { filtered = filtered.filter((a) => a.status === statusFilter); } filtered.forEach((agent) => { const cardEl = document.createElement('div'); cardEl.className = 'agent-card'; const statusClass = `status-${agent.status}`; cardEl.innerHTML = `

${escapeHtml(agent.name)}

${agent.status}
📋 ${agent.workload} active task${agent.workload !== 1 ? 's' : ''}

📋 Current Task

${agent.currentTask || 'No active task'}

🛠️ Tools

${agent.tools.length ? agent.tools.slice(0, 5).map((tool) => `${escapeHtml(tool)}`).join('') : 'No tools'} ${agent.tools.length > 5 ? `+${agent.tools.length - 5} more` : ''}

📄 Recent Files

${agent.files.length ? agent.files.slice(0, 5).map((file) => `${escapeHtml(file)}`).join('') : 'No files'}
`; cardEl.querySelector('.agent-details-btn').addEventListener('click', () => showAgentDetails(agent)); cardEl.querySelector('.agent-assign-btn').addEventListener('click', () => showAssignModal(agent.name)); grid.appendChild(cardEl); }); } function showAgentDetails(agent) { const modal = document.getElementById('agent-modal'); const body = document.getElementById('modal-agent-body'); const title = document.getElementById('modal-agent-name'); if (!modal || !body || !title) return; title.textContent = agent.name; body.innerHTML = `

Status

${agent.status}

Workload

${agent.workload} active task${agent.workload !== 1 ? 's' : ''}

Current Task

${agent.currentTask || 'No active task'}

Active Tasks

Recently Completed

Tools

${agent.tools.length ? agent.tools.map((t) => `${escapeHtml(t)}`).join('') : 'No tools'}

Capabilities

${agent.capabilities.length ? agent.capabilities.map((c) => `${escapeHtml(c)}`).join('') : 'No capabilities defined'}
`; modal.classList.add('active'); } async function showAssignModal(agentName) { const modal = document.getElementById('assign-modal'); const agentNameEl = document.getElementById('assign-agent-name'); const select = document.getElementById('assign-task-select'); if (!modal || !agentNameEl || !select) return; agentNameEl.textContent = agentName; try { const res = await fetch('/api/tasks'); const tasks = await res.json(); const unassignedTasks = tasks.filter((t) => t.status !== 'Done' && (!t.assignee || t.assignee === '')); select.innerHTML = ''; unassignedTasks.forEach((task) => { const option = document.createElement('option'); option.value = task.id; option.textContent = `${task.title} (${task.priority})`; select.appendChild(option); }); select.dataset.agent = agentName; modal.classList.add('active'); } catch (err) { console.error('Failed to load tasks for assignment:', err); } } function initAgentsPage() { const searchInput = document.getElementById('agent-search'); const statusFilter = document.getElementById('agent-status-filter'); const closeBtn = document.getElementById('modal-close'); const assignCloseBtn = document.getElementById('assign-modal-close'); const confirmAssignBtn = document.getElementById('confirm-assign-btn'); if (!searchInput || !statusFilter || !closeBtn || !assignCloseBtn || !confirmAssignBtn) return; searchInput.addEventListener('input', (e) => { renderAgents(e.target.value, statusFilter.value); }); statusFilter.addEventListener('change', (e) => { renderAgents(searchInput.value, e.target.value); }); closeBtn.addEventListener('click', () => { const modal = document.getElementById('agent-modal'); if (modal) modal.classList.remove('active'); }); assignCloseBtn.addEventListener('click', () => { const modal = document.getElementById('assign-modal'); if (modal) modal.classList.remove('active'); }); confirmAssignBtn.addEventListener('click', async () => { const select = document.getElementById('assign-task-select'); if (!select) return; const taskId = select.value; const agentName = select.dataset.agent; if (!taskId) { alert('Please select a task'); return; } try { const res = await fetch(`/api/agents/${agentName}/assign`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId: Number.parseInt(taskId, 10) }), }); if (res.ok) { const modal = document.getElementById('assign-modal'); if (modal) modal.classList.remove('active'); await loadAgents(); } } catch (err) { console.error('Failed to assign task:', err); } }); loadAgents(); } // ============ USAGE ============ async function loadUsage() { const from = document.getElementById('usage-from')?.value; const to = document.getElementById('usage-to')?.value; let statsUrl = '/api/usage/stats'; const params = []; if (from) params.push(`from=${from}`); if (to) params.push(`to=${to}`); if (params.length) statsUrl += `?${params.join('&')}`; try { const statsRes = await fetch(statsUrl); usageStats = await statsRes.json(); const usageRes = await fetch('/api/usage'); const usageData = await usageRes.json(); renderUsageStats(); renderUsageCharts(); renderProviderDetails(usageData); } catch (err) { console.error('Failed to load usage:', err); } } function renderUsageStats() { const requestsEl = document.getElementById('stat-requests'); const tokensEl = document.getElementById('stat-tokens'); const costEl = document.getElementById('stat-cost'); if (!requestsEl || !tokensEl || !costEl || !usageStats) return; requestsEl.textContent = usageStats.totalRequests.toLocaleString(); tokensEl.textContent = usageStats.totalTokens.toLocaleString(); costEl.textContent = `$${usageStats.totalCost.toFixed(2)}`; } function renderUsageCharts() { if (!usageStats || typeof Chart === 'undefined') return; const providerCanvas = document.getElementById('chart-provider'); const agentCanvas = document.getElementById('chart-agent'); if (!providerCanvas || !agentCanvas) return; const providerCtx = providerCanvas.getContext('2d'); const agentCtx = agentCanvas.getContext('2d'); if (!providerCtx || !agentCtx) return; if (providerChart) providerChart.destroy(); const providerLabels = Object.keys(usageStats.byProvider); const providerData = providerLabels.map((p) => usageStats.byProvider[p].requests); providerChart = new Chart(providerCtx, { type: 'doughnut', data: { labels: providerLabels, datasets: [{ data: providerData, backgroundColor: ['#3498db', '#2ecc71', '#e74c3c', '#f39c12', '#9c27b0', '#00bcd4'], }], }, options: { responsive: true, plugins: { legend: { position: 'bottom', labels: { color: '#e0e0e0' }, }, }, }, }); if (agentChart) agentChart.destroy(); const agentLabels = Object.keys(usageStats.byAgent); const agentData = agentLabels.map((a) => usageStats.byAgent[a].requests); agentChart = new Chart(agentCtx, { type: 'bar', data: { labels: agentLabels, datasets: [{ label: 'Requests', data: agentData, backgroundColor: '#3498db', }], }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { color: '#e0e0e0' }, grid: { color: '#444' }, }, x: { ticks: { color: '#e0e0e0' }, grid: { color: '#444' }, }, }, }, }); } function renderProviderDetails(usageData) { const grid = document.getElementById('provider-grid'); if (!grid || !usageData || !usageStats) return; grid.innerHTML = ''; usageData.providers.forEach((provider) => { const providerEl = document.createElement('div'); providerEl.className = 'provider-card'; const providerUsage = usageStats.byProvider[provider.name] || { requests: 0, tokens: 0, cost: 0 }; providerEl.innerHTML = `

${escapeHtml(provider.name)}

Requests ${providerUsage.requests.toLocaleString()}
Tokens ${providerUsage.tokens.toLocaleString()}
Cost $${providerUsage.cost.toFixed(2)}
Models
${provider.models.map((model) => `
${escapeHtml(model.name)} ${escapeHtml(model.type)}
`).join('')}
`; grid.appendChild(providerEl); }); } function initUsagePage() { const fromInput = document.getElementById('usage-from'); const toInput = document.getElementById('usage-to'); const applyBtn = document.getElementById('usage-apply-filter'); const exportJsonBtn = document.getElementById('export-json'); const exportCsvBtn = document.getElementById('export-csv'); if (!fromInput || !toInput || !applyBtn || !exportJsonBtn || !exportCsvBtn) return; const to = new Date(); const from = new Date(); from.setDate(from.getDate() - 30); fromInput.value = from.toISOString().split('T')[0]; toInput.value = to.toISOString().split('T')[0]; applyBtn.addEventListener('click', loadUsage); exportJsonBtn.addEventListener('click', () => { const fromValue = fromInput.value; const toValue = toInput.value; let url = '/api/usage/export?format=json'; if (fromValue) url += `&from=${fromValue}`; if (toValue) url += `&to=${toValue}`; window.open(url, '_blank'); }); exportCsvBtn.addEventListener('click', () => { const fromValue = fromInput.value; const toValue = toInput.value; let url = '/api/usage/export?format=csv'; if (fromValue) url += `&from=${fromValue}`; if (toValue) url += `&to=${toValue}`; window.open(url, '_blank'); }); loadUsage(); } // ============ HELPERS ============ function escapeHtml(text) { if (typeof text !== 'string') return ''; const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }; return text.replace(/[&<>"']/g, (m) => map[m]); } function setupModalBackdropClose() { document.querySelectorAll('.modal').forEach((modal) => { modal.addEventListener('click', (e) => { if (e.target === modal) { modal.classList.remove('active'); } }); }); } // ============ INITIALIZATION ============ document.addEventListener('DOMContentLoaded', () => { setupModalBackdropClose(); if (CURRENT_PAGE === 'tasks') initTasksPage(); if (CURRENT_PAGE === 'wiki') initWikiPage(); if (CURRENT_PAGE === 'agents') initAgentsPage(); if (CURRENT_PAGE === 'usage') initUsagePage(); });