diff --git a/public/app.js b/public/app.js index 2ee8fed..4359708 100644 --- a/public/app.js +++ b/public/app.js @@ -68,112 +68,6 @@ const COLUMNS = { }; 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; @@ -222,41 +116,13 @@ function renderBoard() { 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'}

+
+

${escapeHtml(task.title)}

+ ${task.priority}
-
-

🛠️ 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(' ')}

+

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

+

${task.assignee || 'Unassigned'}

+

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

-
- - -
- `${escapeHtml(tool)}`).join('') : 'No 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` : ''} @@ -754,35 +597,6 @@ function initAgentsPage() { // ============ 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; @@ -807,47 +621,7 @@ async function loadUsage() { } } -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(''); - } -} +function renderUsageStats() { const requestsEl = document.getElementById('stat-requests'); const tokensEl = document.getElementById('stat-tokens'); const costEl = document.getElementById('stat-cost'); @@ -993,7 +767,7 @@ function initUsagePage() { exportJsonBtn.addEventListener('click', () => { const fromValue = fromInput.value; const toValue = toInput.value; - let url = '/api/usage/export/real?format=json'; + let url = '/api/usage/export?format=json'; if (fromValue) url += `&from=${fromValue}`; if (toValue) url += `&to=${toValue}`; window.open(url, '_blank'); @@ -1002,7 +776,7 @@ function initUsagePage() { exportCsvBtn.addEventListener('click', () => { const fromValue = fromInput.value; const toValue = toInput.value; - let url = '/api/usage/export/real?format=csv'; + let url = '/api/usage/export?format=csv'; if (fromValue) url += `&from=${fromValue}`; if (toValue) url += `&to=${toValue}`; window.open(url, '_blank'); @@ -1044,492 +818,4 @@ document.addEventListener("DOMContentLoaded", () => { 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'); - }); -} - -// ============ WEBSOCKET REAL-TIME UPDATES ============ -let ws = null; -let reconnectAttempts = 0; -const MAX_RECONNECT = 5; - -function initWebSocket() { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}`; - - ws = new WebSocket(wsUrl); - - ws.onopen = () => { - console.log('WebSocket connected'); - reconnectAttempts = 0; - }; - - ws.onmessage = (event) => { - try { - const { type, payload } = JSON.parse(event.data); - handleWebSocketMessage(type, payload); - } catch (err) { - console.error('WebSocket message error:', err); - } - }; - - ws.onclose = () => { - console.log('WebSocket disconnected'); - if (reconnectAttempts < MAX_RECONNECT) { - reconnectAttempts++; - setTimeout(initWebSocket, 2000 * reconnectAttempts); - } - }; - - ws.onerror = (err) => { - console.error('WebSocket error:', err); - }; -} - -function handleWebSocketMessage(type, payload) { - switch (type) { - case 'task_created': - if (typeof loadTasks === 'function' && CURRENT_PAGE === 'tasks') { - loadTasks(); - showNotification(`New task: ${payload.title}`); - } - break; - - case 'task_updated': - if (typeof loadTasks === 'function' && CURRENT_PAGE === 'tasks') { - loadTasks(); - } - break; - - case 'task_deleted': - if (typeof loadTasks === 'function' && CURRENT_PAGE === 'tasks') { - loadTasks(); - showNotification('Task deleted'); - } - break; - - case 'usage_updated': - if (typeof loadUsage === 'function' && CURRENT_PAGE === 'usage') { - loadUsage(); - } - break; - - default: - console.log('Unknown WebSocket message:', type); - } -} - -function showNotification(message) { - // Create a simple toast notification - const toast = document.createElement('div'); - toast.className = 'toast-notification'; - toast.textContent = message; - document.body.appendChild(toast); - - setTimeout(() => { - toast.classList.add('fade-out'); - setTimeout(() => toast.remove(), 300); - }, 3000); -} - -// Initialize WebSocket on page load -if (typeof CURRENT_PAGE !== 'undefined') { - initWebSocket(); -}