Updated project files and configuration
Some checks failed
WHOOSH CI / speclint (push) Has been cancelled
WHOOSH CI / contracts (push) Has been cancelled

- Added/updated .gitignore file
- Fixed remote URL configuration
- Updated project structure and files

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Code
2025-09-17 22:51:50 +10:00
parent e5555ae277
commit afccc94998
19 changed files with 3376 additions and 2352 deletions

5
ui/.github/chatmodes/OPus.chatmode.md vendored Normal file
View File

@@ -0,0 +1,5 @@
---
description: 'Description of the custom chat mode.'
tools: []
---
Define the purpose of this chat mode and how AI should behave: response style, available tools, focus areas, and any mode-specific instructions or constraints.

View File

0
ui/README.md Normal file
View File

279
ui/index.html Normal file
View File

@@ -0,0 +1,279 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WHOOSH - Council Formation Engine [External UI]</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@100;200;300;400;500;600;700;800;900&family=Exo:wght@100;200;300;400;500;600;700;800;900&family=Inconsolata:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/ui/styles.css">
</head>
<body>
<header class="header">
<div class="logo">
<strong>WHOOSH</strong>
<span class="tagline">Council Formation Engine</span>
</div>
<div class="status-info">
<div class="status-dot online"></div>
<span id="connection-status">Connected</span>
</div>
</header>
<nav class="nav">
<button class="nav-tab active" data-tab="dashboard">Dashboard</button>
<button class="nav-tab" data-tab="tasks">Tasks</button>
<button class="nav-tab" data-tab="teams">Teams</button>
<button class="nav-tab" data-tab="agents">Agents</button>
<button class="nav-tab" data-tab="config">Configuration</button>
<button class="nav-tab" data-tab="repositories">Repositories</button>
</nav>
<main class="content">
<!-- Dashboard Tab -->
<div id="dashboard" class="tab-content active">
<div class="dashboard-grid">
<div class="card">
<h3><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/Interface/Chart_Bar_Vertical_01.png" alt="Chart" class="card-icon" style="display: inline; vertical-align: text-top;"> System Metrics</h3>
<div class="metric">
<span class="metric-label">Active Councils</span>
<span class="metric-value">0</span>
</div>
<div class="metric">
<span class="metric-label">Deployed Agents</span>
<span class="metric-value">0</span>
</div>
<div class="metric">
<span class="metric-label">Completed Tasks</span>
<span class="metric-value">0</span>
</div>
</div>
<div class="card">
<h3><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/Arrow/Arrow_Reload_02.png" alt="Refresh" class="card-icon" style="display: inline; vertical-align: text-top;"> Recent Activity</h3>
<div id="recent-activity">
<div class="empty-state-icon"><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/File/Folder_Document.png" alt="Empty" style="width: 3rem; height: 3rem; opacity: 0.5;"></div>
<p>No recent activity</p>
</div>
</div>
<div class="card">
<h3><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/Warning/Circle_Check.png" alt="Status" class="card-icon" style="display: inline; vertical-align: text-top;"> System Status</h3>
<div class="metric">
<span class="metric-label">Database</span>
<span class="metric-value success-indicator">✅ Healthy</span>
</div>
<div class="metric">
<span class="metric-label">GITEA Integration</span>
<span class="metric-value success-indicator">✅ Connected</span>
</div>
<div class="metric">
<span class="metric-label">BACKBEAT</span>
<span class="metric-value success-indicator">✅ Active</span>
</div>
</div>
<div class="card">
<div class="metric">
<span class="metric-label">Tempo</span>
<span class="metric-value" id="beat-tempo" style="color: var(--ocean-400);">--</span>
</div>
<div class="metric">
<span class="metric-label">Volume</span>
<span class="metric-value" id="beat-volume" style="color: var(--ocean-400);">--</span>
</div>
<div class="metric">
<span class="metric-label">Phase</span>
<span class="metric-value" id="beat-phase" style="color: var(--ocean-400);">--</span>
</div>
<div style="margin-top: 1rem; height: 60px; background: var(--carbon-800); border-radius: 0; position: relative; overflow: hidden; border: 1px solid var(--mulberry-800);">
<canvas id="pulse-trace" width="100%" height="60" style="width: 100%; height: 60px;"></canvas>
</div>
<div class="backbeat-label">
Live BACKBEAT Pulse
</div>
</div>
</div>
</div>
<!-- Tasks Tab -->
<div id="tasks" class="tab-content">
<div class="card">
<button class="btn btn-primary" onclick="refreshTasks()"><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/Arrow/Arrow_Reload_02.png" alt="Refresh" style="width: 1rem; height: 1rem; margin-right: 0.5rem; vertical-align: text-top;"> Refresh Tasks</button>
</div>
<div class="card">
<div class="tabs">
<h3><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/Edit/List_Check.png" alt="Tasks" class="card-icon" style="display: inline; vertical-align: text-top;"> Active Tasks</h3>
<div id="active-tasks">
<div style="text-align: center; padding: 2rem 0; color: var(--mulberry-300);">
<div class="empty-state-icon"><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/Edit/List_Check.png" alt="No tasks" style="width: 3rem; height: 3rem; opacity: 0.5;"></div>
<p>No active tasks found</p>
</div>
</div>
</div>
<div class="tabs">
<h4>Scheduled Tasks</h4>
<div id="scheduled-tasks">
<div style="text-align: center; padding: 2rem 0; color: var(--mulberry-300);">
<div class="empty-state-icon"><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/Calendar/Calendar.png" alt="No scheduled tasks" style="width: 3rem; height: 3rem; opacity: 0.5;"></div>
<p>No scheduled tasks found</p>
</div>
</div>
</div>
</div>
</div>
<!-- Teams Tab -->
<div id="teams" class="tab-content">
<div class="card">
<h2><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/User/Users_Group.png" alt="Team" style="width: 1.5rem; height: 1.5rem; margin-right: 0.5rem; vertical-align: text-bottom;"> Team Management</h2>
<button class="btn btn-primary" onclick="loadTeams()"><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/Arrow/Arrow_Reload_02.png" alt="Refresh" style="width: 1rem; height: 1rem; margin-right: 0.5rem; vertical-align: text-top;"> Refresh Teams</button>
</div>
<div class="card" id="teams-list">
<div style="text-align: center; padding: 3rem 0; color: var(--mulberry-300);">
<div class="empty-state-icon"><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/User/Users_Group.png" alt="No teams" style="width: 3rem; height: 3rem; opacity: 0.5;"></div>
<p>No teams configured yet</p>
</div>
</div>
</div>
<!-- Agents Tab -->
<div id="agents" class="tab-content">
<div class="card">
<h2><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/System/Window_Check.png" alt="Agents" style="width: 1.5rem; height: 1.5rem; margin-right: 0.5rem; vertical-align: text-bottom;"> Agent Management</h2>
<button class="btn btn-primary" onclick="loadAgents()"><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/Arrow/Arrow_Reload_02.png" alt="Refresh" style="width: 1rem; height: 1rem; margin-right: 0.5rem; vertical-align: text-top;"> Refresh Agents</button>
</div>
<div class="card" id="agents-list">
<div style="text-align: center; padding: 3rem 0; color: var(--mulberry-300);">
<div class="empty-state-icon"><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/System/Window_Check.png" alt="No agents" style="width: 3rem; height: 3rem; opacity: 0.5;"></div>
<p>No agents registered yet</p>
</div>
</div>
</div>
<!-- Configuration Tab -->
<div id="config" class="tab-content">
<h2><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/Interface/Settings.png" alt="Settings" style="width: 1.5rem; height: 1.5rem; margin-right: 0.5rem; vertical-align: text-bottom;"> System Configuration</h2>
<div class="dashboard-grid">
<div class="card">
<h3>GITEA Integration</h3>
<div class="metric">
<span class="metric-label">Base URL</span>
<span class="metric-value">https://gitea.chorus.services</span>
</div>
<div class="metric">
<span class="metric-label">Webhook Path</span>
<span class="metric-value">/webhooks/gitea</span>
</div>
<div class="metric">
<span class="metric-label">Token Status</span>
<span class="metric-value" style="color: var(--eucalyptus-500);"><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/Interface/Check.png" alt="Valid" style="width: 1rem; height: 1rem; margin-right: 0.25rem; vertical-align: text-top;"> Valid</span>
</div>
</div>
<div class="card">
<h3>Repository Management</h3>
<button class="btn btn-primary" onclick="showAddRepositoryForm()">+ Add Repository</button>
<div id="add-repository-form" style="display: none; margin-top: 1rem; background: var(--carbon-800); padding: 1rem; border: 1px solid var(--mulberry-700);">
<h4>Add New Repository</h4>
<form id="repository-form">
<div style="margin-bottom: 1rem;">
<label>Repository Name:</label>
<input type="text" id="repo-name" required style="width: 100%; padding: 8px; border: 1px solid var(--carbon-300); border-radius: 0.375rem;" placeholder="e.g., WHOOSH">
</div>
<div style="margin-bottom: 1rem;">
<label>Owner:</label>
<input type="text" id="repo-owner" required style="width: 100%; padding: 8px; border: 1px solid var(--carbon-300); border-radius: 0.375rem;" placeholder="e.g., tony">
</div>
<div style="margin-bottom: 1rem;">
<label>Repository URL:</label>
<input type="url" id="repo-url" required style="width: 100%; padding: 8px; border: 1px solid var(--carbon-300); border-radius: 0.375rem;" placeholder="https://gitea.chorus.services/tony/WHOOSH">
</div>
<div style="margin-bottom: 1rem;">
<label>Source Type:</label>
<select id="repo-source-type" style="width: 100%; padding: 8px; border: 1px solid var(--carbon-300); border-radius: 0.375rem;">
<option value="git">Git Repository</option>
<option value="gitea">GITEA</option>
<option value="github">GitHub</option>
<option value="gitlab">GitLab</option>
</select>
</div>
<div style="margin-bottom: 1rem;">
<label>Default Branch:</label>
<input type="text" id="repo-branch" value="main" style="width: 100%; padding: 8px; border: 1px solid var(--carbon-300); border-radius: 0.375rem;">
</div>
<div style="margin-bottom: 1rem;">
<label>Description:</label>
<textarea id="repo-description" rows="2" style="width: 100%; padding: 8px; border: 1px solid var(--carbon-300); border-radius: 0.375rem;" placeholder="Brief description of this repository..."></textarea>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="repo-monitor-issues" checked>
Monitor Issues (listen for chorus-entrypoint labels)
</label>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="repo-enable-chorus">
Enable CHORUS Integration
</label>
</div>
<div style="display: flex; gap: 10px;">
<button type="button" onclick="hideAddRepositoryForm()" style="background: var(--carbon-300); color: var(--carbon-600); border: none; padding: 8px 16px; border-radius: 0.375rem; cursor: pointer; margin-right: 10px;">Cancel</button>
<button type="submit" style="background: var(--eucalyptus-500); color: white; border: none; padding: 8px 16px; border-radius: 0.375rem; cursor: pointer; font-weight: 500;">Add Repository</button>
</div>
</form>
</div>
</div>
<div class="card">
<h3><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/Interface/Chart_Bar_Vertical_01.png" alt="Chart" class="card-icon" style="display: inline; vertical-align: text-top;"> Repository Stats</h3>
<div class="metric">
<span class="metric-label">Total Repositories</span>
<span class="metric-value" id="total-repos">--</span>
</div>
<div class="metric">
<span class="metric-label">Active Monitoring</span>
<span class="metric-value" id="active-repos">--</span>
</div>
<div class="metric">
<span class="metric-label">Last Sync</span>
<span class="metric-value" id="last-sync">--</span>
</div>
</div>
</div>
</div>
<!-- Repositories Tab -->
<div id="repositories" class="tab-content">
<div class="card">
<h2>Repository Management</h2>
<button class="btn btn-primary" onclick="loadRepositories()">Refresh Repositories</button>
</div>
<div class="card">
<h3>Monitored Repositories</h3>
<div id="repositories-list">
<p style="text-align: center; color: var(--mulberry-300); padding: 20px;">Loading repositories...</p>
</div>
</div>
</div>
</main>
<script src="/ui/script.js"></script>
</body>
</html>

