document.addEventListener('DOMContentLoaded', () => {
const mainContent = document.getElementById('main-content');
// Simple router
const routes = {
'#dashboard': '
Dashboard
',
'#councils': 'Councils
',
'#tasks': 'Tasks
',
'#repositories': 'Repositories
',
'#analysis': 'Analysis
',
};
function router() {
const hash = window.location.hash || '#dashboard';
const [route, param] = hash.split('/');
if (route === '#councils' && param) {
loadCouncilDetail(param);
} else if (route === '#tasks' && param) {
loadTaskDetail(param);
} else {
mainContent.innerHTML = routes[hash] || 'Page Not Found
';
loadContent(hash);
}
}
async function loadContent(hash) {
switch (hash) {
case '#dashboard':
loadDashboard();
break;
case '#councils':
loadCouncils();
break;
case '#tasks':
loadTasks();
break;
case '#repositories':
loadRepositories();
break;
case '#analysis':
loadAnalysis();
break;
}
}
const loadingSpinner = document.getElementById('loading-spinner');
let activeSpinners = 0;
function showSpinner() {
loadingSpinner.classList.remove('hidden');
}
function hideSpinner() {
loadingSpinner.classList.add('hidden');
}
function incSpinner() {
activeSpinners += 1;
if (activeSpinners === 1) showSpinner();
}
function decSpinner() {
if (activeSpinners > 0) {
activeSpinners -= 1;
}
if (activeSpinners === 0) hideSpinner();
}
async function apiFetch(endpoint) {
incSpinner();
try {
const authHeaders = getAuthHeaders();
const response = await fetch(`/api${endpoint}`, { headers: authHeaders });
if (!response.ok) {
throw new Error(`API request failed: ${response.statusText}`);
}
return response.json();
} finally {
decSpinner();
}
}
async function fetchText(url, options = {}) {
incSpinner();
try {
const authHeaders = getAuthHeaders();
const merged = {
...options,
headers: { ...(options.headers || {}), ...authHeaders },
};
const response = await fetch(url, merged);
if (!response.ok) {
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
}
return response.text();
} finally {
decSpinner();
}
}
function getAuthHeaders() {
const token = (localStorage.getItem('whoosh_token') || getCookie('whoosh_token') || '').trim();
if (!token) return {};
return { 'Authorization': `Bearer ${token}` };
}
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return '';
}
async function loadDashboard() {
const dashboardContent = document.getElementById('dashboard-content');
try {
const health = await apiFetch('/admin/health/details');
const metrics = await fetchText('/metrics');
// A real app would parse the metrics properly
dashboardContent.innerHTML = `
System Status
Status: ${health.status}
Version: ${health.version}
Metrics
${metrics.slice(0, 1000)}...
`;
} catch (error) {
dashboardContent.innerHTML = `Error loading dashboard: ${error.message}
`;
}
}
async function loadCouncils() {
const councilsContent = document.getElementById('councils-content');
try {
const data = await apiFetch('/v1/councils');
councilsContent.innerHTML = `
${data.councils.map(council => `
Status: ${council.status}
`).join('')}
`;
// Wire delete buttons for projects (councils)
const deleteBtns = document.querySelectorAll('.delete-project-btn');
deleteBtns.forEach(btn => {
btn.addEventListener('click', async (event) => {
const projectId = event.target.dataset.projectId;
const projectName = event.target.dataset.projectName || projectId;
if (!confirm(`Delete project "${projectName}"? This removes the council record.`)) return;
try {
await fetchText(`/api/v1/projects/${projectId}`, { method: 'DELETE' });
loadCouncils();
} catch (error) {
alert(`Error deleting project: ${error.message}`);
}
});
});
} catch (error) {
councilsContent.innerHTML = `Error loading councils: ${error.message}
`;
}
}
async function loadCouncilDetail(councilId) {
const councilContent = document.getElementById('main-content');
try {
const council = await apiFetch(`/v1/councils/${councilId}`);
const artifacts = await apiFetch(`/v1/councils/${councilId}/artifacts`);
// Normalize server composition (core_agents / optional_agents) into a flat list for the UI table
const core = (council.core_agents || []).map(a => ({
role_name: a.role_name,
required: true,
status: a.status,
agent_id: a.agent_id,
agent_name: a.agent_name,
}));
const optional = (council.optional_agents || []).map(a => ({
role_name: a.role_name,
required: false,
status: a.status,
agent_id: a.agent_id,
agent_name: a.agent_name,
}));
const agents = [...core, ...optional];
const agentRows = agents.map(agent => `
| ${agent.role_name} |
${agent.required ? 'Core' : 'Optional'} |
${agent.status || 'unknown'} |
${agent.agent_id || '—'} |
${agent.agent_name || '—'} |
`).join('');
const artifactItems = artifacts.artifacts && artifacts.artifacts.length
? artifacts.artifacts.map(artifact => `${artifact.artifact_name} - ${artifact.status}`).join('')
: 'No artifacts recorded yet';
councilContent.innerHTML = `
${council.project_name || ''}
Council Details
Status: ${council.status || 'unknown'}
${council.repository ? `
Repository: ${council.repository}
` : ''}
${council.project_brief ? `
Project Brief: ${council.project_brief}
` : ''}
Role Fulfilment
| Role |
Type |
Status |
Agent ID |
Agent Name |
${agentRows || '| No agents yet |
'}
`;
// Wire delete in detail view
const delBtn = document.getElementById('delete-project-detail');
if (delBtn) {
delBtn.addEventListener('click', async (event) => {
const projectId = event.target.dataset.projectId;
const projectName = event.target.dataset.projectName || projectId;
if (!confirm(`Delete project "${projectName}"?`)) return;
try {
await fetchText(`/api/v1/projects/${projectId}`, { method: 'DELETE' });
window.location.hash = '#councils';
loadCouncils();
} catch (error) {
alert(`Error deleting project: ${error.message}`);
}
});
}
} catch (error) {
councilContent.innerHTML = `Error loading council details: ${error.message}
`;
}
}
async function loadTasks() {
const tasksContent = document.getElementById('tasks-content');
try {
const data = await apiFetch('/v1/tasks');
tasksContent.innerHTML = `
${data.tasks.map(task => `
${task.status}
${task.priority}
${task.repository ? `${task.repository}` : ''}
${task.tech_stack?.length ? `
${task.tech_stack.slice(0, 3).map(tech => `
${tech}
`).join('')}
${task.tech_stack.length > 3 ? `+${task.tech_stack.length - 3} more` : ''}
` : ''}
${task.description ? `
${task.description.substring(0, 150)}${task.description.length > 150 ? '...' : ''}
` : ''}
`).join('')}
`;
} catch (error) {
tasksContent.innerHTML = `Error loading tasks: ${error.message}
`;
}
}
async function loadTaskDetail(taskId) {
const taskContent = document.getElementById('main-content');
try {
const task = await apiFetch(`/v1/tasks/${taskId}`);
taskContent.innerHTML = `
${task.title}
Task Details
Status: ${task.status}
Priority: ${task.priority}
Source: ${task.source_type || 'N/A'}
Repository: ${task.repository || 'N/A'}
Project ID: ${task.project_id || 'N/A'}
${task.external_url ? `
Issue: View on GITEA
` : ''}
${task.estimated_hours || task.complexity_score ? `
${task.estimated_hours ? `
Estimated Hours: ${task.estimated_hours}
` : ''}
${task.complexity_score ? `
Complexity Score: ${task.complexity_score.toFixed(2)}
` : ''}
` : ''}
${task.labels?.length || task.tech_stack?.length ? `
${task.labels?.length ? `
Labels:
${task.labels.map(label => `${label}`).join('')}
` : ''}
${task.tech_stack?.length ? `
Tech Stack:
${task.tech_stack.map(tech => `${tech}`).join('')}
` : ''}
` : ''}
${task.requirements?.length ? `
Requirements:
${task.requirements.map(req => `- ${req}
`).join('')}
` : ''}
Description:
${task.description || 'No description provided'}
${task.assigned_team_id || task.assigned_agent_id ? `
Assignment:
${task.assigned_team_id ? `
Team: ${task.assigned_team_id}
` : ''}
${task.assigned_agent_id ? `
Agent: ${task.assigned_agent_id}
` : ''}
` : ''}
Created: ${new Date(task.created_at).toLocaleString()}
${task.claimed_at ? `
Claimed: ${new Date(task.claimed_at).toLocaleString()}
` : ''}
${task.started_at ? `
Started: ${new Date(task.started_at).toLocaleString()}
` : ''}
${task.completed_at ? `
Completed: ${new Date(task.completed_at).toLocaleString()}
` : ''}
`;
} catch (error) {
taskContent.innerHTML = `Error loading task details: ${error.message}
`;
}
}
async function loadRepositories() {
const repositoriesContent = document.getElementById('repositories-content');
try {
const data = await apiFetch('/v1/repositories');
repositoriesContent.innerHTML = `
Add Repository
${data.repositories.map(repo => `
${repo.full_name}
Status: ${repo.sync_status}
`).join('')}
`;
const addRepoForm = document.getElementById('add-repo-form');
addRepoForm.addEventListener('submit', async (event) => {
event.preventDefault();
const repoName = event.target['repo-name'].value;
try {
// Expect input like "owner/repo"; build WHOOSH payload
const parts = (repoName || '').split('/').map(s => s.trim()).filter(Boolean);
if (parts.length !== 2) {
throw new Error('Please enter repository as "owner/repo"');
}
const [owner, name] = parts;
const url = `https://gitea.chorus.services/${owner}/${name}`;
const payload = {
name,
owner,
url,
source_type: 'gitea',
monitor_issues: true,
monitor_pull_requests: true,
enable_chorus_integration: true,
default_branch: 'main',
is_private: false,
topics: [],
};
await fetchText('/api/v1/repositories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
loadRepositories(); // Refresh the list
} catch (error) {
alert(`Error adding repository: ${error.message}`);
}
});
const syncRepoBtns = document.querySelectorAll('.sync-repo-btn');
syncRepoBtns.forEach(btn => {
btn.addEventListener('click', async (event) => {
const repoId = event.target.dataset.repoId;
try {
await fetchText(`/api/v1/repositories/${repoId}/sync`, { method: 'POST' });
loadRepositories(); // Refresh the list
} catch (error) {
alert(`Error syncing repository: ${error.message}`);
}
});
});
const deleteRepoBtns = document.querySelectorAll('.delete-repo-btn');
deleteRepoBtns.forEach(btn => {
btn.addEventListener('click', async (event) => {
const repoId = event.target.dataset.repoId;
if (!confirm('Delete this repository from monitoring?')) return;
try {
await fetchText(`/api/v1/repositories/${repoId}`, { method: 'DELETE' });
loadRepositories();
} catch (error) {
alert(`Error deleting repository: ${error.message}`);
}
});
});
} catch (error) {
repositoriesContent.innerHTML = `Error loading repositories: ${error.message}
`;
}
}
function loadAnalysis() {
const analysisContent = document.getElementById('analysis-content');
analysisContent.innerHTML = `
`;
const analysisForm = document.getElementById('analysis-form');
analysisForm.addEventListener('submit', async (event) => {
event.preventDefault();
const repoUrl = event.target['repo-url'].value;
const resultsContainer = document.getElementById('analysis-results');
resultsContainer.innerHTML = 'Analyzing...
';
incSpinner();
try {
const response = await fetch('/api/projects/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ repository_url: repoUrl, project_name: 'Analysis' }),
});
if (!response.ok) {
throw new Error(`Analysis request failed: ${response.statusText}`);
}
const result = await response.json();
resultsContainer.innerHTML = `
Analysis Initiated
Analysis ID: ${result.analysis_id}
Status: ${result.status}
Estimated Completion: ${result.estimated_completion_minutes} minutes
Tracking URL: ${result.tracking_url}
Please check the tracking URL for updates.
`;
} catch (error) {
resultsContainer.innerHTML = `Analysis failed: ${error.message}
`;
} finally {
decSpinner();
}
});
}
// Safety: ensure spinner can’t get stuck on due to unhandled errors
window.addEventListener('error', () => { activeSpinners = 0; hideSpinner(); });
window.addEventListener('unhandledrejection', () => { activeSpinners = 0; hideSpinner(); });
// Auth UI
function initAuthUI() {
const statusEl = document.getElementById('auth-status');
const inputEl = document.getElementById('auth-token-input');
const saveBtn = document.getElementById('save-auth-token');
const clearBtn = document.getElementById('clear-auth-token');
function updateStatus() {
const token = (localStorage.getItem('whoosh_token') || getCookie('whoosh_token') || '').trim();
if (token) {
statusEl.textContent = 'Authed';
statusEl.classList.add('authed');
} else {
statusEl.textContent = 'Guest';
statusEl.classList.remove('authed');
}
}
saveBtn.addEventListener('click', () => {
const v = (inputEl.value || '').trim();
if (!v) { alert('Token is empty'); return; }
localStorage.setItem('whoosh_token', v);
updateStatus();
inputEl.value = '';
alert('Token saved. Actions requiring auth should now work.');
});
clearBtn.addEventListener('click', () => {
localStorage.removeItem('whoosh_token');
document.cookie = 'whoosh_token=; Max-Age=0; path=/';
updateStatus();
});
updateStatus();
}
// Initial load
initAuthUI();
router();
window.addEventListener('hashchange', router);
});