Files
WHOOSH/ui/script.js
Claude Code 3373f7b462
Some checks failed
WHOOSH CI / speclint (push) Has been cancelled
WHOOSH CI / contracts (push) Has been cancelled
Add chorus-entrypoint label to standardized label set
**Problem**: The standardized label set was missing the `chorus-entrypoint`
label, which is present in CHORUS repository and required for triggering
council formation for project kickoffs.

**Changes**:
- Added `chorus-entrypoint` label (#ff6b6b) to `EnsureRequiredLabels()`
  in `internal/gitea/client.go`
- Now creates 9 standard labels (was 8):
  1. bug
  2. bzzz-task
  3. chorus-entrypoint (NEW)
  4. duplicate
  5. enhancement
  6. help wanted
  7. invalid
  8. question
  9. wontfix

**Testing**:
- Rebuilt and deployed WHOOSH with updated label configuration
- Synced labels to all 5 monitored repositories (whoosh-ui,
  SequentialThinkingForCHORUS, TEST, WHOOSH, CHORUS)
- Verified all repositories now have complete 9-label set

**Impact**: All CHORUS ecosystem repositories now have consistent labeling
matching the CHORUS repository standard, enabling proper council formation
triggers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 22:06:10 +11:00

600 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

document.addEventListener('DOMContentLoaded', () => {
const mainContent = document.getElementById('main-content');
// Simple router
const routes = {
'#dashboard': '<h2>Dashboard</h2><div id="dashboard-content"></div>',
'#councils': '<h2>Councils</h2><div id="councils-content"></div>',
'#tasks': '<h2>Tasks</h2><div id="tasks-content"></div>',
'#repositories': '<h2>Repositories</h2><div id="repositories-content"></div>',
'#analysis': '<h2>Analysis</h2><div id="analysis-content"></div>',
};
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] || '<h2>Page Not Found</h2>';
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 = `
<div class="dashboard-grid">
<div class="card">
<h3>System Status</h3>
<p>Status: ${health.status}</p>
<p>Version: ${health.version}</p>
</div>
<div class="card">
<h3>Metrics</h3>
<pre>${metrics.slice(0, 1000)}...</pre>
</div>
</div>
`;
} catch (error) {
dashboardContent.innerHTML = `<p class="error">Error loading dashboard: ${error.message}</p>`;
}
}
async function loadCouncils() {
const councilsContent = document.getElementById('councils-content');
try {
const data = await apiFetch('/v1/councils');
councilsContent.innerHTML = `
<div class="grid">
${data.councils.map(council => `
<div class="card">
<h3><a href="#councils/${council.id}">${council.project_name}</a></h3>
<p>Status: ${council.status}</p>
<div style="margin-top: 0.75rem;">
<button class="button danger delete-project-btn" data-project-id="${council.id}" data-project-name="${council.project_name}">Delete</button>
</div>
</div>
`).join('')}
</div>
`;
// 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 = `<p class="error">Error loading councils: ${error.message}</p>`;
}
}
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 => `
<tr>
<td>${agent.role_name}</td>
<td>${agent.required ? 'Core' : 'Optional'}</td>
<td>${agent.status || 'unknown'}</td>
<td>${agent.agent_id || '—'}</td>
<td>${agent.agent_name || '—'}</td>
</tr>
`).join('');
const artifactItems = artifacts.artifacts && artifacts.artifacts.length
? artifacts.artifacts.map(artifact => `<li>${artifact.artifact_name} - ${artifact.status}</li>`).join('')
: '<li>No artifacts recorded yet</li>';
councilContent.innerHTML = `
<h2>${council.project_name || ''}</h2>
<div class="grid">
<div class="card">
<h3>Council Details</h3>
<p>Status: ${council.status || 'unknown'}</p>
${council.repository ? `<p>Repository: <a href="${council.repository}">${council.repository}</a></p>` : ''}
${council.project_brief ? `<p>Project Brief: ${council.project_brief}</p>` : ''}
<div style="margin-top: 0.75rem;">
<button class="button danger" id="delete-project-detail" data-project-id="${council.id}" data-project-name="${council.project_name}">Delete Project</button>
</div>
</div>
<div class="card full-width">
<h3>Role Fulfilment</h3>
<div class="table-wrapper">
<table class="role-table">
<thead>
<tr>
<th>Role</th>
<th>Type</th>
<th>Status</th>
<th>Agent ID</th>
<th>Agent Name</th>
</tr>
</thead>
<tbody>
${agentRows || '<tr><td colspan="5">No agents yet</td></tr>'}
</tbody>
</table>
</div>
</div>
</div>
<div class="card">
<h3>Artifacts</h3>
<ul>
${artifactItems}
</ul>
</div>
`;
// 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 = `<p class="error">Error loading council details: ${error.message}</p>`;
}
}
async function loadTasks() {
const tasksContent = document.getElementById('tasks-content');
try {
const data = await apiFetch('/v1/tasks');
tasksContent.innerHTML = `
<div class="task-list">
${data.tasks.map(task => `
<div class="task-card">
<h3><a href="#tasks/${task.id}">${task.title}</a></h3>
<div class="task-meta">
<span class="badge status-${task.status}">${task.status}</span>
<span class="badge priority-${task.priority}">${task.priority}</span>
${task.repository ? `<span class="repo-badge">${task.repository}</span>` : ''}
</div>
${task.tech_stack?.length ? `
<div class="tags">
${task.tech_stack.slice(0, 3).map(tech => `
<span class="tag tech">${tech}</span>
`).join('')}
${task.tech_stack.length > 3 ? `<span class="tag">+${task.tech_stack.length - 3} more</span>` : ''}
</div>
` : ''}
${task.description ? `
<p class="task-description">${task.description.substring(0, 150)}${task.description.length > 150 ? '...' : ''}</p>
` : ''}
</div>
`).join('')}
</div>
`;
} catch (error) {
tasksContent.innerHTML = `<p class="error">Error loading tasks: ${error.message}</p>`;
}
}
async function loadTaskDetail(taskId) {
const taskContent = document.getElementById('main-content');
try {
const task = await apiFetch(`/v1/tasks/${taskId}`);
taskContent.innerHTML = `
<h2>${task.title}</h2>
<div class="card">
<h3>Task Details</h3>
<!-- Basic Info -->
<div class="grid">
<div>
<p><strong>Status:</strong> <span class="badge status-${task.status}">${task.status}</span></p>
<p><strong>Priority:</strong> <span class="badge priority-${task.priority}">${task.priority}</span></p>
<p><strong>Source:</strong> ${task.source_type || 'N/A'}</p>
</div>
<div>
<p><strong>Repository:</strong> ${task.repository || 'N/A'}</p>
<p><strong>Project ID:</strong> ${task.project_id || 'N/A'}</p>
${task.external_url ? `<p><strong>Issue:</strong> <a href="${task.external_url}" target="_blank">View on GITEA</a></p>` : ''}
</div>
</div>
<!-- Estimation & Complexity -->
${task.estimated_hours || task.complexity_score ? `
<hr>
<div class="grid">
${task.estimated_hours ? `<p><strong>Estimated Hours:</strong> ${task.estimated_hours}</p>` : ''}
${task.complexity_score ? `<p><strong>Complexity Score:</strong> ${task.complexity_score.toFixed(2)}</p>` : ''}
</div>
` : ''}
<!-- Labels & Tech Stack -->
${task.labels?.length || task.tech_stack?.length ? `
<hr>
<div class="grid">
${task.labels?.length ? `
<div>
<p><strong>Labels:</strong></p>
<div class="tags">
${task.labels.map(label => `<span class="tag">${label}</span>`).join('')}
</div>
</div>
` : ''}
${task.tech_stack?.length ? `
<div>
<p><strong>Tech Stack:</strong></p>
<div class="tags">
${task.tech_stack.map(tech => `<span class="tag tech">${tech}</span>`).join('')}
</div>
</div>
` : ''}
</div>
` : ''}
<!-- Requirements -->
${task.requirements?.length ? `
<hr>
<p><strong>Requirements:</strong></p>
<ul>
${task.requirements.map(req => `<li>${req}</li>`).join('')}
</ul>
` : ''}
<!-- Description -->
<hr>
<p><strong>Description:</strong></p>
<div class="description">
${task.description || '<em>No description provided</em>'}
</div>
<!-- Assignment Info -->
${task.assigned_team_id || task.assigned_agent_id ? `
<hr>
<p><strong>Assignment:</strong></p>
<div class="grid">
${task.assigned_team_id ? `<p>Team: ${task.assigned_team_id}</p>` : ''}
${task.assigned_agent_id ? `<p>Agent: ${task.assigned_agent_id}</p>` : ''}
</div>
` : ''}
<!-- Timestamps -->
<hr>
<div class="grid timestamps">
<p><strong>Created:</strong> ${new Date(task.created_at).toLocaleString()}</p>
${task.claimed_at ? `<p><strong>Claimed:</strong> ${new Date(task.claimed_at).toLocaleString()}</p>` : ''}
${task.started_at ? `<p><strong>Started:</strong> ${new Date(task.started_at).toLocaleString()}</p>` : ''}
${task.completed_at ? `<p><strong>Completed:</strong> ${new Date(task.completed_at).toLocaleString()}</p>` : ''}
</div>
</div>
`;
} catch (error) {
taskContent.innerHTML = `<p class="error">Error loading task details: ${error.message}</p>`;
}
}
async function loadRepositories() {
const repositoriesContent = document.getElementById('repositories-content');
try {
const data = await apiFetch('/v1/repositories');
repositoriesContent.innerHTML = `
<div class="card">
<h3>Add Repository</h3>
<form id="add-repo-form">
<label for="repo-name">Repository Name (e.g., owner/repo):</label>
<input type="text" id="repo-name" name="repo-name" required>
<button type="submit" class="button">Add</button>
</form>
</div>
<div class="grid">
${data.repositories.map(repo => `
<div class="card">
<h3>${repo.full_name}</h3>
<p>Status: ${repo.sync_status}</p>
<button class="button sync-repo-btn" data-repo-id="${repo.id}">Sync</button>
<button class="button danger delete-repo-btn" data-repo-id="${repo.id}" style="margin-left: 0.5rem;">Delete</button>
</div>
`).join('')}
</div>
`;
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 = `<p class="error">Error loading repositories: ${error.message}</p>`;
}
}
function loadAnalysis() {
const analysisContent = document.getElementById('analysis-content');
analysisContent.innerHTML = `
<div class="card">
<h3>Project Analysis</h3>
<form id="analysis-form">
<label for="repo-url">Repository URL:</label>
<input type="text" id="repo-url" name="repo-url" required>
<button type="submit" class="button">Analyze</button>
</form>
<div id="analysis-results"></div>
</div>
`;
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 = '<p>Analyzing...</p>';
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 = `
<div class="card">
<h4>Analysis Initiated</h4>
<p>Analysis ID: ${result.analysis_id}</p>
<p>Status: ${result.status}</p>
<p>Estimated Completion: ${result.estimated_completion_minutes} minutes</p>
<p>Tracking URL: <a href="${result.tracking_url}" target="_blank">${result.tracking_url}</a></p>
<p>Please check the tracking URL for updates.</p>
</div>
`;
} catch (error) {
resultsContainer.innerHTML = `<p class="error">Analysis failed: ${error.message}</p>`;
} finally {
decSpinner();
}
});
}
// Safety: ensure spinner cant 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);
});