383 lines
15 KiB
HTML
383 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Firewall Stress Test Dashboard</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3"></script>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: 'SF Mono', 'Fira Code', monospace; background: #0d1117; color: #c9d1d9; }
|
|
.header {
|
|
background: #161b22; border-bottom: 1px solid #30363d; padding: 16px 24px;
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
}
|
|
.header h1 { font-size: 18px; color: #58a6ff; }
|
|
.status-badge {
|
|
padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
.status-active { background: #238636; color: #fff; }
|
|
.status-paused { background: #d29922; color: #000; }
|
|
.status-shutdown { background: #da3633; color: #fff; }
|
|
|
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding: 16px 24px; }
|
|
.grid-full { grid-column: 1 / -1; }
|
|
|
|
.card {
|
|
background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px;
|
|
}
|
|
.card h2 { font-size: 14px; color: #8b949e; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 1px; }
|
|
|
|
.stat-row { display: flex; gap: 24px; flex-wrap: wrap; }
|
|
.stat { text-align: center; }
|
|
.stat .value { font-size: 28px; font-weight: 700; color: #58a6ff; }
|
|
.stat .label { font-size: 11px; color: #8b949e; margin-top: 4px; }
|
|
|
|
.progress-bar {
|
|
width: 100%; height: 6px; background: #30363d; border-radius: 3px; margin-top: 8px;
|
|
}
|
|
.progress-fill { height: 100%; background: #58a6ff; border-radius: 3px; transition: width 1s; }
|
|
|
|
.event-list { max-height: 400px; overflow-y: auto; }
|
|
.event-item {
|
|
padding: 8px 0; border-bottom: 1px solid #21262d; font-size: 13px;
|
|
display: flex; gap: 12px; align-items: center;
|
|
}
|
|
.event-time { color: #8b949e; flex-shrink: 0; width: 80px; }
|
|
.event-type {
|
|
padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600;
|
|
flex-shrink: 0;
|
|
}
|
|
.event-type-reboot_start { background: #da363333; color: #f85149; }
|
|
.event-type-reboot_complete { background: #23863633; color: #3fb950; }
|
|
.event-type-port_down { background: #d2992233; color: #d29922; }
|
|
.event-type-port_up { background: #23863633; color: #3fb950; }
|
|
.event-type-reset_start { background: #58a6ff33; color: #58a6ff; }
|
|
.event-type-reset_complete { background: #58a6ff33; color: #58a6ff; }
|
|
.event-type-reset_settling { background: #58a6ff33; color: #58a6ff; }
|
|
.event-type-carp_mismatch { background: #da363333; color: #f85149; }
|
|
.event-type-error { background: #da363333; color: #f85149; }
|
|
.event-target { color: #c9d1d9; }
|
|
.event-details { color: #8b949e; font-size: 12px; }
|
|
|
|
.chart-container { position: relative; height: 250px; }
|
|
|
|
.fw-cards { display: flex; gap: 12px; flex-wrap: wrap; }
|
|
.fw-card {
|
|
flex: 1; min-width: 200px; background: #0d1117; border: 1px solid #30363d;
|
|
border-radius: 6px; padding: 12px;
|
|
}
|
|
.fw-card .ip { font-size: 14px; font-weight: 600; color: #c9d1d9; }
|
|
.fw-card .role { font-size: 11px; color: #8b949e; }
|
|
.fw-card .status-dot {
|
|
display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px;
|
|
}
|
|
.dot-up { background: #3fb950; }
|
|
.dot-down { background: #f85149; }
|
|
|
|
a.report-link {
|
|
display: inline-block; margin-top: 8px; padding: 8px 16px; background: #238636;
|
|
color: #fff; text-decoration: none; border-radius: 6px; font-size: 13px;
|
|
}
|
|
a.report-link:hover { background: #2ea043; }
|
|
|
|
@media (max-width: 768px) {
|
|
.grid { grid-template-columns: 1fr; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Firewall Stress Test</h1>
|
|
<div>
|
|
<span id="status-badge" class="status-badge status-active">Active</span>
|
|
<span id="elapsed" style="margin-left: 12px; color: #8b949e; font-size: 13px;">0h 0m</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid">
|
|
<!-- Summary stats -->
|
|
<div class="card grid-full">
|
|
<h2>Overview</h2>
|
|
<div class="stat-row">
|
|
<div class="stat">
|
|
<div class="value" id="stat-reboots">0</div>
|
|
<div class="label">Reboots</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="value" id="stat-flaps">0</div>
|
|
<div class="label">Port Flaps</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="value" id="stat-resets">0</div>
|
|
<div class="label">State Resets</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="value" id="stat-bandwidth">--</div>
|
|
<div class="label">Last Bandwidth (Mbps)</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="value" id="stat-loss">--</div>
|
|
<div class="label">Last Loss (%)</div>
|
|
</div>
|
|
</div>
|
|
<div class="progress-bar" style="margin-top: 16px;">
|
|
<div class="progress-fill" id="progress-fill" style="width: 0%;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bandwidth chart -->
|
|
<div class="card grid-full">
|
|
<h2>Bandwidth Over Time</h2>
|
|
<div class="chart-container">
|
|
<canvas id="bandwidth-chart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Firewall status -->
|
|
<div class="card">
|
|
<h2>Firewalls</h2>
|
|
<div class="fw-cards" id="firewall-cards">
|
|
<p style="color: #8b949e;">Loading...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Event timeline -->
|
|
<div class="card">
|
|
<h2>Event Timeline</h2>
|
|
<div class="event-list" id="event-list">
|
|
<p style="color: #8b949e;">Waiting for events...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Report -->
|
|
<div class="card grid-full">
|
|
<h2>Report</h2>
|
|
<p style="color: #8b949e; font-size: 13px;">Generate a full HTML report of the stress test results.</p>
|
|
<a href="/api/report" target="_blank" class="report-link">Generate Report</a>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Bandwidth chart
|
|
const ctx = document.getElementById('bandwidth-chart').getContext('2d');
|
|
const bwChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
datasets: [{
|
|
label: 'Bandwidth (Mbps)',
|
|
data: [],
|
|
borderColor: '#58a6ff',
|
|
backgroundColor: '#58a6ff22',
|
|
fill: true,
|
|
tension: 0.3,
|
|
pointRadius: 2,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: {
|
|
type: 'time',
|
|
time: { unit: 'minute', displayFormats: { minute: 'HH:mm' } },
|
|
grid: { color: '#21262d' },
|
|
ticks: { color: '#8b949e' },
|
|
},
|
|
y: {
|
|
grid: { color: '#21262d' },
|
|
ticks: { color: '#8b949e' },
|
|
title: { display: true, text: 'Mbps', color: '#8b949e' },
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: { display: false },
|
|
},
|
|
animation: { duration: 300 },
|
|
}
|
|
});
|
|
|
|
let eventListCleared = false;
|
|
let firewallReboots = {};
|
|
|
|
function formatElapsed(secs) {
|
|
const h = Math.floor(secs / 3600);
|
|
const m = Math.floor((secs % 3600) / 60);
|
|
return `${h}h ${m}m`;
|
|
}
|
|
|
|
function formatTime(ts) {
|
|
const d = new Date(ts);
|
|
return d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
}
|
|
|
|
function addEvent(event) {
|
|
const list = document.getElementById('event-list');
|
|
if (!eventListCleared) {
|
|
list.innerHTML = '';
|
|
eventListCleared = true;
|
|
}
|
|
|
|
const item = document.createElement('div');
|
|
item.className = 'event-item';
|
|
|
|
const time = document.createElement('span');
|
|
time.className = 'event-time';
|
|
time.textContent = formatTime(event.timestamp);
|
|
|
|
const type_ = document.createElement('span');
|
|
type_.className = `event-type event-type-${event.event_type}`;
|
|
type_.textContent = event.event_type.replace(/_/g, ' ');
|
|
|
|
const target = document.createElement('span');
|
|
target.className = 'event-target';
|
|
target.textContent = event.target;
|
|
|
|
item.appendChild(time);
|
|
item.appendChild(type_);
|
|
item.appendChild(target);
|
|
|
|
if (event.details) {
|
|
const details = document.createElement('span');
|
|
details.className = 'event-details';
|
|
try {
|
|
const d = JSON.parse(event.details);
|
|
details.textContent = Object.entries(d).map(([k,v]) => `${k}=${v}`).join(', ');
|
|
} catch { details.textContent = event.details; }
|
|
item.appendChild(details);
|
|
}
|
|
|
|
list.insertBefore(item, list.firstChild);
|
|
|
|
// Keep max 200 events in DOM
|
|
while (list.children.length > 200) {
|
|
list.removeChild(list.lastChild);
|
|
}
|
|
}
|
|
|
|
// Track last-seen IDs to avoid duplicates
|
|
let lastEventId = 0;
|
|
let lastMetricId = 0;
|
|
let knownBwTimestamps = new Set();
|
|
|
|
// Poll status every 3s
|
|
async function pollStatus() {
|
|
try {
|
|
const resp = await fetch('/api/status');
|
|
const status = await resp.json();
|
|
|
|
const badge = document.getElementById('status-badge');
|
|
badge.textContent = status.state;
|
|
badge.className = `status-badge status-${status.state}`;
|
|
|
|
document.getElementById('elapsed').textContent = formatElapsed(status.elapsed_secs);
|
|
|
|
const pct = Math.min(100, (status.elapsed_secs / status.total_duration_secs) * 100);
|
|
document.getElementById('progress-fill').style.width = pct + '%';
|
|
|
|
document.getElementById('stat-reboots').textContent = status.total_reboots;
|
|
document.getElementById('stat-flaps').textContent = status.total_port_flaps;
|
|
document.getElementById('stat-resets').textContent = status.total_resets;
|
|
|
|
const container = document.getElementById('firewall-cards');
|
|
const allFws = [...status.firewalls_up, ...status.firewalls_down];
|
|
if (allFws.length > 0) {
|
|
container.innerHTML = '';
|
|
for (const ip of status.firewalls_up) {
|
|
container.innerHTML += `<div class="fw-card"><span class="status-dot dot-up"></span><span class="ip">${ip}</span><div class="role">UP</div></div>`;
|
|
}
|
|
for (const ip of status.firewalls_down) {
|
|
container.innerHTML += `<div class="fw-card"><span class="status-dot dot-down"></span><span class="ip">${ip}</span><div class="role">DOWN</div></div>`;
|
|
}
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
// Poll events every 3s
|
|
async function pollEvents() {
|
|
try {
|
|
const resp = await fetch('/api/events?limit=20');
|
|
const events = await resp.json();
|
|
// Events come newest-first; process oldest-first for correct ordering
|
|
const newEvents = events.filter(e => e.id > lastEventId).reverse();
|
|
newEvents.forEach(e => {
|
|
addEvent(e);
|
|
lastEventId = Math.max(lastEventId, e.id);
|
|
});
|
|
} catch {}
|
|
}
|
|
|
|
// Poll metrics every 3s
|
|
async function pollMetrics() {
|
|
try {
|
|
const resp = await fetch('/api/metrics?limit=20');
|
|
const metrics = await resp.json();
|
|
const newMetrics = metrics.filter(m => m.id > lastMetricId);
|
|
newMetrics.forEach(m => {
|
|
lastMetricId = Math.max(lastMetricId, m.id);
|
|
|
|
if (m.metric_type === 'iperf_bandwidth') {
|
|
document.getElementById('stat-bandwidth').textContent = m.value.toFixed(1);
|
|
const ts = m.timestamp;
|
|
if (!knownBwTimestamps.has(ts)) {
|
|
knownBwTimestamps.add(ts);
|
|
bwChart.data.datasets[0].data.push({ x: new Date(ts), y: m.value });
|
|
if (bwChart.data.datasets[0].data.length > 500) {
|
|
bwChart.data.datasets[0].data.shift();
|
|
}
|
|
}
|
|
}
|
|
if (m.metric_type === 'iperf_loss') {
|
|
document.getElementById('stat-loss').textContent = m.value.toFixed(2);
|
|
}
|
|
});
|
|
if (newMetrics.some(m => m.metric_type === 'iperf_bandwidth')) {
|
|
bwChart.update('quiet');
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
// Load initial data
|
|
async function loadInitialData() {
|
|
try {
|
|
const resp = await fetch('/api/events?limit=50');
|
|
const events = await resp.json();
|
|
events.reverse().forEach(e => {
|
|
addEvent(e);
|
|
lastEventId = Math.max(lastEventId, e.id);
|
|
});
|
|
} catch {}
|
|
|
|
try {
|
|
const resp = await fetch('/api/metrics?metric_type=iperf_bandwidth&limit=200');
|
|
const metrics = await resp.json();
|
|
metrics.reverse().forEach(m => {
|
|
lastMetricId = Math.max(lastMetricId, m.id);
|
|
knownBwTimestamps.add(m.timestamp);
|
|
bwChart.data.datasets[0].data.push({ x: new Date(m.timestamp), y: m.value });
|
|
});
|
|
bwChart.update();
|
|
} catch {}
|
|
|
|
// Also get latest loss value
|
|
try {
|
|
const resp = await fetch('/api/metrics?metric_type=iperf_loss&limit=1');
|
|
const metrics = await resp.json();
|
|
if (metrics.length > 0) {
|
|
document.getElementById('stat-loss').textContent = metrics[0].value.toFixed(2);
|
|
lastMetricId = Math.max(lastMetricId, metrics[0].id);
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
loadInitialData();
|
|
pollStatus();
|
|
setInterval(pollStatus, 3000);
|
|
setInterval(pollEvents, 3000);
|
|
setInterval(pollMetrics, 3000);
|
|
</script>
|
|
</body>
|
|
</html>
|