mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
- Introduced `review-deep-dive-results-schema.json` to define the structure for deep-dive iteration analysis results, including root cause analysis, remediation plans, and impact assessments. - Added `review-dimension-results-schema.json` to outline the schema for dimension analysis results, capturing findings across various dimensions such as security and architecture, along with cross-references to related findings.
1143 lines
39 KiB
HTML
1143 lines
39 KiB
HTML
<!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);
|
||
}
|
||
|
||
/* 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;
|
||
}
|
||
|
||
/* 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); }
|
||
}
|
||
|
||
/* 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>
|
||
|
||
<button class="btn export-btn" onclick="exportToMarkdown()">📥 Export Report</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 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 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>
|
||
|
||
<!-- Findings Container -->
|
||
<div class="findings-container">
|
||
<div class="findings-header">
|
||
<h3>Findings <span id="findingsCount">(0)</span></h3>
|
||
<div>
|
||
<select id="sortSelect" class="btn" onchange="sortFindings()">
|
||
<option value="severity">Sort by Severity</option>
|
||
<option value="dimension">Sort by Dimension</option>
|
||
<option value="file">Sort by File</option>
|
||
</select>
|
||
</div>
|
||
</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>
|
||
|
||
<button class="theme-toggle" id="themeToggle">🌙</button>
|
||
|
||
<script>
|
||
// State
|
||
let allFindings = [];
|
||
let filteredFindings = [];
|
||
let currentFilters = {
|
||
dimension: 'all',
|
||
severity: null,
|
||
search: ''
|
||
};
|
||
let pollingInterval = null;
|
||
let reviewState = null;
|
||
|
||
// 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();
|
||
updateProgressUI(progress);
|
||
|
||
// If complete, stop polling and load final results
|
||
if (progress.phase === 'complete') {
|
||
stopPolling();
|
||
await loadFinalResults();
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading progress:', error);
|
||
}
|
||
}
|
||
|
||
// Update progress UI
|
||
function updateProgressUI(progress) {
|
||
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 = '';
|
||
|
||
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
|
||
async function loadFinalResults() {
|
||
try {
|
||
// Load review state
|
||
const stateResponse = await fetch('./review-state.json');
|
||
reviewState = await stateResponse.json();
|
||
|
||
document.getElementById('sessionId').textContent = reviewState.session_id;
|
||
|
||
// Update severity counts
|
||
updateSeverityCounts(reviewState.severity_distribution);
|
||
|
||
// Load all dimension files
|
||
const dimensionPromises = reviewState.dimensions_reviewed.map(dim =>
|
||
fetch(`./dimensions/${dim}.json`)
|
||
.then(r => r.json())
|
||
.catch(err => {
|
||
console.error(`Failed to load ${dim}:`, err);
|
||
return null;
|
||
})
|
||
);
|
||
|
||
const dimensions = await Promise.all(dimensionPromises);
|
||
|
||
// Aggregate all findings
|
||
allFindings = [];
|
||
dimensions.forEach(dim => {
|
||
if (dim && dim.findings) {
|
||
allFindings.push(...dim.findings.map(f => ({
|
||
...f,
|
||
dimension: dim.dimension
|
||
})));
|
||
}
|
||
});
|
||
|
||
renderFindings();
|
||
} catch (error) {
|
||
console.error('Error loading final results:', error);
|
||
}
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// 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 filterBySeverity(severity) {
|
||
currentFilters.severity = currentFilters.severity === severity ? null : severity;
|
||
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;
|
||
}
|
||
|
||
// Severity filter
|
||
if (currentFilters.severity && finding.severity !== currentFilters.severity) {
|
||
return false;
|
||
}
|
||
|
||
// Search filter
|
||
if (currentFilters.search) {
|
||
const searchText = `${finding.title} ${finding.description} ${finding.file}`.toLowerCase();
|
||
if (!searchText.includes(currentFilters.search)) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
});
|
||
|
||
renderFindings();
|
||
}
|
||
|
||
// Sort findings
|
||
function sortFindings() {
|
||
const sortBy = document.getElementById('sortSelect').value;
|
||
|
||
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
||
|
||
filteredFindings.sort((a, b) => {
|
||
if (sortBy === 'severity') {
|
||
return severityOrder[a.severity] - severityOrder[b.severity];
|
||
} else if (sortBy === 'dimension') {
|
||
return a.dimension.localeCompare(b.dimension);
|
||
} else if (sortBy === 'file') {
|
||
return a.file.localeCompare(b.file);
|
||
}
|
||
return 0;
|
||
});
|
||
|
||
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 => `
|
||
<div class="finding-item ${finding.severity}" onclick='showFindingDetail(${JSON.stringify(finding).replace(/'/g, "\\'")})'>
|
||
<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>
|
||
</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');
|
||
a.href = url;
|
||
a.download = `review-report-${reviewState.review_id}.md`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
// Initialize
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initTheme();
|
||
setupSearch();
|
||
startPolling();
|
||
|
||
document.getElementById('themeToggle').addEventListener('click', toggleTheme);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|