Files
Claude-Code-Workflow/.claude/templates/fix-dashboard.html
catlog22 b000359e69 feat: Enhance fix-dashboard.html to consume more JSON fields from review-fix workflow
- Add errors array display with badge and detailed error list in Active Agents
- Enhance task cards with dimension badge, file:line, attempts, and commit hash
- Add Finding Details Modal with complete information on click
- Display fix_strategy and risk_assessment in Active Groups cards
- Add renderAgentErrors(), renderStrategyAndRisk() helper functions
- Increase JSON field consumption rate from ~53% to ~90%

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 19:59:33 +08:00

2363 lines
79 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fix Progress Dashboard - {{SESSION_ID}}</title>
<style>
/* Dark Theme (Default) */
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-card: #1c2128;
--bg-hover: #21262d;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--border-color: #30363d;
--accent-color: #3b82f6;
--success-color: #22c55e;
--danger-color: #ef4444;
--warning-color: #f59e0b;
--info-color: #8b5cf6;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 40px rgba(0, 0, 0, 0.5);
--transition: all 0.3s ease;
}
/* Light Theme */
[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #f6f8fa;
--bg-card: #ffffff;
--bg-hover: #f0f2f5;
--text-primary: #24292f;
--text-secondary: #57606a;
--border-color: #d0d7de;
--accent-color: #0969da;
--success-color: #1a7f37;
--danger-color: #cf222e;
--warning-color: #bf8700;
--info-color: #8250df;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 40px rgba(0, 0, 0, 0.15);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
transition: var(--transition);
}
.container {
max-width: 1600px;
margin: 0 auto;
padding: 20px;
}
/* Header */
header {
background-color: var(--bg-secondary);
padding: 20px 25px;
border-radius: 12px;
box-shadow: var(--shadow);
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left h1 {
font-size: 1.8rem;
margin-bottom: 10px;
}
.session-info {
color: var(--text-secondary);
font-size: 0.9rem;
}
.session-info a {
color: var(--accent-color);
text-decoration: none;
transition: var(--transition);
}
.session-info a:hover {
text-decoration: underline;
}
.header-right {
display: flex;
gap: 10px;
align-items: center;
}
/* Theme Toggle Button */
.theme-toggle {
background-color: var(--bg-card);
border: 2px solid var(--border-color);
color: var(--text-primary);
width: 50px;
height: 50px;
border-radius: 50%;
cursor: pointer;
font-size: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
}
.theme-toggle:hover {
border-color: var(--accent-color);
transform: rotate(180deg);
}
/* Button Styles */
.btn {
padding: 10px 18px;
background-color: var(--bg-card);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
transition: var(--transition);
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn:hover {
background-color: var(--bg-hover);
border-color: var(--accent-color);
transform: translateY(-2px);
}
.btn-primary {
background-color: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
.btn-primary:hover {
background-color: #2563eb;
}
/* Phase Badge */
.phase-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
}
.phase-badge.phase-planning {
background-color: rgba(139, 92, 246, 0.2);
color: var(--info-color);
}
.phase-badge.phase-execution {
background-color: rgba(59, 130, 246, 0.2);
color: var(--accent-color);
animation: pulse 2s infinite;
}
.phase-badge.phase-completion {
background-color: rgba(34, 197, 94, 0.2);
color: var(--success-color);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* Fix Progress Section */
.fix-progress-section {
background-color: var(--bg-card);
padding: 25px;
border-radius: 12px;
box-shadow: var(--shadow);
margin-bottom: 20px;
border: 1px solid var(--border-color);
}
.fix-progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.fix-progress-header h2 {
font-size: 1.5rem;
}
.header-actions {
display: flex;
gap: 10px;
}
/* Progress Bar */
.fix-progress-bar {
height: 16px;
background-color: var(--bg-secondary);
border-radius: 8px;
overflow: hidden;
margin-bottom: 15px;
position: relative;
}
.fix-progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--success-color), #38a169);
transition: width 0.5s ease;
position: relative;
overflow: hidden;
}
.fix-progress-bar-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.progress-text {
text-align: center;
font-size: 1rem;
color: var(--text-secondary);
margin-top: 12px;
}
.progress-text strong {
color: var(--text-primary);
font-size: 1.1rem;
}
/* Planning Phase Indicator */
.planning-phase {
text-align: center;
padding: 50px 20px;
}
.planning-phase .icon {
font-size: 4rem;
margin-bottom: 20px;
animation: bounce 1.5s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-15px); }
}
.planning-phase h3 {
font-size: 1.4rem;
margin-bottom: 12px;
}
.planning-phase p {
color: var(--text-secondary);
font-size: 1.05rem;
}
/* Stage Timeline */
.stage-timeline {
display: flex;
gap: 15px;
margin-bottom: 25px;
overflow-x: auto;
padding-bottom: 10px;
}
.stage-timeline::-webkit-scrollbar {
height: 8px;
}
.stage-timeline::-webkit-scrollbar-track {
background: var(--bg-secondary);
border-radius: 4px;
}
.stage-timeline::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
.stage-timeline::-webkit-scrollbar-thumb:hover {
background: var(--accent-color);
}
.stage-item {
flex: 0 0 auto;
min-width: 180px;
padding: 18px;
background-color: var(--bg-secondary);
border-radius: 10px;
border: 2px solid var(--border-color);
transition: var(--transition);
position: relative;
}
.stage-item::after {
content: '→';
position: absolute;
right: -25px;
top: 50%;
transform: translateY(-50%);
font-size: 1.5rem;
color: var(--text-secondary);
}
.stage-item:last-child::after {
display: none;
}
.stage-item.in-progress {
border-color: var(--accent-color);
box-shadow: 0 0 20px rgba(59, 130, 246, 0.4);
transform: scale(1.05);
}
.stage-item.completed {
border-color: var(--success-color);
opacity: 0.8;
}
.stage-item.completed .stage-number::before {
content: '✓ ';
color: var(--success-color);
}
.stage-number {
font-weight: 700;
font-size: 1.15rem;
margin-bottom: 10px;
}
.stage-mode {
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 5px;
}
.stage-groups {
font-size: 0.8rem;
color: var(--text-secondary);
}
/* Tabs Navigation */
.tabs-nav {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid var(--border-color);
padding-bottom: 10px;
}
.tab-btn {
padding: 10px 20px;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 0.95rem;
font-weight: 600;
border-bottom: 3px solid transparent;
transition: var(--transition);
}
.tab-btn:hover {
color: var(--text-primary);
}
.tab-btn.active {
color: var(--accent-color);
border-bottom-color: var(--accent-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Active Groups Section */
.active-groups-section {
margin-bottom: 25px;
}
.active-groups-section h3 {
font-size: 1.2rem;
margin-bottom: 18px;
display: flex;
align-items: center;
gap: 10px;
}
.active-groups-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 18px;
}
.active-group-card {
background-color: var(--bg-secondary);
padding: 18px;
border-radius: 10px;
border: 2px solid var(--border-color);
transition: var(--transition);
}
.active-group-card:hover {
border-color: var(--accent-color);
transform: translateY(-3px);
box-shadow: var(--shadow-lg);
}
.group-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.group-id {
font-weight: 700;
font-size: 1.05rem;
color: var(--accent-color);
}
.group-status {
padding: 5px 12px;
border-radius: 14px;
font-size: 0.75rem;
font-weight: 700;
background-color: rgba(59, 130, 246, 0.2);
color: var(--accent-color);
}
.group-findings {
color: var(--text-secondary);
font-size: 0.95rem;
margin-bottom: 10px;
}
.group-active-items {
margin-top: 10px;
padding: 10px 12px;
background-color: rgba(59, 130, 246, 0.1);
border-left: 4px solid var(--accent-color);
border-radius: 6px;
font-size: 0.9rem;
word-wrap: break-word;
}
/* Active Agents Section */
.active-agents-section {
margin-top: 25px;
border-top: 2px solid var(--border-color);
padding-top: 25px;
}
.active-agents-section h3 {
font-size: 1.2rem;
margin-bottom: 18px;
display: flex;
align-items: center;
gap: 10px;
}
.active-agent-item {
padding: 18px;
background-color: var(--bg-secondary);
border-radius: 10px;
margin-bottom: 15px;
border: 2px solid var(--border-color);
transition: var(--transition);
}
.active-agent-item:hover {
border-color: var(--accent-color);
transform: translateX(5px);
}
.agent-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.agent-status-icon {
font-size: 2rem;
animation: rotate 2s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.agent-info {
flex: 1;
}
.agent-id {
font-weight: 700;
font-size: 1rem;
display: flex;
align-items: center;
gap: 8px;
}
.agent-status-badge {
display: inline-block;
padding: 4px 12px;
font-size: 0.75rem;
background-color: var(--accent-color);
color: white;
border-radius: 12px;
font-weight: 600;
}
.agent-task {
color: var(--text-primary);
font-size: 0.95rem;
margin: 8px 0;
}
.agent-file {
color: var(--text-secondary);
font-size: 0.85rem;
font-family: 'Courier New', monospace;
}
/* Flow Control Steps */
.flow-control-container {
margin-top: 15px;
padding: 15px;
background-color: rgba(139, 92, 246, 0.08);
border-radius: 8px;
border: 2px solid rgba(139, 92, 246, 0.3);
}
.flow-control-header {
font-size: 0.8rem;
font-weight: 700;
color: var(--text-secondary);
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 1px;
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 8px;
}
.flow-step {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 6px;
font-size: 0.9rem;
transition: var(--transition);
}
.flow-step:hover {
transform: translateX(5px);
}
.flow-step-completed {
background-color: rgba(34, 197, 94, 0.15);
color: var(--success-color);
border-left: 4px solid var(--success-color);
}
.flow-step-in-progress {
background-color: rgba(59, 130, 246, 0.15);
color: var(--accent-color);
animation: pulse 2s ease-in-out infinite;
border-left: 4px solid var(--accent-color);
}
.flow-step-failed {
background-color: rgba(239, 68, 68, 0.15);
color: var(--danger-color);
border-left: 4px solid var(--danger-color);
}
.flow-step-pending {
background-color: rgba(156, 163, 175, 0.1);
color: var(--text-secondary);
border-left: 4px solid var(--border-color);
}
.flow-step-icon {
font-size: 1.2rem;
}
.flow-step-name {
font-weight: 600;
}
/* Task Visualization Section */
.tasks-section {
background-color: var(--bg-card);
padding: 25px;
border-radius: 12px;
box-shadow: var(--shadow);
margin-bottom: 20px;
border: 1px solid var(--border-color);
}
.tasks-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.tasks-header h2 {
font-size: 1.5rem;
}
.task-filters {
display: flex;
gap: 10px;
}
.filter-btn {
padding: 8px 16px;
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
cursor: pointer;
font-size: 0.85rem;
transition: var(--transition);
}
.filter-btn:hover {
border-color: var(--accent-color);
}
.filter-btn.active {
background-color: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
.tasks-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 18px;
}
.task-card {
background-color: var(--bg-secondary);
padding: 18px;
border-radius: 10px;
border: 2px solid var(--border-color);
transition: var(--transition);
cursor: pointer;
}
.task-card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-lg);
}
.task-card.status-pending {
border-left: 4px solid var(--warning-color);
}
.task-card.status-in-progress {
border-left: 4px solid var(--accent-color);
box-shadow: 0 0 15px rgba(59, 130, 246, 0.2);
}
.task-card.status-fixed {
border-left: 4px solid var(--success-color);
opacity: 0.85;
}
.task-card.status-failed {
border-left: 4px solid var(--danger-color);
}
.task-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 12px;
}
.task-id {
font-size: 0.75rem;
color: var(--text-secondary);
font-family: 'Courier New', monospace;
}
.task-status {
padding: 4px 10px;
border-radius: 12px;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
}
.task-status.pending {
background-color: rgba(245, 158, 11, 0.2);
color: var(--warning-color);
}
.task-status.in-progress {
background-color: rgba(59, 130, 246, 0.2);
color: var(--accent-color);
}
.task-status.fixed {
background-color: rgba(34, 197, 94, 0.2);
color: var(--success-color);
}
.task-status.failed {
background-color: rgba(239, 68, 68, 0.2);
color: var(--danger-color);
}
.task-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 8px;
line-height: 1.4;
}
.task-meta {
display: flex;
gap: 15px;
font-size: 0.85rem;
color: var(--text-secondary);
margin-top: 10px;
}
.task-meta-item {
display: flex;
align-items: center;
gap: 5px;
}
/* Fix Summary Section */
.fix-summary-section {
background-color: var(--bg-card);
padding: 25px;
border-radius: 12px;
box-shadow: var(--shadow);
margin-bottom: 20px;
border: 1px solid var(--border-color);
}
.fix-summary-section h2 {
font-size: 1.4rem;
margin-bottom: 20px;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 18px;
}
.summary-card {
background-color: var(--bg-secondary);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 2px solid var(--border-color);
transition: var(--transition);
}
.summary-card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-lg);
}
.summary-icon {
font-size: 2.5rem;
margin-bottom: 12px;
}
.summary-value {
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 8px;
}
.summary-label {
color: var(--text-secondary);
font-size: 0.95rem;
font-weight: 500;
}
.summary-card.fixed {
border-color: var(--success-color);
}
.summary-card.fixed .summary-value {
color: var(--success-color);
}
.summary-card.failed {
border-color: var(--danger-color);
}
.summary-card.failed .summary-value {
color: var(--danger-color);
}
.summary-card.pending {
border-color: var(--warning-color);
}
.summary-card.pending .summary-value {
color: var(--warning-color);
}
/* Empty State */
.empty-state {
text-align: center;
padding: 80px 20px;
color: var(--text-secondary);
}
.empty-state-icon {
font-size: 4rem;
margin-bottom: 20px;
opacity: 0.5;
}
.empty-state p {
font-size: 1.15rem;
}
/* History Drawer */
.history-drawer-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1000;
display: none;
opacity: 0;
transition: opacity 0.3s;
}
.history-drawer-overlay.active {
display: block;
opacity: 1;
}
.history-drawer {
position: fixed;
right: -700px;
top: 0;
width: 700px;
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-header {
padding: 25px;
border-bottom: 2px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
background-color: var(--bg-secondary);
z-index: 10;
}
.history-header h2 {
font-size: 1.5rem;
}
.close-btn {
background: none;
border: none;
color: var(--text-primary);
font-size: 2rem;
cursor: pointer;
padding: 0;
width: 45px;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
transition: var(--transition);
}
.close-btn:hover {
background-color: var(--bg-primary);
transform: rotate(90deg);
}
.history-content {
padding: 25px;
}
.history-item {
padding: 20px;
background-color: var(--bg-card);
border-radius: 10px;
margin-bottom: 18px;
border-left: 5px solid var(--border-color);
transition: var(--transition);
}
.history-item:hover {
transform: translateX(5px);
box-shadow: var(--shadow);
}
.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: 12px;
}
.history-item-time {
color: var(--text-secondary);
font-size: 0.85rem;
}
.history-item-status {
padding: 5px 12px;
border-radius: 14px;
font-size: 0.75rem;
font-weight: 700;
}
.history-item-status.fixed {
background-color: rgba(34, 197, 94, 0.2);
color: var(--success-color);
}
.history-item-status.failed {
background-color: rgba(239, 68, 68, 0.2);
color: var(--danger-color);
}
.history-item-stats {
display: flex;
gap: 18px;
margin-top: 12px;
font-size: 0.9rem;
color: var(--text-secondary);
}
/* Loading Spinner */
.spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid var(--border-color);
border-top-color: var(--accent-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Error Badge */
.error-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 12px;
background-color: rgba(239, 68, 68, 0.15);
color: var(--danger-color);
border-radius: 14px;
font-size: 0.75rem;
font-weight: 700;
margin-left: 8px;
}
/* Error Container */
.error-container {
margin-top: 12px;
padding: 12px;
background-color: rgba(239, 68, 68, 0.1);
border-left: 4px solid var(--danger-color);
border-radius: 6px;
}
.error-header {
font-size: 0.8rem;
font-weight: 700;
color: var(--danger-color);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 1px;
}
.error-item {
padding: 8px 12px;
background-color: var(--bg-card);
border-radius: 6px;
margin-bottom: 6px;
font-size: 0.85rem;
}
.error-item:last-child {
margin-bottom: 0;
}
.error-meta {
color: var(--text-secondary);
font-size: 0.75rem;
margin-top: 4px;
}
/* Task Card Enhancements */
.task-file {
font-size: 0.85rem;
color: var(--text-secondary);
font-family: 'Courier New', monospace;
margin-top: 8px;
padding: 6px 10px;
background-color: rgba(139, 92, 246, 0.08);
border-radius: 6px;
display: inline-block;
}
.task-dimension-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
font-size: 0.7rem;
font-weight: 600;
background-color: rgba(139, 92, 246, 0.15);
color: var(--info-color);
margin-right: 6px;
}
.task-attempts {
color: var(--warning-color);
font-size: 0.75rem;
font-weight: 600;
}
/* Finding Details Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
z-index: 2000;
display: none;
opacity: 0;
transition: opacity 0.3s;
overflow-y: auto;
}
.modal-overlay.active {
display: flex;
opacity: 1;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-content {
background-color: var(--bg-secondary);
border-radius: 12px;
max-width: 900px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
box-shadow: var(--shadow-lg);
transform: scale(0.9);
transition: transform 0.3s;
}
.modal-overlay.active .modal-content {
transform: scale(1);
}
.modal-header {
padding: 25px;
border-bottom: 2px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
background-color: var(--bg-secondary);
z-index: 10;
}
.modal-header h2 {
font-size: 1.4rem;
display: flex;
align-items: center;
gap: 12px;
}
.modal-body {
padding: 25px;
}
.detail-section {
margin-bottom: 25px;
}
.detail-section h3 {
font-size: 1.1rem;
margin-bottom: 12px;
color: var(--accent-color);
display: flex;
align-items: center;
gap: 8px;
}
.detail-grid {
display: grid;
grid-template-columns: 140px 1fr;
gap: 12px;
background-color: var(--bg-card);
padding: 15px;
border-radius: 8px;
}
.detail-label {
font-weight: 700;
color: var(--text-secondary);
font-size: 0.85rem;
}
.detail-value {
color: var(--text-primary);
font-size: 0.9rem;
word-break: break-word;
}
.detail-value code {
background-color: var(--bg-secondary);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
}
/* Strategy and Risk Display */
.strategy-risk-container {
margin-top: 10px;
padding: 12px;
background-color: rgba(139, 92, 246, 0.08);
border-radius: 8px;
border: 1px solid rgba(139, 92, 246, 0.2);
}
.strategy-item, .risk-item {
font-size: 0.85rem;
margin-bottom: 6px;
display: flex;
gap: 8px;
}
.strategy-item:last-child, .risk-item:last-child {
margin-bottom: 0;
}
.strategy-label, .risk-label {
font-weight: 700;
color: var(--info-color);
min-width: 90px;
}
.risk-level {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.7rem;
font-weight: 700;
}
.risk-level.low {
background-color: rgba(34, 197, 94, 0.2);
color: var(--success-color);
}
.risk-level.medium {
background-color: rgba(245, 158, 11, 0.2);
color: var(--warning-color);
}
.risk-level.high {
background-color: rgba(239, 68, 68, 0.2);
color: var(--danger-color);
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
padding: 10px;
}
header {
flex-direction: column;
gap: 15px;
}
.header-right {
width: 100%;
justify-content: space-between;
}
.summary-grid,
.active-groups-grid,
.tasks-grid {
grid-template-columns: 1fr;
}
.history-drawer {
width: 100%;
right: -100%;
}
.stage-timeline {
flex-wrap: nowrap;
}
.modal-content {
max-width: 100%;
}
.detail-grid {
grid-template-columns: 1fr;
gap: 8px;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="header-left">
<h1>🔧 Fix Progress Dashboard</h1>
<div class="session-info">
Session: <strong id="sessionId">{{SESSION_ID}}</strong> |
Fix Session: <strong id="fixSessionId">Loading...</strong> |
<a href="./dashboard.html">← Back to Review Dashboard</a>
</div>
</div>
<div class="header-right">
<button class="btn" onclick="loadFixProgress()">
<span class="spinner" id="refreshSpinner" style="display: none;"></span>
<span id="refreshText">🔄 Refresh</span>
</button>
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle Theme">
<span id="themeIcon">🌙</span>
</button>
</div>
</header>
<!-- Fix Progress Section -->
<div class="fix-progress-section">
<div class="fix-progress-header">
<div style="display: flex; align-items: center; gap: 15px;">
<h2>Fix Progress</h2>
<span class="phase-badge" id="phaseBadge">LOADING</span>
</div>
<div class="header-actions">
<button class="btn" onclick="openHistoryDrawer()">📜 View History</button>
</div>
</div>
<!-- Planning Phase Indicator -->
<div id="planningPhase" class="planning-phase" style="display: none;">
<div class="icon">🤖</div>
<h3>Planning Fixes</h3>
<p>AI is analyzing findings and generating fix plan...</p>
</div>
<!-- Stage Timeline -->
<div class="stage-timeline" id="stageTimeline" style="display: none;">
<!-- Populated by JavaScript -->
</div>
<!-- Execution Phase -->
<div id="executionPhase" style="display: none;">
<!-- Progress Bar -->
<div class="fix-progress-bar">
<div class="fix-progress-bar-fill" id="progressFill" style="width: 0%"></div>
</div>
<div class="progress-text" id="progressText">
Initializing...
</div>
<!-- Tabs Navigation -->
<div class="tabs-nav">
<button class="tab-btn active" data-tab="groups" onclick="switchTab('groups')">🔷 Active Groups</button>
<button class="tab-btn" data-tab="agents" onclick="switchTab('agents')">⚡ Active Agents</button>
</div>
<!-- Active Groups Tab -->
<div class="tab-content active" id="groupsTab">
<div class="active-groups-section" id="activeGroupsSection">
<div class="active-groups-grid" id="activeGroupsGrid">
<!-- Populated by JavaScript -->
</div>
</div>
</div>
<!-- Active Agents Tab -->
<div class="tab-content" id="agentsTab">
<div class="active-agents-section" id="activeAgentsSection">
<div id="activeAgentsList">
<!-- Populated by JavaScript -->
</div>
</div>
</div>
</div>
</div>
<!-- Tasks Visualization Section -->
<div class="tasks-section">
<div class="tasks-header">
<h2>📋 Fix Tasks</h2>
<div class="task-filters">
<button class="filter-btn active" data-status="all" onclick="filterTasks('all')">All</button>
<button class="filter-btn" data-status="pending" onclick="filterTasks('pending')">Pending</button>
<button class="filter-btn" data-status="in-progress" onclick="filterTasks('in-progress')">In Progress</button>
<button class="filter-btn" data-status="fixed" onclick="filterTasks('fixed')">Fixed</button>
<button class="filter-btn" data-status="failed" onclick="filterTasks('failed')">Failed</button>
</div>
</div>
<div class="tasks-grid" id="tasksGrid">
<div class="empty-state">
<div class="empty-state-icon"></div>
<p>Loading tasks...</p>
</div>
</div>
</div>
<!-- Fix Summary Section -->
<div class="fix-summary-section">
<h2>📊 Summary</h2>
<div class="summary-grid">
<div class="summary-card">
<div class="summary-icon">📊</div>
<div class="summary-value" id="totalFindings">0</div>
<div class="summary-label">Total Findings</div>
</div>
<div class="summary-card fixed">
<div class="summary-icon"></div>
<div class="summary-value" id="fixedCount">0</div>
<div class="summary-label">Fixed</div>
</div>
<div class="summary-card failed">
<div class="summary-icon"></div>
<div class="summary-value" id="failedCount">0</div>
<div class="summary-label">Failed</div>
</div>
<div class="summary-card pending">
<div class="summary-icon"></div>
<div class="summary-value" id="pendingCount">0</div>
<div class="summary-label">Pending</div>
</div>
</div>
</div>
</div>
<!-- History Drawer -->
<div class="history-drawer-overlay" id="historyDrawerOverlay" onclick="closeHistoryDrawer()"></div>
<div class="history-drawer" id="historyDrawer">
<div class="history-header">
<h2>📜 Fix History</h2>
<button class="close-btn" onclick="closeHistoryDrawer()">×</button>
</div>
<div class="history-content" id="historyContent">
<div class="empty-state">
<div class="empty-state-icon"></div>
<p>Loading history...</p>
</div>
</div>
</div>
<!-- Finding Details Modal -->
<div class="modal-overlay" id="findingModal" onclick="closeFindingModal(event)">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2><span id="modalFindingIcon">📋</span> Finding Details</h2>
<button class="close-btn" onclick="closeFindingModal()">×</button>
</div>
<div class="modal-body" id="modalBody">
<!-- Populated by JavaScript -->
</div>
</div>
</div>
<script>
const SESSION_ID = '{{SESSION_ID}}';
const REVIEW_DIR = '{{REVIEW_DIR}}';
let fixSession = null;
let progressInterval = null;
let allTasks = [];
let currentFilter = 'all';
// Theme Management
function initTheme() {
const savedTheme = localStorage.getItem('theme') || 'dark';
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('themeIcon').textContent = theme === 'dark' ? '☀️' : '🌙';
}
// Tab Switching
function switchTab(tabName) {
// Update tab buttons
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`.tab-btn[data-tab="${tabName}"]`).classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${tabName}Tab`).classList.add('active');
}
// Task Filtering
function filterTasks(status) {
currentFilter = status;
// Update filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`.filter-btn[data-status="${status}"]`).classList.add('active');
renderTasks();
}
function renderTasks() {
const tasksGrid = document.getElementById('tasksGrid');
let filteredTasks = allTasks;
if (currentFilter !== 'all') {
filteredTasks = allTasks.filter(task => {
if (currentFilter === 'in-progress') {
return task.status === 'in-progress';
}
return task.result === currentFilter;
});
}
if (filteredTasks.length === 0) {
tasksGrid.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<p>No ${currentFilter === 'all' ? '' : currentFilter} tasks found</p>
</div>
`;
return;
}
tasksGrid.innerHTML = filteredTasks.map(task => {
const statusClass = task.status === 'completed' ? task.result : task.status;
const statusText = task.status === 'completed' ? task.result : task.status;
// Build dimension badge
const dimensionBadge = task.dimension ? `<span class="task-dimension-badge">${task.dimension}</span>` : '';
// Build file info
const fileInfo = task.file ? `
<div class="task-file">
📄 ${task.file}${task.line ? `:${task.line}` : ''}
</div>
` : '';
// Build attempts indicator
const attemptsInfo = task.attempts && task.attempts > 1 ? `
<div class="task-meta-item task-attempts">
<span>🔄</span>
<span>${task.attempts} ${task.attempts === 1 ? 'attempt' : 'attempts'}</span>
</div>
` : '';
// Build commit hash
const commitInfo = task.commit_hash ? `
<div class="task-meta-item">
<span>💾</span>
<span>${task.commit_hash.substring(0, 7)}</span>
</div>
` : '';
return `
<div class="task-card status-${statusClass}" onclick="openFindingDetails('${task.finding_id}')" style="cursor: pointer;">
<div class="task-header">
<div class="task-id">${task.finding_id}</div>
<div class="task-status ${statusClass}">${statusText}</div>
</div>
<div style="margin-bottom: 8px;">
${dimensionBadge}
</div>
<div class="task-title">${task.finding_title || 'Untitled Task'}</div>
${fileInfo}
<div class="task-meta">
<div class="task-meta-item">
<span>🏷️</span>
<span>${task.group_id || 'N/A'}</span>
</div>
${attemptsInfo}
${commitInfo}
${task.completed_at ? `
<div class="task-meta-item">
<span>⏱️</span>
<span>${new Date(task.completed_at).toLocaleTimeString()}</span>
</div>
` : ''}
</div>
</div>
`;
}).join('');
}
// Initialize dashboard
document.getElementById('sessionId').textContent = SESSION_ID;
initTheme();
detectFixSession();
function detectFixSession() {
fetch('./fixes/active-fix-session.json')
.then(response => {
if (!response.ok) throw new Error('No active fix session');
return response.json();
})
.then(data => {
fixSession = data;
document.getElementById('fixSessionId').textContent = data.fix_session_id;
startProgressPolling();
})
.catch(() => {
document.getElementById('fixSessionId').textContent = 'No active session';
showNoActiveSession();
});
}
function showNoActiveSession() {
const section = document.querySelector('.fix-progress-section');
section.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">💤</div>
<p>No active fix session</p>
</div>
`;
}
function startProgressPolling() {
loadFixProgress();
if (progressInterval) {
clearInterval(progressInterval);
}
progressInterval = setInterval(() => {
loadFixProgress();
}, 3000);
}
async function loadFixProgress() {
if (!fixSession) return;
// Show loading indicator
const spinner = document.getElementById('refreshSpinner');
const refreshText = document.getElementById('refreshText');
spinner.style.display = 'inline-block';
refreshText.textContent = 'Loading...';
try {
// Load fix-plan.json (includes metadata)
const planResponse = await fetch(`./fixes/${fixSession.fix_session_id}/fix-plan.json`);
if (!planResponse.ok) throw new Error('Fix plan not found');
const fixPlan = await planResponse.json();
// Load all progress files in parallel
const progressFiles = fixPlan.groups.map(g => g.progress_file);
const progressPromises = progressFiles.map(file =>
fetch(`./fixes/${fixSession.fix_session_id}/${file}`)
.then(r => r.ok ? r.json() : null)
.catch(() => null)
);
const progressDataArray = await Promise.all(progressPromises);
// Aggregate progress data
const progressData = aggregateProgressData(fixPlan, progressDataArray.filter(d => d !== null));
// Update UI
updateFixStatus(progressData);
// Stop polling if completed
if (progressData.status === 'completed' || progressData.status === 'failed') {
clearInterval(progressInterval);
progressInterval = null;
await loadFixHistory();
}
} catch (error) {
console.error('Error loading fix progress:', error);
} finally {
spinner.style.display = 'none';
refreshText.textContent = '🔄 Refresh';
}
}
function aggregateProgressData(fixPlan, progressDataArray) {
const allFindings = [];
const activeAgents = [];
let hasAnyInProgress = false;
let hasAnyFailed = false;
let allCompleted = true;
progressDataArray.forEach(progressFile => {
// Collect all findings with enhanced details
if (progressFile.findings) {
progressFile.findings.forEach(finding => {
allFindings.push({
finding_id: finding.finding_id,
finding_title: finding.finding_title,
status: finding.status,
result: finding.result,
group_id: progressFile.group_id,
completed_at: finding.completed_at,
// Enhanced fields
file: finding.file,
line: finding.line,
dimension: finding.dimension,
severity: finding.severity,
category: finding.category,
description: finding.description,
recommendations: finding.recommendations,
attempts: finding.attempts,
commit_hash: finding.commit_hash,
test_passed: finding.test_passed,
error_message: finding.error_message,
started_at: finding.started_at
});
});
}
// Collect active agents with errors
if (progressFile.assigned_agent && progressFile.status === 'in-progress') {
const currentFinding = progressFile.current_finding;
const flowControl = progressFile.flow_control;
activeAgents.push({
agent_id: progressFile.assigned_agent,
group_id: progressFile.group_id,
finding_id: currentFinding ? currentFinding.finding_id : null,
finding_title: currentFinding ? currentFinding.finding_title : 'Working...',
file: currentFinding ? currentFinding.file : null,
status: currentFinding ? currentFinding.status : progressFile.phase,
started_at: progressFile.started_at,
flow_control: flowControl,
errors: progressFile.errors || []
});
}
// 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 fixedCount = allFindings.filter(f => f.result === 'fixed').length;
const failedCount = allFindings.filter(f => f.result === 'failed').length;
const pendingCount = totalFindings - fixedCount - failedCount;
const percentComplete = totalFindings > 0 ? ((fixedCount + failedCount) / totalFindings) * 100 : 0;
// Determine overall status
let overallStatus = 'pending';
if (allCompleted) {
overallStatus = hasAnyFailed ? 'failed' : 'completed';
} else if (hasAnyInProgress) {
overallStatus = 'in_progress';
}
// Determine phase from metadata or infer
let phase = fixPlan.metadata?.status === 'planning' ? 'planning' : 'execution';
if (allCompleted) {
phase = 'completion';
}
// Build stages from timeline
const stages = fixPlan.timeline.stages.map(stage => {
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;
// Update global tasks array
allTasks = allFindings;
// Enhance groups with fix_strategy and risk_assessment from fixPlan
const enhancedGroups = progressDataArray.map(progressFile => {
const planGroup = fixPlan.groups?.find(g => g.group_id === progressFile.group_id);
return {
...progressFile,
fix_strategy: planGroup?.fix_strategy || null,
risk_assessment: planGroup?.risk_assessment || null
};
});
return {
fix_session_id: fixPlan.metadata?.fix_session_id || fixSession.fix_session_id,
review_id: fixPlan.metadata?.review_session_id || SESSION_ID,
status: overallStatus,
phase: phase,
start_time: startTime,
total_findings: totalFindings,
fixed_count: fixedCount,
failed_count: failedCount,
pending_count: pendingCount,
current_stage: currentStage,
total_stages: fixPlan.timeline.stages.length,
percent_complete: percentComplete,
fixes: allFindings,
active_agents: activeAgents,
stages: stages,
groups: enhancedGroups,
execution_strategy: fixPlan.execution_strategy || null
};
}
function updateFixStatus(progressData) {
const phaseBadge = document.getElementById('phaseBadge');
const planningPhase = document.getElementById('planningPhase');
const executionPhase = document.getElementById('executionPhase');
const stageTimeline = document.getElementById('stageTimeline');
// Update phase badge
const phase = progressData.phase || 'planning';
phaseBadge.textContent = phase.toUpperCase();
phaseBadge.className = `phase-badge phase-${phase}`;
// Update stage timeline
updateStageTimeline(progressData);
// Show appropriate phase UI
if (phase === 'planning') {
planningPhase.style.display = 'block';
executionPhase.style.display = 'none';
} else {
planningPhase.style.display = 'none';
executionPhase.style.display = 'block';
// Update progress bar
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
const percentComplete = progressData.percent_complete || 0;
progressFill.style.width = `${percentComplete}%`;
// Format progress text
let startTimeText = '';
if (progressData.start_time) {
const startTime = new Date(progressData.start_time);
const elapsed = Math.floor((Date.now() - startTime.getTime()) / 1000 / 60);
startTimeText = ` | Started ${startTime.toLocaleTimeString()} (${elapsed}m ago)`;
}
progressText.innerHTML = `
<strong>Stage ${progressData.current_stage}/${progressData.total_stages}</strong> |
${progressData.fixed_count + progressData.failed_count}/${progressData.total_findings} findings completed
(<strong>${percentComplete.toFixed(1)}%</strong>)${startTimeText}
`;
// Update active groups
updateActiveGroups(progressData);
// Update active agents
updateActiveAgents(progressData);
}
// Update summary cards
document.getElementById('totalFindings').textContent = progressData.total_findings;
document.getElementById('fixedCount').textContent = progressData.fixed_count;
document.getElementById('failedCount').textContent = progressData.failed_count;
document.getElementById('pendingCount').textContent = progressData.pending_count;
// Render tasks
renderTasks();
}
function updateStageTimeline(progressData) {
const stageTimeline = document.getElementById('stageTimeline');
const stages = progressData.stages || [];
if (stages.length === 0) {
stageTimeline.style.display = 'none';
return;
}
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 activeGroupsSection = document.getElementById('activeGroupsSection');
const activeGroupsGrid = document.getElementById('activeGroupsGrid');
const stages = progressData.stages || [];
const activeStage = stages.find(s => s.status === 'in-progress');
if (!activeStage || activeStage.groups.length === 0) {
activeGroupsGrid.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">✨</div>
<p>No active groups</p>
</div>
`;
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);
}
});
activeGroupsGrid.innerHTML = activeStage.groups.map(groupId => {
const fixes = groupFixes[groupId] || [];
const completedCount = fixes.filter(f => f.result === 'fixed' || f.result === 'failed').length;
// Get group details from enhanced groups
const groupData = progressData.groups?.find(g => g.group_id === groupId);
const fixStrategy = groupData?.fix_strategy;
const riskAssessment = groupData?.risk_assessment;
// Get in-progress findings
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>`
: '';
// Render strategy and risk info
const strategyRiskHtml = renderStrategyAndRisk(fixStrategy, riskAssessment);
return `
<div class="active-group-card">
<div class="group-header">
<div class="group-id">${groupId}</div>
<div class="group-status">${inProgressFindings.length > 0 ? 'In Progress' : 'Pending'}</div>
</div>
<div class="group-findings">${completedCount}/${fixes.length} findings completed</div>
${inProgressText}
${strategyRiskHtml}
</div>
`;
}).join('');
}
function renderStrategyAndRisk(fixStrategy, riskAssessment) {
if (!fixStrategy && !riskAssessment) return '';
let html = '<div class="strategy-risk-container">';
// Strategy info
if (fixStrategy) {
html += '<div class="strategy-item">';
html += '<span class="strategy-label">Strategy:</span>';
html += `<span>${fixStrategy.approach || 'N/A'}</span>`;
html += '</div>';
if (fixStrategy.complexity) {
html += '<div class="strategy-item">';
html += '<span class="strategy-label">Complexity:</span>';
html += `<span>${fixStrategy.complexity}</span>`;
html += '</div>';
}
if (fixStrategy.test_pattern) {
html += '<div class="strategy-item">';
html += '<span class="strategy-label">Test:</span>';
html += `<span style="font-family: monospace; font-size: 0.8rem;">${fixStrategy.test_pattern}</span>`;
html += '</div>';
}
}
// Risk info
if (riskAssessment) {
const riskLevel = riskAssessment.level || riskAssessment.overall || 'unknown';
const riskLevelClass = riskLevel.toLowerCase();
html += '<div class="risk-item">';
html += '<span class="risk-label">Risk:</span>';
html += `<span class="risk-level ${riskLevelClass}">${riskLevel.toUpperCase()}</span>`;
html += '</div>';
if (riskAssessment.impact_scope) {
html += '<div class="risk-item">';
html += '<span class="risk-label">Impact:</span>';
html += `<span>${riskAssessment.impact_scope}</span>`;
html += '</div>';
}
}
html += '</div>';
return html;
}
function updateActiveAgents(progressData) {
const activeAgentsSection = document.getElementById('activeAgentsSection');
const activeAgentsList = document.getElementById('activeAgentsList');
if (!progressData.active_agents || progressData.active_agents.length === 0) {
activeAgentsList.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">🤖</div>
<p>No active agents</p>
</div>
`;
return;
}
activeAgentsList.innerHTML = progressData.active_agents.map(agent => {
const statusIcon = getAgentStatusIcon(agent.status);
const statusText = agent.status ? agent.status.charAt(0).toUpperCase() + agent.status.slice(1).replace(/-/g, ' ') : '';
// Render flow control steps
const flowControlHtml = agent.flow_control && agent.flow_control.implementation_approach && agent.flow_control.implementation_approach.length > 0
? renderFlowControlSteps(agent.flow_control)
: '';
// Render errors if any
const errorsHtml = agent.errors && agent.errors.length > 0
? renderAgentErrors(agent.errors)
: '';
// Error badge count
const errorBadge = agent.errors && agent.errors.length > 0
? `<span class="error-badge">⚠️ ${agent.errors.length} error${agent.errors.length > 1 ? 's' : ''}</span>`
: '';
return `
<div class="active-agent-item">
<div class="agent-header">
<div class="agent-status-icon">${statusIcon}</div>
<div class="agent-info">
<div class="agent-id">
${agent.agent_id} <span style="color: var(--text-secondary);">(${agent.group_id})</span>
${statusText ? `<span class="agent-status-badge">${statusText}</span>` : ''}
${errorBadge}
</div>
<div class="agent-task">${agent.finding_title || 'Initializing...'}</div>
${agent.file ? `<div class="agent-file">📄 ${agent.file}</div>` : ''}
</div>
</div>
${flowControlHtml}
${errorsHtml}
</div>
`;
}).join('');
}
function getAgentStatusIcon(status) {
const icons = {
'analyzing': '🔍',
'fixing': '🔧',
'testing': '🧪',
'committing': '💾'
};
return icons[status] || '⚡';
}
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';
}
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 renderAgentErrors(errors) {
if (!errors || errors.length === 0) return '';
const errorsHtml = errors.slice(-3).map(error => {
const timestamp = error.timestamp ? new Date(error.timestamp).toLocaleTimeString() : '';
return `
<div class="error-item">
<div>${error.message || 'Unknown error'}</div>
<div class="error-meta">
${error.finding_id ? `Finding: ${error.finding_id}` : ''}
${error.error_type ? ` | Type: ${error.error_type}` : ''}
${timestamp ? ` | ${timestamp}` : ''}
</div>
</div>
`;
}).join('');
return `
<div class="error-container">
<div class="error-header">Recent Errors${errors.length > 3 ? ` (showing last 3 of ${errors.length})` : ''}:</div>
${errorsHtml}
</div>
`;
}
// 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('historyContent').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📜</div>
<p>No fix history available</p>
</div>
`;
}
}
function renderFixHistory(historyData) {
const historyContent = document.getElementById('historyContent');
if (!historyData.sessions || historyData.sessions.length === 0) {
historyContent.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📜</div>
<p>No fix history available</p>
</div>
`;
return;
}
historyContent.innerHTML = historyData.sessions.map(session => {
const statusClass = session.total_failed > 0 ? 'failed' : 'fixed';
const statusText = session.total_failed > 0 ? 'Partial' : 'Complete';
const completedAt = new Date(session.completed_at);
return `
<div class="history-item status-${statusClass}">
<div class="history-item-header">
<div class="history-item-time">${completedAt.toLocaleString()}</div>
<div class="history-item-status ${statusClass}">${statusText}</div>
</div>
<div style="font-weight: 700; margin-bottom: 8px; font-size: 1rem;">${session.fix_session_id}</div>
<div class="history-item-stats">
<span>✅ ${session.total_fixed} fixed</span>
<span>❌ ${session.total_failed} failed</span>
<span>📊 ${session.total_findings} total</span>
</div>
</div>
`;
}).join('');
}
// Finding Details Modal Functions
function openFindingDetails(findingId) {
const finding = allTasks.find(f => f.finding_id === findingId);
if (!finding) {
console.error('Finding not found:', findingId);
return;
}
// Set modal icon based on status
const icon = finding.result === 'fixed' ? '✅' : finding.result === 'failed' ? '❌' : '📋';
document.getElementById('modalFindingIcon').textContent = icon;
// Build modal content
const modalBody = document.getElementById('modalBody');
modalBody.innerHTML = `
<!-- Basic Information -->
<div class="detail-section">
<h3>📋 Basic Information</h3>
<div class="detail-grid">
<div class="detail-label">Finding ID:</div>
<div class="detail-value"><code>${finding.finding_id}</code></div>
<div class="detail-label">Title:</div>
<div class="detail-value">${finding.finding_title || 'N/A'}</div>
<div class="detail-label">Status:</div>
<div class="detail-value">
<span class="task-status ${finding.status === 'completed' ? finding.result : finding.status}">
${finding.status === 'completed' ? finding.result : finding.status}
</span>
</div>
<div class="detail-label">Group:</div>
<div class="detail-value">${finding.group_id || 'N/A'}</div>
${finding.dimension ? `
<div class="detail-label">Dimension:</div>
<div class="detail-value"><span class="task-dimension-badge">${finding.dimension}</span></div>
` : ''}
${finding.severity ? `
<div class="detail-label">Severity:</div>
<div class="detail-value">${finding.severity}</div>
` : ''}
${finding.category ? `
<div class="detail-label">Category:</div>
<div class="detail-value">${finding.category}</div>
` : ''}
</div>
</div>
<!-- File Location -->
${finding.file ? `
<div class="detail-section">
<h3>📄 File Location</h3>
<div class="detail-grid">
<div class="detail-label">File:</div>
<div class="detail-value"><code>${finding.file}</code></div>
${finding.line ? `
<div class="detail-label">Line:</div>
<div class="detail-value"><code>${finding.line}</code></div>
` : ''}
</div>
</div>
` : ''}
<!-- Description & Recommendations -->
${finding.description || finding.recommendations ? `
<div class="detail-section">
<h3>📝 Details</h3>
<div class="detail-grid">
${finding.description ? `
<div class="detail-label">Description:</div>
<div class="detail-value">${finding.description}</div>
` : ''}
${finding.recommendations ? `
<div class="detail-label">Recommendations:</div>
<div class="detail-value">${Array.isArray(finding.recommendations) ? finding.recommendations.join('<br>') : finding.recommendations}</div>
` : ''}
</div>
</div>
` : ''}
<!-- Fix Status -->
${finding.status === 'completed' || finding.attempts || finding.commit_hash ? `
<div class="detail-section">
<h3>🔧 Fix Status</h3>
<div class="detail-grid">
${finding.attempts ? `
<div class="detail-label">Attempts:</div>
<div class="detail-value ${finding.attempts > 1 ? 'task-attempts' : ''}">${finding.attempts}</div>
` : ''}
${finding.test_passed !== null && finding.test_passed !== undefined ? `
<div class="detail-label">Tests Passed:</div>
<div class="detail-value">${finding.test_passed ? '✅ Yes' : '❌ No'}</div>
` : ''}
${finding.commit_hash ? `
<div class="detail-label">Commit:</div>
<div class="detail-value"><code>${finding.commit_hash}</code></div>
` : ''}
${finding.started_at ? `
<div class="detail-label">Started At:</div>
<div class="detail-value">${new Date(finding.started_at).toLocaleString()}</div>
` : ''}
${finding.completed_at ? `
<div class="detail-label">Completed At:</div>
<div class="detail-value">${new Date(finding.completed_at).toLocaleString()}</div>
` : ''}
${finding.error_message ? `
<div class="detail-label">Error:</div>
<div class="detail-value" style="color: var(--danger-color);">${finding.error_message}</div>
` : ''}
</div>
</div>
` : ''}
`;
// Show modal
document.getElementById('findingModal').classList.add('active');
}
function closeFindingModal(event) {
if (event && event.target !== event.currentTarget) return;
document.getElementById('findingModal').classList.remove('active');
}
// Auto-refresh every 3 seconds
setInterval(() => {
if (fixSession && progressInterval) {
loadFixProgress();
}
}, 3000);
</script>
</body>
</html>