// ============ THEME ============ const THEME_STORAGE_KEY = 'agentdash-theme'; const themeToggleBtn = document.getElementById('theme-toggle'); const systemThemeMedia = window.matchMedia('(prefers-color-scheme: dark)'); function getSystemTheme() { return systemThemeMedia.matches ? 'dark' : 'light'; } function getSavedTheme() { try { const saved = localStorage.getItem(THEME_STORAGE_KEY); return saved === 'light' || saved === 'dark' ? saved : null; } catch { return null; } } function setSavedTheme(theme) { try { localStorage.setItem(THEME_STORAGE_KEY, theme); } catch { // Ignore localStorage errors (privacy mode, quota, etc.). } } function updateThemeToggleLabel() { if (!themeToggleBtn) return; const isDarkTheme = document.documentElement.getAttribute('data-theme') === 'dark'; const nextTheme = isDarkTheme ? 'light' : 'dark'; themeToggleBtn.textContent = isDarkTheme ? 'Light Mode' : 'Dark Mode'; themeToggleBtn.setAttribute('aria-label', `Switch to ${nextTheme} mode`); } function applyTheme(theme, { persist = false } = {}) { document.documentElement.setAttribute('data-theme', theme); if (persist) setSavedTheme(theme); updateThemeToggleLabel(); if (usageStats) renderUsageCharts(); } function initTheme() { const savedTheme = getSavedTheme(); applyTheme(savedTheme || getSystemTheme()); if (themeToggleBtn) { themeToggleBtn.addEventListener('click', () => { const currentTheme = document.documentElement.getAttribute('data-theme') || getSystemTheme(); const nextTheme = currentTheme === 'dark' ? 'light' : 'dark'; applyTheme(nextTheme, { persist: true }); }); } systemThemeMedia.addEventListener('change', (event) => { if (!getSavedTheme()) { applyTheme(event.matches ? 'dark' : 'light'); } }); } // ============ STATE ============ 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 allTasks = []; let taskFilters = { search: '', status: '', assignee: '', priority: '' }; async function loadTasks() { const res = await fetch('/api/tasks'); allTasks = await res.json(); // Populate assignee filter dropdown const assigneeFilter = document.getElementById('filter-assignee'); if (assigneeFilter) { const assignees = [...new Set(allTasks.map(t => t.assignee).filter(Boolean))]; assigneeFilter.innerHTML = '' + assignees.map(a => ``).join(''); } applyTaskFilters(); } function applyTaskFilters() { let filtered = [...allTasks]; // Search filter if (taskFilters.search) { const search = taskFilters.search.toLowerCase(); filtered = filtered.filter(t => t.title.toLowerCase().includes(search) || (t.description && t.description.toLowerCase().includes(search)) ); } // Status filter if (taskFilters.status) { filtered = filtered.filter(t => t.status === taskFilters.status); } // Assignee filter if (taskFilters.assignee) { filtered = filtered.filter(t => t.assignee === taskFilters.assignee); } // Priority filter if (taskFilters.priority) { filtered = filtered.filter(t => t.priority === taskFilters.priority); } // Reset columns Object.keys(COLUMNS).forEach((status) => { COLUMNS[status].tasks = []; }); // Distribute filtered tasks to columns filtered.forEach((task) => { if (COLUMNS[task.status]) { COLUMNS[task.status].tasks.push(task); } }); renderBoard(); } function initTaskFilters() { const searchInput = document.getElementById('task-search'); const statusFilter = document.getElementById('filter-status'); const assigneeFilter = document.getElementById('filter-assignee'); const priorityFilter = document.getElementById('filter-priority'); const clearBtn = document.getElementById('clear-filters'); if (searchInput) { searchInput.addEventListener('input', (e) => { taskFilters.search = e.target.value; applyTaskFilters(); }); } if (statusFilter) { statusFilter.addEventListener('change', (e) => { taskFilters.status = e.target.value; applyTaskFilters(); }); } if (assigneeFilter) { assigneeFilter.addEventListener('change', (e) => { taskFilters.assignee = e.target.value; applyTaskFilters(); }); } if (priorityFilter) { priorityFilter.addEventListener('change', (e) => { taskFilters.priority = e.target.value; applyTaskFilters(); }); } if (clearBtn) { clearBtn.addEventListener('click', () => { taskFilters = { search: '', status: '', assignee: '', priority: '' }; if (searchInput) searchInput.value = ''; if (statusFilter) statusFilter.value = ''; if (assigneeFilter) assigneeFilter.value = ''; if (priorityFilter) priorityFilter.value = ''; applyTaskFilters(); }); } } 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 = `

