// ==========================================
// 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 `
๐
${totalFindings}
Total Findings
๐ด
${severityCounts.critical}
Critical
๐
${severityCounts.high}
High
๐ฏ
${dimensions.length}
Dimensions
Sort:
${dimensions.map(dim => `
`).join('')}
${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 = `
${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.recommendations.map(r => `- ${escapeHtml(r)}
`).join('')}
` : ''}
${finding.root_cause ? `
๐ฏ Root Cause
${escapeHtml(finding.root_cause)}
` : ''}
${finding.impact ? `
โ ๏ธ Impact
${escapeHtml(finding.impact)}
` : ''}
${finding.references && finding.references.length > 0 ? `
๐ References
${finding.references.map(ref => {
const isUrl = ref.startsWith('http');
return `- ${isUrl ? `${ref}` : ref}
`;
}).join('')}
` : ''}
${finding.metadata && Object.keys(finding.metadata).length > 0 ? `
` : ''}
`;
}
// ==========================================
// 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(`
${percent_complete.toFixed(0)}% Complete ยท Stage ${current_stage}/${total_stages}
`);
// Slide 2: Stats
slides.push(`
${total_findings}
Total
${fixed_count}
Fixed
${failed_count}
Failed
${pending_count + in_progress_count}
Pending
`);
// 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(`
${agentItems}
`);
}
// Build carousel navigation
const navDots = slides.map((_, i) => `
`).join('');
return `
`;
}
/**
* 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;
}
}