// State
let currentProjectId = null;
let projects = [];
let tasks = [];
let queueStatus = null;
let workerStatus = null;
let pendingExecuteTaskId = null; // For pipeline modal
// Elements
const dashboardView = document.getElementById('dashboardView');
const projectView = document.getElementById('projectView');
const projectGrid = document.getElementById('projectGrid');
const taskListEl = document.getElementById('taskList');
const currentProjectTitle = document.getElementById('currentProjectTitle');
const newProjectModal = document.getElementById('newProjectModal');
const settingsModal = document.getElementById('settingsModal');
const statusIndicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('statusText');
// Config
let BACKEND_URL = localStorage.getItem('backend_url') || 'http://localhost:3000';
// Remove trailing slash if present
BACKEND_URL = BACKEND_URL.replace(/\/$/, '');
const WS_URL = BACKEND_URL.replace(/^http/, 'ws');
// --- Navigation ---
function showDashboard() {
currentProjectId = null;
projectView.style.display = 'none';
dashboardView.style.display = 'block';
fetchProjects();
}
function showProject(id) {
currentProjectId = id;
const project = projects.find(p => p.id === id);
if (project) {
currentProjectTitle.textContent = project.name;
dashboardView.style.display = 'none';
projectView.style.display = 'block';
fetchTasks(id);
}
}
function openNewProjectModal() {
document.getElementById('newProjectTitle').value = '';
document.getElementById('newProjectDesc').value = '';
newProjectModal.classList.add('active');
setTimeout(() => document.getElementById('newProjectTitle').focus(), 100);
}
function closeNewProjectModal() {
newProjectModal.classList.remove('active');
}
function openSettingsModal() {
const input = document.getElementById('backendUrlInput');
input.value = BACKEND_URL;
settingsModal.classList.add('active');
setTimeout(() => {
input.focus();
input.select();
}, 100);
}
function closeSettingsModal() {
settingsModal.classList.remove('active');
}
function saveSettings() {
const url = document.getElementById('backendUrlInput').value.trim();
if (url) {
localStorage.setItem('backend_url', url);
location.reload();
}
}
// --- Pipeline Modal ---
function openPipelineModal(taskId) {
pendingExecuteTaskId = taskId;
const task = tasks.find(t => t.id === taskId);
const modal = document.getElementById('pipelineModal');
if (modal) {
document.getElementById('pipelineTaskTitle').textContent = task?.title || 'Task';
modal.classList.add('active');
}
}
function closePipelineModal() {
const modal = document.getElementById('pipelineModal');
if (modal) modal.classList.remove('active');
pendingExecuteTaskId = null;
}
function selectPipelinePreset(preset) {
// Update radio buttons
document.querySelectorAll('input[name="pipelinePreset"]').forEach(r => r.checked = false);
const radio = document.querySelector(`input[name="pipelinePreset"][value="${preset}"]`);
if (radio) radio.checked = true;
// Update custom checkboxes based on preset
const presets = {
quick: ['implementation'],
standard: ['planning', 'implementation', 'testing'],
full: ['research', 'planning', 'implementation', 'testing']
};
const stages = presets[preset] || [];
document.querySelectorAll('.stage-checkbox').forEach(cb => {
cb.checked = stages.includes(cb.value);
});
}
function getSelectedStages() {
const preset = document.querySelector('input[name="pipelinePreset"]:checked')?.value;
if (preset && preset !== 'custom') {
const presets = {
quick: ['implementation'],
standard: ['planning', 'implementation', 'testing'],
full: ['research', 'planning', 'implementation', 'testing']
};
return presets[preset];
}
// Custom selection
return Array.from(document.querySelectorAll('.stage-checkbox:checked')).map(cb => cb.value);
}
async function executePipeline() {
if (!pendingExecuteTaskId) {
alert('No task selected');
return;
}
const stages = getSelectedStages();
if (stages.length === 0) {
alert('Please select at least one stage');
return;
}
// Save task ID before closing modal (closePipelineModal sets it to null)
const taskId = pendingExecuteTaskId;
closePipelineModal();
try {
const res = await fetch(`${BACKEND_URL}/api/tasks/${taskId}/execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ stages }),
credentials: 'include'
});
if (!res.ok) throw new Error((await res.json()).error);
const result = await res.json();
console.log('Pipeline started:', result);
} catch (err) {
alert('Failed to start pipeline: ' + err.message);
}
}
// --- API & Logic ---
async function fetchProjects() {
try {
const res = await fetch(`${BACKEND_URL}/api/projects`, { credentials: 'include' });
if (res.ok) {
projects = await res.json();
renderProjects();
updateStatus(true);
}
} catch (err) {
console.error(err);
updateStatus(false);
}
}
async function createProject() {
const title = document.getElementById('newProjectTitle').value.trim();
const description = document.getElementById('newProjectDesc').value.trim();
if (!title) return;
try {
await fetch(`${BACKEND_URL}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, description }),
credentials: 'include'
});
closeNewProjectModal();
} catch (err) {
console.error(err);
}
}
async function deleteProject(e, id) {
e.stopPropagation();
if (!confirm('Delete this project and all its tasks?')) return;
try {
await fetch(`${BACKEND_URL}/api/projects/${id}`, { method: 'DELETE', credentials: 'include' });
} catch (err) {
console.error(err);
}
}
async function fetchTasks(projectId) {
try {
const res = await fetch(`${BACKEND_URL}/api/tasks?project_id=${projectId}`, { credentials: 'include' });
if (res.ok) {
tasks = await res.json();
renderTasks();
}
} catch (err) {
console.error(err);
}
}
async function addTask() {
const input = document.getElementById('taskInput');
const title = input.value.trim();
if (!title || !currentProjectId) return;
try {
await fetch(`${BACKEND_URL}/api/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, project_id: currentProjectId }),
credentials: 'include'
});
input.value = '';
} catch (err) {
console.error(err);
}
}
async function toggleTask(id, currentStatus) {
const newStatus = currentStatus === 'pending' ? 'completed' : 'pending';
try {
await fetch(`${BACKEND_URL}/api/tasks/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus }),
credentials: 'include'
});
} catch (err) {
console.error(err);
}
}
async function deleteTask(id) {
try {
await fetch(`${BACKEND_URL}/api/tasks/${id}`, { method: 'DELETE', credentials: 'include' });
} catch (err) {
console.error(err);
}
}
async function executeTask(id, btn) {
// Open pipeline modal instead of direct execution
openPipelineModal(id);
}
// Quick execute with default preset
async function quickExecuteTask(id, preset = 'standard') {
try {
const res = await fetch(`${BACKEND_URL}/api/tasks/${id}/execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ preset }),
credentials: 'include'
});
if (!res.ok) throw new Error((await res.json()).error);
} catch (err) {
alert('Execution failed: ' + err.message);
}
}
// --- Rendering ---
function renderProjects() {
projectGrid.innerHTML = '';
if (projects.length === 0) {
document.getElementById('noProjects').style.display = 'block';
return;
}
document.getElementById('noProjects').style.display = 'none';
projects.forEach(p => {
const card = document.createElement('div');
card.className = 'glass-card project-card';
card.onclick = () => showProject(p.id);
// Content Container
const content = document.createElement('div');
// Header
const header = document.createElement('div');
header.style.cssText = "display:flex; justify-content:space-between; align-items:start;";
const title = document.createElement('div');
title.className = 'project-title';
title.textContent = p.name;
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn-danger';
deleteBtn.innerHTML = '×';
deleteBtn.title = 'Delete Project';
deleteBtn.onclick = (e) => {
e.stopPropagation();
deleteProject(e, p.id);
};
header.appendChild(title);
header.appendChild(deleteBtn);
// Description
const desc = document.createElement('div');
desc.className = 'project-desc';
desc.textContent = p.description || '';
content.appendChild(header);
content.appendChild(desc);
// Progress Container
const progressContainer = document.createElement('div');
progressContainer.className = 'progress-container';
const progressLabel = document.createElement('div');
progressLabel.className = 'progress-label';
const labelText = document.createElement('span');
labelText.textContent = 'Progress';
const labelValue = document.createElement('span');
labelValue.textContent = `${p.progress}%`;
progressLabel.appendChild(labelText);
progressLabel.appendChild(labelValue);
const progressBarBg = document.createElement('div');
progressBarBg.className = 'progress-bar-bg';
const progressBarFill = document.createElement('div');
progressBarFill.className = 'progress-bar-fill';
progressBarFill.style.width = `${p.progress}%`;
progressBarBg.appendChild(progressBarFill);
progressContainer.appendChild(progressLabel);
progressContainer.appendChild(progressBarBg);
card.appendChild(content);
card.appendChild(progressContainer);
projectGrid.appendChild(card);
});
}
function renderTasks() {
taskListEl.innerHTML = '';
if (tasks.length === 0) {
document.getElementById('noTasks').style.display = 'block';
return;
}
document.getElementById('noTasks').style.display = 'none';
tasks.forEach(t => {
const li = document.createElement('li');
// Detect pipeline state from description prefix
const isPipeline = t.description?.startsWith('[PIPELINE:');
const isStage = t.description?.startsWith('[STAGE:');
const isCompleted = t.status === 'completed';
const isFailed = t.description?.startsWith('[FAILED]');
// Add status-specific classes
let statusClass = '';
if (isCompleted) statusClass = 'completed';
else if (isPipeline || isStage) statusClass = 'queued';
else if (isFailed) statusClass = 'failed';
li.className = `task-item ${statusClass}`;
// Checkbox
const checkbox = document.createElement('div');
checkbox.className = 'checkbox';
checkbox.onclick = (e) => {
e.stopPropagation();
toggleTask(t.id, t.status);
};
// Content
const content = document.createElement('div');
content.className = 'task-content';
const title = document.createElement('div');
title.className = 'task-title';
title.textContent = t.title;
content.appendChild(title);
// Show status badge for pipeline tasks
if (isPipeline) {
const badge = document.createElement('span');
badge.className = 'status-badge queued-badge';
// Extract stages from [PIPELINE: research→planning→...]
const stagesMatch = t.description.match(/\[PIPELINE: ([^\]]+)\]/);
badge.textContent = `⚙️ Pipeline: ${stagesMatch ? stagesMatch[1] : 'Starting...'}`;
content.appendChild(badge);
} else if (isStage) {
const badge = document.createElement('span');
badge.className = 'status-badge queued-badge';
const stageMatch = t.description.match(/\[STAGE: ([^\]]+)\]/);
badge.textContent = `⏳ Stage: ${stageMatch ? stageMatch[1] : 'Processing...'}`;
content.appendChild(badge);
} else if (isFailed) {
const badge = document.createElement('span');
badge.className = 'status-badge failed-badge';
badge.textContent = '❌ Failed';
content.appendChild(badge);
} else if (t.description) {
const desc = document.createElement('div');
desc.className = 'task-desc';
// Clean up description display
let displayDesc = t.description;
if (displayDesc.startsWith('[COMPLETED]')) {
displayDesc = displayDesc.replace('[COMPLETED]', '').trim();
}
desc.textContent = displayDesc.substring(0, 200);
content.appendChild(desc);
}
// Execute Button - Opens pipeline modal
const executeBtn = document.createElement('button');
executeBtn.className = 'btn-secondary';
executeBtn.style.cssText = "padding: 5px 10px; margin-right: 5px;";
const isProcessing = isPipeline || isStage;
executeBtn.innerHTML = isProcessing ? '⏳' : '▶️';
executeBtn.title = isProcessing ? 'Task is processing...' : 'Execute with Pipeline';
executeBtn.disabled = isProcessing;
executeBtn.onclick = (e) => {
e.stopPropagation();
openPipelineModal(t.id);
};
// Quick Execute Button (standard preset)
const quickBtn = document.createElement('button');
quickBtn.className = 'btn-secondary';
quickBtn.style.cssText = "padding: 5px 10px; margin-right: 5px;";
quickBtn.innerHTML = '⚡';
quickBtn.title = 'Quick Execute (Standard Pipeline)';
quickBtn.disabled = isProcessing;
quickBtn.onclick = (e) => {
e.stopPropagation();
quickExecuteTask(t.id, 'standard');
};
// Delete Button
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn-danger';
deleteBtn.innerHTML = '×';
deleteBtn.onclick = (e) => {
e.stopPropagation();
deleteTask(t.id);
};
li.appendChild(checkbox);
li.appendChild(content);
li.appendChild(executeBtn);
li.appendChild(quickBtn);
li.appendChild(deleteBtn);
taskListEl.appendChild(li);
});
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function updateStatus(connected) {
if (connected) {
statusIndicator.className = 'status-indicator status-connected';
statusText.textContent = 'Connected';
} else {
statusIndicator.className = 'status-indicator status-disconnected';
statusText.textContent = 'Disconnected';
}
}
// --- WebSocket ---
let ws;
function connectWs() {
ws = new WebSocket(WS_URL);
ws.onopen = () => updateStatus(true);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'update_projects') {
projects = data.projects;
if (!currentProjectId) renderProjects();
}
if (data.type === 'update_tasks') {
// Only update if we are viewing the relevant project
if (currentProjectId && data.projectId === currentProjectId) {
tasks = data.tasks;
renderTasks();
}
}
if (data.type === 'queue_status') {
queueStatus = data.queue;
workerStatus = data.workers;
updateQueueDisplay();
}
};
ws.onclose = () => {
updateStatus(false);
setTimeout(connectWs, 3000);
};
}
function updateQueueDisplay() {
// Update queue status in UI if element exists
const queueEl = document.getElementById('queueStatus');
if (queueEl && queueStatus) {
const activeWorkers = workerStatus?.activeWorkers || 0;
const totalTasks = queueStatus.totalTasks || 0;
queueEl.textContent = `Workers: ${activeWorkers} | Queue: ${totalTasks} tasks`;
}
}
// Init
fetchProjects();
connectWs();
// Expose functions to window for inline onclick handlers
window.showProject = showProject;
window.deleteProject = deleteProject;
window.toggleTask = toggleTask;
window.executeTask = executeTask;
window.quickExecuteTask = quickExecuteTask;
window.deleteTask = deleteTask;
window.openPipelineModal = openPipelineModal;
window.closePipelineModal = closePipelineModal;
window.selectPipelinePreset = selectPipelinePreset;
window.executePipeline = executePipeline;