botui/ui/suite/tools/compliance.html

1038 lines
37 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Compliance Report - General Bots Suite</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/ws.js"></script>
<link rel="stylesheet" href="../css/app.css">
<style>
:root {
--bg-primary: #0a0a0f;
--bg-secondary: #12121a;
--bg-tertiary: #1a1a25;
--bg-card: #15151f;
--text-primary: #e8e8f0;
--text-secondary: #9898a8;
--text-muted: #606070;
--accent-blue: #3b82f6;
--accent-green: #10b981;
--accent-yellow: #f59e0b;
--accent-red: #ef4444;
--accent-purple: #8b5cf6;
--border-color: #2a2a3a;
--severity-critical: #dc2626;
--severity-high: #ea580c;
--severity-medium: #d97706;
--severity-low: #65a30d;
--severity-info: #0891b2;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
}
.page-container {
max-width: 1400px;
margin: 0 auto;
padding: 24px;
}
/* Header */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 1px solid var(--border-color);
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.back-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.back-btn:hover {
background: var(--bg-card);
color: var(--text-primary);
}
.back-btn svg {
width: 20px;
height: 20px;
}
.page-title {
font-size: 28px;
font-weight: 700;
}
.page-subtitle {
color: var(--text-secondary);
font-size: 14px;
margin-top: 4px;
}
.header-actions {
display: flex;
gap: 12px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: 1px solid var(--border-color);
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn:hover {
background: var(--bg-card);
}
.btn-primary {
background: var(--accent-blue);
border-color: var(--accent-blue);
color: white;
}
.btn-primary:hover {
background: #2563eb;
}
.btn svg {
width: 18px;
height: 18px;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
margin-bottom: 32px;
}
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
}
.stat-card.critical::before { background: var(--severity-critical); }
.stat-card.high::before { background: var(--severity-high); }
.stat-card.medium::before { background: var(--severity-medium); }
.stat-card.low::before { background: var(--severity-low); }
.stat-card.info::before { background: var(--severity-info); }
.stat-label {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 32px;
font-weight: 700;
}
.stat-card.critical .stat-value { color: var(--severity-critical); }
.stat-card.high .stat-value { color: var(--severity-high); }
.stat-card.medium .stat-value { color: var(--severity-medium); }
.stat-card.low .stat-value { color: var(--severity-low); }
.stat-card.info .stat-value { color: var(--severity-info); }
.stat-change {
font-size: 12px;
color: var(--text-muted);
margin-top: 8px;
}
/* Filter Bar */
.filter-bar {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding: 16px;
background: var(--bg-secondary);
border-radius: 12px;
border: 1px solid var(--border-color);
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-label {
font-size: 13px;
color: var(--text-secondary);
}
.filter-select {
padding: 8px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 13px;
cursor: pointer;
}
.filter-input {
padding: 8px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 13px;
width: 200px;
}
.filter-input:focus {
outline: none;
border-color: var(--accent-blue);
}
.filter-spacer {
flex: 1;
}
/* Results Table */
.results-container {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
}
.results-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
}
.results-title {
font-size: 16px;
font-weight: 600;
}
.results-count {
font-size: 13px;
color: var(--text-secondary);
}
.results-table {
width: 100%;
border-collapse: collapse;
}
.results-table th {
text-align: left;
padding: 12px 20px;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
}
.results-table td {
padding: 16px 20px;
font-size: 14px;
border-bottom: 1px solid var(--border-color);
vertical-align: top;
}
.results-table tr:last-child td {
border-bottom: none;
}
.results-table tr:hover {
background: var(--bg-tertiary);
}
.severity-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 16px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.severity-badge.critical {
background: rgba(220, 38, 38, 0.15);
color: var(--severity-critical);
}
.severity-badge.high {
background: rgba(234, 88, 12, 0.15);
color: var(--severity-high);
}
.severity-badge.medium {
background: rgba(217, 119, 6, 0.15);
color: var(--severity-medium);
}
.severity-badge.low {
background: rgba(101, 163, 13, 0.15);
color: var(--severity-low);
}
.severity-badge.info {
background: rgba(8, 145, 178, 0.15);
color: var(--severity-info);
}
.severity-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.severity-badge.critical .severity-dot { background: var(--severity-critical); }
.severity-badge.high .severity-dot { background: var(--severity-high); }
.severity-badge.medium .severity-dot { background: var(--severity-medium); }
.severity-badge.low .severity-dot { background: var(--severity-low); }
.severity-badge.info .severity-dot { background: var(--severity-info); }
.issue-type {
display: flex;
align-items: center;
gap: 10px;
}
.issue-icon {
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.issue-icon svg {
width: 18px;
height: 18px;
}
.issue-icon.password {
background: rgba(239, 68, 68, 0.15);
color: var(--accent-red);
}
.issue-icon.security {
background: rgba(249, 115, 22, 0.15);
color: var(--accent-yellow);
}
.issue-icon.deprecated {
background: rgba(139, 92, 246, 0.15);
color: var(--accent-purple);
}
.issue-icon.code {
background: rgba(59, 130, 246, 0.15);
color: var(--accent-blue);
}
.issue-icon.config {
background: rgba(16, 185, 129, 0.15);
color: var(--accent-green);
}
.issue-title {
font-weight: 500;
margin-bottom: 2px;
}
.issue-category {
font-size: 12px;
color: var(--text-muted);
}
.file-path {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 13px;
color: var(--text-secondary);
background: var(--bg-tertiary);
padding: 4px 8px;
border-radius: 4px;
}
.file-line {
font-size: 12px;
color: var(--text-muted);
margin-top: 4px;
}
.issue-description {
color: var(--text-secondary);
max-width: 400px;
}
.code-snippet {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 12px;
background: var(--bg-primary);
padding: 8px 12px;
border-radius: 6px;
margin-top: 8px;
border: 1px solid var(--border-color);
color: var(--accent-red);
white-space: pre-wrap;
word-break: break-all;
}
.action-btn {
padding: 6px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover {
background: var(--bg-card);
}
/* Progress Indicator */
.scan-progress {
display: none;
flex-direction: column;
align-items: center;
padding: 60px 24px;
}
.scan-progress.active {
display: flex;
}
.progress-spinner {
width: 48px;
height: 48px;
border: 3px solid var(--border-color);
border-top-color: var(--accent-blue);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.progress-text {
font-size: 16px;
color: var(--text-primary);
margin-bottom: 8px;
}
.progress-detail {
font-size: 13px;
color: var(--text-secondary);
}
/* Empty State */
.empty-state {
text-align: center;
padding: 80px 24px;
}
.empty-state svg {
width: 64px;
height: 64px;
color: var(--text-muted);
margin-bottom: 20px;
}
.empty-state-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
}
.empty-state-text {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 24px;
}
/* Bot Selector */
.bot-selector {
display: flex;
gap: 12px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.bot-chip {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 24px;
cursor: pointer;
transition: all 0.2s;
}
.bot-chip:hover {
background: var(--bg-tertiary);
}
.bot-chip.selected {
background: var(--accent-blue);
border-color: var(--accent-blue);
}
.bot-chip-icon {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--bg-tertiary);
display: flex;
align-items: center;
justify-content: center;
}
.bot-chip.selected .bot-chip-icon {
background: rgba(255,255,255,0.2);
}
.bot-chip-icon svg {
width: 14px;
height: 14px;
}
.bot-chip-name {
font-size: 14px;
font-weight: 500;
}
.bot-chip-count {
font-size: 12px;
padding: 2px 8px;
background: var(--bg-tertiary);
border-radius: 10px;
color: var(--text-secondary);
}
.bot-chip.selected .bot-chip-count {
background: rgba(255,255,255,0.2);
color: white;
}
/* HTMX Indicators */
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline-flex;
}
.htmx-request.htmx-indicator {
display: inline-flex;
}
/* Responsive */
@media (max-width: 1200px) {
.stats-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.filter-bar {
flex-wrap: wrap;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.results-table {
display: block;
overflow-x: auto;
}
}
</style>
</head>
<body>
<div class="page-container">
<!-- Header -->
<header class="page-header">
<div class="header-left">
<a href="/suite/tools/" class="back-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"/>
</svg>
</a>
<div>
<h1 class="page-title">API Compliance Report</h1>
<p class="page-subtitle">Security scan for all bots - Check for passwords, fragile</div> code, and misconfigurations</p>
</div>
</div>
<div class="header-actions">
<button class="btn"
hx-get="/api/v1/compliance/export"
hx-swap="none">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
Export Report
</button>
<button class="btn btn-primary" id="scan-btn"
hx-post="/api/v1/compliance/scan"
hx-target="#scan-results"
hx-indicator="#scan-progress"
hx-include="[name='bots']">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
Run Compliance Scan
</button>
</div>
</header>
<!-- Bot Selector -->
<div class="bot-selector" id="bot-selector"
hx-get="/api/v1/compliance/bots"
hx-trigger="load"
hx-swap="innerHTML">
<div class="bot-chip selected">
<input type="checkbox" name="bots" value="all" checked style="display: none;">
<div class="bot-chip-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7"/>
<rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/>
</svg>
</div>
<span class="bot-chip-name">All Bots</span>
<span class="bot-chip-count">12</span>
</div>
</div>
<!-- Stats Grid -->
<div class="stats-grid" id="stats-grid">
<div class="stat-card critical">
<div class="stat-label">Critical</div>
<div class="stat-value" id="stat-critical">0</div>
<div class="stat-change">Requires immediate action</div>
</div>
<div class="stat-card high">
<div class="stat-label">High</div>
<div class="stat-value" id="stat-high">0</div>
<div class="stat-change">Security risk</div>
</div>
<div class="stat-card medium">
<div class="stat-label">Medium</div>
<div class="stat-value" id="stat-medium">0</div>
<div class="stat-change">Should be addressed</div>
</div>
<div class="stat-card low">
<div class="stat-label">Low</div>
<div class="stat-value" id="stat-low">0</div>
<div class="stat-change">Best practice</div>
</div>
<div class="stat-card info">
<div class="stat-label">Info</div>
<div class="stat-value" id="stat-info">0</div>
<div class="stat-change">Informational</div>
</div>
</div>
<!-- Filter Bar -->
<div class="filter-bar">
<div class="filter-group">
<span class="filter-label">Severity:</span>
<select class="filter-select" id="filter-severity"
hx-get="/api/v1/compliance/results"
hx-target="#results-body"
hx-include="[name='filter-type'], [name='filter-search']">
<option value="all">All Severities</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
<option value="info">Info</option>
</select>
</div>
<div class="filter-group">
<span class="filter-label">Type:</span>
<select class="filter-select" id="filter-type" name="filter-type"
hx-get="/api/v1/compliance/results"
hx-target="#results-body"
hx-include="[name='filter-severity'], [name='filter-search']">
<option value="all">All Types</option>
<option value="password">Password in Config</option>
<option value="hardcoded">Hardcoded Secrets</option>
<option value="deprecated">Deprecated Keywords</option>
<option value="fragile">Fragile Code</option>
<option value="config">Configuration Issues</option>
</select>
</div>
<div class="filter-spacer"></div>
<div class="filter-group">
<input type="text" class="filter-input" placeholder="Search issues..." name="filter-search"
hx-get="/api/v1/compliance/results"
hx-target="#results-body"
hx-trigger="keyup changed delay:300ms"
hx-include="[name='filter-severity'], [name='filter-type']">
</div>
</div>
<!-- Results Container -->
<div class="results-container">
<div class="results-header">
<span class="results-title">Compliance Issues</span>
<span class="results-count" id="results-count">0 issues found</span>
</div>
<!-- Scan Progress -->
<div class="scan-progress htmx-indicator" id="scan-progress">
<div class="progress-spinner"></div>
<div class="progress-text">Scanning all .bas files...</div>
<div class="progress-detail">Checking for security issues and misconfigurations</div>
</div>
<!-- Scan Results -->
<div id="scan-results">
<table class="results-table">
<thead>
<tr>
<th>Severity</th>
<th>Issue Type</th>
<th>Location</th>
<th>Description</th>
<th>Action</th>
</tr>
</thead>
<tbody id="results-body">
<!-- Example rows - will be populated by HTMX -->
<tr>
<td>
<span class="severity-badge critical">
<span class="severity-dot"></span>
Critical
</span>
</td>
<td>
<div class="issue-type">
<div class="issue-icon password">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
</div>
<div>
<div class="issue-title">Password in Config</div>
<div class="issue-category">Security</div>
</div>
</div>
</td>
<td>
<div class="file-path">marketing.gbai</div>/poster.bas</div>
<div class="file-line">Line 12</div>
</td>
<td>
<div class="issue-description">
Hardcoded password found in BASIC file. Move to Vault.
<div class="code-snippet">POST TO INSTAGRAM username, password, image</div>
</div>
</td>
<td>
<button class="action-btn"
hx-get="/api/v1/compliance/fix/1"
hx-target="closest tr"
hx-swap="outerHTML">
Fix
</button>
</td>
</tr>
<tr>
<td>
<span class="severity-badge high">
<span class="severity-dot"></span>
High
</span>
</td>
<td>
<div class="issue-type">
<div class="issue-icon security">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
</div>
<div>
<div class="issue-title">Hardcoded Secret</div>
<div class="issue-category">Security</div>
</div>
</div>
</td>
<td>
<div class="file-path">api-client.gbai/msft-partner.bas</div>
<div class="file-line">Line 7</div>
</td>
<td>
<div class="issue-description">
Client secret found in source code. Use environment variables.
<div class="code-snippet">client_secret = "abc123..."</div>
</div>
</td>
<td>
<button class="action-btn">Fix</button>
</td>
</tr>
<tr>
<td>
<span class="severity-badge medium">
<span class="severity-dot"></span>
Medium
</span>
</td>
<td>
<div class="issue-type">
<div class="issue-icon deprecated">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
</div>
<div>
<div class="issue-title">Deprecated Keyword</div>
<div class="issue-category">Code Quality</div>
</div>
</div>
</td>
<td>
<div class="file-path">default.gbai/start.bas</div>
<div class="file-line">Line 45</div>
</td>
<td>
<div class="issue-description">
Using deprecated IF...input pattern. Use HEAR AS instead.
<div class="code-snippet">IF input = "yes" THEN</div>
</div>
</td>
<td>
<button class="action-btn">Fix</button>
</td>
</tr>
<tr>
<td>
<span class="severity-badge low">
<span class="severity-dot"></span>
Low
</span>
</td>
<td>
<div class="issue-type">
<div class="issue-icon code">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="16 18 22 12 16 6"/>
<polyline points="8 6 2 12 8 18"/>
</svg>
</div>
<div>
<div class="issue-title">Underscore in Keyword</div>
<div class="issue-category">Naming Convention</div>
</div>
</div>
</td>
<td>
<div class="file-path">crm.gbai/contacts.bas</div>
<div class="file-line">Line 23</div>
</td>
<td>
<div class="issue-description">
Keywords should use spaces not underscores.
<div class="code-snippet">GET_BOT_MEMORY → GET BOT MEMORY</div>
</div>
</td>
<td>
<button class="action-btn">Fix</button>
</td>
</tr>
<tr>
<td>
<span class="severity-badge info">
<span class="severity-dot"></span>
Info
</span>
</td>
<td>
<div class="issue-type">
<div class="issue-icon config">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
</div>
<div>
<div class="issue-title">Missing Vault Config</div>
<div class="issue-category">Configuration</div>
</div>
</div>
</td>
<td>
<div class="file-path">bank.gbai/config.csv</div>
<div class="file-line">-</div>
</td>
<td>
<div class="issue-description">
Bot is not configured to use Vault for secrets management. Consider enabling for better security.
</div>
</td>
<td>
<button class="action-btn">Configure</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
// Bot chip selection
document.addEventListener('click', (e) => {
const chip = e.target.closest('.bot-chip');
if (chip) {
// Toggle selection
chip.classList.toggle('selected');
// Update hidden checkbox
const checkbox = chip.querySelector('input[type="checkbox"]');
if (checkbox) {
checkbox.checked = chip.classList.contains('selected');
}
}
});
// Update stats after scan
document.body.addEventListener('htmx:afterSwap', (e) => {
if (e.detail.target.id === 'scan-results') {
// Update stats from response headers or parse results
updateStats();
}
});
function updateStats() {
// Count issues by severity from the results table
const rows = document.querySelectorAll('#results-body tr');
let stats = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
rows.forEach(row => {
const badge = row.querySelector('.severity-badge');
if (badge) {
if (badge.classList.contains('critical')) stats.critical++;
else if (badge.classList.contains('high')) stats.high++;
else if (badge.classList.contains('medium')) stats.medium++;
else if (badge.classList.contains('low')) stats.low++;
else if (badge.classList.contains('info')) stats.info++;
}
});
document.getElementById('stat-critical').textContent = stats.critical;
document.getElementById('stat-high').textContent = stats.high;
document.getElementById('stat-medium').textContent = stats.medium;
document.getElementById('stat-low').textContent = stats.low;
document.getElementById('stat-info').textContent = stats.info;
const total = stats.critical + stats.high + stats.medium + stats.low + stats.info;
document.getElementById('results-count').textContent = `${total} issues found`;
}
// Initial stats calculation
document.addEventListener('DOMContentLoaded', () => {
updateStats();
});
// Keyboard shortcut to run scan
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
document.getElementById('scan-btn').click();
}
});
</script>
</body>
</html>