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 >
92101let selectedDir = null ;
93102let es = null ;
103+ let examplesList = [ ] ;
94104
95105async 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+
297388loadExamples ( ) ;
298389</ script >
299390</ body >
0 commit comments