Remove backup HTML template for workflow dashboard

This commit is contained in:
catlog22
2025-12-07 12:59:59 +08:00
parent a9a2004d4a
commit 724545ebd6
33 changed files with 17288 additions and 7682 deletions

View File

@@ -0,0 +1,180 @@
// ============================================
// FIX SESSION VIEW
// ============================================
// Fix session detail page rendering
function renderFixSessionDetailPage(session) {
const isActive = session._isActive !== false;
const tasks = session.tasks || [];
// Calculate fix statistics
const totalTasks = tasks.length;
const fixedCount = tasks.filter(t => t.status === 'completed' && t.result === 'fixed').length;
const failedCount = tasks.filter(t => t.status === 'completed' && t.result === 'failed').length;
const pendingCount = tasks.filter(t => t.status === 'pending').length;
const inProgressCount = tasks.filter(t => t.status === 'in_progress').length;
const percentComplete = totalTasks > 0 ? ((fixedCount + failedCount) / totalTasks * 100) : 0;
return `
<div class="session-detail-page session-type-fix">
<!-- Header -->
<div class="detail-header">
<button class="btn-back" onclick="goBackToSessions()">
<span class="back-icon">←</span>
<span>Back to Sessions</span>
</button>
<div class="detail-title-row">
<h2 class="detail-session-id">🔧 ${escapeHtml(session.session_id)}</h2>
<div class="detail-badges">
<span class="session-type-badge test-fix">Fix</span>
<span class="session-status ${isActive ? 'active' : 'archived'}">
${isActive ? 'ACTIVE' : 'ARCHIVED'}
</span>
</div>
</div>
</div>
<!-- Fix Progress Section -->
<div class="fix-progress-section">
<div class="fix-progress-header">
<h3>🔧 Fix Progress</h3>
<span class="phase-badge ${session.phase || 'execution'}">${session.phase || 'Execution'}</span>
</div>
<!-- Progress Bar -->
<div class="fix-progress-bar">
<div class="fix-progress-bar-fill" style="width: ${percentComplete}%"></div>
</div>
<div class="progress-text">
<strong>${fixedCount + failedCount}/${totalTasks}</strong> completed (${percentComplete.toFixed(1)}%)
</div>
<!-- Summary Cards -->
<div class="fix-summary-grid">
<div class="summary-card">
<div class="summary-icon">📊</div>
<div class="summary-value">${totalTasks}</div>
<div class="summary-label">Total Tasks</div>
</div>
<div class="summary-card fixed">
<div class="summary-icon">✅</div>
<div class="summary-value">${fixedCount}</div>
<div class="summary-label">Fixed</div>
</div>
<div class="summary-card failed">
<div class="summary-icon">❌</div>
<div class="summary-value">${failedCount}</div>
<div class="summary-label">Failed</div>
</div>
<div class="summary-card pending">
<div class="summary-icon">⏳</div>
<div class="summary-value">${pendingCount}</div>
<div class="summary-label">Pending</div>
</div>
</div>
<!-- Stage Timeline (if available) -->
${session.stages && session.stages.length > 0 ? `
<div class="stage-timeline">
${session.stages.map((stage, idx) => `
<div class="stage-item ${stage.status || 'pending'}">
<div class="stage-number">Stage ${idx + 1}</div>
<div class="stage-mode">${stage.execution_mode === 'parallel' ? '⚡ Parallel' : '➡️ Serial'}</div>
<div class="stage-groups">${stage.groups?.length || 0} groups</div>
</div>
`).join('')}
</div>
` : ''}
</div>
<!-- Fix Tasks Grid -->
<div class="fix-tasks-section">
<div class="tasks-header">
<h3>📋 Fix Tasks</h3>
<div class="task-filters">
<button class="filter-btn active" data-status="all" onclick="filterFixTasks('all')">All</button>
<button class="filter-btn" data-status="pending" onclick="filterFixTasks('pending')">Pending</button>
<button class="filter-btn" data-status="in_progress" onclick="filterFixTasks('in_progress')">In Progress</button>
<button class="filter-btn" data-status="fixed" onclick="filterFixTasks('fixed')">Fixed</button>
<button class="filter-btn" data-status="failed" onclick="filterFixTasks('failed')">Failed</button>
</div>
</div>
<div class="fix-tasks-grid" id="fixTasksGrid">
${renderFixTasksGrid(tasks)}
</div>
</div>
<!-- Session Info -->
<div class="detail-info-bar">
<div class="info-item">
<span class="info-label">Created:</span>
<span class="info-value">${formatDate(session.created_at)}</span>
</div>
${session.archived_at ? `
<div class="info-item">
<span class="info-label">Archived:</span>
<span class="info-value">${formatDate(session.archived_at)}</span>
</div>
` : ''}
<div class="info-item">
<span class="info-label">Project:</span>
<span class="info-value">${escapeHtml(session.project || '-')}</span>
</div>
</div>
</div>
`;
}
function renderFixTasksGrid(tasks) {
if (!tasks || tasks.length === 0) {
return `
<div class="empty-state">
<div class="empty-icon">📋</div>
<div class="empty-text">No fix tasks found</div>
</div>
`;
}
return tasks.map(task => {
const statusClass = task.status === 'completed' ? (task.result || 'completed') : task.status;
const statusText = task.status === 'completed' ? (task.result || 'completed') : task.status;
return `
<div class="fix-task-card status-${statusClass}" data-status="${statusClass}">
<div class="task-card-header">
<span class="task-id-badge">${escapeHtml(task.task_id || task.id || 'N/A')}</span>
<span class="task-status-badge ${statusClass}">${statusText}</span>
</div>
<div class="task-card-title">${escapeHtml(task.title || 'Untitled Task')}</div>
${task.finding_title ? `<div class="task-finding">${escapeHtml(task.finding_title)}</div>` : ''}
${task.file ? `<div class="task-file">📄 ${escapeHtml(task.file)}${task.line ? ':' + task.line : ''}</div>` : ''}
<div class="task-card-meta">
${task.dimension ? `<span class="task-dimension">${escapeHtml(task.dimension)}</span>` : ''}
${task.attempts && task.attempts > 1 ? `<span class="task-attempts">🔄 ${task.attempts} attempts</span>` : ''}
${task.commit_hash ? `<span class="task-commit">💾 ${task.commit_hash.substring(0, 7)}</span>` : ''}
</div>
</div>
`;
}).join('');
}
function initFixSessionPage(session) {
// Initialize event handlers for fix session page
// Filter handlers are inline onclick
}
function filterFixTasks(status) {
// Update filter buttons
document.querySelectorAll('.task-filters .filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.status === status);
});
// Filter task cards
document.querySelectorAll('.fix-task-card').forEach(card => {
if (status === 'all' || card.dataset.status === status) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
}

View File

@@ -0,0 +1,108 @@
// ==========================================
// HOME VIEW - Dashboard Homepage
// ==========================================
function renderDashboard() {
updateStats();
updateBadges();
renderSessions();
document.getElementById('generatedAt').textContent = workflowData.generatedAt || new Date().toISOString();
}
function updateStats() {
const stats = workflowData.statistics || {};
document.getElementById('statTotalSessions').textContent = stats.totalSessions || 0;
document.getElementById('statActiveSessions').textContent = stats.activeSessions || 0;
document.getElementById('statTotalTasks').textContent = stats.totalTasks || 0;
document.getElementById('statCompletedTasks').textContent = stats.completedTasks || 0;
}
function updateBadges() {
const active = workflowData.activeSessions || [];
const archived = workflowData.archivedSessions || [];
document.getElementById('badgeAll').textContent = active.length + archived.length;
document.getElementById('badgeActive').textContent = active.length;
document.getElementById('badgeArchived').textContent = archived.length;
// Lite Tasks badges
const liteTasks = workflowData.liteTasks || {};
document.getElementById('badgeLitePlan').textContent = liteTasks.litePlan?.length || 0;
document.getElementById('badgeLiteFix').textContent = liteTasks.liteFix?.length || 0;
}
function renderSessions() {
const container = document.getElementById('mainContent');
let sessions = [];
if (currentFilter === 'all' || currentFilter === 'active') {
sessions = sessions.concat((workflowData.activeSessions || []).map(s => ({ ...s, _isActive: true })));
}
if (currentFilter === 'all' || currentFilter === 'archived') {
sessions = sessions.concat((workflowData.archivedSessions || []).map(s => ({ ...s, _isActive: false })));
}
if (sessions.length === 0) {
container.innerHTML = `
<div class="empty-state" style="grid-column: 1/-1;">
<div class="empty-icon">📭</div>
<div class="empty-title">No Sessions Found</div>
<div class="empty-text">No workflow sessions match your current filter.</div>
</div>
`;
return;
}
container.innerHTML = `<div class="sessions-grid">${sessions.map(session => renderSessionCard(session)).join('')}</div>`;
}
function renderSessionCard(session) {
const tasks = session.tasks || [];
const taskCount = session.taskCount || tasks.length;
const completed = tasks.filter(t => t.status === 'completed').length;
const progress = taskCount > 0 ? Math.round((completed / taskCount) * 100) : 0;
// Use _isActive flag set during rendering, default to true
const isActive = session._isActive !== false;
const date = session.created_at;
// Get session type badge
const sessionType = session.type || 'workflow';
const typeBadge = sessionType !== 'workflow' ? `<span class="session-type-badge ${sessionType}">${sessionType}</span>` : '';
// Store session data for modal
const sessionKey = `session-${session.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
sessionDataStore[sessionKey] = session;
return `
<div class="session-card" onclick="showSessionDetailPage('${sessionKey}')">
<div class="session-header">
<div class="session-title">${escapeHtml(session.session_id || 'Unknown')}</div>
<div class="session-badges">
${typeBadge}
<span class="session-status ${isActive ? 'active' : 'archived'}">
${isActive ? 'ACTIVE' : 'ARCHIVED'}
</span>
</div>
</div>
<div class="session-body">
<div class="session-meta">
<span class="session-meta-item">📅 ${formatDate(date)}</span>
<span class="session-meta-item">📋 ${taskCount} tasks</span>
</div>
${taskCount > 0 ? `
<div class="progress-container">
<span class="progress-label">Progress</span>
<div class="progress-bar-wrapper">
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div>
</div>
<span class="progress-text">${completed}/${taskCount} (${progress}%)</span>
</div>
</div>
` : ''}
</div>
</div>
`;
}

View File

@@ -0,0 +1,382 @@
// ============================================
// LITE TASKS VIEW
// ============================================
// Lite-plan and lite-fix task list and detail rendering
function renderLiteTasks() {
const container = document.getElementById('mainContent');
const liteTasks = workflowData.liteTasks || {};
const sessions = currentLiteType === 'lite-plan'
? liteTasks.litePlan || []
: liteTasks.liteFix || [];
if (sessions.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-icon">⚡</div>
<div class="empty-title">No ${currentLiteType} Sessions</div>
<div class="empty-text">No sessions found in .workflow/.${currentLiteType}/</div>
</div>
`;
return;
}
container.innerHTML = `<div class="sessions-grid">${sessions.map(session => renderLiteTaskCard(session)).join('')}</div>`;
// Initialize collapsible sections
document.querySelectorAll('.collapsible-header').forEach(header => {
header.addEventListener('click', () => toggleSection(header));
});
// Render flowcharts for expanded tasks
sessions.forEach(session => {
session.tasks?.forEach(task => {
if (task.flow_control?.implementation_approach) {
renderFlowchartForTask(session.id, task);
}
});
});
}
function renderLiteTaskCard(session) {
const tasks = session.tasks || [];
// Store session data for detail page
const sessionKey = `lite-${session.type}-${session.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
liteTaskDataStore[sessionKey] = session;
return `
<div class="session-card lite-task-card" onclick="showLiteTaskDetailPage('${sessionKey}')" style="cursor: pointer;">
<div class="session-header">
<div class="session-title">${escapeHtml(session.id)}</div>
<span class="session-status ${session.type}">
${session.type === 'lite-plan' ? '📝 PLAN' : '🔧 FIX'}
</span>
</div>
<div class="session-body">
<div class="session-meta">
<span class="session-meta-item">📅 ${formatDate(session.createdAt)}</span>
<span class="session-meta-item">📋 ${tasks.length} tasks</span>
</div>
</div>
</div>
`;
}
// Lite Task Detail Page
function showLiteTaskDetailPage(sessionKey) {
const session = liteTaskDataStore[sessionKey];
if (!session) return;
currentView = 'liteTaskDetail';
currentSessionDetailKey = sessionKey;
// Also store in sessionDataStore for tab switching compatibility
sessionDataStore[sessionKey] = {
...session,
session_id: session.id,
created_at: session.createdAt,
path: session.path,
type: session.type
};
const container = document.getElementById('mainContent');
const tasks = session.tasks || [];
const plan = session.plan || {};
const progress = session.progress || { total: 0, completed: 0, percentage: 0 };
const completed = tasks.filter(t => t.status === 'completed').length;
const inProgress = tasks.filter(t => t.status === 'in_progress').length;
const pending = tasks.filter(t => t.status === 'pending').length;
container.innerHTML = `
<div class="session-detail-page lite-task-detail-page">
<!-- Header -->
<div class="detail-header">
<button class="btn-back" onclick="goBackToLiteTasks()">
<span class="back-icon">←</span>
<span>Back to ${session.type === 'lite-plan' ? 'Lite Plan' : 'Lite Fix'}</span>
</button>
<div class="detail-title-row">
<h2 class="detail-session-id">${session.type === 'lite-plan' ? '📝' : '🔧'} ${escapeHtml(session.id)}</h2>
<div class="detail-badges">
<span class="session-type-badge ${session.type}">${session.type}</span>
</div>
</div>
</div>
<!-- Session Info Bar -->
<div class="detail-info-bar">
<div class="info-item">
<span class="info-label">Created:</span>
<span class="info-value">${formatDate(session.createdAt)}</span>
</div>
<div class="info-item">
<span class="info-label">Tasks:</span>
<span class="info-value">${tasks.length} tasks</span>
</div>
</div>
<!-- Tab Navigation -->
<div class="detail-tabs">
<button class="detail-tab active" data-tab="tasks" onclick="switchLiteDetailTab('tasks')">
<span class="tab-icon">📋</span>
<span class="tab-text">Tasks</span>
<span class="tab-count">${tasks.length}</span>
</button>
<button class="detail-tab" data-tab="plan" onclick="switchLiteDetailTab('plan')">
<span class="tab-icon">📐</span>
<span class="tab-text">Plan</span>
</button>
<button class="detail-tab" data-tab="context" onclick="switchLiteDetailTab('context')">
<span class="tab-icon">📦</span>
<span class="tab-text">Context</span>
</button>
<button class="detail-tab" data-tab="summary" onclick="switchLiteDetailTab('summary')">
<span class="tab-icon">📝</span>
<span class="tab-text">Summary</span>
</button>
</div>
<!-- Tab Content -->
<div class="detail-tab-content" id="liteDetailTabContent">
${renderLiteTasksTab(session, tasks, completed, inProgress, pending)}
</div>
</div>
`;
// Initialize collapsible sections
setTimeout(() => {
document.querySelectorAll('.collapsible-header').forEach(header => {
header.addEventListener('click', () => toggleSection(header));
});
}, 50);
}
function goBackToLiteTasks() {
currentView = 'liteTasks';
currentSessionDetailKey = null;
updateContentTitle();
renderLiteTasks();
}
function switchLiteDetailTab(tabName) {
// Update active tab
document.querySelectorAll('.detail-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabName);
});
const session = liteTaskDataStore[currentSessionDetailKey];
if (!session) return;
const contentArea = document.getElementById('liteDetailTabContent');
const tasks = session.tasks || [];
const completed = tasks.filter(t => t.status === 'completed').length;
const inProgress = tasks.filter(t => t.status === 'in_progress').length;
const pending = tasks.filter(t => t.status === 'pending').length;
switch (tabName) {
case 'tasks':
contentArea.innerHTML = renderLiteTasksTab(session, tasks, completed, inProgress, pending);
// Re-initialize collapsible sections
setTimeout(() => {
document.querySelectorAll('.collapsible-header').forEach(header => {
header.addEventListener('click', () => toggleSection(header));
});
}, 50);
break;
case 'plan':
contentArea.innerHTML = renderLitePlanTab(session);
break;
case 'context':
loadAndRenderLiteContextTab(session, contentArea);
break;
case 'summary':
loadAndRenderLiteSummaryTab(session, contentArea);
break;
}
}
function renderLiteTasksTab(session, tasks, completed, inProgress, pending) {
// Populate drawer tasks for click-to-open functionality
currentDrawerTasks = tasks;
if (tasks.length === 0) {
return `
<div class="tab-empty-state">
<div class="empty-icon">📋</div>
<div class="empty-title">No Tasks</div>
<div class="empty-text">This session has no tasks defined.</div>
</div>
`;
}
return `
<div class="tasks-tab-content">
<div class="tasks-list" id="liteTasksListContent">
${tasks.map(task => renderLiteTaskDetailItem(session.id, task)).join('')}
</div>
</div>
`;
}
function renderLiteTaskDetailItem(sessionId, task) {
const rawTask = task._raw || task;
const taskJsonId = `task-json-${sessionId}-${task.id}`.replace(/[^a-zA-Z0-9-]/g, '-');
taskJsonStore[taskJsonId] = rawTask;
// Get preview info for lite tasks
const action = rawTask.action || '';
const scope = rawTask.scope || '';
const modCount = rawTask.modification_points?.length || 0;
const implCount = rawTask.implementation?.length || 0;
const acceptCount = rawTask.acceptance?.length || 0;
return `
<div class="detail-task-item-full lite-task-item" onclick="openTaskDrawerForLite('${sessionId}', '${escapeHtml(task.id)}')" style="cursor: pointer;" title="Click to view details">
<div class="task-item-header-lite">
<span class="task-id-badge">${escapeHtml(task.id)}</span>
<span class="task-title">${escapeHtml(task.title || 'Untitled')}</span>
<button class="btn-view-json" onclick="event.stopPropagation(); showJsonModal('${taskJsonId}', '${escapeHtml(task.id)}')">{ } JSON</button>
</div>
<div class="task-item-meta-lite">
${action ? `<span class="meta-badge action">${escapeHtml(action)}</span>` : ''}
${scope ? `<span class="meta-badge scope">${escapeHtml(scope)}</span>` : ''}
${modCount > 0 ? `<span class="meta-badge mods">${modCount} mods</span>` : ''}
${implCount > 0 ? `<span class="meta-badge impl">${implCount} steps</span>` : ''}
${acceptCount > 0 ? `<span class="meta-badge accept">${acceptCount} acceptance</span>` : ''}
</div>
</div>
`;
}
function getMetaPreviewForLite(task, rawTask) {
const meta = task.meta || {};
const parts = [];
if (meta.type || rawTask.action) parts.push(meta.type || rawTask.action);
if (meta.scope || rawTask.scope) parts.push(meta.scope || rawTask.scope);
return parts.join(' | ') || 'No meta';
}
function openTaskDrawerForLite(sessionId, taskId) {
const session = liteTaskDataStore[currentSessionDetailKey];
if (!session) return;
const task = session.tasks?.find(t => t.id === taskId);
if (!task) return;
// Set current drawer tasks and session context
currentDrawerTasks = session.tasks || [];
window._currentDrawerSession = session;
document.getElementById('drawerTaskTitle').textContent = task.title || taskId;
// Use dedicated lite task drawer renderer
document.getElementById('drawerContent').innerHTML = renderLiteTaskDrawerContent(task, session);
document.getElementById('taskDetailDrawer').classList.add('open');
document.getElementById('drawerOverlay').classList.add('active');
}
function renderLitePlanTab(session) {
const plan = session.plan;
if (!plan) {
return `
<div class="tab-empty-state">
<div class="empty-icon">📐</div>
<div class="empty-title">No Plan Data</div>
<div class="empty-text">No plan.json found for this session.</div>
</div>
`;
}
return `
<div class="plan-tab-content">
<!-- Summary -->
${plan.summary ? `
<div class="plan-section">
<h4 class="plan-section-title">📋 Summary</h4>
<p class="plan-summary-text">${escapeHtml(plan.summary)}</p>
</div>
` : ''}
<!-- Approach -->
${plan.approach ? `
<div class="plan-section">
<h4 class="plan-section-title">🎯 Approach</h4>
<p class="plan-approach-text">${escapeHtml(plan.approach)}</p>
</div>
` : ''}
<!-- Focus Paths -->
${plan.focus_paths?.length ? `
<div class="plan-section">
<h4 class="plan-section-title">📁 Focus Paths</h4>
<div class="path-tags">
${plan.focus_paths.map(p => `<span class="path-tag">${escapeHtml(p)}</span>`).join('')}
</div>
</div>
` : ''}
<!-- Metadata -->
<div class="plan-section">
<h4 class="plan-section-title"> Metadata</h4>
<div class="plan-meta-grid">
${plan.estimated_time ? `<div class="meta-item"><span class="meta-label">Estimated Time:</span> ${escapeHtml(plan.estimated_time)}</div>` : ''}
${plan.complexity ? `<div class="meta-item"><span class="meta-label">Complexity:</span> ${escapeHtml(plan.complexity)}</div>` : ''}
${plan.recommended_execution ? `<div class="meta-item"><span class="meta-label">Execution:</span> ${escapeHtml(plan.recommended_execution)}</div>` : ''}
</div>
</div>
<!-- Raw JSON -->
<div class="plan-section">
<h4 class="plan-section-title">{ } Raw JSON</h4>
<pre class="json-content">${escapeHtml(JSON.stringify(plan, null, 2))}</pre>
</div>
</div>
`;
}
async function loadAndRenderLiteContextTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading context data...</div>';
try {
if (window.SERVER_MODE && session.path) {
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=context`);
if (response.ok) {
const data = await response.json();
contentArea.innerHTML = renderLiteContextContent(data.context, session);
return;
}
}
// Fallback: show plan context if available
contentArea.innerHTML = renderLiteContextContent(null, session);
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load context: ${err.message}</div>`;
}
}
async function loadAndRenderLiteSummaryTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading summaries...</div>';
try {
if (window.SERVER_MODE && session.path) {
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=summary`);
if (response.ok) {
const data = await response.json();
contentArea.innerHTML = renderSummaryContent(data.summaries);
return;
}
}
// Fallback
contentArea.innerHTML = `
<div class="tab-empty-state">
<div class="empty-icon">📝</div>
<div class="empty-title">No Summaries</div>
<div class="empty-text">No summaries found in .summaries/</div>
</div>
`;
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load summaries: ${err.message}</div>`;
}
}

View File

@@ -0,0 +1,243 @@
// ==========================================
// PROJECT OVERVIEW VIEW
// ==========================================
function renderProjectOverview() {
const container = document.getElementById('mainContent');
const project = workflowData.projectOverview;
if (!project) {
container.innerHTML = `
<div class="flex flex-col items-center justify-center py-16 text-center">
<div class="text-6xl mb-4">📋</div>
<h3 class="text-xl font-semibold text-foreground mb-2">No Project Overview</h3>
<p class="text-muted-foreground mb-4">
Run <code class="px-2 py-1 bg-muted rounded text-sm font-mono">/workflow:init</code> to initialize project analysis
</p>
</div>
`;
return;
}
container.innerHTML = `
<!-- Project Header -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<div class="flex items-start justify-between mb-4">
<div>
<h2 class="text-2xl font-bold text-foreground mb-2">${escapeHtml(project.projectName)}</h2>
<p class="text-muted-foreground">${escapeHtml(project.description || 'No description available')}</p>
</div>
<div class="text-sm text-muted-foreground text-right">
<div>Initialized: ${formatDate(project.initializedAt)}</div>
<div class="mt-1">Mode: <span class="font-mono text-xs px-2 py-0.5 bg-muted rounded">${escapeHtml(project.metadata?.analysis_mode || 'unknown')}</span></div>
</div>
</div>
</div>
<!-- Technology Stack -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>💻</span> Technology Stack
</h3>
<!-- Languages -->
<div class="mb-5">
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Languages</h4>
<div class="flex flex-wrap gap-3">
${project.technologyStack.languages.map(lang => `
<div class="flex items-center gap-2 px-3 py-2 bg-background border border-border rounded-lg ${lang.primary ? 'ring-2 ring-primary' : ''}">
<span class="font-semibold text-foreground">${escapeHtml(lang.name)}</span>
<span class="text-xs text-muted-foreground">${lang.file_count} files</span>
${lang.primary ? '<span class="text-xs px-1.5 py-0.5 bg-primary text-primary-foreground rounded">Primary</span>' : ''}
</div>
`).join('') || '<span class="text-muted-foreground text-sm">No languages detected</span>'}
</div>
</div>
<!-- Frameworks -->
<div class="mb-5">
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Frameworks</h4>
<div class="flex flex-wrap gap-2">
${project.technologyStack.frameworks.map(fw => `
<span class="px-3 py-1.5 bg-success-light text-success rounded-lg text-sm font-medium">${escapeHtml(fw)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">No frameworks detected</span>'}
</div>
</div>
<!-- Build Tools -->
<div class="mb-5">
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Build Tools</h4>
<div class="flex flex-wrap gap-2">
${project.technologyStack.build_tools.map(tool => `
<span class="px-3 py-1.5 bg-warning-light text-warning rounded-lg text-sm font-medium">${escapeHtml(tool)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">No build tools detected</span>'}
</div>
</div>
<!-- Test Frameworks -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Test Frameworks</h4>
<div class="flex flex-wrap gap-2">
${project.technologyStack.test_frameworks.map(fw => `
<span class="px-3 py-1.5 bg-accent text-accent-foreground rounded-lg text-sm font-medium">${escapeHtml(fw)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">No test frameworks detected</span>'}
</div>
</div>
</div>
<!-- Architecture -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>🏗️</span> Architecture
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
<!-- Style -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Style</h4>
<div class="px-3 py-2 bg-background border border-border rounded-lg">
<span class="text-foreground font-medium">${escapeHtml(project.architecture.style)}</span>
</div>
</div>
<!-- Layers -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Layers</h4>
<div class="flex flex-wrap gap-2">
${project.architecture.layers.map(layer => `
<span class="px-2 py-1 bg-muted text-foreground rounded text-sm">${escapeHtml(layer)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">None</span>'}
</div>
</div>
<!-- Patterns -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Patterns</h4>
<div class="flex flex-wrap gap-2">
${project.architecture.patterns.map(pattern => `
<span class="px-2 py-1 bg-muted text-foreground rounded text-sm">${escapeHtml(pattern)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">None</span>'}
</div>
</div>
</div>
</div>
<!-- Key Components -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>⚙️</span> Key Components
</h3>
${project.keyComponents.length > 0 ? `
<div class="space-y-3">
${project.keyComponents.map(comp => {
const importanceColors = {
high: 'border-l-4 border-l-destructive bg-destructive/5',
medium: 'border-l-4 border-l-warning bg-warning/5',
low: 'border-l-4 border-l-muted-foreground bg-muted'
};
const importanceBadges = {
high: '<span class="px-2 py-0.5 text-xs font-semibold bg-destructive text-destructive-foreground rounded">High</span>',
medium: '<span class="px-2 py-0.5 text-xs font-semibold bg-warning text-foreground rounded">Medium</span>',
low: '<span class="px-2 py-0.5 text-xs font-semibold bg-muted text-muted-foreground rounded">Low</span>'
};
return `
<div class="p-4 ${importanceColors[comp.importance] || importanceColors.low} rounded-lg">
<div class="flex items-start justify-between mb-2">
<h4 class="font-semibold text-foreground">${escapeHtml(comp.name)}</h4>
${importanceBadges[comp.importance] || ''}
</div>
<p class="text-sm text-muted-foreground mb-2">${escapeHtml(comp.description)}</p>
<code class="text-xs font-mono text-primary">${escapeHtml(comp.path)}</code>
</div>
`;
}).join('')}
</div>
` : '<p class="text-muted-foreground text-sm">No key components identified</p>'}
</div>
<!-- Development Index -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>📝</span> Development History
</h3>
${renderDevelopmentIndex(project.developmentIndex)}
</div>
<!-- Statistics -->
<div class="bg-card border border-border rounded-lg p-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>📊</span> Statistics
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="text-center p-4 bg-background rounded-lg">
<div class="text-3xl font-bold text-primary mb-1">${project.statistics.total_features || 0}</div>
<div class="text-sm text-muted-foreground">Total Features</div>
</div>
<div class="text-center p-4 bg-background rounded-lg">
<div class="text-3xl font-bold text-success mb-1">${project.statistics.total_sessions || 0}</div>
<div class="text-sm text-muted-foreground">Total Sessions</div>
</div>
<div class="text-center p-4 bg-background rounded-lg">
<div class="text-sm text-muted-foreground mb-1">Last Updated</div>
<div class="text-sm font-medium text-foreground">${formatDate(project.statistics.last_updated)}</div>
</div>
</div>
</div>
`;
}
function renderDevelopmentIndex(devIndex) {
if (!devIndex) return '<p class="text-muted-foreground text-sm">No development history available</p>';
const categories = [
{ key: 'feature', label: 'Features', icon: '✨', badgeClass: 'bg-primary-light text-primary' },
{ key: 'enhancement', label: 'Enhancements', icon: '⚡', badgeClass: 'bg-success-light text-success' },
{ key: 'bugfix', label: 'Bug Fixes', icon: '🐛', badgeClass: 'bg-destructive/10 text-destructive' },
{ key: 'refactor', label: 'Refactorings', icon: '🔧', badgeClass: 'bg-warning-light text-warning' },
{ key: 'docs', label: 'Documentation', icon: '📚', badgeClass: 'bg-muted text-muted-foreground' }
];
const totalEntries = categories.reduce((sum, cat) => sum + (devIndex[cat.key]?.length || 0), 0);
if (totalEntries === 0) {
return '<p class="text-muted-foreground text-sm">No development history entries</p>';
}
return `
<div class="space-y-4">
${categories.map(cat => {
const entries = devIndex[cat.key] || [];
if (entries.length === 0) return '';
return `
<div>
<h4 class="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<span>${cat.icon}</span>
<span>${cat.label}</span>
<span class="text-xs px-2 py-0.5 ${cat.badgeClass} rounded-full">${entries.length}</span>
</h4>
<div class="space-y-2">
${entries.slice(0, 5).map(entry => `
<div class="p-3 bg-background border border-border rounded-lg hover:shadow-sm transition-shadow">
<div class="flex items-start justify-between mb-1">
<h5 class="font-medium text-foreground text-sm">${escapeHtml(entry.title)}</h5>
<span class="text-xs text-muted-foreground">${formatDate(entry.date)}</span>
</div>
${entry.description ? `<p class="text-sm text-muted-foreground mb-1">${escapeHtml(entry.description)}</p>` : ''}
<div class="flex items-center gap-2 text-xs">
${entry.sub_feature ? `<span class="px-2 py-0.5 bg-muted rounded">${escapeHtml(entry.sub_feature)}</span>` : ''}
${entry.status ? `<span class="px-2 py-0.5 ${entry.status === 'completed' ? 'bg-success-light text-success' : 'bg-warning-light text-warning'} rounded">${escapeHtml(entry.status)}</span>` : ''}
</div>
</div>
`).join('')}
${entries.length > 5 ? `<div class="text-sm text-muted-foreground text-center py-2">... and ${entries.length - 5} more</div>` : ''}
</div>
</div>
`;
}).join('')}
</div>
`;
}

View File

@@ -0,0 +1,176 @@
// ==========================================
// REVIEW SESSION DETAIL PAGE
// ==========================================
function renderReviewSessionDetailPage(session) {
const isActive = session._isActive !== false;
const tasks = session.tasks || [];
const dimensions = session.reviewDimensions || [];
// Calculate review statistics
const totalFindings = dimensions.reduce((sum, d) => sum + (d.findings?.length || 0), 0);
const criticalCount = dimensions.reduce((sum, d) =>
sum + (d.findings?.filter(f => f.severity === 'critical').length || 0), 0);
const highCount = dimensions.reduce((sum, d) =>
sum + (d.findings?.filter(f => f.severity === 'high').length || 0), 0);
return `
<div class="session-detail-page session-type-review">
<!-- Header -->
<div class="detail-header">
<button class="btn-back" onclick="goBackToSessions()">
<span class="back-icon">←</span>
<span>Back to Sessions</span>
</button>
<div class="detail-title-row">
<h2 class="detail-session-id">🔍 ${escapeHtml(session.session_id)}</h2>
<div class="detail-badges">
<span class="session-type-badge review">Review</span>
<span class="session-status ${isActive ? 'active' : 'archived'}">
${isActive ? 'ACTIVE' : 'ARCHIVED'}
</span>
</div>
</div>
</div>
<!-- Review Progress Section -->
<div class="review-progress-section">
<div class="review-progress-header">
<h3>📊 Review Progress</h3>
<span class="phase-badge ${session.phase || 'in-progress'}">${session.phase || 'In Progress'}</span>
</div>
<!-- Summary Cards -->
<div class="review-summary-grid">
<div class="summary-card">
<div class="summary-icon">📊</div>
<div class="summary-value">${totalFindings}</div>
<div class="summary-label">Total Findings</div>
</div>
<div class="summary-card critical">
<div class="summary-icon">🔴</div>
<div class="summary-value">${criticalCount}</div>
<div class="summary-label">Critical</div>
</div>
<div class="summary-card high">
<div class="summary-icon">🟠</div>
<div class="summary-value">${highCount}</div>
<div class="summary-label">High</div>
</div>
<div class="summary-card">
<div class="summary-icon">📋</div>
<div class="summary-value">${dimensions.length}</div>
<div class="summary-label">Dimensions</div>
</div>
</div>
<!-- Dimension Timeline -->
<div class="dimension-timeline" id="dimensionTimeline">
${dimensions.map((dim, idx) => `
<div class="dimension-item ${dim.status || 'pending'}" data-dimension="${dim.name}">
<div class="dimension-number">D${idx + 1}</div>
<div class="dimension-name">${escapeHtml(dim.name || 'Unknown')}</div>
<div class="dimension-stats">${dim.findings?.length || 0} findings</div>
</div>
`).join('')}
</div>
</div>
<!-- Findings Grid -->
<div class="review-findings-section">
<div class="findings-header">
<h3>🔍 Findings by Dimension</h3>
<div class="findings-filters">
<button class="filter-btn active" data-severity="all" onclick="filterReviewFindings('all')">All</button>
<button class="filter-btn" data-severity="critical" onclick="filterReviewFindings('critical')">Critical</button>
<button class="filter-btn" data-severity="high" onclick="filterReviewFindings('high')">High</button>
<button class="filter-btn" data-severity="medium" onclick="filterReviewFindings('medium')">Medium</button>
</div>
</div>
<div class="findings-grid" id="reviewFindingsGrid">
${renderReviewFindingsGrid(dimensions)}
</div>
</div>
<!-- Session Info -->
<div class="detail-info-bar">
<div class="info-item">
<span class="info-label">Created:</span>
<span class="info-value">${formatDate(session.created_at)}</span>
</div>
${session.archived_at ? `
<div class="info-item">
<span class="info-label">Archived:</span>
<span class="info-value">${formatDate(session.archived_at)}</span>
</div>
` : ''}
<div class="info-item">
<span class="info-label">Project:</span>
<span class="info-value">${escapeHtml(session.project || '-')}</span>
</div>
</div>
</div>
`;
}
function renderReviewFindingsGrid(dimensions) {
if (!dimensions || dimensions.length === 0) {
return `
<div class="empty-state">
<div class="empty-icon">🔍</div>
<div class="empty-text">No review dimensions found</div>
</div>
`;
}
let html = '';
dimensions.forEach(dim => {
const findings = dim.findings || [];
if (findings.length === 0) return;
html += `
<div class="dimension-findings-group" data-dimension="${dim.name}">
<div class="dimension-group-header">
<span class="dimension-badge">${escapeHtml(dim.name)}</span>
<span class="dimension-count">${findings.length} findings</span>
</div>
<div class="findings-cards">
${findings.map(f => `
<div class="finding-card severity-${f.severity || 'medium'}" data-severity="${f.severity || 'medium'}">
<div class="finding-card-header">
<span class="severity-badge ${f.severity || 'medium'}">${f.severity || 'medium'}</span>
${f.fix_status ? `<span class="fix-status-badge status-${f.fix_status}">${f.fix_status}</span>` : ''}
</div>
<div class="finding-card-title">${escapeHtml(f.title || 'Finding')}</div>
<div class="finding-card-desc">${escapeHtml((f.description || '').substring(0, 100))}${f.description?.length > 100 ? '...' : ''}</div>
${f.file ? `<div class="finding-card-file">📄 ${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}</div>` : ''}
</div>
`).join('')}
</div>
</div>
`;
});
return html || '<div class="empty-state"><div class="empty-text">No findings</div></div>';
}
function initReviewSessionPage(session) {
// Initialize event handlers for review session page
// Filter handlers are inline onclick
}
function filterReviewFindings(severity) {
// Update filter buttons
document.querySelectorAll('.findings-filters .filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.severity === severity);
});
// Filter finding cards
document.querySelectorAll('.finding-card').forEach(card => {
if (severity === 'all' || card.dataset.severity === severity) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
}

View File

@@ -0,0 +1,761 @@
// ============================================
// SESSION DETAIL VIEW
// ============================================
// Standard workflow session detail page rendering
function showSessionDetailPage(sessionKey) {
const session = sessionDataStore[sessionKey];
if (!session) return;
currentView = 'sessionDetail';
currentSessionDetailKey = sessionKey;
updateContentTitle();
const container = document.getElementById('mainContent');
const sessionType = session.type || 'workflow';
// Render specialized pages for review and test-fix sessions
if (sessionType === 'review' || sessionType === 'review-cycle') {
container.innerHTML = renderReviewSessionDetailPage(session);
initReviewSessionPage(session);
return;
}
if (sessionType === 'test-fix' || sessionType === 'fix') {
container.innerHTML = renderFixSessionDetailPage(session);
initFixSessionPage(session);
return;
}
// Default workflow session detail page
const tasks = session.tasks || [];
const completed = tasks.filter(t => t.status === 'completed').length;
const inProgress = tasks.filter(t => t.status === 'in_progress').length;
const pending = tasks.filter(t => t.status === 'pending').length;
const isActive = session._isActive !== false;
container.innerHTML = `
<div class="session-detail-page">
<!-- Header -->
<div class="detail-header">
<button class="btn-back" onclick="goBackToSessions()">
<span class="back-icon">←</span>
<span>Back to Sessions</span>
</button>
<div class="detail-title-row">
<h2 class="detail-session-id">${escapeHtml(session.session_id)}</h2>
<div class="detail-badges">
<span class="session-type-badge ${session.type || 'workflow'}">${session.type || 'workflow'}</span>
<span class="session-status ${isActive ? 'active' : 'archived'}">
${isActive ? 'ACTIVE' : 'ARCHIVED'}
</span>
</div>
</div>
</div>
<!-- Session Info Bar -->
<div class="detail-info-bar">
<div class="info-item">
<span class="info-label">Created:</span>
<span class="info-value">${formatDate(session.created_at)}</span>
</div>
${session.archived_at ? `
<div class="info-item">
<span class="info-label">Archived:</span>
<span class="info-value">${formatDate(session.archived_at)}</span>
</div>
` : ''}
<div class="info-item">
<span class="info-label">Project:</span>
<span class="info-value">${escapeHtml(session.project || '-')}</span>
</div>
<div class="info-item">
<span class="info-label">Tasks:</span>
<span class="info-value">${completed}/${tasks.length} completed</span>
</div>
</div>
<!-- Tab Navigation -->
<div class="detail-tabs">
<button class="detail-tab active" data-tab="tasks" onclick="switchDetailTab('tasks')">
<span class="tab-icon">📋</span>
<span class="tab-text">Tasks</span>
<span class="tab-count">${tasks.length}</span>
</button>
<button class="detail-tab" data-tab="context" onclick="switchDetailTab('context')">
<span class="tab-icon">📦</span>
<span class="tab-text">Context</span>
</button>
<button class="detail-tab" data-tab="summary" onclick="switchDetailTab('summary')">
<span class="tab-icon">📝</span>
<span class="tab-text">Summary</span>
</button>
<button class="detail-tab" data-tab="impl-plan" onclick="switchDetailTab('impl-plan')">
<span class="tab-icon">📐</span>
<span class="tab-text">IMPL Plan</span>
</button>
${session.hasReview ? `
<button class="detail-tab" data-tab="review" onclick="switchDetailTab('review')">
<span class="tab-icon">🔍</span>
<span class="tab-text">Review</span>
</button>
` : ''}
</div>
<!-- Tab Content -->
<div class="detail-tab-content" id="detailTabContent">
${renderTasksTab(session, tasks, completed, inProgress, pending)}
</div>
</div>
`;
}
function goBackToSessions() {
currentView = 'sessions';
currentSessionDetailKey = null;
updateContentTitle();
renderSessions();
}
function switchDetailTab(tabName) {
// Update active tab
document.querySelectorAll('.detail-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabName);
});
const session = sessionDataStore[currentSessionDetailKey];
if (!session) return;
const contentArea = document.getElementById('detailTabContent');
const tasks = session.tasks || [];
const completed = tasks.filter(t => t.status === 'completed').length;
const inProgress = tasks.filter(t => t.status === 'in_progress').length;
const pending = tasks.filter(t => t.status === 'pending').length;
switch (tabName) {
case 'tasks':
contentArea.innerHTML = renderTasksTab(session, tasks, completed, inProgress, pending);
break;
case 'context':
loadAndRenderContextTab(session, contentArea);
break;
case 'summary':
loadAndRenderSummaryTab(session, contentArea);
break;
case 'impl-plan':
loadAndRenderImplPlanTab(session, contentArea);
break;
case 'review':
loadAndRenderReviewTab(session, contentArea);
break;
}
}
function renderTasksTab(session, tasks, completed, inProgress, pending) {
// Populate drawer tasks for click-to-open functionality
currentDrawerTasks = tasks;
// Auto-load full task details in server mode
if (window.SERVER_MODE && session.path) {
// Schedule auto-load after DOM render
setTimeout(() => loadFullTaskDetails(), 50);
}
// Show task list with loading state or basic list
const showLoading = window.SERVER_MODE && session.path;
return `
<div class="tasks-tab-content">
<!-- Combined Stats & Actions Bar -->
<div class="task-toolbar">
<div class="task-stats-bar">
<span class="task-stat completed">✓ ${completed} completed</span>
<span class="task-stat in-progress">⟳ ${inProgress} in progress</span>
<span class="task-stat pending">○ ${pending} pending</span>
</div>
<div class="toolbar-divider"></div>
<div class="task-bulk-actions">
<span class="bulk-label">Quick Actions:</span>
<button class="bulk-action-btn" onclick="bulkSetAllStatus('pending')" title="Set all tasks to pending">
<span class="bulk-icon">○</span> All Pending
</button>
<button class="bulk-action-btn" onclick="bulkSetAllStatus('in_progress')" title="Set all tasks to in progress">
<span class="bulk-icon">⟳</span> All In Progress
</button>
<button class="bulk-action-btn completed" onclick="bulkSetAllStatus('completed')" title="Set all tasks to completed">
<span class="bulk-icon">✓</span> All Completed
</button>
</div>
</div>
<div class="tasks-list" id="tasksListContent">
${showLoading ? `
<div class="tab-loading">Loading task details...</div>
` : (tasks.length === 0 ? `
<div class="tab-empty-state">
<div class="empty-icon">📋</div>
<div class="empty-title">No Tasks</div>
<div class="empty-text">This session has no tasks defined.</div>
</div>
` : tasks.map(task => renderDetailTaskItem(task)).join(''))}
</div>
</div>
`;
}
async function loadFullTaskDetails() {
const session = sessionDataStore[currentSessionDetailKey];
if (!session || !window.SERVER_MODE || !session.path) return;
const tasksContainer = document.getElementById('tasksListContent');
tasksContainer.innerHTML = '<div class="tab-loading">Loading full task details...</div>';
try {
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=tasks`);
if (response.ok) {
const data = await response.json();
if (data.tasks && data.tasks.length > 0) {
// Populate drawer tasks for click-to-open functionality
currentDrawerTasks = data.tasks;
tasksContainer.innerHTML = data.tasks.map(task => renderDetailTaskItem(task)).join('');
} else {
tasksContainer.innerHTML = `
<div class="tab-empty-state">
<div class="empty-icon">📋</div>
<div class="empty-title">No Task Files</div>
<div class="empty-text">No IMPL-*.json files found in .task/</div>
</div>
`;
}
}
} catch (err) {
tasksContainer.innerHTML = `<div class="tab-error">Failed to load tasks: ${err.message}</div>`;
}
}
function renderDetailTaskItem(task) {
const taskId = task.task_id || task.id || 'Unknown';
const status = task.status || 'pending';
// Status options for dropdown
const statusOptions = ['pending', 'in_progress', 'completed'];
return `
<div class="detail-task-item ${status} status-${status}" data-task-id="${escapeHtml(taskId)}">
<div class="task-item-header">
<span class="task-id-badge">${escapeHtml(taskId)}</span>
<span class="task-title" onclick="openTaskDrawer('${escapeHtml(taskId)}')" style="cursor: pointer; flex: 1;">
${escapeHtml(task.title || task.meta?.title || 'Untitled')}
</span>
<div class="task-status-control" onclick="event.stopPropagation()">
<select class="task-status-select ${status}" onchange="updateSingleTaskStatus('${escapeHtml(taskId)}', this.value)" data-current="${status}">
${statusOptions.map(opt => `
<option value="${opt}" ${opt === status ? 'selected' : ''}>${formatStatusLabel(opt)}</option>
`).join('')}
</select>
</div>
</div>
</div>
`;
}
function formatStatusLabel(status) {
const labels = {
'pending': '○ Pending',
'in_progress': '⟳ In Progress',
'completed': '✓ Completed'
};
return labels[status] || status;
}
function getMetaPreview(task) {
const meta = task.meta || {};
const parts = [];
if (meta.type) parts.push(meta.type);
if (meta.action) parts.push(meta.action);
if (meta.scope) parts.push(meta.scope);
return parts.join(' | ') || 'No meta';
}
function getTaskContextPreview(task) {
const items = [];
const ctx = task.context || {};
if (ctx.requirements?.length) items.push(`${ctx.requirements.length} reqs`);
if (ctx.focus_paths?.length) items.push(`${ctx.focus_paths.length} paths`);
if (task.modification_points?.length) items.push(`${task.modification_points.length} mods`);
if (task.description) items.push('desc');
return items.join(' | ') || 'No context';
}
function getFlowPreview(task) {
const steps = task.flow_control?.implementation_approach?.length || task.implementation?.length || 0;
return steps > 0 ? `${steps} steps` : 'No steps';
}
function renderTaskContext(task) {
const sections = [];
const ctx = task.context || {};
// Description
if (task.description) {
sections.push(`
<div class="context-field">
<label>description:</label>
<p>${escapeHtml(task.description)}</p>
</div>
`);
}
// Requirements
if (ctx.requirements?.length) {
sections.push(`
<div class="context-field">
<label>requirements:</label>
<ul>${ctx.requirements.map(r => `<li>${escapeHtml(r)}</li>`).join('')}</ul>
</div>
`);
}
// Focus paths
if (ctx.focus_paths?.length) {
sections.push(`
<div class="context-field">
<label>focus_paths:</label>
<div class="path-tags">${ctx.focus_paths.map(p => `<span class="path-tag">${escapeHtml(p)}</span>`).join('')}</div>
</div>
`);
}
// Modification points
if (task.modification_points?.length) {
sections.push(`
<div class="context-field">
<label>modification_points:</label>
<div class="mod-points">
${task.modification_points.map(m => `
<div class="mod-point">
<span class="array-item path-item">${escapeHtml(m.file || m)}</span>
${m.target ? `<span class="mod-target">→ ${escapeHtml(m.target)}</span>` : ''}
${m.change ? `<p class="mod-change">${escapeHtml(m.change)}</p>` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
// Acceptance criteria
const acceptance = ctx.acceptance || task.acceptance || [];
if (acceptance.length) {
sections.push(`
<div class="context-field">
<label>acceptance:</label>
<ul>${acceptance.map(a => `<li>${escapeHtml(a)}</li>`).join('')}</ul>
</div>
`);
}
return sections.length > 0
? `<div class="context-fields">${sections.join('')}</div>`
: '<div class="field-value json-value-null">No context data</div>';
}
function renderFlowControl(task) {
const sections = [];
const fc = task.flow_control || {};
// Implementation approach
const steps = fc.implementation_approach || task.implementation || [];
if (steps.length) {
sections.push(`
<div class="context-field">
<label>implementation_approach:</label>
<ol class="impl-steps">
${steps.map(s => `<li>${escapeHtml(typeof s === 'string' ? s : s.step || s.action || JSON.stringify(s))}</li>`).join('')}
</ol>
</div>
`);
}
// Pre-analysis
const preAnalysis = fc.pre_analysis || task.pre_analysis || [];
if (preAnalysis.length) {
sections.push(`
<div class="context-field">
<label>pre_analysis:</label>
<ul>${preAnalysis.map(p => `<li>${escapeHtml(p)}</li>`).join('')}</ul>
</div>
`);
}
// Target files
const targetFiles = fc.target_files || task.target_files || [];
if (targetFiles.length) {
sections.push(`
<div class="context-field">
<label>target_files:</label>
<div class="path-tags">${targetFiles.map(f => `<span class="path-tag">${escapeHtml(f)}</span>`).join('')}</div>
</div>
`);
}
return sections.length > 0
? `<div class="context-fields">${sections.join('')}</div>`
: '<div class="field-value json-value-null">No flow control data</div>';
}
async function loadAndRenderContextTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading context data...</div>';
try {
// Try to load context-package.json from server
if (window.SERVER_MODE && session.path) {
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=context`);
if (response.ok) {
const data = await response.json();
contentArea.innerHTML = renderContextContent(data.context);
return;
}
}
// Fallback: show placeholder
contentArea.innerHTML = `
<div class="tab-empty-state">
<div class="empty-icon">📦</div>
<div class="empty-title">Context Data</div>
<div class="empty-text">Context data will be loaded from context-package.json</div>
</div>
`;
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load context: ${err.message}</div>`;
}
}
async function loadAndRenderSummaryTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading summaries...</div>';
try {
if (window.SERVER_MODE && session.path) {
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=summary`);
if (response.ok) {
const data = await response.json();
contentArea.innerHTML = renderSummaryContent(data.summaries);
return;
}
}
contentArea.innerHTML = `
<div class="tab-empty-state">
<div class="empty-icon">📝</div>
<div class="empty-title">Summaries</div>
<div class="empty-text">Session summaries will be loaded from .summaries/</div>
</div>
`;
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load summaries: ${err.message}</div>`;
}
}
async function loadAndRenderImplPlanTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading IMPL plan...</div>';
try {
if (window.SERVER_MODE && session.path) {
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=impl-plan`);
if (response.ok) {
const data = await response.json();
contentArea.innerHTML = renderImplPlanContent(data.implPlan);
return;
}
}
contentArea.innerHTML = `
<div class="tab-empty-state">
<div class="empty-icon">📐</div>
<div class="empty-title">IMPL Plan</div>
<div class="empty-text">IMPL plan will be loaded from IMPL_PLAN.md</div>
</div>
`;
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load IMPL plan: ${err.message}</div>`;
}
}
async function loadAndRenderReviewTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading review data...</div>';
try {
if (window.SERVER_MODE && session.path) {
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=review`);
if (response.ok) {
const data = await response.json();
contentArea.innerHTML = renderReviewContent(data.review);
return;
}
}
contentArea.innerHTML = `
<div class="tab-empty-state">
<div class="empty-icon">🔍</div>
<div class="empty-title">Review Data</div>
<div class="empty-text">Review data will be loaded from review files</div>
</div>
`;
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load review: ${err.message}</div>`;
}
}
function showRawSessionJson(sessionKey) {
const session = sessionDataStore[sessionKey];
if (!session) return;
// Close current modal
const currentModal = document.querySelector('.session-modal-overlay');
if (currentModal) currentModal.remove();
// Show JSON modal
const overlay = document.createElement('div');
overlay.className = 'json-modal-overlay active';
overlay.innerHTML = `
<div class="json-modal">
<div class="json-modal-header">
<div class="json-modal-title">
<span class="session-id-badge">${escapeHtml(session.session_id)}</span>
<span>Session JSON</span>
</div>
<button class="json-modal-close" onclick="closeJsonModal(this)">&times;</button>
</div>
<div class="json-modal-body">
<pre class="json-modal-content">${escapeHtml(JSON.stringify(session, null, 2))}</pre>
</div>
<div class="json-modal-footer">
<button class="json-modal-copy" onclick="copyJsonToClipboard(this)">Copy to Clipboard</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) closeJsonModal();
});
}
// ==========================================
// TASK STATUS MANAGEMENT
// ==========================================
async function updateSingleTaskStatus(taskId, newStatus) {
const session = sessionDataStore[currentSessionDetailKey];
if (!session || !window.SERVER_MODE || !session.path) {
showToast('Status update requires server mode', 'error');
return;
}
try {
const response = await fetch('/api/update-task-status', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionPath: session.path,
taskId: taskId,
newStatus: newStatus
})
});
const result = await response.json();
if (result.success) {
// Update UI
updateTaskItemUI(taskId, newStatus);
updateTaskStatsBar();
showToast(`Task ${taskId}${formatStatusLabel(newStatus)}`, 'success');
} else {
showToast(result.error || 'Failed to update status', 'error');
// Revert select
revertTaskSelect(taskId);
}
} catch (error) {
showToast('Error updating status: ' + error.message, 'error');
revertTaskSelect(taskId);
}
}
async function bulkSetAllStatus(newStatus) {
const session = sessionDataStore[currentSessionDetailKey];
if (!session || !window.SERVER_MODE || !session.path) {
showToast('Bulk update requires server mode', 'error');
return;
}
const taskIds = currentDrawerTasks.map(t => t.task_id || t.id);
if (taskIds.length === 0) return;
if (!confirm(`Set all ${taskIds.length} tasks to "${formatStatusLabel(newStatus)}"?`)) {
return;
}
try {
const response = await fetch('/api/bulk-update-task-status', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionPath: session.path,
taskIds: taskIds,
newStatus: newStatus
})
});
const result = await response.json();
if (result.success) {
// Update all task UIs
taskIds.forEach(id => updateTaskItemUI(id, newStatus));
updateTaskStatsBar();
showToast(`All ${taskIds.length} tasks → ${formatStatusLabel(newStatus)}`, 'success');
} else {
showToast(result.error || 'Failed to bulk update', 'error');
}
} catch (error) {
showToast('Error in bulk update: ' + error.message, 'error');
}
}
async function bulkSetPendingToInProgress() {
const session = sessionDataStore[currentSessionDetailKey];
if (!session || !window.SERVER_MODE || !session.path) {
showToast('Bulk update requires server mode', 'error');
return;
}
const pendingTaskIds = currentDrawerTasks
.filter(t => (t.status || 'pending') === 'pending')
.map(t => t.task_id || t.id);
if (pendingTaskIds.length === 0) {
showToast('No pending tasks to start', 'info');
return;
}
try {
const response = await fetch('/api/bulk-update-task-status', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionPath: session.path,
taskIds: pendingTaskIds,
newStatus: 'in_progress'
})
});
const result = await response.json();
if (result.success) {
pendingTaskIds.forEach(id => updateTaskItemUI(id, 'in_progress'));
updateTaskStatsBar();
showToast(`${pendingTaskIds.length} tasks: Pending → In Progress`, 'success');
} else {
showToast(result.error || 'Failed to update', 'error');
}
} catch (error) {
showToast('Error: ' + error.message, 'error');
}
}
async function bulkSetInProgressToCompleted() {
const session = sessionDataStore[currentSessionDetailKey];
if (!session || !window.SERVER_MODE || !session.path) {
showToast('Bulk update requires server mode', 'error');
return;
}
const inProgressTaskIds = currentDrawerTasks
.filter(t => t.status === 'in_progress')
.map(t => t.task_id || t.id);
if (inProgressTaskIds.length === 0) {
showToast('No in-progress tasks to complete', 'info');
return;
}
try {
const response = await fetch('/api/bulk-update-task-status', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionPath: session.path,
taskIds: inProgressTaskIds,
newStatus: 'completed'
})
});
const result = await response.json();
if (result.success) {
inProgressTaskIds.forEach(id => updateTaskItemUI(id, 'completed'));
updateTaskStatsBar();
showToast(`${inProgressTaskIds.length} tasks: In Progress → Completed`, 'success');
} else {
showToast(result.error || 'Failed to update', 'error');
}
} catch (error) {
showToast('Error: ' + error.message, 'error');
}
}
function updateTaskItemUI(taskId, newStatus) {
const taskItem = document.querySelector(`.detail-task-item[data-task-id="${taskId}"]`);
if (!taskItem) return;
// Update classes
taskItem.className = `detail-task-item ${newStatus} status-${newStatus}`;
// Update select
const select = taskItem.querySelector('.task-status-select');
if (select) {
select.value = newStatus;
select.className = `task-status-select ${newStatus}`;
select.dataset.current = newStatus;
}
// Update drawer tasks data
const task = currentDrawerTasks.find(t => (t.task_id || t.id) === taskId);
if (task) {
task.status = newStatus;
}
}
function updateTaskStatsBar() {
const completed = currentDrawerTasks.filter(t => t.status === 'completed').length;
const inProgress = currentDrawerTasks.filter(t => t.status === 'in_progress').length;
const pending = currentDrawerTasks.filter(t => (t.status || 'pending') === 'pending').length;
const statsBar = document.querySelector('.task-stats-bar');
if (statsBar) {
statsBar.innerHTML = `
<span class="task-stat completed">✓ ${completed} completed</span>
<span class="task-stat in-progress">⟳ ${inProgress} in progress</span>
<span class="task-stat pending">○ ${pending} pending</span>
`;
}
}
function revertTaskSelect(taskId) {
const taskItem = document.querySelector(`.detail-task-item[data-task-id="${taskId}"]`);
if (!taskItem) return;
const select = taskItem.querySelector('.task-status-select');
if (select) {
select.value = select.dataset.current;
}
}
function showToast(message, type = 'info') {
// Remove existing toast
const existing = document.querySelector('.status-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.className = `status-toast ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
// Auto-remove after 3 seconds
setTimeout(() => {
toast.classList.add('fade-out');
setTimeout(() => toast.remove(), 300);
}, 3000);
}