// ========================================== // REVIEW SESSION DETAIL PAGE // ========================================== // Enhanced with multi-select, filters, split preview panel, and export fix JSON // Review session state let reviewSessionState = { allFindings: [], filteredFindings: [], selectedFindings: new Set(), currentFilters: { dimension: 'all', severities: new Set(), search: '' }, sortConfig: { field: 'severity', order: 'desc' }, previewFinding: null, dimensions: [], session: null }; function renderReviewSessionDetailPage(session) { const isActive = session._isActive !== false; const dimensions = session.reviewDimensions || []; // Store session and dimensions reviewSessionState.session = session; reviewSessionState.dimensions = dimensions; // Build flat findings array with dimension info const allFindings = []; let findingIndex = 0; dimensions.forEach(dim => { (dim.findings || []).forEach(f => { allFindings.push({ id: f.id || `finding-${findingIndex++}`, title: f.title || 'Finding', description: f.description || '', severity: (f.severity || 'medium').toLowerCase(), dimension: dim.name || 'unknown', category: f.category || '', file: f.file || '', line: f.line || '', code_context: f.code_context || f.snippet || '', recommendations: f.recommendations || (f.recommendation ? [f.recommendation] : []), root_cause: f.root_cause || '', impact: f.impact || '', references: f.references || [], metadata: f.metadata || {}, fix_status: f.fix_status || null }); }); }); reviewSessionState.allFindings = allFindings; reviewSessionState.filteredFindings = [...allFindings]; reviewSessionState.selectedFindings.clear(); reviewSessionState.previewFinding = null; // Calculate statistics const totalFindings = allFindings.length; const severityCounts = { critical: allFindings.filter(f => f.severity === 'critical').length, high: allFindings.filter(f => f.severity === 'high').length, medium: allFindings.filter(f => f.severity === 'medium').length, low: allFindings.filter(f => f.severity === 'low').length }; return `

๐Ÿ” ${escapeHtml(session.session_id)}

Review ${isActive ? 'ACTIVE' : 'ARCHIVED'}

๐Ÿ“Š Review Progress

${(session.phase || 'In Progress').toUpperCase()}
๐Ÿ“Š
${totalFindings}
Total Findings
๐Ÿ”ด
${severityCounts.critical}
Critical
๐ŸŸ 
${severityCounts.high}
High
๐ŸŽฏ
${dimensions.length}
Dimensions
๐Ÿ”ด ${severityCounts.critical} ๐ŸŸ  ${severityCounts.high} ๐ŸŸก ${severityCounts.medium} ๐ŸŸข ${severityCounts.low}
0 selected
Severity:
Sort:
${dimensions.map(dim => ` `).join('')}
${totalFindings} findings
${renderReviewSessionFindingsList(allFindings)}
๐Ÿ‘†
Click on a finding to preview details
Created: ${formatDate(session.created_at)}
${session.archived_at ? `
Archived: ${formatDate(session.archived_at)}
` : ''}
Project: ${escapeHtml(session.project || '-')}
`; } // ========================================== // Findings List Rendering // ========================================== function renderReviewSessionFindingsList(findings) { if (findings.length === 0) { return `
โœจ No findings match your filters
`; } return findings.map(finding => `
${finding.severity} ${escapeHtml(finding.dimension)} ${finding.fix_status ? `${finding.fix_status}` : ''}
${escapeHtml(finding.title)}
${finding.file ? `
๐Ÿ“„ ${escapeHtml(finding.file)}${finding.line ? ':' + finding.line : ''}
` : ''}
`).join(''); } // ========================================== // Preview Panel // ========================================== function previewReviewSessionFinding(findingId) { const finding = reviewSessionState.allFindings.find(f => f.id === findingId); if (!finding) return; reviewSessionState.previewFinding = finding; // Update active state in list document.querySelectorAll('.review-finding-item').forEach(item => { item.classList.toggle('previewing', item.dataset.findingId === findingId); }); const previewPanel = document.getElementById('reviewSessionPreviewPanel'); if (!previewPanel) return; previewPanel.innerHTML = `
${finding.severity} ${escapeHtml(finding.dimension)} ${finding.category ? `${escapeHtml(finding.category)}` : ''} ${finding.fix_status ? `${finding.fix_status}` : ''}