1
ui/package.json Normal file
View File

@@ -0,0 +1 @@
{}

705
ui/script.js Normal file
View File

@@ -0,0 +1,705 @@
// WHOOSH Dashboard JavaScript
// Global state
let pulseChart = null;
// Initialize on DOM load
document.addEventListener('DOMContentLoaded', function() {
initializeTabs();
loadDashboard();
initializePulseVisualization();
// Setup form submission handler
const repositoryForm = document.getElementById('repository-form');
if (repositoryForm) {
repositoryForm.addEventListener('submit', handleRepositorySubmit);
}
});
// Tab management
function initializeTabs() {
const tabs = document.querySelectorAll('.nav-tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => showTab(tab.dataset.tab));
});
}
function showTab(tabId) {
// Hide all tab contents
const contents = document.querySelectorAll('.tab-content');
contents.forEach(content => {
content.classList.remove('active');
});
// Remove active class from all tabs
const tabs = document.querySelectorAll('.nav-tab');
tabs.forEach(tab => {
tab.classList.remove('active');
});
// Show selected tab content
const selectedContent = document.getElementById(tabId);
if (selectedContent) {
selectedContent.classList.add('active');
}
// Activate selected tab
const selectedTab = document.querySelector(`[data-tab="${tabId}"]`);
if (selectedTab) {
selectedTab.classList.add('active');
}
// Load content for specific tabs
switch(tabId) {
case 'tasks':
loadTasks();
break;
case 'teams':
loadTeams();
break;
case 'agents':
loadAgents();
break;
case 'repositories':
loadRepositories();
break;
}
}
// Dashboard data loading
function loadDashboard() {
loadSystemMetrics();
loadRecentActivity();
loadSystemStatus();
}
function loadSystemMetrics() {
fetch('/api/v1/metrics')
.then(response => response.json())
.then(data => {
updateMetric('active-councils', data.active_councils || 0);
updateMetric('deployed-agents', data.deployed_agents || 0);
updateMetric('completed-tasks', data.completed_tasks || 0);
})
.catch(error => {
console.error('Error loading metrics:', error);
});
}
function loadRecentActivity() {
fetch('/api/v1/activity/recent')
.then(response => response.json())
.then(data => {
const container = document.getElementById('recent-activity');
if (data && data.length > 0) {
container.innerHTML = data.map(activity =>
`<div class="activity-item">
<strong>${activity.title}</strong>
<div style="font-size: 0.78rem; color: var(--mulberry-300);">${activity.timestamp}</div>
</div>`
).join('');
} else {
container.innerHTML = `
<div class="empty-state-icon"><img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/File/Folder_Document.png" alt="Empty" style="width: 3rem; height: 3rem; opacity: 0.5;"></div>
<p>No recent activity</p>
`;
}
})
.catch(error => {
console.error('Error loading recent activity:', error);
});
}
function loadSystemStatus() {
fetch('/api/v1/status')
.then(response => response.json())
.then(data => {
updateStatus('database', data.database || 'healthy');
updateStatus('gitea-integration', data.gitea || 'connected');
updateStatus('backbeat', data.backbeat || 'active');
})
.catch(error => {
console.error('Error loading system status:', error);
});
}
function updateMetric(id, value) {
const element = document.querySelector(`[data-metric="${id}"], .metric-value`);
if (element) {
element.textContent = value;
}
}
function updateStatus(component, status) {
// Status indicators are currently hardcoded in HTML
console.log(`Status update: ${component} = ${status}`);
}
// BACKBEAT pulse visualization
function initializePulseVisualization() {
const canvas = document.getElementById('pulse-trace');
if (!canvas) return;
const ctx = canvas.getContext('2d');
canvas.width = canvas.offsetWidth;
canvas.height = 60;
// Initialize pulse visualization
updatePulseVisualization();
// Update every second
setInterval(updatePulseVisualization, 1000);
}
function updatePulseVisualization() {
fetch('/api/v1/backbeat/status')
.then(response => response.json())
.then(data => {
updateBeatMetrics(data);
drawPulseTrace(data);
})
.catch(error => {
// Use mock data for demonstration
const mockData = {
tempo: Math.floor(Math.random() * 40) + 60,
volume: Math.floor(Math.random() * 30) + 70,
phase: ['rising', 'peak', 'falling', 'valley'][Math.floor(Math.random() * 4)],
trace: Array.from({length: 50}, () => Math.random() * 100)
};
updateBeatMetrics(mockData);
drawPulseTrace(mockData);
});
}
function updateBeatMetrics(data) {
const tempoEl = document.getElementById('beat-tempo');
const volumeEl = document.getElementById('beat-volume');
const phaseEl = document.getElementById('beat-phase');
if (tempoEl) tempoEl.textContent = data.tempo + ' BPM';
if (volumeEl) volumeEl.textContent = data.volume + '%';
if (phaseEl) phaseEl.textContent = data.phase;
}
function drawPulseTrace(data) {
const canvas = document.getElementById('pulse-trace');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// Clear canvas
ctx.fillStyle = 'var(--carbon-800)';
ctx.fillRect(0, 0, width, height);
if (!data.trace || data.trace.length === 0) return;
// Draw pulse trace
ctx.strokeStyle = 'var(--ocean-400)';
ctx.lineWidth = 2;
ctx.beginPath();
const stepX = width / (data.trace.length - 1);
data.trace.forEach((point, index) => {
const x = index * stepX;
const y = height - (point / 100 * height);
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
}
// Task management
function refreshTasks() {
loadTasks();
}
function loadTasks() {
Promise.all([
fetch('/api/v1/tasks/active').then(r => r.json()).catch(() => []),
fetch('/api/v1/tasks/scheduled').then(r => r.json()).catch(() => [])
]).then(([activeTasks, scheduledTasks]) => {
renderTasks('active-tasks', activeTasks);
renderTasks('scheduled-tasks', scheduledTasks);
});
}
function renderTasks(containerId, tasks) {
const container = document.getElementById(containerId);
if (!container) return;
if (!tasks || tasks.length === 0) {
const isActive = containerId === 'active-tasks';
const icon = isActive ? 'List_Check.png' : 'Calendar.png';
const message = isActive ? 'No active tasks found' : 'No scheduled tasks found';
container.innerHTML = `
<div style="text-align: center; padding: 2rem 0; color: var(--mulberry-300);">
<div class="empty-state-icon">
<img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/Edit/${icon}" alt="No tasks" style="width: 3rem; height: 3rem; opacity: 0.5;">
</div>
<p>${message}</p>
</div>
`;
return;
}
container.innerHTML = tasks.map(task => {
const priorityClass = task.priority ? `priority-${task.priority.toLowerCase()}` : '';
return `
<div class="task-item ${priorityClass}">
<div class="task-title">${task.title || 'Untitled Task'}</div>
<div class="task-meta">
<span>Priority: ${task.priority || 'Normal'}</span>
<span>${task.created_at || ''}</span>
</div>
</div>
`;
}).join('');
}
// Team management
function loadTeams() {
fetch('/api/v1/teams')
.then(response => response.json())
.then(teams => {
renderTeams(teams);
})
.catch(error => {
console.error('Error loading teams:', error);
renderTeams([]);
});
}
function renderTeams(teams) {
const container = document.getElementById('teams-list');
if (!container) return;
if (!teams || teams.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 3rem 0; color: var(--mulberry-300);">
<div class="empty-state-icon">
<img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/User/Users_Group.png" alt="No teams" style="width: 3rem; height: 3rem; opacity: 0.5;">
</div>
<p>No teams configured yet</p>
</div>
`;
return;
}
container.innerHTML = teams.map(team => `
<div class="team-member">
<div class="agent-status ${team.status || 'offline'}"></div>
<div>
<strong>${team.name}</strong>
<div style="font-size: 0.78rem; color: var(--mulberry-300);">${team.description || ''}</div>
</div>
</div>
`).join('');
}
// Agent management
function loadAgents() {
fetch('/api/v1/agents')
.then(response => response.json())
.then(agents => {
renderAgents(agents);
})
.catch(error => {
console.error('Error loading agents:', error);
renderAgents([]);
});
}
function renderAgents(agents) {
const container = document.getElementById('agents-list');
if (!container) return;
if (!agents || agents.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 3rem 0; color: var(--mulberry-300);">
<div class="empty-state-icon">
<img src="https://brand.chorus.services/icons/coolicons.v4.1/coolicons%20PNG/White/System/Window_Check.png" alt="No agents" style="width: 3rem; height: 3rem; opacity: 0.5;">
</div>
<p>No agents registered yet</p>
</div>
`;
return;
}
container.innerHTML = agents.map(agent => `
<div class="agent-card">
<div style="display: flex; align-items: center; margin-bottom: 0.44rem;">
<div class="agent-status ${agent.status || 'offline'}"></div>
<strong>${agent.name}</strong>
</div>
<div style="font-size: 0.78rem; color: var(--mulberry-300);">
${agent.description || 'No description available'}
</div>
</div>
`).join('');
}
// Repository management
function showAddRepositoryForm() {
document.getElementById('add-repository-form').style.display = 'block';
}
function hideAddRepositoryForm() {
document.getElementById('add-repository-form').style.display = 'none';
document.getElementById('repository-form').reset();
}
function handleRepositorySubmit(e) {
e.preventDefault();
const formData = {
name: document.getElementById('repo-name').value.trim(),
owner: document.getElementById('repo-owner').value.trim(),
url: document.getElementById('repo-url').value.trim(),
source_type: document.getElementById('repo-source-type').value,
default_branch: document.getElementById('repo-branch').value.trim() || 'main',
description: document.getElementById('repo-description').value.trim(),
monitor_issues: document.getElementById('repo-monitor-issues').checked,
enable_chorus_integration: document.getElementById('repo-enable-chorus').checked
};
if (!formData.name || !formData.owner || !formData.url) {
alert('Please fill in all required fields');
return;
}
fetch('/api/v1/repositories', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
if (data.error) {
alert('Error adding repository: ' + data.error);
} else {
alert('Repository added successfully!');
hideAddRepositoryForm();
loadRepositories();
updateRepositoryStats();
}
})
.catch(error => {
console.error('Error adding repository:', error);
alert('Error adding repository');
});
}
function loadRepositories() {
fetch('/api/v1/repositories')
.then(response => response.json())
.then(repositories => {
renderRepositories(repositories);
updateRepositoryStats();
})
.catch(error => {
console.error('Error loading repositories:', error);
renderRepositories([]);
});
}
function updateRepositoryStats() {
fetch('/api/v1/repositories/stats')
.then(response => response.json())
.then(stats => {
const totalEl = document.getElementById('total-repos');
const activeEl = document.getElementById('active-repos');
const lastSyncEl = document.getElementById('last-sync');
if (totalEl) totalEl.textContent = stats.total || 0;
if (activeEl) activeEl.textContent = stats.active || 0;
if (lastSyncEl) lastSyncEl.textContent = stats.last_sync || 'Never';
})
.catch(error => {
console.error('Error loading repository stats:', error);
});
}
function renderRepositories(repositories) {
const container = document.getElementById('repositories-list');
if (!container) return;
if (!repositories || repositories.length === 0) {
container.innerHTML = '<p style="text-align: center; color: var(--mulberry-300); padding: 20px;">No repositories found</p>';
return;
}
const html = repositories.map(repo =>
'<div class="repository-item">' +
'<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px;">' +
'<h4 style="margin: 0; color: var(--carbon-100);">' + repo.full_name + '</h4>' +
'<div style="display: flex; align-items: center;">' +
'<div style="width: 8px; height: 8px; border-radius: 50%; background: ' + getStatusColor(repo.status) + '; margin-right: 6px;"></div>' +
'<span style="font-size: 0.67rem; color: var(--mulberry-300); text-transform: uppercase;">' + (repo.status || 'unknown') + '</span>' +
'</div>' +
'</div>' +
'<div class="repository-meta">' +
'<div>Language: <strong>' + (repo.language || 'Not detected') + '</strong></div>' +
'<div>Default Branch: <strong>' + (repo.default_branch || 'main') + '</strong></div>' +
'<div>Source: <strong>' + (repo.source_type || 'git') + '</strong></div>' +
(repo.description ? '<div style="margin-top: 4px;">Description: <em>' + repo.description + '</em></div>' : '') +
'</div>' +
'<div style="margin: 8px 0; font-size: 0.67rem; color: var(--mulberry-300);">' +
'<div>Issues: ' + (repo.monitor_issues ? '✅ Monitored' : '❌ Not monitored') + '</div>' +
'<div>Pull Requests: ' + (repo.monitor_pull_requests ? '✅ Monitored' : '❌ Not monitored') + '</div>' +
'<div>Releases: ' + (repo.monitor_releases ? '✅ Monitored' : '❌ Not monitored') + '</div>' +
'<div>CHORUS: ' + (repo.enable_chorus_integration ? '✅ Enabled' : '❌ Disabled') + '</div>' +
'</div>' +
'<div style="display: flex; gap: 8px; margin-top: 12px;">' +
'<button onclick="syncRepository(\'' + repo.id + '\')" style="background: var(--ocean-600); color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' +
'Sync' +
'</button>' +
'<button onclick="ensureLabels(\'' + repo.id + '\')" style="background: var(--eucalyptus-600); color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' +
'Ensure Labels' +
'</button>' +
'<button onclick="editRepository(\'' + repo.id + '\')" style="background: var(--coral-700); color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' +
'Edit' +
'</button>' +
'<button onclick="deleteRepository(\'' + repo.id + '\', \'' + repo.full_name + '\')" style="background: var(--coral-500); color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' +
'Delete' +
'</button>' +
'</div>' +
'</div>'
).join('');
container.innerHTML = html;
}
function getStatusColor(status) {
switch(status) {
case 'active': return 'var(--eucalyptus-500)';
case 'pending': return 'var(--coral-700)';
case 'error': return 'var(--coral-500)';
case 'disabled': return 'var(--carbon-400)';
default: return 'var(--carbon-500)';
}
}
function syncRepository(repoId) {
fetch('/api/v1/repositories/' + repoId + '/sync', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
alert('Repository sync triggered: ' + data.message);
loadRepositories(); // Reload to show updated status
})
.catch(error => {
console.error('Error syncing repository:', error);
alert('Error syncing repository');
});
}
function ensureLabels(repoId) {
fetch('/api/v1/repositories/' + repoId + '/ensure-labels', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.error) {
alert('Error ensuring labels: ' + data.error);
} else {
alert('Labels ensured successfully for ' + data.owner + '/' + data.name + '\n\nRequired labels created:\n• bzzz-task\n• whoosh-monitored\n• priority-high\n• priority-medium\n• priority-low');
}
})
.catch(error => {
console.error('Error ensuring labels:', error);
alert('Error ensuring labels');
});
}
function editRepository(repoId) {
// Fetch repository details first
fetch('/api/v1/repositories/' + repoId)
.then(response => response.json())
.then(repo => {
showEditModal(repo);
})
.catch(error => {
console.error('Error fetching repository:', error);
alert('Error fetching repository details');
});
}
function showEditModal(repo) {
// Create modal overlay
const overlay = document.createElement('div');
overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; ' +
'background: rgba(0,0,0,0.5); display: flex; align-items: center; ' +
'justify-content: center; z-index: 1000;';
// Create modal content
const modal = document.createElement('div');
modal.style.cssText = 'background: white; border-radius: 8px; padding: 24px; ' +
'max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto;';
modal.innerHTML =
'<h3 style="margin: 0 0 20px 0; color: var(--carbon-800);">Edit Repository</h3>' +
'<div style="margin-bottom: 16px;">' +
'<strong>' + repo.full_name + '</strong>' +
'<div style="font-size: 0.67rem; color: var(--mulberry-300);">ID: ' + repo.id + '</div>' +
'</div>' +
'<form id="editRepoForm">' +
'<div style="margin-bottom: 16px;">' +
'<label style="display: block; margin-bottom: 4px; font-weight: bold;">Description:</label>' +
'<input type="text" id="description" value="' + (repo.description || '') + '" ' +
'style="width: 100%; padding: 8px; border: 1px solid var(--carbon-300); border-radius: 0.375rem;">' +
'</div>' +
'<div style="margin-bottom: 16px;">' +
'<label style="display: block; margin-bottom: 4px; font-weight: bold;">Default Branch:</label>' +
'<input type="text" id="defaultBranch" value="' + (repo.default_branch || 'main') + '" ' +
'style="width: 100%; padding: 8px; border: 1px solid var(--carbon-300); border-radius: 0.375rem;">' +
'</div>' +
'<div style="margin-bottom: 16px;">' +
'<label style="display: block; margin-bottom: 4px; font-weight: bold;">Language:</label>' +
'<input type="text" id="language" value="' + (repo.language || '') + '" ' +
'style="width: 100%; padding: 8px; border: 1px solid var(--carbon-300); border-radius: 0.375rem;">' +
'</div>' +
'<div style="margin-bottom: 16px;">' +
'<h4 style="margin: 0 0 8px 0;">Monitoring Options:</h4>' +
'<div style="margin-bottom: 8px;">' +
'<label style="display: flex; align-items: center;">' +
'<input type="checkbox" id="monitorIssues" ' + (repo.monitor_issues ? 'checked' : '') + ' style="margin-right: 8px;">' +
'Monitor Issues' +
'</label>' +
'</div>' +
'<div style="margin-bottom: 8px;">' +
'<label style="display: flex; align-items: center;">' +
'<input type="checkbox" id="monitorPRs" ' + (repo.monitor_pull_requests ? 'checked' : '') + ' style="margin-right: 8px;">' +
'Monitor Pull Requests' +
'</label>' +
'</div>' +
'<div style="margin-bottom: 8px;">' +
'<label style="display: flex; align-items: center;">' +
'<input type="checkbox" id="monitorReleases" ' + (repo.monitor_releases ? 'checked' : '') + ' style="margin-right: 8px;">' +
'Monitor Releases' +
'</label>' +
'</div>' +
'</div>' +
'<div style="margin-bottom: 16px;">' +
'<h4 style="margin: 0 0 8px 0;">CHORUS Integration:</h4>' +
'<div style="margin-bottom: 8px;">' +
'<label style="display: flex; align-items: center;">' +
'<input type="checkbox" id="enableChorus" ' + (repo.enable_chorus_integration ? 'checked' : '') + ' style="margin-right: 8px;">' +
'Enable CHORUS Integration' +
'</label>' +
'</div>' +
'<div style="margin-bottom: 8px;">' +
'<label style="display: flex; align-items: center;">' +
'<input type="checkbox" id="autoAssignTeams" ' + (repo.auto_assign_teams ? 'checked' : '') + ' style="margin-right: 8px;">' +
'Auto-assign Teams' +
'</label>' +
'</div>' +
'</div>' +
'<div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px;">' +
'<button type="button" onclick="closeEditModal()" ' +
'style="background: var(--carbon-300); color: var(--carbon-600); border: none; padding: 10px 16px; border-radius: 4px; cursor: pointer;">' +
'Cancel' +
'</button>' +
'<button type="submit" ' +
'style="background: var(--ocean-600); color: white; border: none; padding: 10px 16px; border-radius: 4px; cursor: pointer;">' +
'Save Changes' +
'</button>' +
'</div>' +
'</form>';
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Store modal reference globally so we can close it
window.currentEditModal = overlay;
window.currentRepoId = repo.id;
// Handle form submission
document.getElementById('editRepoForm').addEventListener('submit', function(e) {
e.preventDefault();
saveRepositoryChanges();
});
// Close modal on overlay click
overlay.addEventListener('click', function(e) {
if (e.target === overlay) {
closeEditModal();
}
});
}
function closeEditModal() {
if (window.currentEditModal) {
document.body.removeChild(window.currentEditModal);
window.currentEditModal = null;
window.currentRepoId = null;
}
}
function saveRepositoryChanges() {
const formData = {
description: document.getElementById('description').value.trim() || null,
default_branch: document.getElementById('defaultBranch').value.trim() || null,
language: document.getElementById('language').value.trim() || null,
monitor_issues: document.getElementById('monitorIssues').checked,
monitor_pull_requests: document.getElementById('monitorPRs').checked,
monitor_releases: document.getElementById('monitorReleases').checked,
enable_chorus_integration: document.getElementById('enableChorus').checked,
auto_assign_teams: document.getElementById('autoAssignTeams').checked
};
fetch('/api/v1/repositories/' + window.currentRepoId, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
alert('Repository updated successfully!');
closeEditModal();
loadRepositories(); // Reload the list to show changes
})
.catch(error => {
console.error('Error updating repository:', error);
alert('Error updating repository');
});
}
function deleteRepository(repoId, fullName) {
if (confirm('Are you sure you want to delete repository "' + fullName + '"? This will stop monitoring and cannot be undone.')) {
fetch('/api/v1/repositories/' + repoId, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
alert('Repository deleted: ' + data.message);
loadRepositories(); // Reload the list
})
.catch(error => {
console.error('Error deleting repository:', error);
alert('Error deleting repository');
});
}
}

