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.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.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 => `
-
-
-
-
- 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.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.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();
-}