Skip to content

Commit ccf3d59

Browse files
feat(web): implement "Run All" button for executing examples sequentially
- Added a "Run All" button to the UI, allowing users to execute all examples in sequence. - Introduced a summary table to display results for each example, including status and execution time. - Enhanced the loading of examples and improved the user experience with visual feedback during execution. This feature streamlines the testing process and provides a comprehensive overview of results.
1 parent cee6376 commit ccf3d59

2 files changed

Lines changed: 92 additions & 1 deletion

File tree

hiddify-health

16.1 KB
Binary file not shown.

internal/web/static/index.html

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,21 @@
5757
.history-section{margin-top:16px}
5858
.history-row{font-size:.75rem;padding:4px 0;border-bottom:1px solid #1a202c;display:flex;gap:8px;align-items:center}
5959
.welcome{color:#4a5568;padding:40px;text-align:center;font-size:.9rem}
60+
#btn-run-all{width:calc(100% - 32px);margin:10px 16px;background:#065f46;color:#fff}
61+
#btn-run-all:hover{background:#047857}
62+
#btn-run-all:disabled{background:#374151;color:#6b7280;cursor:not-allowed}
63+
#summary-table{width:100%;border-collapse:collapse;font-size:.82rem}
64+
#summary-table th,#summary-table td{padding:7px 10px;text-align:left;border-bottom:1px solid #1a202c}
65+
#summary-table th{color:#718096;font-size:.72rem;text-transform:uppercase;position:sticky;top:0;background:#0d1117}
66+
#summary-table tbody tr{cursor:pointer;transition:background .15s}
67+
#summary-table tbody tr:hover{background:#1e2533}
6068
</style>
6169
</head>
6270
<body>
6371

6472
<div id="sidebar">
6573
<h1>🛡 Hiddify Health</h1>
74+
<button id="btn-run-all">▶ Run All</button>
6675
<div id="examples"><div class="welcome">Loading…</div></div>
6776
</div>
6877

@@ -91,10 +100,12 @@ <h2 id="title">Select an example</h2>
91100
<script>
92101
let selectedDir = null;
93102
let es = null;
103+
let examplesList = [];
94104

95105
async function loadExamples() {
96106
const res = await fetch('/api/examples');
97107
const items = await res.json() || [];
108+
examplesList = items;
98109
const el = document.getElementById('examples');
99110
if (!items.length) { el.innerHTML='<div class="welcome">No examples found.<br>Add run.json files to the examples/ directory.</div>'; return; }
100111
el.innerHTML = '';
@@ -282,7 +293,7 @@ <h2 id="title">Select an example</h2>
282293
section.className = 'history-section';
283294
let html = '<h3 style="font-size:.8rem;margin-bottom:6px">History</h3>';
284295
recs.slice(0,10).forEach(r => {
285-
const d = new Date(r.StartedAt * 1000).toLocaleString();
296+
const d = new Date(r.StartedAt).toLocaleString();
286297
const vLabel = r.Variant ? `<span style="color:#a78bfa;font-size:.7rem">${r.Variant}</span> ` : '';
287298
html += `<div class="history-row">
288299
<span class="${r.Pass?'check-ok':'check-fail'}">${r.Pass?'✓':'✗'}</span>
@@ -294,6 +305,86 @@ <h2 id="title">Select an example</h2>
294305
panel.appendChild(section);
295306
}
296307

308+
// --- Run All: run every example sequentially, show summary table ---
309+
const CHECKS_ORDER = ['dns','http','quic','download','upload','ping'];
310+
let runAllActive = false;
311+
312+
document.getElementById('btn-run-all').onclick = runAll;
313+
314+
async function runAll() {
315+
if (runAllActive || !examplesList.length) return;
316+
runAllActive = true;
317+
const btnAll = document.getElementById('btn-run-all');
318+
btnAll.disabled = true;
319+
btnAll.textContent = '⏳ Running all…';
320+
document.getElementById('btn-run').disabled = true;
321+
document.getElementById('title').textContent = 'Run All — summary';
322+
document.getElementById('deploy-row').style.display = 'none';
323+
document.getElementById('mode-badge').style.display = 'none';
324+
document.querySelectorAll('.example').forEach(e => e.classList.remove('active'));
325+
selectedDir = null;
326+
327+
const log = document.getElementById('log-panel');
328+
log.innerHTML = `<table id="summary-table"><thead><tr>
329+
<th>Example</th><th>Variant</th>${CHECKS_ORDER.map(c=>`<th>${c}</th>`).join('')}
330+
<th>Censor</th><th>Time</th><th>Status</th>
331+
</tr></thead><tbody></tbody></table>`;
332+
document.getElementById('checks-list').innerHTML = '<h3>Results</h3>';
333+
const tbody = log.querySelector('tbody');
334+
335+
for (const item of examplesList) {
336+
const phRow = document.createElement('tr');
337+
phRow.innerHTML = `<td>${item.name}</td><td colspan="${CHECKS_ORDER.length+4}"><span class="badge running">RUNNING</span></td>`;
338+
tbody.appendChild(phRow);
339+
await runOneCollect(item, tbody, phRow);
340+
}
341+
342+
runAllActive = false;
343+
btnAll.disabled = false;
344+
btnAll.textContent = '▶ Run All';
345+
loadExamples(); // refresh sidebar badges
346+
}
347+
348+
function runOneCollect(item, tbody, phRow) {
349+
return new Promise(resolve => {
350+
const src = new EventSource(`/api/run?dir=${encodeURIComponent(item.dir)}`);
351+
const results = [];
352+
let finished = false;
353+
src.addEventListener('result', e => { try { results.push(JSON.parse(e.data)); } catch(_){} });
354+
const finish = () => {
355+
if (finished) return;
356+
finished = true;
357+
src.close();
358+
phRow.remove();
359+
if (!results.length) {
360+
const tr = document.createElement('tr');
361+
tr.innerHTML = `<td>${item.name}</td><td colspan="${CHECKS_ORDER.length+4}"><span class="badge fail">ERROR — no result (see example page for log)</span></td>`;
362+
tr.onclick = () => selectExample(item);
363+
tbody.appendChild(tr);
364+
}
365+
results.forEach(r => {
366+
const byName = {};
367+
(r.Checks||[]).forEach(c => byName[c.Name] = c.OK);
368+
const cells = CHECKS_ORDER.map(c =>
369+
byName[c] === undefined ? '<td style="color:#4a5568">—</td>'
370+
: `<td class="${byName[c]?'check-ok':'check-fail'}">${byName[c]?'✓':'✗'}</td>`).join('');
371+
const variant = r.Variant && r.Variant !== r.Name ? r.Variant : '';
372+
const ms = Math.round((r.Duration||0)/1e6);
373+
const censor = r.Fingerprint ? (r.Fingerprint.Verdict||'') : '';
374+
const tr = document.createElement('tr');
375+
tr.innerHTML = `<td>${r.Name||item.name}</td><td style="color:#a78bfa">${variant}</td>${cells}
376+
<td>${censor}</td><td style="color:#718096">${ms}ms</td>
377+
<td><span class="badge ${r.Pass?'pass':'fail'}">${r.Pass?'PASS':'FAIL'}</span></td>`;
378+
tr.onclick = () => selectExample(item);
379+
tbody.appendChild(tr);
380+
});
381+
resolve();
382+
};
383+
src.addEventListener('done', finish);
384+
src.onerror = () => finish(); // network-level error — move on to next example
385+
});
386+
}
387+
297388
loadExamples();
298389
</script>
299390
</body>

0 commit comments

Comments
 (0)