${escapeHtml(finding.title)}

${finding.file ? `
๐Ÿ“ Location
${escapeHtml(finding.file)}${finding.line ? ':' + finding.line : ''}
` : ''}
๐Ÿ“ Description
${escapeHtml(finding.description)}
${finding.code_context ? `
๐Ÿ’ป Code Context
${escapeHtml(finding.code_context)}
` : ''} ${finding.recommendations && finding.recommendations.length > 0 ? `
โœ… Recommendations
` : ''} ${finding.root_cause ? `
๐ŸŽฏ Root Cause
${escapeHtml(finding.root_cause)}
` : ''} ${finding.impact ? `
โš ๏ธ Impact
${escapeHtml(finding.impact)}
` : ''} ${finding.references && finding.references.length > 0 ? `
๐Ÿ”— References
` : ''} ${finding.metadata && Object.keys(finding.metadata).length > 0 ? `
โ„น๏ธ Metadata
` : ''}
`; } // ========================================== // Selection Management // ========================================== function toggleReviewSessionFindingSelection(findingId, event) { if (event) { event.stopPropagation(); } if (reviewSessionState.selectedFindings.has(findingId)) { reviewSessionState.selectedFindings.delete(findingId); } else { reviewSessionState.selectedFindings.add(findingId); } updateReviewSessionSelectionUI(); // Update preview panel button if this finding is being previewed if (reviewSessionState.previewFinding && reviewSessionState.previewFinding.id === findingId) { previewReviewSessionFinding(findingId); } } function selectAllReviewSessionFindings() { reviewSessionState.allFindings.forEach(f => reviewSessionState.selectedFindings.add(f.id)); updateReviewSessionSelectionUI(); } function selectVisibleReviewSessionFindings() { reviewSessionState.filteredFindings.forEach(f => reviewSessionState.selectedFindings.add(f.id)); updateReviewSessionSelectionUI(); } function selectReviewSessionBySeverity(severity) { reviewSessionState.allFindings .filter(f => f.severity === severity) .forEach(f => reviewSessionState.selectedFindings.add(f.id)); updateReviewSessionSelectionUI(); } function clearReviewSessionSelection() { reviewSessionState.selectedFindings.clear(); updateReviewSessionSelectionUI(); } function updateReviewSessionSelectionUI() { // Update counter const counter = document.getElementById('reviewSessionSelectionCounter'); if (counter) { counter.textContent = `${reviewSessionState.selectedFindings.size} selected`; } // Update export button const exportBtn = document.getElementById('reviewSessionExportBtn'); if (exportBtn) { exportBtn.disabled = reviewSessionState.selectedFindings.size === 0; } // Update checkbox states in list document.querySelectorAll('.review-finding-item').forEach(item => { const findingId = item.dataset.findingId; const isSelected = reviewSessionState.selectedFindings.has(findingId); item.classList.toggle('selected', isSelected); const checkbox = item.querySelector('.finding-checkbox'); if (checkbox) { checkbox.checked = isSelected; } }); } // ========================================== // Filtering & Sorting // ========================================== function filterReviewSessionByDimension(dimension) { reviewSessionState.currentFilters.dimension = dimension; // Update tab active state document.querySelectorAll('.dim-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.dimension === dimension); }); // Update dimension timeline highlight document.querySelectorAll('.dimension-item').forEach(item => { item.classList.toggle('active', dimension === 'all' || item.dataset.dimension === dimension); }); applyReviewSessionFilters(); } function toggleReviewSessionSeverity(severity) { if (reviewSessionState.currentFilters.severities.has(severity)) { reviewSessionState.currentFilters.severities.delete(severity); } else { reviewSessionState.currentFilters.severities.add(severity); } // Update filter chip UI const filterChip = document.getElementById(`rs-filter-${severity}`); if (filterChip) { filterChip.classList.toggle('active', reviewSessionState.currentFilters.severities.has(severity)); const checkbox = filterChip.querySelector('input[type="checkbox"]'); if (checkbox) { checkbox.checked = reviewSessionState.currentFilters.severities.has(severity); } } applyReviewSessionFilters(); } function onReviewSessionSearch(searchText) { reviewSessionState.currentFilters.search = searchText.toLowerCase(); applyReviewSessionFilters(); } function applyReviewSessionFilters() { reviewSessionState.filteredFindings = reviewSessionState.allFindings.filter(finding => { // Dimension filter if (reviewSessionState.currentFilters.dimension !== 'all') { if (finding.dimension !== reviewSessionState.currentFilters.dimension) { return false; } } // Severity filter (multi-select) if (reviewSessionState.currentFilters.severities.size > 0) { if (!reviewSessionState.currentFilters.severities.has(finding.severity)) { return false; } } // Search filter if (reviewSessionState.currentFilters.search) { const searchText = `${finding.title} ${finding.description} ${finding.file} ${finding.category}`.toLowerCase(); if (!searchText.includes(reviewSessionState.currentFilters.search)) { return false; } } return true; }); sortReviewSessionFindings(); } function sortReviewSessionFindings() { const sortBy = document.getElementById('reviewSessionSortSelect')?.value || 'severity'; reviewSessionState.sortConfig.field = sortBy; const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; reviewSessionState.filteredFindings.sort((a, b) => { let comparison = 0; if (sortBy === 'severity') { comparison = severityOrder[a.severity] - severityOrder[b.severity]; } else if (sortBy === 'dimension') { comparison = a.dimension.localeCompare(b.dimension); } else if (sortBy === 'file') { comparison = (a.file || '').localeCompare(b.file || ''); } return reviewSessionState.sortConfig.order === 'asc' ? comparison : -comparison; }); renderFilteredReviewSessionFindings(); } function toggleReviewSessionSortOrder() { reviewSessionState.sortConfig.order = reviewSessionState.sortConfig.order === 'asc' ? 'desc' : 'asc'; const icon = document.getElementById('reviewSessionSortOrderIcon'); if (icon) { icon.textContent = reviewSessionState.sortConfig.order === 'asc' ? 'โ†‘' : 'โ†“'; } sortReviewSessionFindings(); } function resetReviewSessionFilters() { // Reset state reviewSessionState.currentFilters.dimension = 'all'; reviewSessionState.currentFilters.severities.clear(); reviewSessionState.currentFilters.search = ''; reviewSessionState.sortConfig.field = 'severity'; reviewSessionState.sortConfig.order = 'desc'; // Reset UI document.querySelectorAll('.dim-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.dimension === 'all'); }); document.querySelectorAll('.filter-chip').forEach(chip => { chip.classList.remove('active'); const checkbox = chip.querySelector('input[type="checkbox"]'); if (checkbox) checkbox.checked = false; }); document.querySelectorAll('.dimension-item').forEach(item => { item.classList.remove('active'); }); const searchInput = document.getElementById('reviewSessionSearchInput'); if (searchInput) searchInput.value = ''; const sortSelect = document.getElementById('reviewSessionSortSelect'); if (sortSelect) sortSelect.value = 'severity'; const sortIcon = document.getElementById('reviewSessionSortOrderIcon'); if (sortIcon) sortIcon.textContent = 'โ†“'; // Re-apply filters reviewSessionState.filteredFindings = [...reviewSessionState.allFindings]; sortReviewSessionFindings(); } function renderFilteredReviewSessionFindings() { const listContainer = document.getElementById('reviewSessionFindingsList'); const countEl = document.getElementById('reviewSessionFindingsCount'); if (listContainer) { listContainer.innerHTML = renderReviewSessionFindingsList(reviewSessionState.filteredFindings); } if (countEl) { countEl.textContent = `${reviewSessionState.filteredFindings.length} findings`; } } // ========================================== // Export Fix JSON // ========================================== function exportReviewSessionFixJson() { if (reviewSessionState.selectedFindings.size === 0) { showToast('Please select at least one finding to export', 'error'); return; } const selectedFindingsData = reviewSessionState.allFindings.filter(f => reviewSessionState.selectedFindings.has(f.id) ); const session = reviewSessionState.session; const sessionId = session?.session_id || 'unknown'; const exportId = `fix-${Date.now()}`; const exportData = { export_id: exportId, export_timestamp: new Date().toISOString(), review_id: `review-${sessionId}`, session_id: sessionId, findings_count: selectedFindingsData.length, findings: selectedFindingsData.map(f => ({ id: f.id, title: f.title, description: f.description, severity: f.severity, dimension: f.dimension, category: f.category || 'uncategorized', file: f.file, line: f.line, code_context: f.code_context || null, recommendations: f.recommendations || [], root_cause: f.root_cause || null })) }; // Convert to JSON and download const jsonStr = JSON.stringify(exportData, null, 2); const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const filename = `fix-export-${exportId}.json`; a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); // Show success notification const severityCounts = { critical: selectedFindingsData.filter(f => f.severity === 'critical').length, high: selectedFindingsData.filter(f => f.severity === 'high').length, medium: selectedFindingsData.filter(f => f.severity === 'medium').length, low: selectedFindingsData.filter(f => f.severity === 'low').length }; showToast(`Exported ${selectedFindingsData.length} findings (Critical: ${severityCounts.critical}, High: ${severityCounts.high}, Medium: ${severityCounts.medium}, Low: ${severityCounts.low})`, 'success'); } // ========================================== // Page Initialization // ========================================== function initReviewSessionPage(session) { // Reset state when page loads reviewSessionState.session = session; // Event handlers are inline onclick - no additional setup needed // Start fix progress polling if in server mode if (window.SERVER_MODE && session?.session_id) { startFixProgressPolling(session.session_id); } } // Legacy filter function for compatibility function filterReviewFindings(severity) { if (severity === 'all') { reviewSessionState.currentFilters.severities.clear(); } else { reviewSessionState.currentFilters.severities.clear(); reviewSessionState.currentFilters.severities.add(severity); } applyReviewSessionFilters(); } // ========================================== // FIX PROGRESS TRACKING // ========================================== // Fix progress state let fixProgressState = { fixPlan: null, progressData: null, pollInterval: null, currentSlide: 0 }; /** * Discover and load fix-plan.json for the current review session * Searches in: .review/fixes/{fix-session-id}/fix-plan.json */ async function loadFixProgress(sessionId) { if (!window.SERVER_MODE) { return null; } try { // First, discover active fix session const activeFixResponse = await fetch(`/api/file?path=${encodeURIComponent(projectPath + '/.review/fixes/active-fix-session.json')}`); if (!activeFixResponse.ok) { return null; } const activeFixSession = await activeFixResponse.json(); const fixSessionId = activeFixSession.fix_session_id; // Load fix-plan.json const planPath = `${projectPath}/.review/fixes/${fixSessionId}/fix-plan.json`; const planResponse = await fetch(`/api/file?path=${encodeURIComponent(planPath)}`); if (!planResponse.ok) { return null; } const fixPlan = await planResponse.json(); // Load progress files for each group const progressPromises = (fixPlan.groups || []).map(async (group) => { const progressPath = `${projectPath}/.review/fixes/${fixSessionId}/${group.progress_file}`; try { const response = await fetch(`/api/file?path=${encodeURIComponent(progressPath)}`); return response.ok ? await response.json() : null; } catch { return null; } }); const progressDataArray = await Promise.all(progressPromises); // Aggregate progress data const aggregated = aggregateFixProgress(fixPlan, progressDataArray.filter(d => d !== null)); fixProgressState.fixPlan = fixPlan; fixProgressState.progressData = aggregated; return aggregated; } catch (err) { console.error('Failed to load fix progress:', err); return null; } } /** * Aggregate progress from multiple group progress files */ function aggregateFixProgress(fixPlan, progressDataArray) { let totalFindings = 0; let fixedCount = 0; let failedCount = 0; let inProgressCount = 0; let pendingCount = 0; const activeAgents = []; progressDataArray.forEach(progress => { if (progress.findings) { progress.findings.forEach(f => { totalFindings++; if (f.result === 'fixed') fixedCount++; else if (f.result === 'failed') failedCount++; else if (f.status === 'in-progress') inProgressCount++; else pendingCount++; }); } if (progress.assigned_agent && progress.status === 'in-progress') { activeAgents.push({ agent_id: progress.assigned_agent, group_id: progress.group_id, current_finding: progress.current_finding }); } }); // Determine phase let phase = 'planning'; if (fixPlan.metadata?.status === 'executing' || inProgressCount > 0 || fixedCount > 0 || failedCount > 0) { phase = 'execution'; } if (totalFindings > 0 && pendingCount === 0 && inProgressCount === 0) { phase = 'completion'; } // Calculate stage progress const stages = (fixPlan.timeline?.stages || []).map(stage => { const groupStatuses = stage.groups.map(groupId => { const progress = progressDataArray.find(p => p.group_id === groupId); return progress ? progress.status : 'pending'; }); let status = 'pending'; if (groupStatuses.every(s => s === 'completed' || s === 'failed')) status = 'completed'; else if (groupStatuses.some(s => s === 'in-progress')) status = 'in-progress'; return { stage: stage.stage, status, groups: stage.groups }; }); const currentStage = stages.findIndex(s => s.status === 'in-progress' || s.status === 'pending') + 1 || stages.length; const percentComplete = totalFindings > 0 ? ((fixedCount + failedCount) / totalFindings) * 100 : 0; return { fix_session_id: fixPlan.metadata?.fix_session_id, phase, total_findings: totalFindings, fixed_count: fixedCount, failed_count: failedCount, in_progress_count: inProgressCount, pending_count: pendingCount, percent_complete: percentComplete, current_stage: currentStage, total_stages: stages.length, stages, active_agents: activeAgents }; } /** * Render fix progress tracking card (carousel style) */ function renderFixProgressCard(progressData) { if (!progressData) { return ''; } const { phase, total_findings, fixed_count, failed_count, in_progress_count, pending_count, percent_complete, current_stage, total_stages, stages, active_agents, fix_session_id } = progressData; // Phase badge class const phaseClass = phase === 'planning' ? 'phase-planning' : phase === 'execution' ? 'phase-execution' : 'phase-completion'; const phaseIcon = phase === 'planning' ? '๐Ÿ“' : phase === 'execution' ? 'โšก' : 'โœ…'; // Build stage dots const stageDots = stages.map((s, i) => { const dotClass = s.status === 'completed' ? 'completed' : s.status === 'in-progress' ? 'active' : ''; return ``; }).join(''); // Build carousel slides const slides = []; // Slide 1: Overview slides.push(` `); // Slide 2: Stats slides.push(` `); // Slide 3: Active agents (if any) if (active_agents.length > 0) { const agentItems = active_agents.slice(0, 2).map(a => `
๐Ÿค– ${a.current_finding?.finding_title || 'Working...'}
`).join(''); slides.push(` `); } // Build carousel navigation const navDots = slides.map((_, i) => ` `).join(''); return `
๐Ÿ”ง Fix Progress
${stageDots}
`; } /** * Navigate fix progress carousel */ function navigateFixCarousel(direction) { const track = document.getElementById('fixCarouselTrack'); if (!track) return; const slides = track.querySelectorAll('.fix-carousel-slide'); const totalSlides = slides.length; if (typeof direction === 'number') { fixProgressState.currentSlide = direction; } else if (direction === 'next') { fixProgressState.currentSlide = (fixProgressState.currentSlide + 1) % totalSlides; } else if (direction === 'prev') { fixProgressState.currentSlide = (fixProgressState.currentSlide - 1 + totalSlides) % totalSlides; } track.style.transform = `translateX(-${fixProgressState.currentSlide * 100}%)`; // Update nav dots document.querySelectorAll('.fix-nav-dot').forEach((dot, i) => { dot.classList.toggle('active', i === fixProgressState.currentSlide); }); } /** * Start polling for fix progress updates */ function startFixProgressPolling(sessionId) { if (fixProgressState.pollInterval) { clearInterval(fixProgressState.pollInterval); } // Initial load loadFixProgress(sessionId).then(data => { if (data) { updateFixProgressUI(data); } }); // Poll every 5 seconds fixProgressState.pollInterval = setInterval(async () => { const data = await loadFixProgress(sessionId); if (data) { updateFixProgressUI(data); // Stop polling if completed if (data.phase === 'completion') { clearInterval(fixProgressState.pollInterval); fixProgressState.pollInterval = null; } } }, 5000); } /** * Update fix progress UI */ function updateFixProgressUI(progressData) { const container = document.getElementById('fixProgressSection'); if (!container) return; container.innerHTML = renderFixProgressCard(progressData); fixProgressState.currentSlide = 0; } /** * Stop fix progress polling */ function stopFixProgressPolling() { if (fixProgressState.pollInterval) { clearInterval(fixProgressState.pollInterval); fixProgressState.pollInterval = null; } }