mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
- Reorder CSS and JS file loading in dashboard-generator.js for consistency - Simplify dashboard.css by removing redundant styles and consolidating to Tailwind-based approach - Add backup files for dashboard.html, dashboard.css, and review-cycle-dashboard.html - Create new Tailwind-based dashboard template (dashboard_tailwind.html) and test variant - Add tailwind.config.js for Tailwind CSS configuration - Enhance data-aggregator.js to load full task data for archived sessions (previously only counted) - Add meta, context, and flow_control fields to task objects for richer data representation - Implement review data loading for archived sessions to match active session behavior - Improve task sorting consistency across active and archived sessions - Reduce CSS file size by ~70% through Tailwind utility consolidation while maintaining visual parity
2817 lines
102 KiB
Plaintext
2817 lines
102 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Code Review Dashboard - {{SESSION_ID}}</title>
|
||
<style>
|
||
:root {
|
||
--bg-primary: #f5f7fa;
|
||
--bg-secondary: #ffffff;
|
||
--bg-card: #ffffff;
|
||
--text-primary: #1a202c;
|
||
--text-secondary: #718096;
|
||
--border-color: #e2e8f0;
|
||
--accent-color: #4299e1;
|
||
--success-color: #48bb78;
|
||
--warning-color: #ed8936;
|
||
--danger-color: #f56565;
|
||
--critical-color: #c53030;
|
||
--high-color: #f56565;
|
||
--medium-color: #ed8936;
|
||
--low-color: #48bb78;
|
||
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
[data-theme="dark"] {
|
||
--bg-primary: #1a202c;
|
||
--bg-secondary: #2d3748;
|
||
--bg-card: #2d3748;
|
||
--text-primary: #f7fafc;
|
||
--text-secondary: #a0aec0;
|
||
--border-color: #4a5568;
|
||
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px 0 rgba(0, 0, 0, 0.2);
|
||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||
background-color: var(--bg-primary);
|
||
color: var(--text-primary);
|
||
line-height: 1.6;
|
||
transition: background-color 0.3s, color 0.3s;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1600px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
header {
|
||
background-color: var(--bg-secondary);
|
||
box-shadow: var(--shadow);
|
||
padding: 20px;
|
||
margin-bottom: 30px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 2rem;
|
||
margin-bottom: 10px;
|
||
color: var(--accent-color);
|
||
}
|
||
|
||
.header-meta {
|
||
display: flex;
|
||
gap: 20px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
margin-top: 15px;
|
||
font-size: 0.9rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.header-controls {
|
||
display: flex;
|
||
gap: 15px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.search-box {
|
||
flex: 1;
|
||
min-width: 250px;
|
||
position: relative;
|
||
}
|
||
|
||
.search-box input {
|
||
width: 100%;
|
||
padding: 10px 15px;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 6px;
|
||
background-color: var(--bg-primary);
|
||
color: var(--text-primary);
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
background-color: var(--bg-card);
|
||
color: var(--text-primary);
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.btn.active {
|
||
background-color: var(--accent-color);
|
||
color: white;
|
||
border-color: var(--accent-color);
|
||
}
|
||
|
||
.export-btn-fix {
|
||
background-color: var(--success-color);
|
||
color: white;
|
||
border-color: var(--success-color);
|
||
}
|
||
|
||
.export-btn-fix:hover {
|
||
background-color: #38a169;
|
||
}
|
||
|
||
.export-btn-fix:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* Selection Controls */
|
||
.selection-controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.selection-counter {
|
||
color: var(--text-secondary);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.selection-btn {
|
||
padding: 6px 12px;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
/* Checkbox Styles */
|
||
.finding-checkbox {
|
||
width: 20px;
|
||
height: 20px;
|
||
cursor: pointer;
|
||
accent-color: var(--accent-color);
|
||
}
|
||
|
||
/* Fix Status Badges */
|
||
.fix-status-badge {
|
||
display: inline-block;
|
||
padding: 4px 10px;
|
||
border-radius: 12px;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.fix-status-badge.status-pending {
|
||
background-color: transparent;
|
||
color: var(--text-secondary);
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.fix-status-badge.status-in-progress {
|
||
background-color: var(--accent-color);
|
||
color: white;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
.fix-status-badge.status-fixed {
|
||
background-color: var(--success-color);
|
||
color: white;
|
||
}
|
||
|
||
.fix-status-badge.status-failed {
|
||
background-color: var(--danger-color);
|
||
color: white;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.7; }
|
||
}
|
||
|
||
/* Fix Progress Section */
|
||
.fix-progress-section {
|
||
background-color: var(--bg-card);
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
box-shadow: var(--shadow);
|
||
margin-bottom: 20px;
|
||
display: none; /* Hidden by default */
|
||
}
|
||
|
||
.fix-progress-section.active {
|
||
display: block;
|
||
}
|
||
|
||
.fix-progress-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.fix-progress-bar {
|
||
height: 8px;
|
||
background-color: var(--bg-primary);
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.fix-progress-bar-fill {
|
||
height: 100%;
|
||
background-color: var(--success-color);
|
||
transition: width 0.3s;
|
||
}
|
||
|
||
.active-agents-list {
|
||
margin-top: 15px;
|
||
border-top: 1px solid var(--border-color);
|
||
padding-top: 15px;
|
||
}
|
||
|
||
.active-agent-item {
|
||
padding: 10px;
|
||
background-color: var(--bg-primary);
|
||
border-radius: 6px;
|
||
margin-bottom: 8px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.active-agent-item .agent-task {
|
||
color: var(--text-primary);
|
||
font-size: 0.9rem;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.active-agent-item .agent-file {
|
||
color: var(--text-secondary);
|
||
font-size: 0.85rem;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.agent-status-badge {
|
||
display: inline-block;
|
||
padding: 2px 8px;
|
||
margin-left: 8px;
|
||
font-size: 0.75rem;
|
||
background-color: var(--accent-color);
|
||
color: white;
|
||
border-radius: 10px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.group-active-items {
|
||
margin-top: 8px;
|
||
padding: 6px 8px;
|
||
background-color: rgba(59, 130, 246, 0.1);
|
||
border-left: 3px solid var(--accent-color);
|
||
border-radius: 4px;
|
||
font-size: 0.85rem;
|
||
color: var(--text-primary);
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
/* Flow Control Steps */
|
||
.flow-control-container {
|
||
margin-top: 12px;
|
||
padding: 10px;
|
||
background-color: rgba(139, 92, 246, 0.05);
|
||
border-radius: 6px;
|
||
border: 1px solid rgba(139, 92, 246, 0.2);
|
||
}
|
||
|
||
.flow-control-header {
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 8px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.flow-steps {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.flow-step {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 10px;
|
||
border-radius: 4px;
|
||
font-size: 0.85rem;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.flow-step-completed {
|
||
background-color: rgba(34, 197, 94, 0.1);
|
||
color: #22c55e;
|
||
}
|
||
|
||
.flow-step-in-progress {
|
||
background-color: rgba(59, 130, 246, 0.1);
|
||
color: var(--accent-color);
|
||
animation: pulse 2s ease-in-out infinite;
|
||
}
|
||
|
||
.flow-step-failed {
|
||
background-color: rgba(239, 68, 68, 0.1);
|
||
color: #ef4444;
|
||
}
|
||
|
||
.flow-step-pending {
|
||
background-color: rgba(156, 163, 175, 0.1);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.flow-step-icon {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.flow-step-name {
|
||
font-weight: 500;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.6; }
|
||
}
|
||
|
||
.collapse-btn {
|
||
padding: 6px 12px;
|
||
font-size: 0.85rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* History Timeline */
|
||
.history-drawer {
|
||
position: fixed;
|
||
right: -600px;
|
||
top: 0;
|
||
width: 600px;
|
||
height: 100vh;
|
||
background-color: var(--bg-secondary);
|
||
box-shadow: var(--shadow-lg);
|
||
transition: right 0.3s;
|
||
z-index: 1001;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.history-drawer.active {
|
||
right: 0;
|
||
}
|
||
|
||
.history-timeline {
|
||
padding: 20px;
|
||
}
|
||
|
||
.history-item {
|
||
padding: 15px;
|
||
background-color: var(--bg-card);
|
||
border-radius: 6px;
|
||
margin-bottom: 15px;
|
||
border-left: 4px solid var(--accent-color);
|
||
}
|
||
|
||
.history-item.status-fixed {
|
||
border-left-color: var(--success-color);
|
||
}
|
||
|
||
.history-item.status-failed {
|
||
border-left-color: var(--danger-color);
|
||
}
|
||
|
||
.history-item-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.history-item-title {
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.history-item-timestamp {
|
||
font-size: 0.85rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.history-item-meta {
|
||
font-size: 0.85rem;
|
||
color: var(--text-secondary);
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.history-item-tests {
|
||
margin-top: 10px;
|
||
padding: 10px;
|
||
background-color: var(--bg-primary);
|
||
border-radius: 4px;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
/* Stage Timeline */
|
||
.stage-timeline {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin: 20px 0;
|
||
padding: 15px;
|
||
background-color: var(--bg-secondary);
|
||
border-radius: 8px;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.stage-item {
|
||
flex: 1;
|
||
min-width: 150px;
|
||
padding: 12px;
|
||
background-color: var(--bg-card);
|
||
border-radius: 6px;
|
||
border: 2px solid var(--border-color);
|
||
text-align: center;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.stage-item.pending {
|
||
opacity: 0.6;
|
||
border-color: var(--border-color);
|
||
}
|
||
|
||
.stage-item.in-progress {
|
||
border-color: var(--accent-color);
|
||
box-shadow: 0 0 10px rgba(59, 130, 246, 0.3);
|
||
}
|
||
|
||
.stage-item.completed {
|
||
border-color: var(--success-color);
|
||
background-color: rgba(34, 197, 94, 0.05);
|
||
}
|
||
|
||
.stage-number {
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.stage-mode {
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.stage-groups {
|
||
font-size: 0.75rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* Active Groups List */
|
||
.active-groups-list {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||
gap: 15px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.active-group-card {
|
||
background-color: var(--bg-card);
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
border-left: 4px solid var(--accent-color);
|
||
}
|
||
|
||
.group-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.group-id {
|
||
font-weight: 600;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.group-status {
|
||
font-size: 0.75rem;
|
||
padding: 4px 8px;
|
||
border-radius: 12px;
|
||
background-color: var(--accent-color);
|
||
color: white;
|
||
}
|
||
|
||
.group-name {
|
||
font-size: 0.9rem;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.group-findings {
|
||
font-size: 0.85rem;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
/* Severity Summary Cards */
|
||
.severity-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.severity-card {
|
||
background-color: var(--bg-card);
|
||
padding: 25px;
|
||
border-radius: 8px;
|
||
box-shadow: var(--shadow);
|
||
transition: transform 0.2s;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.severity-card:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: var(--shadow-lg);
|
||
}
|
||
|
||
.severity-icon {
|
||
font-size: 2.5rem;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.severity-value {
|
||
font-size: 2.5rem;
|
||
font-weight: bold;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.severity-card.critical .severity-value { color: var(--critical-color); }
|
||
.severity-card.high .severity-value { color: var(--high-color); }
|
||
.severity-card.medium .severity-value { color: var(--medium-color); }
|
||
.severity-card.low .severity-value { color: var(--low-color); }
|
||
|
||
.severity-label {
|
||
color: var(--text-secondary);
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
/* Dimension Summary Table */
|
||
.dimension-summary-section {
|
||
background-color: var(--bg-card);
|
||
padding: 25px;
|
||
border-radius: 8px;
|
||
box-shadow: var(--shadow);
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.dimension-summary-header {
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
margin-bottom: 20px;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.dimension-summary-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
.dimension-summary-table th {
|
||
text-align: left;
|
||
padding: 12px 16px;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.dimension-summary-table th:not(:first-child) {
|
||
text-align: center;
|
||
}
|
||
|
||
.dimension-summary-table td {
|
||
padding: 16px;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.dimension-summary-table td:not(:first-child) {
|
||
text-align: center;
|
||
}
|
||
|
||
.dimension-summary-table tr:last-child td {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.dimension-summary-table tr:hover {
|
||
background-color: var(--bg-primary);
|
||
}
|
||
|
||
.dimension-name {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.dimension-icon {
|
||
width: 28px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 6px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.dimension-icon.security { background-color: rgba(239, 68, 68, 0.15); color: #ef4444; }
|
||
.dimension-icon.architecture { background-color: rgba(139, 92, 246, 0.15); color: #8b5cf6; }
|
||
.dimension-icon.quality { background-color: rgba(34, 197, 94, 0.15); color: #22c55e; }
|
||
.dimension-icon.action-items { background-color: rgba(59, 130, 246, 0.15); color: #3b82f6; }
|
||
.dimension-icon.performance { background-color: rgba(234, 179, 8, 0.15); color: #eab308; }
|
||
.dimension-icon.maintainability { background-color: rgba(236, 72, 153, 0.15); color: #ec4899; }
|
||
.dimension-icon.best-practices { background-color: rgba(249, 115, 22, 0.15); color: #f97316; }
|
||
|
||
.count-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 28px;
|
||
height: 28px;
|
||
padding: 0 8px;
|
||
border-radius: 14px;
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.count-badge.critical { background-color: rgba(197, 48, 48, 0.2); color: var(--critical-color); }
|
||
.count-badge.high { background-color: rgba(245, 101, 101, 0.2); color: var(--high-color); }
|
||
.count-badge.medium { background-color: rgba(237, 137, 54, 0.2); color: var(--medium-color); }
|
||
.count-badge.low { background-color: rgba(72, 187, 120, 0.2); color: var(--low-color); }
|
||
.count-badge.total { background-color: var(--bg-primary); color: var(--text-primary); font-weight: 700; }
|
||
|
||
.count-badge.zero {
|
||
background-color: transparent;
|
||
color: var(--text-secondary);
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.status-indicator {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.status-indicator.reviewed {
|
||
background-color: var(--success-color);
|
||
color: white;
|
||
}
|
||
|
||
.status-indicator.pending {
|
||
background-color: var(--border-color);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.status-indicator.in-progress {
|
||
background-color: var(--accent-color);
|
||
color: white;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
/* Progress Indicator */
|
||
.progress-section {
|
||
background-color: var(--bg-card);
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
box-shadow: var(--shadow);
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.progress-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.phase-badge {
|
||
padding: 6px 12px;
|
||
border-radius: 12px;
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.phase-parallel { background-color: #c6f6d5; color: #22543d; }
|
||
.phase-aggregate { background-color: #feebc8; color: #7c2d12; }
|
||
.phase-iterate { background-color: #bee3f8; color: #2c5282; }
|
||
.phase-complete { background-color: #d9f99d; color: #365314; }
|
||
|
||
[data-theme="dark"] .phase-parallel { background-color: #22543d; color: #c6f6d5; }
|
||
[data-theme="dark"] .phase-aggregate { background-color: #7c2d12; color: #feebc8; }
|
||
[data-theme="dark"] .phase-iterate { background-color: #2c5282; color: #bee3f8; }
|
||
[data-theme="dark"] .phase-complete { background-color: #365314; color: #d9f99d; }
|
||
|
||
.progress-bar {
|
||
width: 100%;
|
||
height: 12px;
|
||
background-color: var(--bg-primary);
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, var(--accent-color), var(--success-color));
|
||
transition: width 0.5s ease;
|
||
}
|
||
|
||
/* Dimension Tabs */
|
||
.dimension-tabs {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 20px;
|
||
border-bottom: 2px solid var(--border-color);
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.tab {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
background: none;
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
color: var(--text-secondary);
|
||
border-bottom: 3px solid transparent;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.tab:hover {
|
||
color: var(--text-primary);
|
||
background-color: var(--bg-primary);
|
||
border-radius: 6px 6px 0 0;
|
||
}
|
||
|
||
.tab.active {
|
||
color: var(--accent-color);
|
||
border-bottom-color: var(--accent-color);
|
||
}
|
||
|
||
/* Findings List */
|
||
.findings-container {
|
||
background-color: var(--bg-card);
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.findings-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.findings-list {
|
||
display: grid;
|
||
gap: 15px;
|
||
}
|
||
|
||
.finding-item {
|
||
background-color: var(--bg-primary);
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
border-left: 4px solid var(--border-color);
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.finding-item:hover {
|
||
transform: translateX(4px);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.finding-item.critical { border-left-color: var(--critical-color); }
|
||
.finding-item.high { border-left-color: var(--high-color); }
|
||
.finding-item.medium { border-left-color: var(--medium-color); }
|
||
.finding-item.low { border-left-color: var(--low-color); }
|
||
|
||
.finding-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: start;
|
||
margin-bottom: 10px;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
.finding-badges {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.severity-badge {
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.severity-badge.critical { background-color: var(--critical-color); color: white; }
|
||
.severity-badge.high { background-color: var(--high-color); color: white; }
|
||
.severity-badge.medium { background-color: var(--medium-color); color: white; }
|
||
.severity-badge.low { background-color: var(--low-color); color: white; }
|
||
|
||
.dimension-badge {
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-size: 0.75rem;
|
||
font-weight: 500;
|
||
background-color: var(--accent-color);
|
||
color: white;
|
||
}
|
||
|
||
.finding-title {
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.finding-file {
|
||
font-size: 0.85rem;
|
||
color: var(--text-secondary);
|
||
font-family: monospace;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.finding-description {
|
||
color: var(--text-secondary);
|
||
font-size: 0.9rem;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* Detail Drawer */
|
||
.drawer {
|
||
position: fixed;
|
||
top: 0;
|
||
right: -600px;
|
||
width: 600px;
|
||
height: 100vh;
|
||
background-color: var(--bg-secondary);
|
||
box-shadow: -4px 0 15px rgba(0, 0, 0, 0.2);
|
||
transition: right 0.3s ease;
|
||
z-index: 1000;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.drawer.open {
|
||
right: 0;
|
||
}
|
||
|
||
.drawer-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
display: none;
|
||
z-index: 999;
|
||
}
|
||
|
||
.drawer-overlay.show {
|
||
display: block;
|
||
}
|
||
|
||
.drawer-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px;
|
||
border-bottom: 1px solid var(--border-color);
|
||
position: sticky;
|
||
top: 0;
|
||
background-color: var(--bg-secondary);
|
||
z-index: 10;
|
||
}
|
||
|
||
.drawer-title {
|
||
font-size: 1.3rem;
|
||
font-weight: 600;
|
||
flex: 1;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 2rem;
|
||
cursor: pointer;
|
||
color: var(--text-secondary);
|
||
padding: 0;
|
||
width: 40px;
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
background-color: var(--bg-primary);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.drawer-content {
|
||
padding: 20px;
|
||
}
|
||
|
||
.drawer-section {
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.drawer-section-title {
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
margin-bottom: 15px;
|
||
color: var(--accent-color);
|
||
}
|
||
|
||
.code-snippet {
|
||
background-color: var(--bg-primary);
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
border-left: 3px solid var(--accent-color);
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.85rem;
|
||
overflow-x: auto;
|
||
white-space: pre;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.recommendation-box {
|
||
background-color: var(--success-color);
|
||
background-image: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, transparent 100%);
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
color: white;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.metadata-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 15px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.metadata-item {
|
||
background-color: var(--bg-primary);
|
||
padding: 12px;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.metadata-label {
|
||
font-size: 0.75rem;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
font-weight: 600;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.metadata-value {
|
||
font-size: 0.95rem;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.reference-list {
|
||
list-style: none;
|
||
padding: 0;
|
||
}
|
||
|
||
.reference-list li {
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.reference-list li:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.reference-list a {
|
||
color: var(--accent-color);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.reference-list a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* Empty State */
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.empty-state-icon {
|
||
font-size: 4rem;
|
||
margin-bottom: 20px;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
/* Theme Toggle */
|
||
.theme-toggle {
|
||
position: fixed;
|
||
bottom: 30px;
|
||
right: 30px;
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: 50%;
|
||
background-color: var(--accent-color);
|
||
color: white;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: 1.5rem;
|
||
box-shadow: var(--shadow-lg);
|
||
transition: all 0.3s;
|
||
z-index: 900;
|
||
}
|
||
|
||
.theme-toggle:hover {
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
/* Export Button */
|
||
.export-btn {
|
||
background-color: var(--success-color);
|
||
color: white;
|
||
border-color: var(--success-color);
|
||
}
|
||
|
||
.export-btn:hover {
|
||
background-color: #38a169;
|
||
}
|
||
|
||
/* Loading Indicator */
|
||
.loading-indicator {
|
||
display: inline-block;
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||
border-radius: 50%;
|
||
border-top-color: white;
|
||
animation: spin 1s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* Filter controls */
|
||
.filter-section {
|
||
background-color: var(--bg-card);
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
box-shadow: var(--shadow);
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.filter-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.filter-title {
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.filter-controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.filter-label {
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.filter-checkbox-group {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-checkbox-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 6px 12px;
|
||
border-radius: 6px;
|
||
background-color: var(--bg-primary);
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
border: 2px solid transparent;
|
||
}
|
||
|
||
.filter-checkbox-item:hover {
|
||
background-color: var(--accent-color);
|
||
color: white;
|
||
}
|
||
|
||
.filter-checkbox-item.active {
|
||
background-color: var(--accent-color);
|
||
color: white;
|
||
border-color: var(--accent-color);
|
||
}
|
||
|
||
.filter-checkbox-item input[type="checkbox"] {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.sort-order-btn {
|
||
padding: 8px 12px;
|
||
min-width: 100px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.sort-order-btn .icon {
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (max-width: 1024px) {
|
||
.drawer {
|
||
width: 100%;
|
||
right: -100%;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.severity-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
|
||
h1 {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.header-controls {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.search-box {
|
||
width: 100%;
|
||
}
|
||
|
||
.dimension-tabs {
|
||
overflow-x: auto;
|
||
flex-wrap: nowrap;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<header>
|
||
<h1>🔍 Code Review Dashboard</h1>
|
||
<div class="header-meta">
|
||
<span>📋 Session: <strong id="sessionId">Loading...</strong></span>
|
||
<span>🆔 Review ID: <strong id="reviewId">Loading...</strong></span>
|
||
<span>🕒 Last Updated: <strong id="lastUpdate">Loading...</strong></span>
|
||
</div>
|
||
|
||
<div class="header-controls">
|
||
<div class="search-box">
|
||
<input type="text" id="searchInput" placeholder="🔍 Search findings..." />
|
||
</div>
|
||
|
||
<div class="selection-controls">
|
||
<span class="selection-counter" id="selectionCounter">0 findings selected</span>
|
||
<button class="btn selection-btn" onclick="selectAll()">Select All</button>
|
||
<button class="btn selection-btn" onclick="deselectAll()">Deselect All</button>
|
||
</div>
|
||
|
||
<button class="btn export-btn" onclick="exportToMarkdown()">📥 Export Report</button>
|
||
<button class="btn export-btn-fix" id="exportFixBtn" onclick="exportSelectedFindings()" disabled>🔧 Export Selected for Fixing</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Fix Progress Section -->
|
||
<div class="fix-progress-section" id="fixProgressSection">
|
||
<div class="fix-progress-header">
|
||
<h3>Fix Progress</h3>
|
||
<span class="phase-badge" id="fixPhaseBadge">PLANNING</span>
|
||
<div style="display: flex; gap: 10px;">
|
||
<button class="btn collapse-btn" onclick="toggleFixProgress()">Collapse</button>
|
||
<button class="btn collapse-btn" onclick="openHistoryDrawer()">📜 View Fix History</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Planning Phase Indicator -->
|
||
<div id="planningPhase" style="display: none; text-align: center; padding: 20px;">
|
||
<div style="font-size: 2rem; margin-bottom: 10px;">🤖</div>
|
||
<div style="font-size: 1.1rem; font-weight: 600; margin-bottom: 5px;">Planning Fixes</div>
|
||
<div style="color: var(--text-secondary);">AI is analyzing findings and generating fix plan...</div>
|
||
</div>
|
||
|
||
<!-- Stage Timeline (visible in planning after plan generation) -->
|
||
<div class="stage-timeline" id="stageTimeline" style="display: none;">
|
||
<!-- Stages populated by JavaScript -->
|
||
</div>
|
||
|
||
<!-- Execution Phase UI -->
|
||
<div id="executionPhase" style="display: none;">
|
||
<!-- Progress Bar -->
|
||
<div class="fix-progress-bar">
|
||
<div class="fix-progress-bar-fill" id="fixProgressFill" style="width: 0%"></div>
|
||
</div>
|
||
<div style="text-align: center; font-size: 0.9rem; color: var(--text-secondary); margin-top: 10px;">
|
||
<span id="fixProgressText">No active fix session</span>
|
||
</div>
|
||
|
||
<!-- Active Groups -->
|
||
<div class="active-groups-list" id="activeGroupsList">
|
||
<!-- Active groups populated by JavaScript -->
|
||
</div>
|
||
|
||
<!-- Active Agents -->
|
||
<div class="active-agents-list" id="activeAgentsList">
|
||
<!-- Active agents populated by JavaScript -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Progress Section -->
|
||
<div class="progress-section">
|
||
<div class="progress-header">
|
||
<h3>Review Progress</h3>
|
||
<span class="phase-badge" id="phaseBadge">LOADING</span>
|
||
</div>
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" id="progressFill" style="width: 0%"></div>
|
||
</div>
|
||
<div style="text-align: center; font-size: 0.9rem; color: var(--text-secondary); margin-top: 10px;">
|
||
<span id="progressText">Initializing...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Severity Summary Cards -->
|
||
<div class="severity-grid">
|
||
<div class="severity-card critical" data-severity="critical" onclick="filterBySeverity('critical')">
|
||
<div class="severity-icon">🔴</div>
|
||
<div class="severity-value" id="criticalCount">0</div>
|
||
<div class="severity-label">Critical</div>
|
||
</div>
|
||
<div class="severity-card high" data-severity="high" onclick="filterBySeverity('high')">
|
||
<div class="severity-icon">🟠</div>
|
||
<div class="severity-value" id="highCount">0</div>
|
||
<div class="severity-label">High</div>
|
||
</div>
|
||
<div class="severity-card medium" data-severity="medium" onclick="filterBySeverity('medium')">
|
||
<div class="severity-icon">🟡</div>
|
||
<div class="severity-value" id="mediumCount">0</div>
|
||
<div class="severity-label">Medium</div>
|
||
</div>
|
||
<div class="severity-card low" data-severity="low" onclick="filterBySeverity('low')">
|
||
<div class="severity-icon">🟢</div>
|
||
<div class="severity-value" id="lowCount">0</div>
|
||
<div class="severity-label">Low</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Dimension Summary Table -->
|
||
<div class="dimension-summary-section">
|
||
<div class="dimension-summary-header">Findings by Dimension</div>
|
||
<table class="dimension-summary-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Dimension</th>
|
||
<th>Critical</th>
|
||
<th>High</th>
|
||
<th>Medium</th>
|
||
<th>Low</th>
|
||
<th>Total</th>
|
||
<th>Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="dimensionSummaryBody">
|
||
<!-- Populated by JavaScript -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Dimension Tabs -->
|
||
<div class="dimension-tabs" id="dimensionTabs">
|
||
<button class="tab active" data-dimension="all" onclick="filterByDimension('all')">All Findings</button>
|
||
<button class="tab" data-dimension="security" onclick="filterByDimension('security')">Security</button>
|
||
<button class="tab" data-dimension="architecture" onclick="filterByDimension('architecture')">Architecture</button>
|
||
<button class="tab" data-dimension="quality" onclick="filterByDimension('quality')">Quality</button>
|
||
<button class="tab" data-dimension="action-items" onclick="filterByDimension('action-items')">Action Items</button>
|
||
<button class="tab" data-dimension="performance" onclick="filterByDimension('performance')">Performance</button>
|
||
<button class="tab" data-dimension="maintainability" onclick="filterByDimension('maintainability')">Maintainability</button>
|
||
<button class="tab" data-dimension="best-practices" onclick="filterByDimension('best-practices')">Best Practices</button>
|
||
</div>
|
||
|
||
<!-- Advanced Filters -->
|
||
<div class="filter-section">
|
||
<div class="filter-header">
|
||
<div class="filter-title">🎯 Advanced Filters & Sort</div>
|
||
<button class="btn" onclick="resetFilters()">Reset All Filters</button>
|
||
</div>
|
||
<div class="filter-controls">
|
||
<!-- Severity Filter -->
|
||
<div class="filter-group">
|
||
<span class="filter-label">Severity:</span>
|
||
<div class="filter-checkbox-group">
|
||
<label class="filter-checkbox-item" id="filter-critical">
|
||
<input type="checkbox" value="critical" onchange="toggleSeverityFilter('critical')">
|
||
<span>🔴 Critical</span>
|
||
</label>
|
||
<label class="filter-checkbox-item" id="filter-high">
|
||
<input type="checkbox" value="high" onchange="toggleSeverityFilter('high')">
|
||
<span>🟠 High</span>
|
||
</label>
|
||
<label class="filter-checkbox-item" id="filter-medium">
|
||
<input type="checkbox" value="medium" onchange="toggleSeverityFilter('medium')">
|
||
<span>🟡 Medium</span>
|
||
</label>
|
||
<label class="filter-checkbox-item" id="filter-low">
|
||
<input type="checkbox" value="low" onchange="toggleSeverityFilter('low')">
|
||
<span>🟢 Low</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sort Controls -->
|
||
<div class="filter-group">
|
||
<span class="filter-label">Sort:</span>
|
||
<select id="sortSelect" class="btn" onchange="sortFindings()">
|
||
<option value="severity">By Severity</option>
|
||
<option value="dimension">By Dimension</option>
|
||
<option value="file">By File</option>
|
||
<option value="title">By Title</option>
|
||
</select>
|
||
<button class="btn sort-order-btn" id="sortOrderBtn" onclick="toggleSortOrder()">
|
||
<span class="icon" id="sortOrderIcon">↓</span>
|
||
<span id="sortOrderText">Descending</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Selection Actions -->
|
||
<div class="filter-group">
|
||
<span class="filter-label">Select:</span>
|
||
<button class="btn selection-btn" onclick="selectAllVisible()">Select Visible</button>
|
||
<button class="btn selection-btn" onclick="selectBySeverity('critical')">Critical Only</button>
|
||
<button class="btn selection-btn" onclick="deselectAll()">Clear</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Findings Container -->
|
||
<div class="findings-container">
|
||
<div class="findings-header">
|
||
<h3>Findings <span id="findingsCount">(0)</span></h3>
|
||
</div>
|
||
<div class="findings-list" id="findingsList">
|
||
<div class="empty-state">
|
||
<div class="empty-state-icon">⏳</div>
|
||
<p>Loading findings...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Detail Drawer -->
|
||
<div class="drawer-overlay" id="drawerOverlay" onclick="closeDrawer()"></div>
|
||
<div class="drawer" id="findingDrawer">
|
||
<div class="drawer-header">
|
||
<div class="drawer-title" id="drawerTitle">Finding Details</div>
|
||
<button class="close-btn" onclick="closeDrawer()">×</button>
|
||
</div>
|
||
<div class="drawer-content" id="drawerContent">
|
||
<!-- Content populated by JavaScript -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- History Timeline Drawer -->
|
||
<div class="drawer-overlay" id="historyDrawerOverlay" onclick="closeHistoryDrawer()"></div>
|
||
<div class="history-drawer" id="historyDrawer">
|
||
<div class="drawer-header">
|
||
<div class="drawer-title">📜 Fix History</div>
|
||
<button class="close-btn" onclick="closeHistoryDrawer()">×</button>
|
||
</div>
|
||
<div class="history-timeline" id="historyTimeline">
|
||
<!-- History items populated by JavaScript -->
|
||
</div>
|
||
</div>
|
||
|
||
<button class="theme-toggle" id="themeToggle">🌙</button>
|
||
|
||
<script>
|
||
// State
|
||
let allFindings = [];
|
||
let filteredFindings = [];
|
||
let currentFilters = {
|
||
dimension: 'all',
|
||
severities: new Set(), // ✨ NEW: Multiple severity selection
|
||
search: ''
|
||
};
|
||
let sortConfig = {
|
||
field: 'severity',
|
||
order: 'desc' // ✨ NEW: 'asc' or 'desc'
|
||
};
|
||
let pollingInterval = null;
|
||
let reviewState = null;
|
||
|
||
// Fix-related state
|
||
let selectedFindings = new Set();
|
||
let fixSession = null;
|
||
let fixProgressInterval = null;
|
||
let fixProgressCollapsed = false;
|
||
|
||
// Selection management
|
||
function toggleFindingSelection(findingId, event) {
|
||
event.stopPropagation(); // Prevent triggering showFindingDetail
|
||
|
||
if (selectedFindings.has(findingId)) {
|
||
selectedFindings.delete(findingId);
|
||
} else {
|
||
selectedFindings.add(findingId);
|
||
}
|
||
|
||
updateSelectionUI();
|
||
}
|
||
|
||
function selectAll() {
|
||
allFindings.forEach(finding => {
|
||
selectedFindings.add(finding.id);
|
||
});
|
||
updateSelectionUI();
|
||
}
|
||
|
||
// ✨ NEW: Select only currently visible findings
|
||
function selectAllVisible() {
|
||
filteredFindings.forEach(finding => {
|
||
selectedFindings.add(finding.id);
|
||
});
|
||
updateSelectionUI();
|
||
}
|
||
|
||
// ✨ NEW: Select findings by severity
|
||
function selectBySeverity(severity) {
|
||
allFindings.forEach(finding => {
|
||
if (finding.severity.toLowerCase() === severity) {
|
||
selectedFindings.add(finding.id);
|
||
}
|
||
});
|
||
updateSelectionUI();
|
||
}
|
||
|
||
function deselectAll() {
|
||
selectedFindings.clear();
|
||
updateSelectionUI();
|
||
}
|
||
|
||
// ✨ NEW: Toggle severity filter
|
||
function toggleSeverityFilter(severity) {
|
||
if (currentFilters.severities.has(severity)) {
|
||
currentFilters.severities.delete(severity);
|
||
document.getElementById(`filter-${severity}`).classList.remove('active');
|
||
} else {
|
||
currentFilters.severities.add(severity);
|
||
document.getElementById(`filter-${severity}`).classList.add('active');
|
||
}
|
||
applyFilters();
|
||
}
|
||
|
||
// ✨ NEW: Toggle sort order
|
||
function toggleSortOrder() {
|
||
sortConfig.order = sortConfig.order === 'asc' ? 'desc' : 'asc';
|
||
|
||
// Update UI
|
||
const icon = document.getElementById('sortOrderIcon');
|
||
const text = document.getElementById('sortOrderText');
|
||
|
||
if (sortConfig.order === 'asc') {
|
||
icon.textContent = '↑';
|
||
text.textContent = 'Ascending';
|
||
} else {
|
||
icon.textContent = '↓';
|
||
text.textContent = 'Descending';
|
||
}
|
||
|
||
sortFindings();
|
||
}
|
||
|
||
// ✨ NEW: Reset all filters
|
||
function resetFilters() {
|
||
// Reset severity filters
|
||
currentFilters.severities.clear();
|
||
document.querySelectorAll('.filter-checkbox-item').forEach(item => {
|
||
item.classList.remove('active');
|
||
const checkbox = item.querySelector('input[type="checkbox"]');
|
||
if (checkbox) checkbox.checked = false;
|
||
});
|
||
|
||
// Reset dimension filter
|
||
currentFilters.dimension = 'all';
|
||
document.querySelectorAll('.tab').forEach(tab => {
|
||
tab.classList.toggle('active', tab.dataset.dimension === 'all');
|
||
});
|
||
|
||
// Reset search
|
||
currentFilters.search = '';
|
||
document.getElementById('searchInput').value = '';
|
||
|
||
// Reset sort
|
||
sortConfig.field = 'severity';
|
||
sortConfig.order = 'desc';
|
||
document.getElementById('sortSelect').value = 'severity';
|
||
document.getElementById('sortOrderIcon').textContent = '↓';
|
||
document.getElementById('sortOrderText').textContent = 'Descending';
|
||
|
||
applyFilters();
|
||
}
|
||
|
||
function updateSelectionUI() {
|
||
// Update counter
|
||
const counter = document.getElementById('selectionCounter');
|
||
counter.textContent = `${selectedFindings.size} finding${selectedFindings.size !== 1 ? 's' : ''} selected`;
|
||
|
||
// Update export button state
|
||
const exportBtn = document.getElementById('exportFixBtn');
|
||
exportBtn.disabled = selectedFindings.size === 0;
|
||
|
||
// Update checkbox states
|
||
document.querySelectorAll('.finding-checkbox').forEach(checkbox => {
|
||
checkbox.checked = selectedFindings.has(checkbox.dataset.findingId);
|
||
});
|
||
}
|
||
|
||
// Export selected findings for fixing
|
||
async function exportSelectedFindings() {
|
||
if (selectedFindings.size === 0) {
|
||
alert('Please select at least one finding to export');
|
||
return;
|
||
}
|
||
|
||
// Gather selected findings
|
||
const selectedFindingsData = allFindings.filter(f => selectedFindings.has(f.id));
|
||
|
||
// Create export data structure
|
||
const exportData = {
|
||
export_id: `${Date.now()}`,
|
||
export_timestamp: new Date().toISOString(),
|
||
review_id: reviewState?.review_id || 'unknown',
|
||
session_id: reviewState?.session_id || 'unknown',
|
||
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-${exportData.export_id}.json`;
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
|
||
// ✨ Show export path and usage instructions with session directory recommendation
|
||
const reviewDir = window.location.pathname.replace('/dashboard.html', '').replace('/.review/dashboard.html', '/.review');
|
||
// Extract absolute path from file:// URL
|
||
const absolutePath = decodeURIComponent(window.location.pathname).replace(/^\/([A-Z]:)/, '$1');
|
||
const sessionDir = absolutePath.replace('/.review/dashboard.html', '');
|
||
const reviewSessionDir = `${sessionDir}/.review`;
|
||
|
||
const notification = `
|
||
✅ Exported ${selectedFindingsData.length} finding${selectedFindingsData.length !== 1 ? 's' : ''} for automated fixing!
|
||
|
||
📁 File: ${filename}
|
||
📂 Browser download: Your Downloads folder
|
||
|
||
📋 Recommended Location:
|
||
${reviewSessionDir}/
|
||
|
||
🔄 Move to session directory (recommended):
|
||
# On Windows:
|
||
move "%USERPROFILE%\\Downloads\\${filename}" "${reviewSessionDir.replace(/\//g, '\\')}\\${filename}"
|
||
|
||
# On Mac/Linux:
|
||
mv ~/Downloads/${filename} ${reviewSessionDir}/${filename}
|
||
|
||
🔧 Usage (after moving file):
|
||
/workflow:review-fix ${reviewSessionDir}/${filename}
|
||
|
||
Or directly from Downloads:
|
||
/workflow:review-fix ~/Downloads/${filename}
|
||
|
||
📋 Review Session: ${exportData.session_id}
|
||
📊 Findings by Severity:
|
||
• 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}
|
||
|
||
💡 The automated fix workflow will:
|
||
1. Group findings by file and relationship
|
||
2. Execute fixes in parallel where safe
|
||
3. Run tests after each fix
|
||
4. Track progress in this dashboard
|
||
`.trim();
|
||
|
||
alert(notification);
|
||
}
|
||
|
||
// Fix progress tracking
|
||
function detectFixSession() {
|
||
// Check if there's an active fix session in the review directory
|
||
fetch('./fixes/active-fix-session.json')
|
||
.then(response => {
|
||
if (response.ok) {
|
||
return response.json();
|
||
}
|
||
throw new Error('No active fix session');
|
||
})
|
||
.then(data => {
|
||
fixSession = data;
|
||
startFixProgressPolling(data.fix_session_id);
|
||
})
|
||
.catch(() => {
|
||
// No active fix session, hide progress section
|
||
document.getElementById('fixProgressSection').classList.remove('active');
|
||
});
|
||
}
|
||
|
||
function startFixProgressPolling(fixSessionId) {
|
||
// Show progress section
|
||
document.getElementById('fixProgressSection').classList.add('active');
|
||
|
||
// Initial load
|
||
loadFixProgress(fixSessionId);
|
||
|
||
// Start polling every 3 seconds
|
||
if (fixProgressInterval) {
|
||
clearInterval(fixProgressInterval);
|
||
}
|
||
fixProgressInterval = setInterval(() => {
|
||
loadFixProgress(fixSessionId);
|
||
}, 3000);
|
||
}
|
||
|
||
async function loadFixProgress(fixSessionId) {
|
||
try {
|
||
// Step 1: Load fix plan to get progress file list
|
||
const planResponse = await fetch(`./fixes/${fixSessionId}/fix-plan.json`);
|
||
if (!planResponse.ok) throw new Error('Fix plan not found');
|
||
const fixPlan = await planResponse.json();
|
||
|
||
// Step 2: Load all progress files in parallel
|
||
const progressFiles = fixPlan.groups.map(g => g.progress_file);
|
||
const progressPromises = progressFiles.map(file =>
|
||
fetch(`./fixes/${fixSessionId}/${file}`)
|
||
.then(r => r.ok ? r.json() : null)
|
||
.catch(() => null)
|
||
);
|
||
const progressDataArray = await Promise.all(progressPromises);
|
||
|
||
// Step 3: Aggregate data from all progress files
|
||
const progressData = aggregateProgressData(fixPlan, progressDataArray.filter(d => d !== null));
|
||
|
||
updateFixStatus(progressData);
|
||
|
||
// If complete, stop polling and update history
|
||
if (progressData.status === 'completed' || progressData.status === 'failed') {
|
||
clearInterval(fixProgressInterval);
|
||
fixProgressInterval = null;
|
||
|
||
// Reload fix history
|
||
await loadFixHistory();
|
||
|
||
// Mark findings as fixed
|
||
if (progressData.status === 'completed') {
|
||
progressData.fixes.forEach(fix => {
|
||
if (fix.status === 'fixed') {
|
||
markFindingAsFixed(fix.finding_id);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading fix progress:', error);
|
||
}
|
||
}
|
||
|
||
function aggregateProgressData(fixPlan, progressDataArray) {
|
||
// Aggregate all findings from progress files
|
||
const allFindings = [];
|
||
const activeAgents = [];
|
||
let hasAnyInProgress = false;
|
||
let hasAnyFailed = false;
|
||
let allCompleted = true;
|
||
|
||
progressDataArray.forEach(progressFile => {
|
||
// Collect all findings with group_id
|
||
if (progressFile.findings) {
|
||
progressFile.findings.forEach(finding => {
|
||
allFindings.push({
|
||
finding_id: finding.finding_id,
|
||
finding_title: finding.finding_title,
|
||
status: finding.status,
|
||
group_id: progressFile.group_id,
|
||
completion_time: finding.completion_time
|
||
});
|
||
});
|
||
}
|
||
|
||
// Collect active agents
|
||
if (progressFile.assigned_agent && progressFile.status === 'in-progress') {
|
||
const currentFinding = progressFile.current_finding;
|
||
activeAgents.push({
|
||
agent_id: progressFile.assigned_agent,
|
||
group_id: progressFile.group_id,
|
||
current_task: currentFinding ? currentFinding.finding_title : 'Working...',
|
||
finding_id: currentFinding ? currentFinding.finding_id : null,
|
||
finding_title: currentFinding ? currentFinding.finding_title : null,
|
||
file: currentFinding ? currentFinding.file : null,
|
||
status: progressFile.phase || 'analyzing',
|
||
started_at: progressFile.started_at
|
||
});
|
||
}
|
||
|
||
// Track statuses
|
||
if (progressFile.status === 'in-progress') hasAnyInProgress = true;
|
||
if (progressFile.status === 'failed') hasAnyFailed = true;
|
||
if (progressFile.status !== 'completed' && progressFile.status !== 'failed') {
|
||
allCompleted = false;
|
||
}
|
||
});
|
||
|
||
// Calculate completion metrics
|
||
const totalFindings = allFindings.length;
|
||
const completedCount = allFindings.filter(f => f.status === 'fixed' || f.status === 'failed').length;
|
||
const percentComplete = totalFindings > 0 ? (completedCount / totalFindings) * 100 : 0;
|
||
|
||
// Determine overall status
|
||
let overallStatus = 'pending';
|
||
if (allCompleted) {
|
||
overallStatus = hasAnyFailed ? 'failed' : 'completed';
|
||
} else if (hasAnyInProgress) {
|
||
overallStatus = 'in_progress';
|
||
}
|
||
|
||
// Determine phase
|
||
let phase = 'planning';
|
||
if (hasAnyInProgress || allCompleted) {
|
||
phase = allCompleted ? 'completion' : 'execution';
|
||
}
|
||
|
||
// Build stages from fix plan and update with current progress
|
||
const stages = fixPlan.timeline.stages.map(stage => {
|
||
// Check if all groups in this stage are completed
|
||
const groupStatuses = stage.groups.map(groupId => {
|
||
const progressFile = progressDataArray.find(p => p.group_id === groupId);
|
||
return progressFile ? progressFile.status : 'pending';
|
||
});
|
||
|
||
let stageStatus = 'pending';
|
||
if (groupStatuses.every(s => s === 'completed' || s === 'failed')) {
|
||
stageStatus = 'completed';
|
||
} else if (groupStatuses.some(s => s === 'in-progress')) {
|
||
stageStatus = 'in-progress';
|
||
}
|
||
|
||
return {
|
||
stage: stage.stage,
|
||
status: stageStatus,
|
||
groups: stage.groups,
|
||
execution_mode: stage.execution_mode
|
||
};
|
||
});
|
||
|
||
// Get earliest start time
|
||
const startTimes = progressDataArray
|
||
.map(p => p.started_at)
|
||
.filter(t => t)
|
||
.map(t => new Date(t).getTime());
|
||
const startTime = startTimes.length > 0 ? new Date(Math.min(...startTimes)).toISOString() : null;
|
||
|
||
// Find current stage
|
||
const currentStageObj = stages.find(s => s.status === 'in-progress') || stages.find(s => s.status === 'pending');
|
||
const currentStage = currentStageObj ? currentStageObj.stage : stages.length;
|
||
|
||
return {
|
||
fix_session_id: fixPlan.fix_session_id,
|
||
review_id: fixPlan.review_id,
|
||
status: overallStatus,
|
||
phase: phase,
|
||
start_time: startTime,
|
||
total_findings: totalFindings,
|
||
current_stage: currentStage,
|
||
total_stages: fixPlan.timeline.stages.length,
|
||
percent_complete: percentComplete,
|
||
fixes: allFindings,
|
||
active_agents: activeAgents,
|
||
stages: stages
|
||
};
|
||
}
|
||
|
||
function updateFixStatus(progressData) {
|
||
const fixPhaseBadge = document.getElementById('fixPhaseBadge');
|
||
const planningPhase = document.getElementById('planningPhase');
|
||
const executionPhase = document.getElementById('executionPhase');
|
||
const stageTimeline = document.getElementById('stageTimeline');
|
||
|
||
// Update phase badge
|
||
const phase = progressData.phase || 'planning';
|
||
fixPhaseBadge.textContent = phase.toUpperCase();
|
||
fixPhaseBadge.className = `phase-badge phase-${phase}`;
|
||
|
||
// Update stage timeline (show if stages exist, regardless of phase)
|
||
updateStageTimeline(progressData);
|
||
|
||
// Show appropriate phase UI
|
||
if (phase === 'planning') {
|
||
planningPhase.style.display = 'block';
|
||
executionPhase.style.display = 'none';
|
||
} else if (phase === 'execution' || phase === 'completion') {
|
||
planningPhase.style.display = 'none';
|
||
executionPhase.style.display = 'block';
|
||
|
||
// Update progress bar
|
||
const progressFill = document.getElementById('fixProgressFill');
|
||
const progressText = document.getElementById('fixProgressText');
|
||
const percentComplete = progressData.percent_complete || 0;
|
||
progressFill.style.width = `${percentComplete}%`;
|
||
|
||
const completedCount = progressData.fixes?.filter(f => f.status === 'fixed' || f.status === 'failed').length || 0;
|
||
const totalCount = progressData.total_findings || 0;
|
||
const currentStage = progressData.current_stage || 1;
|
||
const totalStages = progressData.total_stages || 1;
|
||
|
||
// Format start time
|
||
let startTimeText = '';
|
||
if (progressData.start_time) {
|
||
const startTime = new Date(progressData.start_time);
|
||
const elapsed = Math.floor((Date.now() - startTime.getTime()) / 1000 / 60); // minutes
|
||
startTimeText = ` • Started ${startTime.toLocaleTimeString()} (${elapsed}m ago)`;
|
||
}
|
||
|
||
progressText.textContent = `Stage ${currentStage}/${totalStages}: ${completedCount}/${totalCount} findings completed (${percentComplete.toFixed(1)}%)${startTimeText}`;
|
||
|
||
// Update active groups
|
||
updateActiveGroups(progressData);
|
||
|
||
// Update active agents
|
||
updateActiveAgents(progressData);
|
||
}
|
||
}
|
||
|
||
function updateStageTimeline(progressData) {
|
||
const stageTimeline = document.getElementById('stageTimeline');
|
||
const stages = progressData.stages || [];
|
||
|
||
if (stages.length === 0) {
|
||
stageTimeline.style.display = 'none';
|
||
stageTimeline.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
// Show timeline when stages are available
|
||
stageTimeline.style.display = 'flex';
|
||
stageTimeline.innerHTML = stages.map(stage => `
|
||
<div class="stage-item ${stage.status}">
|
||
<div class="stage-number">Stage ${stage.stage}</div>
|
||
<div class="stage-mode">${stage.execution_mode === 'parallel' ? '⚡ Parallel' : '➡️ Serial'}</div>
|
||
<div class="stage-groups">${stage.groups.length} group${stage.groups.length !== 1 ? 's' : ''}</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function updateActiveGroups(progressData) {
|
||
const activeGroupsList = document.getElementById('activeGroupsList');
|
||
const stages = progressData.stages || [];
|
||
const activeStage = stages.find(s => s.status === 'in-progress');
|
||
|
||
if (!activeStage) {
|
||
activeGroupsList.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
// Get fixes for groups in active stage
|
||
const groupFixes = {};
|
||
progressData.fixes?.forEach(fix => {
|
||
if (fix.group_id && activeStage.groups.includes(fix.group_id)) {
|
||
if (!groupFixes[fix.group_id]) {
|
||
groupFixes[fix.group_id] = [];
|
||
}
|
||
groupFixes[fix.group_id].push(fix);
|
||
}
|
||
});
|
||
|
||
const groupCards = activeStage.groups.map(groupId => {
|
||
const fixes = groupFixes[groupId] || [];
|
||
const inProgressCount = fixes.filter(f => f.status === 'in-progress').length;
|
||
const completedCount = fixes.filter(f => f.status === 'fixed' || f.status === 'failed').length;
|
||
|
||
// Get in-progress finding titles
|
||
const inProgressFindings = fixes
|
||
.filter(f => f.status === 'in-progress')
|
||
.map(f => f.finding_title)
|
||
.filter(title => title);
|
||
|
||
const inProgressText = inProgressFindings.length > 0
|
||
? `<div class="group-active-items">🔧 ${inProgressFindings.join(', ')}</div>`
|
||
: '';
|
||
|
||
return `
|
||
<div class="active-group-card">
|
||
<div class="group-header">
|
||
<div class="group-id">${groupId}</div>
|
||
<div class="group-status">${inProgressCount > 0 ? 'In Progress' : 'Pending'}</div>
|
||
</div>
|
||
<div class="group-findings">${completedCount}/${fixes.length} findings completed</div>
|
||
${inProgressText}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
activeGroupsList.innerHTML = groupCards || '';
|
||
}
|
||
|
||
function getAgentStatusIcon(status) {
|
||
const icons = {
|
||
'analyzing': '🔍',
|
||
'fixing': '🔧',
|
||
'testing': '🧪',
|
||
'committing': '💾'
|
||
};
|
||
return icons[status] || '⚡';
|
||
}
|
||
|
||
function updateActiveAgents(progressData) {
|
||
const activeAgentsList = document.getElementById('activeAgentsList');
|
||
|
||
if (!progressData.active_agents || progressData.active_agents.length === 0) {
|
||
activeAgentsList.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
activeAgentsList.innerHTML = '<h4 style="margin-bottom: 10px;">Active Agents:</h4>' +
|
||
progressData.active_agents.map(agent => {
|
||
const statusIcon = getAgentStatusIcon(agent.status);
|
||
const statusText = agent.status ? agent.status.charAt(0).toUpperCase() + agent.status.slice(1) : '';
|
||
const findingTitle = agent.finding_title || agent.finding_id || '';
|
||
|
||
// Render flow control steps if available
|
||
const flowControlHtml = agent.flow_control && agent.flow_control.steps && agent.flow_control.steps.length > 0
|
||
? `<div class="agent-flow-control">${renderFlowControlSteps(agent.flow_control)}</div>`
|
||
: '';
|
||
|
||
return `
|
||
<div class="active-agent-item">
|
||
<div class="agent-status">
|
||
${statusIcon} ${agent.agent_id} (${agent.group_id || 'unknown'})
|
||
${statusText ? `<span class="agent-status-badge">${statusText}</span>` : ''}
|
||
</div>
|
||
<div class="agent-task">${agent.current_task || findingTitle || 'Initializing...'}</div>
|
||
${agent.file ? `<div class="agent-file">📄 ${agent.file}</div>` : ''}
|
||
${flowControlHtml}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderFlowControlSteps(flowControl) {
|
||
if (!flowControl || !flowControl.implementation_approach || flowControl.implementation_approach.length === 0) {
|
||
return '';
|
||
}
|
||
|
||
const stepsHtml = flowControl.implementation_approach.map(step => {
|
||
let stepIcon = '';
|
||
let stepClass = '';
|
||
|
||
if (step.status === 'completed') {
|
||
stepIcon = '✅';
|
||
stepClass = 'flow-step-completed';
|
||
} else if (step.status === 'in-progress') {
|
||
stepIcon = '⏳';
|
||
stepClass = 'flow-step-in-progress';
|
||
} else if (step.status === 'failed') {
|
||
stepIcon = '❌';
|
||
stepClass = 'flow-step-failed';
|
||
} else {
|
||
stepIcon = '⏸';
|
||
stepClass = 'flow-step-pending';
|
||
}
|
||
|
||
// Format step name: "analyze_context" -> "Analyze Context"
|
||
const stepName = step.action || step.step.split('_').map(word =>
|
||
word.charAt(0).toUpperCase() + word.slice(1)
|
||
).join(' ');
|
||
|
||
return `
|
||
<div class="flow-step ${stepClass}">
|
||
<span class="flow-step-icon">${stepIcon}</span>
|
||
<span class="flow-step-name">${stepName}</span>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
return `
|
||
<div class="flow-control-container">
|
||
<div class="flow-control-header">Progress Steps:</div>
|
||
<div class="flow-steps">${stepsHtml}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function markFindingAsFixed(findingId) {
|
||
const finding = allFindings.find(f => f.id === findingId);
|
||
if (finding) {
|
||
finding.fix_status = 'fixed';
|
||
renderFindings(); // Re-render to show fix badge
|
||
}
|
||
}
|
||
|
||
function toggleFixProgress() {
|
||
fixProgressCollapsed = !fixProgressCollapsed;
|
||
const section = document.getElementById('fixProgressSection');
|
||
section.classList.toggle('collapsed', fixProgressCollapsed);
|
||
}
|
||
|
||
// History drawer functions
|
||
function openHistoryDrawer() {
|
||
document.getElementById('historyDrawer').classList.add('active');
|
||
document.getElementById('historyDrawerOverlay').classList.add('active');
|
||
loadFixHistory();
|
||
}
|
||
|
||
function closeHistoryDrawer() {
|
||
document.getElementById('historyDrawer').classList.remove('active');
|
||
document.getElementById('historyDrawerOverlay').classList.remove('active');
|
||
}
|
||
|
||
async function loadFixHistory() {
|
||
try {
|
||
const response = await fetch('./fixes/fix-history.json');
|
||
if (!response.ok) throw new Error('Fix history not found');
|
||
|
||
const historyData = await response.json();
|
||
renderFixHistory(historyData);
|
||
} catch (error) {
|
||
console.error('Error loading fix history:', error);
|
||
document.getElementById('historyTimeline').innerHTML = `
|
||
<div class="empty-state">
|
||
<div class="empty-state-icon">📜</div>
|
||
<p>No fix history available</p>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
function renderFixHistory(historyData) {
|
||
const timeline = document.getElementById('historyTimeline');
|
||
|
||
if (!historyData.sessions || historyData.sessions.length === 0) {
|
||
timeline.innerHTML = `
|
||
<div class="empty-state">
|
||
<div class="empty-state-icon">📜</div>
|
||
<p>No fix sessions yet</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
timeline.innerHTML = historyData.sessions.map(session => {
|
||
const timestamp = new Date(session.timestamp).toLocaleString();
|
||
const statusEmoji = session.status === 'completed' ? '✅' :
|
||
session.status === 'failed' ? '❌' : '⏳';
|
||
|
||
return `
|
||
<div class="history-item">
|
||
<div class="history-header">
|
||
<span class="history-status">${statusEmoji} ${session.status}</span>
|
||
<span class="history-time">${timestamp}</span>
|
||
</div>
|
||
<div class="history-details">
|
||
<div><strong>Session:</strong> ${session.fix_session_id}</div>
|
||
<div><strong>Findings:</strong> ${session.findings_count} issues</div>
|
||
<div><strong>Fixed:</strong> ${session.fixed_count || 0} issues</div>
|
||
<div><strong>Failed:</strong> ${session.failed_count || 0} issues</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Theme management
|
||
function initTheme() {
|
||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||
updateThemeIcon(savedTheme);
|
||
}
|
||
|
||
function toggleTheme() {
|
||
const currentTheme = document.documentElement.getAttribute('data-theme');
|
||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||
document.documentElement.setAttribute('data-theme', newTheme);
|
||
localStorage.setItem('theme', newTheme);
|
||
updateThemeIcon(newTheme);
|
||
}
|
||
|
||
function updateThemeIcon(theme) {
|
||
document.getElementById('themeToggle').textContent = theme === 'dark' ? '☀️' : '🌙';
|
||
}
|
||
|
||
// Polling mechanism
|
||
function startPolling() {
|
||
loadProgress();
|
||
pollingInterval = setInterval(() => {
|
||
loadProgress();
|
||
}, 5000); // Poll every 5 seconds
|
||
}
|
||
|
||
function stopPolling() {
|
||
if (pollingInterval) {
|
||
clearInterval(pollingInterval);
|
||
pollingInterval = null;
|
||
}
|
||
}
|
||
|
||
// Load progress from review-progress.json
|
||
async function loadProgress() {
|
||
try {
|
||
const response = await fetch('./review-progress.json');
|
||
if (!response.ok) throw new Error('Progress file not found');
|
||
|
||
const progress = await response.json();
|
||
|
||
// ✨ NEW: Load available dimensions incrementally and get actual count
|
||
const loadedCount = await loadAvailableDimensions();
|
||
|
||
// ✨ NEW: Update progress UI with actual file count
|
||
updateProgressUI(progress, loadedCount);
|
||
|
||
// If complete, stop polling
|
||
if (progress.phase === 'complete') {
|
||
stopPolling();
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading progress:', error);
|
||
}
|
||
}
|
||
|
||
// ✨ NEW: Load all available dimension files (even if review not complete)
|
||
async function loadAvailableDimensions() {
|
||
try {
|
||
// Load review state to get expected dimensions
|
||
const stateResponse = await fetch('./review-state.json');
|
||
if (!stateResponse.ok) return;
|
||
|
||
reviewState = await stateResponse.json();
|
||
document.getElementById('sessionId').textContent = reviewState.session_id;
|
||
|
||
// Try to load all expected dimensions
|
||
const allDimensions = reviewState.metadata?.dimensions ||
|
||
Object.keys(dimensionConfig);
|
||
|
||
const dimensionPromises = allDimensions.map(dim =>
|
||
fetch(`./dimensions/${dim}.json`)
|
||
.then(r => {
|
||
if (r.ok) return r.json();
|
||
return null; // Dimension not ready yet
|
||
})
|
||
.catch(err => {
|
||
// Silently skip dimensions that don't exist yet
|
||
return null;
|
||
})
|
||
);
|
||
|
||
const dimensions = await Promise.all(dimensionPromises);
|
||
|
||
// Aggregate findings from completed dimensions
|
||
allFindings = [];
|
||
const loadedDimensions = [];
|
||
const severityCounts = { critical: 0, high: 0, medium: 0, low: 0 };
|
||
|
||
dimensions.forEach(dim => {
|
||
if (dim) {
|
||
// Handle both array and object formats
|
||
const dimData = Array.isArray(dim) ? dim[0] : dim;
|
||
|
||
if (dimData && dimData.findings) {
|
||
loadedDimensions.push(dimData.dimension);
|
||
|
||
// Aggregate findings
|
||
allFindings.push(...dimData.findings.map(f => ({
|
||
...f,
|
||
dimension: dimData.dimension
|
||
})));
|
||
|
||
// Aggregate severity counts
|
||
if (dimData.summary) {
|
||
severityCounts.critical += dimData.summary.critical || 0;
|
||
severityCounts.high += dimData.summary.high || 0;
|
||
severityCounts.medium += dimData.summary.medium || 0;
|
||
severityCounts.low += dimData.summary.low || 0;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// Update severity counts with real data
|
||
if (loadedDimensions.length > 0) {
|
||
updateSeverityCounts(severityCounts);
|
||
updateDimensionSummary();
|
||
renderFindings();
|
||
}
|
||
|
||
// ✨ NEW: Return loaded count for progress calculation
|
||
return {
|
||
loaded: loadedDimensions.length,
|
||
total: allDimensions.length,
|
||
findings: allFindings.length
|
||
};
|
||
} catch (error) {
|
||
console.error('Error loading available dimensions:', error);
|
||
return { loaded: 0, total: 7, findings: 0 };
|
||
}
|
||
}
|
||
|
||
// Update progress UI
|
||
function updateProgressUI(progress, loadedCount) {
|
||
document.getElementById('reviewId').textContent = progress.review_id || 'N/A';
|
||
document.getElementById('lastUpdate').textContent = new Date(progress.last_update).toLocaleString();
|
||
|
||
const phaseBadge = document.getElementById('phaseBadge');
|
||
phaseBadge.textContent = progress.phase.toUpperCase();
|
||
phaseBadge.className = `phase-badge phase-${progress.phase}`;
|
||
|
||
let percentComplete = 0;
|
||
let progressText = '';
|
||
|
||
// ✨ NEW: Calculate progress based on actual loaded dimension files
|
||
if (loadedCount && loadedCount.total > 0) {
|
||
percentComplete = Math.round((loadedCount.loaded / loadedCount.total) * 100);
|
||
|
||
if (progress.phase === 'parallel' || progress.phase === 'aggregate') {
|
||
progressText = `Parallel Review: ${loadedCount.loaded}/${loadedCount.total} dimensions completed`;
|
||
if (loadedCount.findings > 0) {
|
||
progressText += ` • ${loadedCount.findings} finding${loadedCount.findings !== 1 ? 's' : ''} discovered`;
|
||
}
|
||
} else if (progress.phase === 'iterate' && progress.progress.deep_dive) {
|
||
percentComplete = progress.progress.deep_dive.percent_complete;
|
||
progressText = `Deep-Dive: ${progress.progress.deep_dive.analyzed}/${progress.progress.deep_dive.total_findings} findings analyzed`;
|
||
} else if (progress.phase === 'complete') {
|
||
percentComplete = 100;
|
||
progressText = `Review Complete • ${loadedCount.findings} total finding${loadedCount.findings !== 1 ? 's' : ''}`;
|
||
}
|
||
} else {
|
||
// Fallback to original logic if loadedCount not available
|
||
if (progress.progress.parallel_review) {
|
||
percentComplete = progress.progress.parallel_review.percent_complete;
|
||
progressText = `Parallel Review: ${progress.progress.parallel_review.completed}/${progress.progress.parallel_review.total_dimensions} dimensions`;
|
||
}
|
||
|
||
if (progress.progress.deep_dive) {
|
||
percentComplete = progress.progress.deep_dive.percent_complete;
|
||
progressText = `Deep-Dive: ${progress.progress.deep_dive.analyzed}/${progress.progress.deep_dive.total_findings} findings`;
|
||
}
|
||
|
||
if (progress.phase === 'complete') {
|
||
percentComplete = 100;
|
||
progressText = 'Review Complete';
|
||
}
|
||
}
|
||
|
||
document.getElementById('progressFill').style.width = `${percentComplete}%`;
|
||
document.getElementById('progressText').textContent = progressText;
|
||
}
|
||
|
||
// Load final results (now delegates to loadAvailableDimensions)
|
||
async function loadFinalResults() {
|
||
// ✨ NEW: This function now just ensures one final load
|
||
// since loadAvailableDimensions() handles all the heavy lifting
|
||
await loadAvailableDimensions();
|
||
}
|
||
|
||
// Update severity counts
|
||
function updateSeverityCounts(distribution) {
|
||
document.getElementById('criticalCount').textContent = distribution.critical || 0;
|
||
document.getElementById('highCount').textContent = distribution.high || 0;
|
||
document.getElementById('mediumCount').textContent = distribution.medium || 0;
|
||
document.getElementById('lowCount').textContent = distribution.low || 0;
|
||
}
|
||
|
||
// Dimension summary table configuration
|
||
const dimensionConfig = {
|
||
'security': { icon: '🔒', label: 'Security' },
|
||
'architecture': { icon: '🏛', label: 'Architecture' },
|
||
'quality': { icon: '✅', label: 'Quality' },
|
||
'action-items': { icon: '📋', label: 'Action-Items' },
|
||
'performance': { icon: '⚡', label: 'Performance' },
|
||
'maintainability': { icon: '🔧', label: 'Maintainability' },
|
||
'best-practices': { icon: '📚', label: 'Best-Practices' }
|
||
};
|
||
|
||
// Update dimension summary table
|
||
function updateDimensionSummary() {
|
||
const tbody = document.getElementById('dimensionSummaryBody');
|
||
if (!tbody) return;
|
||
|
||
// Get all dimension names from config
|
||
const dimensions = Object.keys(dimensionConfig);
|
||
|
||
// Calculate counts per dimension
|
||
const dimensionStats = {};
|
||
dimensions.forEach(dim => {
|
||
dimensionStats[dim] = { critical: 0, high: 0, medium: 0, low: 0, total: 0, reviewed: false };
|
||
});
|
||
|
||
// Aggregate findings by dimension
|
||
allFindings.forEach(finding => {
|
||
const dim = finding.dimension;
|
||
if (dimensionStats[dim]) {
|
||
const severity = finding.severity.toLowerCase();
|
||
if (dimensionStats[dim][severity] !== undefined) {
|
||
dimensionStats[dim][severity]++;
|
||
}
|
||
dimensionStats[dim].total++;
|
||
// ✨ NEW: Mark as reviewed if we have findings from this dimension
|
||
dimensionStats[dim].reviewed = true;
|
||
}
|
||
});
|
||
|
||
// Also check reviewed status from reviewState (backward compatibility)
|
||
if (reviewState && reviewState.dimensions_reviewed) {
|
||
reviewState.dimensions_reviewed.forEach(dim => {
|
||
if (dimensionStats[dim]) {
|
||
dimensionStats[dim].reviewed = true;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Generate table rows
|
||
const rows = dimensions.map(dim => {
|
||
const config = dimensionConfig[dim];
|
||
const stats = dimensionStats[dim];
|
||
const hasFindings = stats.total > 0;
|
||
|
||
// Create count badge with appropriate styling
|
||
const createCountBadge = (count, severity) => {
|
||
const zeroClass = count === 0 ? ' zero' : '';
|
||
return `<span class="count-badge ${severity}${zeroClass}">${count}</span>`;
|
||
};
|
||
|
||
// ✨ NEW: Enhanced status indicator with 3 states
|
||
let statusClass, statusIcon, statusTooltip;
|
||
if (stats.reviewed && hasFindings) {
|
||
statusClass = 'reviewed';
|
||
statusIcon = '✓';
|
||
statusTooltip = 'Completed';
|
||
} else if (stats.reviewed && !hasFindings) {
|
||
statusClass = 'reviewed';
|
||
statusIcon = '✓';
|
||
statusTooltip = 'Completed (no findings)';
|
||
} else {
|
||
statusClass = 'pending';
|
||
statusIcon = '⏳';
|
||
statusTooltip = 'Processing...';
|
||
}
|
||
|
||
return `
|
||
<tr onclick="filterByDimension('${dim}')" style="cursor: pointer;">
|
||
<td>
|
||
<div class="dimension-name">
|
||
<div class="dimension-icon ${dim}">${config.icon}</div>
|
||
<span>${config.label}</span>
|
||
</div>
|
||
</td>
|
||
<td>${createCountBadge(stats.critical, 'critical')}</td>
|
||
<td>${createCountBadge(stats.high, 'high')}</td>
|
||
<td>${createCountBadge(stats.medium, 'medium')}</td>
|
||
<td>${createCountBadge(stats.low, 'low')}</td>
|
||
<td><span class="count-badge total">${stats.total}</span></td>
|
||
<td><span class="status-indicator ${statusClass}" title="${statusTooltip}">${statusIcon}</span></td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
|
||
tbody.innerHTML = rows;
|
||
}
|
||
|
||
// Filter functions
|
||
function filterByDimension(dimension) {
|
||
currentFilters.dimension = dimension;
|
||
|
||
// Update tab UI
|
||
document.querySelectorAll('.tab').forEach(tab => {
|
||
tab.classList.toggle('active', tab.dataset.dimension === dimension);
|
||
});
|
||
|
||
applyFilters();
|
||
}
|
||
|
||
function setupSearch() {
|
||
const searchInput = document.getElementById('searchInput');
|
||
searchInput.addEventListener('input', (e) => {
|
||
currentFilters.search = e.target.value.toLowerCase();
|
||
applyFilters();
|
||
});
|
||
}
|
||
|
||
function applyFilters() {
|
||
filteredFindings = allFindings.filter(finding => {
|
||
// Dimension filter
|
||
if (currentFilters.dimension !== 'all' && finding.dimension !== currentFilters.dimension) {
|
||
return false;
|
||
}
|
||
|
||
// ✨ NEW: Multi-select severity filter
|
||
if (currentFilters.severities.size > 0) {
|
||
if (!currentFilters.severities.has(finding.severity.toLowerCase())) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Search filter
|
||
if (currentFilters.search) {
|
||
const searchText = `${finding.title} ${finding.description} ${finding.file} ${finding.category || ''}`.toLowerCase();
|
||
if (!searchText.includes(currentFilters.search)) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
});
|
||
|
||
// Auto-sort after filtering
|
||
sortFindings();
|
||
}
|
||
|
||
// ✨ UPDATED: Sort findings with order support
|
||
function sortFindings() {
|
||
const sortBy = document.getElementById('sortSelect').value;
|
||
sortConfig.field = sortBy;
|
||
|
||
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
||
|
||
filteredFindings.sort((a, b) => {
|
||
let comparison = 0;
|
||
|
||
if (sortBy === 'severity') {
|
||
comparison = severityOrder[a.severity.toLowerCase()] - severityOrder[b.severity.toLowerCase()];
|
||
} else if (sortBy === 'dimension') {
|
||
comparison = a.dimension.localeCompare(b.dimension);
|
||
} else if (sortBy === 'file') {
|
||
comparison = a.file.localeCompare(b.file);
|
||
} else if (sortBy === 'title') {
|
||
comparison = a.title.localeCompare(b.title);
|
||
}
|
||
|
||
// ✨ NEW: Apply sort order (asc/desc)
|
||
return sortConfig.order === 'asc' ? comparison : -comparison;
|
||
});
|
||
|
||
renderFindings();
|
||
}
|
||
|
||
// Render findings list
|
||
function renderFindings() {
|
||
const container = document.getElementById('findingsList');
|
||
document.getElementById('findingsCount').textContent = `(${filteredFindings.length})`;
|
||
|
||
if (filteredFindings.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="empty-state">
|
||
<div class="empty-state-icon">✨</div>
|
||
<p>No findings match your filters</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = filteredFindings.map(finding => {
|
||
// Determine fix status badge
|
||
const fixStatusBadge = finding.fix_status ?
|
||
`<span class="fix-status-badge status-${finding.fix_status}">
|
||
${finding.fix_status === 'pending' ? '⏳ Pending' :
|
||
finding.fix_status === 'in-progress' ? '⚡ In Progress' :
|
||
finding.fix_status === 'fixed' ? '✅ Fixed' :
|
||
finding.fix_status === 'failed' ? '❌ Failed' : ''}
|
||
</span>` : '';
|
||
|
||
return `
|
||
<div class="finding-item ${finding.severity}" onclick='showFindingDetail(${JSON.stringify(finding).replace(/'/g, "\\'")})' style="display: flex; gap: 12px;">
|
||
<input type="checkbox"
|
||
class="finding-checkbox"
|
||
data-finding-id="${finding.id}"
|
||
onclick='toggleFindingSelection("${finding.id}", event)'
|
||
${selectedFindings.has(finding.id) ? 'checked' : ''}>
|
||
<div style="flex: 1;">
|
||
<div class="finding-header">
|
||
<div class="finding-title">${finding.title}</div>
|
||
<div class="finding-badges">
|
||
<span class="severity-badge ${finding.severity}">${finding.severity}</span>
|
||
<span class="dimension-badge">${finding.dimension}</span>
|
||
</div>
|
||
</div>
|
||
<div class="finding-file">📄 ${finding.file}:${finding.line}</div>
|
||
<div class="finding-description">${finding.description.substring(0, 200)}${finding.description.length > 200 ? '...' : ''}</div>
|
||
${fixStatusBadge}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Show finding detail in drawer
|
||
function showFindingDetail(finding) {
|
||
const drawer = document.getElementById('findingDrawer');
|
||
const overlay = document.getElementById('drawerOverlay');
|
||
const content = document.getElementById('drawerContent');
|
||
|
||
document.getElementById('drawerTitle').textContent = finding.title;
|
||
|
||
content.innerHTML = `
|
||
<div class="drawer-section">
|
||
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
|
||
<span class="severity-badge ${finding.severity}">${finding.severity}</span>
|
||
<span class="dimension-badge">${finding.dimension}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="drawer-section">
|
||
<div class="drawer-section-title">📄 Location</div>
|
||
<div class="metadata-grid">
|
||
<div class="metadata-item">
|
||
<div class="metadata-label">File</div>
|
||
<div class="metadata-value" style="font-family: monospace; font-size: 0.85rem;">${finding.file}</div>
|
||
</div>
|
||
<div class="metadata-item">
|
||
<div class="metadata-label">Line</div>
|
||
<div class="metadata-value">${finding.line}</div>
|
||
</div>
|
||
${finding.category ? `
|
||
<div class="metadata-item">
|
||
<div class="metadata-label">Category</div>
|
||
<div class="metadata-value">${finding.category}</div>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="drawer-section">
|
||
<div class="drawer-section-title">📝 Description</div>
|
||
<p style="line-height: 1.8;">${finding.description}</p>
|
||
</div>
|
||
|
||
${finding.snippet ? `
|
||
<div class="drawer-section">
|
||
<div class="drawer-section-title">💻 Code Snippet</div>
|
||
<div class="code-snippet">${escapeHtml(finding.snippet)}</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div class="drawer-section">
|
||
<div class="drawer-section-title">✅ Recommendation</div>
|
||
<div class="recommendation-box">
|
||
${finding.recommendation}
|
||
</div>
|
||
</div>
|
||
|
||
${finding.impact ? `
|
||
<div class="drawer-section">
|
||
<div class="drawer-section-title">⚠️ Impact</div>
|
||
<p style="line-height: 1.8;">${finding.impact}</p>
|
||
</div>
|
||
` : ''}
|
||
|
||
${finding.references && finding.references.length > 0 ? `
|
||
<div class="drawer-section">
|
||
<div class="drawer-section-title">🔗 References</div>
|
||
<ul class="reference-list">
|
||
${finding.references.map(ref => {
|
||
const isUrl = ref.startsWith('http');
|
||
return `<li>${isUrl ? `<a href="${ref}" target="_blank">${ref}</a>` : ref}</li>`;
|
||
}).join('')}
|
||
</ul>
|
||
</div>
|
||
` : ''}
|
||
|
||
${finding.metadata ? `
|
||
<div class="drawer-section">
|
||
<div class="drawer-section-title">ℹ️ Metadata</div>
|
||
<div class="metadata-grid">
|
||
${finding.metadata.cwe_id ? `
|
||
<div class="metadata-item">
|
||
<div class="metadata-label">CWE ID</div>
|
||
<div class="metadata-value">${finding.metadata.cwe_id}</div>
|
||
</div>
|
||
` : ''}
|
||
${finding.metadata.owasp_category ? `
|
||
<div class="metadata-item">
|
||
<div class="metadata-label">OWASP Category</div>
|
||
<div class="metadata-value">${finding.metadata.owasp_category}</div>
|
||
</div>
|
||
` : ''}
|
||
${finding.metadata.pattern_type ? `
|
||
<div class="metadata-item">
|
||
<div class="metadata-label">Pattern Type</div>
|
||
<div class="metadata-value">${finding.metadata.pattern_type}</div>
|
||
</div>
|
||
` : ''}
|
||
${finding.metadata.complexity_score ? `
|
||
<div class="metadata-item">
|
||
<div class="metadata-label">Complexity Score</div>
|
||
<div class="metadata-value">${finding.metadata.complexity_score}</div>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
`;
|
||
|
||
drawer.classList.add('open');
|
||
overlay.classList.add('show');
|
||
}
|
||
|
||
function closeDrawer() {
|
||
document.getElementById('findingDrawer').classList.remove('open');
|
||
document.getElementById('drawerOverlay').classList.remove('show');
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const map = {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
};
|
||
return text.replace(/[&<>"']/g, m => map[m]);
|
||
}
|
||
|
||
// Export to Markdown
|
||
function exportToMarkdown() {
|
||
if (!reviewState) {
|
||
alert('Review data not yet loaded. Please wait.');
|
||
return;
|
||
}
|
||
|
||
let markdown = `# Code Review Report\n\n`;
|
||
markdown += `**Session**: ${reviewState.session_id}\n`;
|
||
markdown += `**Review ID**: ${reviewState.review_id}\n`;
|
||
markdown += `**Date**: ${new Date().toLocaleDateString()}\n`;
|
||
markdown += `**Dimensions**: ${reviewState.dimensions_reviewed.join(', ')}\n\n`;
|
||
|
||
markdown += `## Summary\n\n`;
|
||
markdown += `- **Total Findings**: ${allFindings.length}\n`;
|
||
markdown += `- **Critical**: ${reviewState.severity_distribution.critical}\n`;
|
||
markdown += `- **High**: ${reviewState.severity_distribution.high}\n`;
|
||
markdown += `- **Medium**: ${reviewState.severity_distribution.medium}\n`;
|
||
markdown += `- **Low**: ${reviewState.severity_distribution.low}\n\n`;
|
||
|
||
// Group by dimension
|
||
reviewState.dimensions_reviewed.forEach(dim => {
|
||
const dimFindings = allFindings.filter(f => f.dimension === dim);
|
||
if (dimFindings.length === 0) return;
|
||
|
||
markdown += `## ${dim.charAt(0).toUpperCase() + dim.slice(1)} (${dimFindings.length} findings)\n\n`;
|
||
|
||
dimFindings.forEach(finding => {
|
||
markdown += `### ${finding.title}\n\n`;
|
||
markdown += `**Severity**: ${finding.severity} \n`;
|
||
markdown += `**File**: \`${finding.file}:${finding.line}\` \n`;
|
||
markdown += `**Category**: ${finding.category || 'N/A'} \n\n`;
|
||
markdown += `${finding.description}\n\n`;
|
||
markdown += `**Recommendation**: ${finding.recommendation}\n\n`;
|
||
if (finding.impact) {
|
||
markdown += `**Impact**: ${finding.impact}\n\n`;
|
||
}
|
||
markdown += `---\n\n`;
|
||
});
|
||
});
|
||
|
||
// Download
|
||
const blob = new Blob([markdown], { type: 'text/markdown' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
const filename = `review-report-${reviewState.review_id}.md`;
|
||
a.download = filename;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
|
||
// ✨ Show export path notification with session directory recommendation
|
||
const reviewDir = window.location.pathname.replace('/dashboard.html', '').replace('/.review/dashboard.html', '/.review');
|
||
// Extract absolute path from file:// URL
|
||
const absolutePath = decodeURIComponent(window.location.pathname).replace(/^\/([A-Z]:)/, '$1');
|
||
const sessionDir = absolutePath.replace('/.review/dashboard.html', '');
|
||
const reportsDir = `${sessionDir}/.review/reports`;
|
||
|
||
const notification = `
|
||
✅ Report exported successfully!
|
||
|
||
📁 File: ${filename}
|
||
📂 Browser download: Your Downloads folder
|
||
|
||
📋 Recommended Location:
|
||
${reportsDir}/
|
||
|
||
🔄 Move to session directory:
|
||
# On Windows:
|
||
move "%USERPROFILE%\\Downloads\\${filename}" "${reportsDir.replace(/\//g, '\\')}\\${filename}"
|
||
|
||
# On Mac/Linux:
|
||
mv ~/Downloads/${filename} ${reportsDir}/${filename}
|
||
|
||
💡 All review files in:
|
||
${reportsDir}/
|
||
`.trim();
|
||
|
||
alert(notification);
|
||
}
|
||
|
||
// Initialize
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initTheme();
|
||
setupSearch();
|
||
startPolling();
|
||
detectFixSession(); // Check for active fix sessions
|
||
|
||
document.getElementById('themeToggle').addEventListener('click', toggleTheme);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|