This commit is contained in:
2025-12-27 01:13:30 +01:00
commit aa1f5e30d9
14 changed files with 1500 additions and 0 deletions

View File

@@ -0,0 +1,407 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TestArena | Modern Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
:root {
--primary: #6366f1;
--primary-glow: rgba(99, 102, 241, 0.5);
--secondary: #ec4899;
--accent: #8b5cf6;
--bg: #0f172a;
--card-bg: rgba(30, 41, 59, 0.7);
--text: #f8fafc;
--text-muted: #94a3b8;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--glass: rgba(255, 255, 255, 0.05);
--glass-border: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Outfit', sans-serif;
background: radial-gradient(circle at top right, #1e1b4b, #0f172a);
color: var(--text);
min-height: 100vh;
padding: 2rem;
overflow-x: hidden;
}
.container {
max-width: 1200px;
margin: 0 auto;
position: relative;
}
/* Decorative blobs */
.blob {
position: absolute;
width: 300px;
height: 300px;
background: var(--primary-glow);
filter: blur(100px);
border-radius: 50%;
z-index: -1;
animation: move 20s infinite alternate;
}
@keyframes move {
from {
transform: translate(-10%, -10%);
}
to {
transform: translate(20%, 20%);
}
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 3rem;
padding: 1.5rem;
background: var(--glass);
backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
border-radius: 1.5rem;
}
.logo {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.75rem;
font-weight: 700;
background: linear-gradient(to right, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.nav-links {
display: flex;
gap: 1.5rem;
}
.nav-links a {
color: var(--text);
text-decoration: none;
font-weight: 600;
transition: color 0.3s;
}
.nav-links a:hover {
color: var(--primary);
}
.status-badge {
padding: 0.5rem 1rem;
border-radius: 1rem;
font-size: 0.875rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--glass);
border: 1px solid var(--glass-border);
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--warning);
box-shadow: 0 0 10px var(--warning);
}
.dot.online {
background: var(--success);
box-shadow: 0 0 10px var(--success);
}
.grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2rem;
}
.card {
background: var(--card-bg);
backdrop-filter: blur(16px);
border: 1px solid var(--glass-border);
border-radius: 1.5rem;
padding: 2rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
h2 {
font-size: 1.25rem;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0 0.75rem;
}
th {
text-align: left;
color: var(--text-muted);
font-weight: 600;
padding: 0 1rem;
font-size: 0.875rem;
}
td {
padding: 1rem;
background: rgba(255, 255, 255, 0.03);
}
td:first-child {
border-radius: 1rem 0 0 1rem;
}
td:last-child {
border-radius: 0 1rem 1rem 0;
}
.status-pill {
padding: 0.25rem 0.75rem;
border-radius: 0.75rem;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
.status-waiting {
background: rgba(148, 163, 184, 0.1);
color: #94a3b8;
}
.status-running {
background: rgba(99, 102, 241, 0.1);
color: #818cf8;
border: 1px solid rgba(99, 102, 241, 0.3);
}
.status-finished {
background: rgba(16, 185, 129, 0.1);
color: #34d399;
}
.status-aborted {
background: rgba(239, 68, 68, 0.1);
color: #f87171;
}
.btn-abort {
background: rgba(239, 68, 68, 0.1);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.2);
padding: 0.4rem 0.8rem;
border-radius: 0.75rem;
cursor: pointer;
font-weight: 600;
transition: all 0.3s;
}
.btn-abort:hover {
background: var(--danger);
color: white;
}
.log-container {
background: #020617;
border-radius: 1rem;
padding: 1.25rem;
height: 400px;
overflow-y: auto;
font-family: 'Fira Code', monospace;
font-size: 0.8125rem;
line-height: 1.6;
border: 1px solid var(--glass-border);
}
.log-entry {
margin-bottom: 0.5rem;
display: flex;
gap: 0.75rem;
}
.log-time {
color: var(--primary);
opacity: 0.7;
}
.log-msg {
color: #cbd5e1;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--glass-border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
</style>
</head>
<body>
<div class="blob"></div>
<div class="container">
<header>
<div class="logo">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
TestArena
</div>
<nav class="nav-links">
<a href="/">Dashboard</a>
<a href="/results/" target="_blank">Browse Results</a>
</nav>
<div id="connection-status" class="status-badge">
<div class="dot"></div>
<span>Connecting...</span>
</div>
</header>
<div class="grid">
<div class="card">
<h2>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<line x1="3" y1="9" x2="21" y2="9" />
<line x1="9" y1="21" x2="9" y2="9" />
</svg>
Queue Monitor
</h2>
<table id="queue-table">
<thead>
<tr>
<th>Queue ID</th>
<th>Environment</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Dynamic content -->
</tbody>
</table>
</div>
<div class="card">
<h2>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
Live System Logs
</h2>
<div id="logs" class="log-container">
<div class="log-entry">
<span class="log-time">23:34:52</span>
<span class="log-msg">System initialized. Waiting for connection...</span>
</div>
</div>
</div>
</div>
</div>
<script>
async function fetchStatus() {
try {
const response = await fetch('/api/queues');
const queues = await response.json();
const tbody = document.querySelector('#queue-table tbody');
tbody.innerHTML = '';
queues.forEach(q => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td style="font-weight: 600;">${q.id}</td>
<td><span style="opacity: 0.8;">${q.environment}</span></td>
<td><span class="status-pill status-${q.status.toLowerCase()}">${q.status}</span></td>
<td>
<button class="btn-abort" onclick="abortQueue('${q.id}')">Abort</button>
</td>
`;
tbody.appendChild(tr);
});
const badge = document.getElementById('connection-status');
badge.querySelector('.dot').classList.add('online');
badge.querySelector('span').textContent = 'System Online';
} catch (e) {
const badge = document.getElementById('connection-status');
badge.querySelector('.dot').classList.remove('online');
badge.querySelector('span').textContent = 'Connection Lost';
}
}
async function abortQueue(id) {
if (confirm(`Are you sure you want to abort queue ${id}?`)) {
try {
await fetch(`/api/abort/${id}`, { method: 'POST' });
addLog(`Aborted queue: ${id}`, 'danger');
fetchStatus();
} catch (e) {
addLog(`Failed to abort queue: ${id}`, 'danger');
}
}
}
function addLog(msg, type = 'info') {
const logs = document.getElementById('logs');
const entry = document.createElement('div');
entry.className = 'log-entry';
const time = new Date().toLocaleTimeString([], { hour12: false });
entry.innerHTML = `
<span class="log-time">${time}</span>
<span class="log-msg">${msg}</span>
`;
logs.appendChild(entry);
logs.scrollTop = logs.scrollHeight;
}
// Initial fetch and poll
fetchStatus();
setInterval(fetchStatus, 3000);
// Simulate some system logs
setTimeout(() => addLog("Database connection established."), 1000);
setTimeout(() => addLog("Background worker is polling for tasks..."), 2000);
</script>
</body>
</html>