${agent.emoji || '🤖'} ${escapeHtml(agent.name)}

${agent.status}
🧠 ${agent.model || 'unknown'} ⏰ ${agent.lastActivity ? new Date(agent.lastActivity).toLocaleString() : 'Never'}
📋 ${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'}
`${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' }), }); initDragAndDrop(); 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(); initDragAndDrop(); loadTasks(); }); initTaskFilters(); populateAgentDropdown(); initDragAndDrop(); 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 = `

${agent.emoji || '🤖'} ${escapeHtml(agent.name)}

${agent.status}
🧠 ${agent.model || 'unknown'} ⏰ ${agent.lastActivity ? new Date(agent.lastActivity).toLocaleString() : 'Never'}
📋 ${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'}
`${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() { // Try real usage first, fallback to tracked usage try { const from = document.getElementById("usage-from")?.value; const to = document.getElementById("usage-to")?.value; let url = "/api/usage/real"; const params = []; if (from) params.push(`from=${from}`); if (to) params.push(`to=${to}`); if (params.length) url += `?${params.join("&")}`; const realRes = await fetch(url); if (realRes.ok) { const realData = await realRes.json(); usageStats = { totalRequests: Object.values(realData.models || {}).reduce((sum, m) => sum + (m.requests || 0), 0), totalTokens: realData.totals?.total || 0, totalCost: realData.totals?.cost || 0, agents: realData.agents, models: realData.models }; renderUsageStats(); if (typeof renderRealUsageCharts === 'function') { renderRealUsageCharts(realData); } return; } } catch (e) { console.warn("Real usage not available, falling back"); } 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 renderRealUsageCharts(data) { if (typeof Chart === 'undefined') return; // Agent chart const agentCanvas = document.getElementById('chart-agent'); if (agentCanvas && data.agents) { const agents = Object.entries(data.agents) .filter(([_, v]) => v.total > 0) .sort((a, b) => b[1].total - a[1].total) .slice(0, 10); new Chart(agentCanvas, { type: 'bar', data: { labels: agents.map(([k]) => k), datasets: [{ label: 'Tokens', data: agents.map(([_, v]) => v.total), backgroundColor: 'rgba(54, 162, 235, 0.6)' }] }, options: { responsive: true, indexAxis: 'y' } }); } // Provider grid const grid = document.getElementById('provider-grid'); if (grid && data.models) { grid.innerHTML = Object.entries(data.models) .map(([model, stats]) => `

${model}

Requests: ${stats.requests || 0}

Tokens: ${(stats.input || 0) + (stats.output || 0)}

`).join(''); } } 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/real?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/real?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", () => { const CURRENT_PAGE = document.body?.dataset?.page || "tasks"; initTheme(); setupModalBackdropClose(); if (CURRENT_PAGE === 'tasks') initTasksPage(); if (CURRENT_PAGE === 'wiki') initWikiPage(); if (CURRENT_PAGE === 'agents') initAgentsPage(); if (CURRENT_PAGE === 'usage') initUsagePage(); if (CURRENT_PAGE === 'gitea') initGiteaPage(); }); // ============ GITEA DASHBOARD ============ let giteaData = { repos: [], reviews: [], activity: [] }; // Tab switching document.querySelectorAll('.gitea-tabs .tab-btn').forEach(btn => { btn.addEventListener('click', () => { // Update active tab button document.querySelectorAll('.gitea-tabs .tab-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); // Show corresponding content const tabName = btn.dataset.tab; document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); document.getElementById(`${tabName}-tab`).classList.add('active'); // Load data for tab if (tabName === 'swarm') loadGiteaSwarm(); else if (tabName === 'reviews') loadGiteaReviews(); else if (tabName === 'activity') loadGiteaActivity(); }); }); async function loadGiteaSwarm() { try { const response = await fetch('/api/gitea/swarm'); const repos = await response.json(); giteaData.repos = repos; // Update stats const totalRepos = repos.length; const totalPRs = repos.reduce((sum, r) => sum + r.open_prs, 0); const totalIssues = repos.reduce((sum, r) => sum + r.open_issues, 0); const totalBranches = repos.reduce((sum, r) => sum + r.branches, 0); document.getElementById('total-repos').textContent = totalRepos; document.getElementById('total-prs').textContent = totalPRs; document.getElementById('total-issues').textContent = totalIssues; document.getElementById('total-branches').textContent = totalBranches; // Render repo list const repoList = document.getElementById('repo-list'); if (repos.length === 0) { repoList.innerHTML = '

No repositories found

'; return; } repoList.innerHTML = repos.map(repo => `

${repo.name}

⭐ ${repo.stars} 🍴 ${repo.forks}
🔀 ${repo.open_prs} PRs 🐛 ${repo.open_issues} Issues 🌿 ${repo.branches} Branches
`).join(''); } catch (error) { console.error('Error loading Gitea swarm:', error); document.getElementById('repo-list').innerHTML = `

Failed to load repositories: ${error.message}

`; } } async function loadGiteaReviews() { try { const response = await fetch('/api/gitea/reviews'); const reviews = await response.json(); giteaData.reviews = reviews; const reviewsList = document.getElementById('reviews-list'); if (reviews.length === 0) { reviewsList.innerHTML = '

No pending reviews

'; return; } reviewsList.innerHTML = reviews.map(pr => `
${pr.repo} #${pr.pr_number}

${pr.pr_title}

by ${pr.author} ${new Date(pr.created_at).toLocaleDateString()}
${pr.labels.map(label => `${label.name}` ).join('')}
${pr.draft ? 'Draft' : ''} ${!pr.mergeable ? 'Merge Conflict' : ''}
`).join(''); } catch (error) { console.error('Error loading Gitea reviews:', error); document.getElementById('reviews-list').innerHTML = `

Failed to load reviews: ${error.message}

`; } } async function loadGiteaActivity() { try { const response = await fetch('/api/gitea/activity'); const activities = await response.json(); giteaData.activity = activities; const activityFeed = document.getElementById('activity-feed'); if (activities.length === 0) { activityFeed.innerHTML = '

No recent activity

'; return; } activityFeed.innerHTML = activities.map(act => `
${getActivityIcon(act.op_type)}
${act.repo} ${timeAgo(act.created_at)}
${act.content || act.op_type}
`).join(''); } catch (error) { console.error('Error loading Gitea activity:', error); document.getElementById('activity-feed').innerHTML = `

Failed to load activity: ${error.message}

`; } } function getActivityIcon(type) { const icons = { 'create': '✨', 'push': '📤', 'merge': '🔀', 'close': '✅', 'reopen': '🔄', 'comment': '💬', 'label': '🏷️', 'milestone': '🎯', 'assign': '👤', 'review': '👀', 'release': '🚀' }; return icons[type] || '📝'; } function timeAgo(dateString) { const now = new Date(); const date = new Date(dateString); const seconds = Math.floor((now - date) / 1000); if (seconds < 60) return 'just now'; if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`; return date.toLocaleDateString(); } // Auto-load Gitea data on page load if (document.querySelector('.gitea-dashboard')) { loadGiteaSwarm(); // Refresh every 30 seconds setInterval(() => { const activeTab = document.querySelector('.gitea-tabs .tab-btn.active'); if (activeTab) { const tabName = activeTab.dataset.tab; if (tabName === 'swarm') loadGiteaSwarm(); else if (tabName === 'reviews') loadGiteaReviews(); else if (tabName === 'activity') loadGiteaActivity(); } }, 30000); } // ============ GITEA PAGE ============ let giteaData = { repos: [], prs: [], activity: [] }; function initGiteaPage() { loadGiteaData(); document.getElementById('refresh-gitea')?.addEventListener('click', loadGiteaData); // Tab switching document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', (e) => { const tab = e.target.dataset.tab; document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); e.target.classList.add('active'); document.getElementById(`tab-${tab}`)?.classList.add('active'); }); }); } async function loadGiteaData() { try { const [swarmRes, reviewsRes, activityRes] = await Promise.all([ fetch('/api/gitea/swarm'), fetch('/api/gitea/reviews'), fetch('/api/gitea/activity') ]); giteaData.repos = await swarmRes.json(); giteaData.prs = await reviewsRes.json(); giteaData.activity = await activityRes.json(); renderGiteaStats(); renderGiteaRepos(); renderGiteaPRs(); renderGiteaActivity(); } catch (err) { console.error('Failed to load Gitea data:', err); } } function renderGiteaStats() { document.getElementById('stat-repos').textContent = giteaData.repos.length; const totalPRs = giteaData.repos.reduce((sum, r) => sum + (r.open_prs || 0), 0); document.getElementById('stat-prs').textContent = totalPRs; const pendingReviews = giteaData.prs.filter(p => p.review_required).length; document.getElementById('stat-reviews').textContent = pendingReviews; } function renderGiteaRepos() { const grid = document.getElementById('repos-grid'); if (!grid) return; if (!giteaData.repos.length) { grid.innerHTML = '

No repositories found

'; return; } grid.innerHTML = giteaData.repos.map(repo => `

${escapeHtml(repo.name)}

${escapeHtml(repo.full_name || '')}

⭐ ${repo.stars || 0} 🔀 ${repo.open_prs || 0} 🐛 ${repo.open_issues || 0}
Updated: ${repo.updated_at ? new Date(repo.updated_at).toLocaleDateString() : 'N/A'}
View →
`).join(''); } function renderGiteaPRs() { const list = document.getElementById('prs-list'); if (!list) return; // Collect all PRs from all repos const allPRs = []; giteaData.repos.forEach(repo => { if (repo.prs) { repo.prs.forEach(pr => { allPRs.push({ ...pr, repo: repo.name }); }); } }); if (!allPRs.length) { list.innerHTML = '

No open pull requests

'; return; } list.innerHTML = allPRs.map(pr => `
#${pr.number} ${escapeHtml(pr.title)} ${pr.state}
📦 ${pr.repo} 👤 ${pr.user?.login || 'Unknown'}
`).join(''); } function renderGiteaActivity() { const list = document.getElementById('activity-list'); if (!list) return; if (!giteaData.activity || !giteaData.activity.length) { list.innerHTML = '

No recent activity

'; return; } list.innerHTML = giteaData.activity.slice(0, 20).map(activity => `
${activity.type || '📝'} ${escapeHtml(activity.message || activity.repo?.name || 'Activity')} ${activity.created_at ? new Date(activity.created_at).toLocaleString() : ''}
`).join(''); } // ============ DRAG AND DROP ============ let draggedTask = null; function initDragAndDrop() { const board = document.getElementById('board'); if (!board) return; // Add drag listeners to columns document.querySelectorAll('.column').forEach(column => { column.addEventListener('dragover', handleDragOver); column.addEventListener('drop', handleDrop); column.addEventListener('dragleave', handleDragLeave); }); } function handleDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('drag-over'); } function handleDragLeave(e) { e.currentTarget.classList.remove('drag-over'); } async function handleDrop(e) { e.preventDefault(); const column = e.currentTarget; column.classList.remove('drag-over'); if (!draggedTask) return; const newStatus = column.dataset.status; if (newStatus === draggedTask.status) return; // Update task status via API try { await fetch(`/api/tasks/${draggedTask.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: newStatus }) }); // Reload tasks await initDragAndDrop(); loadTasks(); } catch (err) { console.error('Failed to update task:', err); } draggedTask = null; } function makeTaskCardDraggable(cardEl, task) { cardEl.draggable = true; cardEl.addEventListener('dragstart', (e) => { draggedTask = task; cardEl.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; }); cardEl.addEventListener('dragend', () => { cardEl.classList.remove('dragging'); }); }