6363 # btn-run-all : hover {background : # 047857 }
6464 # btn-run-all : disabled {background : # 374151 ;color : # 6b7280 ;cursor : not-allowed}
6565 # core-filter {margin : 0 16px 8px ;padding : 6px 8px ;background : # 0d1117 ;border : 1px solid # 2d3748 ;border-radius : 6px ;color : # e2e8f0 ;font-size : .8rem ;width : calc (100% - 32px )}
66+ # global-deploy {margin : 0 16px 8px ;padding : 6px 8px ;background : # 0d1117 ;border : 1px solid # 2d3748 ;border-radius : 6px ;color : # e2e8f0 ;font-size : .72rem ;font-family : monospace;width : calc (100% - 32px )}
67+ # global-deploy ::placeholder {color : # 4a5568 }
6668 # summary-table {width : 100% ;border-collapse : collapse;font-size : .82rem }
6769 # summary-table th , # summary-table td {padding : 7px 10px ;text-align : left;border-bottom : 1px solid # 1a202c }
6870 # summary-table th {color : # 718096 ;font-size : .72rem ;text-transform : uppercase;position : sticky;top : 0 ;background : # 0d1117 }
7577< div id ="sidebar ">
7678 < h1 > 🛡 Hiddify Health</ h1 >
7779 < button id ="btn-run-all "> ▶ Run All</ button >
80+ < input id ="global-deploy " type ="text " placeholder ="Global SSH (ssh://user:pass@host:22) — applies to Run All " title ="Deploy server to this host for every example in Run All ">
7881 < select id ="core-filter " title ="Filter by core "> < option value =""> All cores</ option > </ select >
7982 < div id ="examples "> < div class ="welcome "> Loading…</ div > </ div >
8083</ div >
@@ -360,10 +363,17 @@ <h2 id="title">Select an example</h2>
360363document . getElementById ( 'btn-run-all' ) . onclick = runAll ;
361364document . getElementById ( 'core-filter' ) . addEventListener ( 'change' , renderExamples ) ;
362365
366+ // summaryRows holds the collected Run All results; the table is re-rendered
367+ // from this array whenever sort/filter changes.
368+ let summaryRows = [ ] ;
369+ let sortKey = 'name' , sortDir = 1 ;
370+ let statusFilter = 'all' ; // all | pass | fail
371+
363372async function runAll ( ) {
364373 const toRun = visibleExamples ( ) ;
365374 if ( runAllActive || ! toRun . length ) return ;
366375 runAllActive = true ;
376+ summaryRows = [ ] ;
367377 const btnAll = document . getElementById ( 'btn-run-all' ) ;
368378 btnAll . disabled = true ;
369379 btnAll . textContent = '⏳ Running all…' ;
@@ -373,20 +383,20 @@ <h2 id="title">Select an example</h2>
373383 document . getElementById ( 'mode-badge' ) . style . display = 'none' ;
374384 document . querySelectorAll ( '.example' ) . forEach ( e => e . classList . remove ( 'active' ) ) ;
375385 selectedDir = null ;
376-
377- const log = document . getElementById ( 'log-panel' ) ;
378- log . innerHTML = `<table id="summary-table"><thead><tr>
379- <th>Example</th><th>Variant</th>${ CHECKS_ORDER . map ( c => `<th>${ c } </th>` ) . join ( '' ) }
380- <th>Censor</th><th>Time</th><th>Status</th>
381- </tr></thead><tbody></tbody></table>` ;
382386 document . getElementById ( 'checks-list' ) . innerHTML = '<h3>Results</h3>' ;
383- const tbody = log . querySelector ( 'tbody' ) ;
384387
385388 for ( const item of toRun ) {
386- const phRow = document . createElement ( 'tr' ) ;
387- phRow . innerHTML = `<td>${ item . name } </td><td colspan="${ CHECKS_ORDER . length + 4 } "><span class="badge running">RUNNING</span></td>` ;
388- tbody . appendChild ( phRow ) ;
389- await runOneCollect ( item , tbody , phRow ) ;
389+ summaryRows . push ( { name : item . name , variant : '' , item, running : true } ) ;
390+ renderSummary ( ) ;
391+ const got = await runOneCollect ( item ) ;
392+ // Remove the placeholder for this item and append real rows.
393+ summaryRows = summaryRows . filter ( r => ! ( r . item === item && r . running ) ) ;
394+ if ( ! got . length ) {
395+ summaryRows . push ( { name : item . name , variant : '' , item, error : true , pass : false } ) ;
396+ } else {
397+ got . forEach ( r => summaryRows . push ( rowFromResult ( r , item ) ) ) ;
398+ }
399+ renderSummary ( ) ;
390400 }
391401
392402 runAllActive = false ;
@@ -395,44 +405,116 @@ <h2 id="title">Select an example</h2>
395405 loadExamples ( ) ; // refresh sidebar badges
396406}
397407
398- function runOneCollect ( item , tbody , phRow ) {
408+ function speedOf ( checks , names ) {
409+ for ( const c of ( checks || [ ] ) ) if ( names . includes ( c . Name ) && c . Throughput ) return c . Throughput ;
410+ return 0 ;
411+ }
412+
413+ function rowFromResult ( r , item ) {
414+ const byName = { } ;
415+ ( r . Checks || [ ] ) . forEach ( c => byName [ c . Name ] = c ) ;
416+ return {
417+ name : r . Name || item . name ,
418+ variant : ( r . Variant && r . Variant !== r . Name ) ? r . Variant : '' ,
419+ item, pass : ! ! r . Pass ,
420+ censor : r . Fingerprint ? ( r . Fingerprint . Verdict || '' ) : '' ,
421+ ms : Math . round ( ( r . Duration || 0 ) / 1e6 ) ,
422+ dl : speedOf ( r . Checks , [ 'download' , 'speedtest' ] ) ,
423+ ul : speedOf ( r . Checks , [ 'upload' ] ) ,
424+ checks : byName ,
425+ } ;
426+ }
427+
428+ const SUMMARY_COLS = [
429+ { key :'name' , label :'Example' } ,
430+ { key :'variant' , label :'Variant' } ,
431+ ...CHECKS_ORDER . map ( c => ( { key :'chk_' + c , label :c , check :c } ) ) ,
432+ { key :'dl' , label :'↓ DL' } ,
433+ { key :'ul' , label :'↑ UL' } ,
434+ { key :'censor' , label :'Censor' } ,
435+ { key :'ms' , label :'Time' } ,
436+ { key :'pass' , label :'Status' } ,
437+ ] ;
438+
439+ function sortVal ( row , key ) {
440+ if ( key . startsWith ( 'chk_' ) ) { const c = row . checks && row . checks [ key . slice ( 4 ) ] ; return c ? ( c . OK ?2 :( c . Optional ?1 :0 ) ) : - 1 ; }
441+ if ( key === 'pass' ) return row . pass ? 1 : 0 ;
442+ return row [ key ] !== undefined ? row [ key ] : '' ;
443+ }
444+
445+ function renderSummary ( ) {
446+ const log = document . getElementById ( 'log-panel' ) ;
447+ let rows = summaryRows . slice ( ) ;
448+ if ( statusFilter !== 'all' ) rows = rows . filter ( r => r . running || ( statusFilter === 'pass' ?r . pass :! r . pass ) ) ;
449+ // Stable sort (placeholders/running rows keep insertion order at bottom).
450+ rows . sort ( ( a , b ) => {
451+ if ( a . running !== b . running ) return a . running ? 1 : - 1 ;
452+ const va = sortVal ( a , sortKey ) , vb = sortVal ( b , sortKey ) ;
453+ if ( va < vb ) return - 1 * sortDir ; if ( va > vb ) return 1 * sortDir ; return 0 ;
454+ } ) ;
455+
456+ const arrow = k => k === sortKey ? ( sortDir > 0 ?' ▲' :' ▼' ) : '' ;
457+ const head = SUMMARY_COLS . map ( c => `<th data-key="${ c . key } " style="cursor:pointer">${ c . label } ${ arrow ( c . key ) } </th>` ) . join ( '' ) ;
458+
459+ const body = rows . map ( r => {
460+ if ( r . running ) return `<tr><td>${ r . name } </td><td colspan="${ SUMMARY_COLS . length - 1 } "><span class="badge running">RUNNING</span></td></tr>` ;
461+ if ( r . error ) return `<tr data-dir="${ r . item . dir } "><td>${ r . name } </td><td colspan="${ SUMMARY_COLS . length - 1 } "><span class="badge fail">ERROR — no result (open example for log)</span></td></tr>` ;
462+ const cells = CHECKS_ORDER . map ( name => {
463+ const c = r . checks [ name ] ;
464+ if ( ! c ) return '<td style="color:#4a5568">—</td>' ;
465+ const cls = c . OK ? 'check-ok' : ( c . Optional ? 'check-warn' : 'check-fail' ) ;
466+ return `<td class="${ cls } ">${ c . OK ?'✓' :( c . Optional ?'!' :'✗' ) } </td>` ;
467+ } ) . join ( '' ) ;
468+ return `<tr data-dir="${ r . item . dir } ">
469+ <td>${ r . name } </td><td style="color:#a78bfa">${ r . variant } </td>${ cells }
470+ <td style="color:#718096">${ r . dl ?fmtSpeed ( r . dl ) :'—' } </td>
471+ <td style="color:#718096">${ r . ul ?fmtSpeed ( r . ul ) :'—' } </td>
472+ <td>${ r . censor } </td><td style="color:#718096">${ r . ms } ms</td>
473+ <td><span class="badge ${ r . pass ?'pass' :'fail' } ">${ r . pass ?'PASS' :'FAIL' } </span></td>
474+ </tr>` ;
475+ } ) . join ( '' ) ;
476+
477+ const counts = summaryRows . filter ( r => ! r . running ) ;
478+ const np = counts . filter ( r => r . pass ) . length , nf = counts . length - np ;
479+ log . innerHTML = `
480+ <div style="margin-bottom:8px;font-size:.8rem;color:#a0aec0">
481+ <span class="check-ok">${ np } pass</span> · <span class="check-fail">${ nf } fail</span>
482+ | Filter:
483+ <select id="status-filter" style="background:#0d1117;border:1px solid #2d3748;border-radius:5px;color:#e2e8f0;font-size:.78rem;padding:2px 5px">
484+ <option value="all"${ statusFilter === 'all' ?' selected' :'' } >All</option>
485+ <option value="pass"${ statusFilter === 'pass' ?' selected' :'' } >Pass only</option>
486+ <option value="fail"${ statusFilter === 'fail' ?' selected' :'' } >Fail only</option>
487+ </select>
488+ </div>
489+ <table id="summary-table"><thead><tr>${ head } </tr></thead><tbody>${ body } </tbody></table>` ;
490+
491+ log . querySelectorAll ( 'th[data-key]' ) . forEach ( th => th . onclick = ( ) => {
492+ const k = th . dataset . key ;
493+ if ( k === sortKey ) sortDir = - sortDir ; else { sortKey = k ; sortDir = 1 ; }
494+ renderSummary ( ) ;
495+ } ) ;
496+ log . querySelectorAll ( 'tbody tr[data-dir]' ) . forEach ( tr => tr . onclick = ( ) => {
497+ const it = examplesList . find ( e => e . dir === tr . dataset . dir ) ;
498+ if ( it ) selectExample ( it ) ;
499+ } ) ;
500+ const sf = document . getElementById ( 'status-filter' ) ;
501+ if ( sf ) sf . onchange = ( ) => { statusFilter = sf . value ; renderSummary ( ) ; } ;
502+ }
503+
504+ function runOneCollect ( item ) {
399505 return new Promise ( resolve => {
400- const src = new EventSource ( `/api/run?dir=${ encodeURIComponent ( item . dir ) } ` ) ;
506+ let runURL = `/api/run?dir=${ encodeURIComponent ( item . dir ) } ` ;
507+ const gd = document . getElementById ( 'global-deploy' ) . value . trim ( ) ;
508+ if ( gd ) runURL += `&deploy=${ encodeURIComponent ( gd ) } ` ;
509+ const src = new EventSource ( runURL ) ;
401510 const results = [ ] ;
402511 let finished = false ;
403512 src . addEventListener ( 'result' , e => { try { results . push ( JSON . parse ( e . data ) ) ; } catch ( _ ) { } } ) ;
404513 const finish = ( ) => {
405514 if ( finished ) return ;
406515 finished = true ;
407516 src . close ( ) ;
408- phRow . remove ( ) ;
409- if ( ! results . length ) {
410- const tr = document . createElement ( 'tr' ) ;
411- 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>` ;
412- tr . onclick = ( ) => selectExample ( item ) ;
413- tbody . appendChild ( tr ) ;
414- }
415- results . forEach ( r => {
416- const byName = { } ;
417- ( r . Checks || [ ] ) . forEach ( c => byName [ c . Name ] = c ) ;
418- const cells = CHECKS_ORDER . map ( name => {
419- const c = byName [ name ] ;
420- if ( c === undefined ) return '<td style="color:#4a5568">—</td>' ;
421- const cls = c . OK ? 'check-ok' : ( c . Optional ? 'check-warn' : 'check-fail' ) ;
422- const sym = c . OK ? '✓' : ( c . Optional ? '!' : '✗' ) ;
423- return `<td class="${ cls } ">${ sym } </td>` ;
424- } ) . join ( '' ) ;
425- const variant = r . Variant && r . Variant !== r . Name ? r . Variant : '' ;
426- const ms = Math . round ( ( r . Duration || 0 ) / 1e6 ) ;
427- const censor = r . Fingerprint ? ( r . Fingerprint . Verdict || '' ) : '' ;
428- const tr = document . createElement ( 'tr' ) ;
429- tr . innerHTML = `<td>${ r . Name || item . name } </td><td style="color:#a78bfa">${ variant } </td>${ cells }
430- <td>${ censor } </td><td style="color:#718096">${ ms } ms</td>
431- <td><span class="badge ${ r . Pass ?'pass' :'fail' } ">${ r . Pass ?'PASS' :'FAIL' } </span></td>` ;
432- tr . onclick = ( ) => selectExample ( item ) ;
433- tbody . appendChild ( tr ) ;
434- } ) ;
435- resolve ( ) ;
517+ resolve ( results ) ;
436518 } ;
437519 src . addEventListener ( 'done' , finish ) ;
438520 src . onerror = ( ) => finish ( ) ; // network-level error — move on to next example
0 commit comments