// ============ NAVIGATION ============
const navLinks = document.querySelectorAll('.nav-link');
const pages = document.querySelectorAll('.page');
navLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const targetPage = link.dataset.page;
// Update active nav link
navLinks.forEach(l => l.classList.remove('active'));
link.classList.add('active');
// Show target page
pages.forEach(page => {
page.classList.remove('active');
if (page.id === `page-${targetPage}`) {
page.classList.add('active');
}
});
// Load page data
if (targetPage === 'wiki') loadWiki();
if (targetPage === 'agents') loadAgents();
if (targetPage === 'usage') loadUsage();
});
});
// ============ TASK DASHBOARD ============
const COLUMNS = {
'Backlog': { title: '📋 Backlog', tasks: [] },
'Todo': { title: '📝 Todo', tasks: [] },
'In Progress': { title: '🔄 In Progress', tasks: [] },
'Review': { title: '👀 Review', tasks: [] },
'Done': { title: '✅ Done', tasks: [] }
};
async function loadTasks() {
const res = await fetch('/api/tasks');
const tasks = await res.json();
// Reset columns
Object.keys(COLUMNS).forEach(status => {
COLUMNS[status].tasks = [];
});
// Group tasks by status
tasks.forEach(task => {
if (COLUMNS[task.status]) {
COLUMNS[task.status].tasks.push(task);
}
});
renderBoard();
}
function renderBoard() {
const board = document.getElementById('board');
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(' ')}
`;
// Checkbox handler
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);
});
}
// Task form
document.getElementById('task-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const task = {
title: formData.get('title'),
description: formData.get('description'),
assignee: formData.get('assignee'),
priority: formData.get('priority'),
status: formData.get('status') || 'Backlog',
tags: formData.get('tags').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();
});
// Populate agent dropdown
async function populateAgentDropdown() {
try {
const res = await fetch('/api/agents');
const agents = await res.json();
const select = document.getElementById('assignee');
if (!select) return;
// Keep the first option ("Select agent...")
const firstOption = select.options[0];
select.innerHTML = '';
select.appendChild(firstOption);
// Add agent options
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);
}
}
// ============ WIKI ============
let wikiPages = [];
let currentWikiPage = null;
let isEditingWiki = false;
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');
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);
});
}
// Wiki search
document.getElementById('wiki-search').addEventListener('input', (e) => {
renderWikiList(e.target.value);
});
// New wiki page
document.getElementById('wiki-new-btn').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);
}
});
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();
// Update UI
document.getElementById('wiki-page-title').textContent = currentWikiPage.metadata.title || filename;
document.getElementById('wiki-page-actions').style.display = 'flex';
// Render markdown
const contentEl = document.getElementById('wiki-content');
contentEl.style.display = 'block';
document.getElementById('wiki-editor').style.display = 'none';
if (typeof marked !== 'undefined') {
contentEl.innerHTML = marked.parse(currentWikiPage.content);
} else {
contentEl.innerHTML = `${escapeHtml(currentWikiPage.content)}`;
}
// Update list selection
renderWikiList(document.getElementById('wiki-search').value);
} catch (err) {
console.error('Failed to load wiki page:', err);
}
}
// Edit wiki page
document.getElementById('wiki-edit-btn').addEventListener('click', () => {
if (!currentWikiPage) return;
isEditingWiki = true;
document.getElementById('wiki-content').style.display = 'none';
document.getElementById('wiki-editor').style.display = 'block';
document.getElementById('wiki-edit-title').value = currentWikiPage.metadata.title || '';
document.getElementById('wiki-edit-content').value = currentWikiPage.content;
});
// Save wiki page
document.getElementById('wiki-save-btn').addEventListener('click', async () => {
if (!currentWikiPage) return;
const content = document.getElementById('wiki-edit-content').value;
try {
const res = await fetch(`/api/wiki/${currentWikiPage.filename}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
if (res.ok) {
isEditingWiki = false;
await selectWikiPage(currentWikiPage.filename);
}
} catch (err) {
console.error('Failed to save wiki page:', err);
}
});
// Cancel edit
document.getElementById('wiki-cancel-btn').addEventListener('click', () => {
isEditingWiki = false;
document.getElementById('wiki-editor').style.display = 'none';
document.getElementById('wiki-content').style.display = 'block';
});
// Delete wiki page
document.getElementById('wiki-delete-btn').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;
document.getElementById('wiki-page-title').textContent = 'Select a page';
document.getElementById('wiki-page-actions').style.display = 'none';
document.getElementById('wiki-content').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);
}
});
// ============ AGENTS ============
let allAgents = [];
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');
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.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'}
`;
// Details button
cardEl.querySelector('.agent-details-btn').addEventListener('click', () => showAgentDetails(agent));
// Assign button
cardEl.querySelector('.agent-assign-btn').addEventListener('click', () => showAssignModal(agent.name));
grid.appendChild(cardEl);
});
}
// Agent search
document.getElementById('agent-search').addEventListener('input', (e) => {
const statusFilter = document.getElementById('agent-status-filter').value;
renderAgents(e.target.value, statusFilter);
});
// Agent status filter
document.getElementById('agent-status-filter').addEventListener('change', (e) => {
const searchFilter = document.getElementById('agent-search').value;
renderAgents(searchFilter, e.target.value);
});
// Agent details modal
function showAgentDetails(agent) {
const modal = document.getElementById('agent-modal');
const body = document.getElementById('modal-agent-body');
document.getElementById('modal-agent-name').textContent = agent.name;
body.innerHTML = `
Workload
${agent.workload} active task${agent.workload !== 1 ? 's' : ''}
Current Task
${agent.currentTask || 'No active task'}
Active Tasks
${agent.activeTasks.length
? agent.activeTasks.map(t => `- ${escapeHtml(t.title)} ${t.priority}
`).join('')
: '- No active tasks
'}
Recently Completed
${agent.completedTasks.length
? agent.completedTasks.map(t => `- ${escapeHtml(t.title)}
`).join('')
: '- No completed tasks
'}
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');
}
// Close agent modal
document.getElementById('modal-close').addEventListener('click', () => {
document.getElementById('agent-modal').classList.remove('active');
});
// Assign task modal
async function showAssignModal(agentName) {
const modal = document.getElementById('assign-modal');
document.getElementById('assign-agent-name').textContent = agentName;
// Load unassigned tasks
try {
const res = await fetch('/api/tasks');
const tasks = await res.json();
const unassignedTasks = tasks.filter(t => t.status !== 'Done' && (!t.assignee || t.assignee === ''));
const select = document.getElementById('assign-task-select');
select.innerHTML = '';
unassignedTasks.forEach(task => {
const option = document.createElement('option');
option.value = task.id;
option.textContent = `${task.title} (${task.priority})`;
select.appendChild(option);
});
// Store agent name for assignment
select.dataset.agent = agentName;
modal.classList.add('active');
} catch (err) {
console.error('Failed to load tasks for assignment:', err);
}
}
// Close assign modal
document.getElementById('assign-modal-close').addEventListener('click', () => {
document.getElementById('assign-modal').classList.remove('active');
});
// Confirm assignment
document.getElementById('confirm-assign-btn').addEventListener('click', async () => {
const select = document.getElementById('assign-task-select');
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: parseInt(taskId) })
});
if (res.ok) {
document.getElementById('assign-modal').classList.remove('active');
await loadAgents();
await loadTasks();
}
} catch (err) {
console.error('Failed to assign task:', err);
}
});
// ============ USAGE ============
let usageStats = null;
let providerChart = null;
let agentChart = null;
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 {
// Load stats
const statsRes = await fetch(statsUrl);
usageStats = await statsRes.json();
// Load basic usage info
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() {
document.getElementById('stat-requests').textContent = usageStats.totalRequests.toLocaleString();
document.getElementById('stat-tokens').textContent = usageStats.totalTokens.toLocaleString();
document.getElementById('stat-cost').textContent = '$' + usageStats.totalCost.toFixed(2);
}
function renderUsageCharts() {
// Provider chart
const providerCtx = document.getElementById('chart-provider').getContext('2d');
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' }
}
}
}
});
// Agent chart
const agentCtx = document.getElementById('chart-agent').getContext('2d');
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');
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);
});
}
// Apply date filter
document.getElementById('usage-apply-filter').addEventListener('click', loadUsage);
// Export JSON
document.getElementById('export-json').addEventListener('click', () => {
const from = document.getElementById('usage-from').value;
const to = document.getElementById('usage-to').value;
let url = '/api/usage/export?format=json';
if (from) url += `&from=${from}`;
if (to) url += `&to=${to}`;
window.open(url, '_blank');
});
// Export CSV
document.getElementById('export-csv').addEventListener('click', () => {
const from = document.getElementById('usage-from').value;
const to = document.getElementById('usage-to').value;
let url = '/api/usage/export?format=csv';
if (from) url += `&from=${from}`;
if (to) url += `&to=${to}`;
window.open(url, '_blank');
});
// ============ HELPERS ============
function escapeHtml(text) {
if (typeof text !== 'string') return '';
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
}
// ============ INITIALIZATION ============
document.addEventListener('DOMContentLoaded', () => {
populateAgentDropdown();
loadTasks();
// Set default date range (last 30 days)
const to = new Date();
const from = new Date();
from.setDate(from.getDate() - 30);
document.getElementById('usage-from').value = from.toISOString().split('T')[0];
document.getElementById('usage-to').value = to.toISOString().split('T')[0];
});
// Close modals on outside click
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('active');
}
});
});