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 => `

${council.project_name}

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

    ${agentRows || ''}
    Role Type Status Agent ID Agent Name
    No agents yet

    Artifacts

    `; // 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.title}

    ${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:

    ` : ''}

    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 = `

    Project Analysis

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