463
ui/styles.css Normal file
View File

@@ -0,0 +1,463 @@
/* CHORUS Brand Variables */
:root {
font-size: 18px; /* CHORUS proportional base */
/* Carbon Colors (Primary Neutral) */
--carbon-950: #000000;
--carbon-900: #0a0a0a;
--carbon-800: #1a1a1a;
--carbon-700: #2a2a2a;
--carbon-600: #666666;
--carbon-500: #808080;
--carbon-400: #a0a0a0;
--carbon-300: #c0c0c0;
--carbon-200: #e0e0e0;
--carbon-100: #f0f0f0;
--carbon-50: #f8f8f8;
/* Mulberry Colors (Brand Accent) */
--mulberry-950: #0b0213;
--mulberry-900: #1a1426;
--mulberry-800: #2a2639;
--mulberry-700: #3a384c;
--mulberry-600: #4a4a5f;
--mulberry-500: #5a5c72;
--mulberry-400: #7a7e95;
--mulberry-300: #9aa0b8;
--mulberry-200: #bac2db;
--mulberry-100: #dae4fe;
--mulberry-50: #f0f4ff;
/* Ocean Colors (Primary Action) */
--ocean-950: #2a3441;
--ocean-900: #3a4654;
--ocean-800: #4a5867;
--ocean-700: #5a6c80;
--ocean-600: #6a7e99;
--ocean-500: #7a90b2;
--ocean-400: #8ba3c4;
--ocean-300: #9bb6d6;
--ocean-200: #abc9e8;
--ocean-100: #bbdcfa;
--ocean-50: #cbefff;
/* Eucalyptus Colors (Success) */
--eucalyptus-950: #2a3330;
--eucalyptus-900: #3a4540;
--eucalyptus-800: #4a5750;
--eucalyptus-700: #515d54;
--eucalyptus-600: #5a6964;
--eucalyptus-500: #6a7974;
--eucalyptus-400: #7a8a7f;
--eucalyptus-300: #8a9b8f;
--eucalyptus-200: #9aac9f;
--eucalyptus-100: #aabdaf;
--eucalyptus-50: #bacfbf;
/* Coral Colors (Error/Warning) */
--coral-700: #dc2626;
--coral-500: #ef4444;
--coral-300: #fca5a5;
}
/* Base Styles with CHORUS Branding */
body {
font-family: 'Inter Tight', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 0;
background: var(--carbon-950);
color: var(--carbon-100);
line-height: 1.6;
}
/* CHORUS Dark Mode Header */
.header {
background: linear-gradient(135deg, var(--carbon-900) 0%, var(--mulberry-900) 100%);
color: white;
padding: 1.33rem 0; /* 24px at 18px base */
border-bottom: 1px solid var(--mulberry-800);
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
padding-left: 1.33rem;
padding-right: 1.33rem;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.33rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-family: 'Exo', sans-serif;
font-size: 1.33rem; /* 24px at 18px base */
font-weight: 300;
color: white;
display: flex;
align-items: center;
gap: 0.67rem;
}
.logo .tagline {
font-size: 0.78rem;
color: var(--mulberry-300);
font-weight: 400;
}
.logo::before {
content: "";
font-size: 1.5rem;
}
.status-info {
display: flex;
align-items: center;
color: var(--eucalyptus-400);
font-size: 0.78rem;
}
.status-dot {
width: 0.67rem;
height: 0.67rem;
border-radius: 50%;
background: var(--eucalyptus-400);
margin-right: 0.44rem;
display: inline-block;
}
/* CHORUS Navigation */
.nav {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.33rem;
display: flex;
border-bottom: 1px solid var(--mulberry-800);
background: var(--carbon-900);
}
.nav-tab {
padding: 0.83rem 1.39rem;
cursor: pointer;
border-bottom: 3px solid transparent;
font-weight: 500;
transition: all 0.2s;
color: var(--mulberry-300);
background: none;
border: none;
font-family: inherit;
}
.nav-tab.active {
border-bottom-color: var(--ocean-500);
color: var(--ocean-300);
background: var(--carbon-800);
}
.nav-tab:hover {
background: var(--carbon-800);
color: var(--ocean-400);
}
.content {
max-width: 1200px;
margin: 0 auto;
padding: 1.33rem;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* CHORUS Card System */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 1.33rem;
margin-bottom: 2rem;
}
.card {
background: var(--carbon-900);
border-radius: 0;
padding: 1.33rem;
box-shadow: 0 0.22rem 0.89rem rgba(0,0,0,0.3);
border: 1px solid var(--mulberry-800);
}
.card h3 {
margin: 0 0 1rem 0;
color: var(--carbon-100);
font-size: 1rem;
display: flex;
align-items: center;
font-weight: 600;
}
.card h2 {
margin: 0 0 1rem 0;
color: var(--carbon-100);
font-size: 1.33rem;
display: flex;
align-items: center;
font-weight: 600;
}
.card-icon {
width: 1.33rem;
height: 1.33rem;
margin-right: 0.67rem;
}
/* Metrics with CHORUS Colors */
.metric {
display: flex;
justify-content: space-between;
margin: 0.44rem 0;
padding: 0.44rem 0;
}
.metric:not(:last-child) {
border-bottom: 1px solid var(--mulberry-900);
}
.metric-label {
color: var(--mulberry-300);
}
.metric-value {
font-weight: 600;
color: var(--carbon-100);
}
/* Task Items with CHORUS Brand Colors */
.task-item {
background: var(--carbon-800);
border-radius: 0;
padding: 0.89rem;
margin-bottom: 0.67rem;
border-left: 4px solid var(--mulberry-600);
}
.task-item.priority-high {
border-left-color: var(--coral-500);
}
.task-item.priority-medium {
border-left-color: var(--ocean-500);
}
.task-item.priority-low {
border-left-color: var(--eucalyptus-500);
}
.task-title {
font-weight: 600;
color: var(--carbon-100);
margin-bottom: 0.44rem;
}
.task-meta {
display: flex;
justify-content: space-between;
color: var(--mulberry-300);
font-size: 0.78rem;
}
/* Agent Cards */
.agent-card {
background: var(--carbon-800);
border-radius: 0;
padding: 0.89rem;
margin-bottom: 0.67rem;
}
.agent-status {
width: 0.44rem;
height: 0.44rem;
border-radius: 50%;
margin-right: 0.44rem;
display: inline-block;
}
.agent-status.online {
background: var(--eucalyptus-400);
}
.agent-status.offline {
background: var(--carbon-500);
}
.team-member {
display: flex;
align-items: center;
padding: 0.44rem;
background: var(--carbon-900);
border-radius: 0;
margin-bottom: 0.44rem;
}
/* CHORUS Button System */
.btn {
padding: 0.44rem 0.89rem;
border-radius: 0.375rem;
border: none;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
font-family: 'Inter Tight', sans-serif;
}
.btn-primary {
background: var(--ocean-600);
color: white;
}
.btn-primary:hover {
background: var(--ocean-500);
}
.btn-secondary {
background: var(--mulberry-700);
color: var(--mulberry-200);
}
.btn-secondary:hover {
background: var(--mulberry-600);
}
/* Empty States */
.empty-state {
text-align: center;
padding: 2.22rem 1.33rem;
color: var(--mulberry-300);
}
.empty-state-icon {
font-size: 2.67rem;
margin-bottom: 0.89rem;
text-align: center;
}
/* BackBeat Pulse Visualization */
#pulse-trace {
background: var(--carbon-800);
border-radius: 0;
border: 1px solid var(--mulberry-800);
}
/* Additional CHORUS Styling */
.backbeat-label {
color: var(--mulberry-300);
font-size: 0.67rem;
text-align: center;
margin-top: 0.44rem;
}
/* Modal and Overlay Styling */
.modal-overlay {
background: rgba(0, 0, 0, 0.8) !important;
}
.modal-content {
background: var(--carbon-900) !important;
color: var(--carbon-100) !important;
border: 1px solid var(--mulberry-800) !important;
}
.modal-content input, .modal-content select, .modal-content textarea {
background: var(--carbon-800);
color: var(--carbon-100);
border: 1px solid var(--mulberry-700);
border-radius: 0;
padding: 0.44rem 0.67rem;
font-family: inherit;
}
.modal-content input:focus, .modal-content select:focus, .modal-content textarea:focus {
border-color: var(--ocean-500);
outline: none;
}
.modal-content label {
color: var(--mulberry-200);
display: block;
margin-bottom: 0.33rem;
font-weight: 500;
}
/* Repository Cards */
.repository-item {
background: var(--carbon-800);
border-radius: 0;
padding: 0.89rem;
margin-bottom: 0.67rem;
border: 1px solid var(--mulberry-800);
}
.repository-item h4 {
color: var(--carbon-100);
margin: 0 0 0.44rem 0;
}
.repository-meta {
color: var(--mulberry-300);
font-size: 0.78rem;
margin-bottom: 0.44rem;
}
/* Success/Error States */
.success-indicator {
color: var(--eucalyptus-400);
}
.error-indicator {
color: var(--coral-500);
}
.warning-indicator {
color: var(--ocean-400);
}
/* Tabs styling */
.tabs {
margin-bottom: 1.33rem;
}
.tabs h4 {
color: var(--carbon-100);
margin-bottom: 0.67rem;
font-size: 0.89rem;
font-weight: 600;
}
/* Form styling improvements */
form {
display: flex;
flex-direction: column;
gap: 1rem;
}
form > div {
display: flex;
flex-direction: column;
gap: 0.33rem;
}
form label {
font-weight: 500;
color: var(--mulberry-200);
}
form input[type="checkbox"] {
margin-right: 0.5rem;
accent-color: var(--ocean-500);
}