mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-13 02:41:50 +08:00
Remove backup HTML template for workflow dashboard
This commit is contained in:
180
ccw/src/templates/dashboard-js/views/fix-session.js
Normal file
180
ccw/src/templates/dashboard-js/views/fix-session.js
Normal 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';
|
||||
}
|
||||
});
|
||||
}
|
||||
108
ccw/src/templates/dashboard-js/views/home.js
Normal file
108
ccw/src/templates/dashboard-js/views/home.js
Normal 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>
|
||||
`;
|
||||
}
|
||||
382
ccw/src/templates/dashboard-js/views/lite-tasks.js
Normal file
382
ccw/src/templates/dashboard-js/views/lite-tasks.js
Normal 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>`;
|
||||
}
|
||||
}
|
||||
243
ccw/src/templates/dashboard-js/views/project-overview.js
Normal file
243
ccw/src/templates/dashboard-js/views/project-overview.js
Normal 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>
|
||||
`;
|
||||
}
|
||||
176
ccw/src/templates/dashboard-js/views/review-session.js
Normal file
176
ccw/src/templates/dashboard-js/views/review-session.js
Normal 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';
|
||||
}
|
||||
});
|
||||
}
|
||||
761
ccw/src/templates/dashboard-js/views/session-detail.js
Normal file
761
ccw/src/templates/dashboard-js/views/session-detail.js
Normal 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)">×</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);
|
||||
}
|
||||
Reference in New Issue
Block a user