6464 .nav-links a { color : rgba (255 , 255 , 255 , 0.8 ); text-decoration : none; margin-left : 16px ; }
6565 .nav-links a : hover { color : white; }
6666 .chart-container { position : relative; height : 300px ; }
67+ .anomaly-alert { cursor : pointer; }
68+ .anomaly-alert : hover { opacity : 0.9 ; }
69+ .stats-clickable tr { cursor : pointer; }
70+ .stats-clickable tr : hover { background-color : # e9ecef ; }
71+ .detail-panel {
72+ background : white;
73+ border-radius : 12px ;
74+ box-shadow : 0 4px 20px rgba (0 , 0 , 0 , 0.15 );
75+ padding : 24px ;
76+ margin-bottom : 24px ;
77+ border-left : 4px solid # 667eea ;
78+ }
79+ .detail-panel .detail-header {
80+ display : flex;
81+ justify-content : space-between;
82+ align-items : center;
83+ margin-bottom : 16px ;
84+ }
85+ .detail-panel .detail-header h5 { margin : 0 ; }
86+ .event-card {
87+ background : # f8f9fa ;
88+ border-radius : 8px ;
89+ padding : 14px ;
90+ margin-bottom : 10px ;
91+ border : 1px solid # e9ecef ;
92+ }
93+ .event-card : hover { border-color : # 667eea ; }
94+ .event-field { margin-bottom : 4px ; }
95+ .event-field .field-name {
96+ font-weight : 600 ;
97+ color : # 6c757d ;
98+ font-size : 0.8rem ;
99+ text-transform : uppercase;
100+ }
101+ .event-field .field-value { font-size : 0.9rem ; }
102+ .node-link { color : # 667eea ; text-decoration : underline; cursor : pointer; }
103+ .node-link : hover { color : # 764ba2 ; }
67104 </ style >
68105</ head >
69106< body >
@@ -311,6 +348,26 @@ <h5><i class="bi bi-heart-pulse"></i> Runtime Health Overview</h5>
311348 </ div >
312349 </ div >
313350
351+ <!-- Event Detail Drill-down Panel -->
352+ < div class ="container-fluid px-4 " id ="detailContainer " style ="display:none; ">
353+ < div class ="detail-panel " id ="detailPanel ">
354+ < div class ="detail-header ">
355+ < h5 > < i class ="bi bi-list-ul "> </ i > < span id ="detailTitle "> Event Details</ span > </ h5 >
356+ < div >
357+ < span class ="text-muted me-3 " id ="detailCount "> </ span >
358+ < button class ="btn btn-sm btn-outline-secondary " id ="detailClose ">
359+ < i class ="bi bi-x-lg "> </ i > Close
360+ </ button >
361+ </ div >
362+ </ div >
363+ < div id ="detailLoading " class ="text-center py-3 " style ="display:none; ">
364+ < div class ="spinner-border spinner-border-sm text-primary "> </ div >
365+ < span class ="ms-2 "> Loading events...</ span >
366+ </ div >
367+ < div id ="detailContent "> </ div >
368+ </ div >
369+ </ div >
370+
314371 <!-- Loading Overlay -->
315372 < div class ="loading-overlay " id ="loadingOverlay " style ="display:none; ">
316373 < div class ="spinner-border text-primary " style ="width:3rem;height:3rem; " role ="status ">
@@ -503,8 +560,156 @@ <h5><i class="bi bi-heart-pulse"></i> Runtime Health Overview</h5>
503560 $ ( '#skipCount' ) . text ( formatNumber ( totals . skip ) ) ;
504561 }
505562
563+ function escapeHtml ( str ) {
564+ if ( ! str ) return '' ;
565+ return str . replace ( / & / g, '&' ) . replace ( / < / g, '<' )
566+ . replace ( / > / g, '>' ) . replace ( / " / g, '"' ) ;
567+ }
568+
569+ function resultBadge ( result ) {
570+ var colors = {
571+ pass : 'bg-success' , fail : 'bg-danger' ,
572+ incomplete : 'bg-info' , skip : 'bg-secondary'
573+ } ;
574+ var cls = colors [ result ] || 'bg-secondary' ;
575+ return '<span class="badge ' + cls + '">' + ( result || 'N/A' ) + '</span>' ;
576+ }
577+
578+ function renderEventCard ( evt ) {
579+ var ts = evt . ts ? new Date ( evt . ts ) . toLocaleString ( ) : 'N/A' ;
580+ var html = '<div class="event-card">' ;
581+ html += '<div class="row">' ;
582+
583+ // Left column: key fields
584+ html += '<div class="col-md-8">' ;
585+ html += '<div class="d-flex align-items-center mb-2">' ;
586+ html += '<span class="badge bg-secondary me-2">' + escapeHtml ( evt . kind ) + '</span>' ;
587+ html += resultBadge ( evt . result ) ;
588+ if ( evt . is_infra_error ) {
589+ html += ' <span class="badge ms-1" style="background:#fd7e14">infra error</span>' ;
590+ }
591+ html += '<small class="text-muted ms-auto">' + ts + '</small>' ;
592+ html += '</div>' ;
593+
594+ var fields = [
595+ [ 'Runtime' , evt . runtime ] ,
596+ [ 'Device Type' , evt . device_type ] ,
597+ [ 'Device ID' , evt . device_id ] ,
598+ [ 'Job Name' , evt . job_name ] ,
599+ [ 'Test Name' , evt . test_name ] ,
600+ [ 'Tree' , evt . tree ] ,
601+ [ 'Branch' , evt . branch ] ,
602+ [ 'Arch' , evt . arch ] ,
603+ ] ;
604+ fields . forEach ( function ( f ) {
605+ if ( f [ 1 ] ) {
606+ html += '<div class="event-field d-inline-block me-4">' ;
607+ html += '<span class="field-name">' + f [ 0 ] + ': </span>' ;
608+ html += '<span class="field-value">' + escapeHtml ( f [ 1 ] ) + '</span>' ;
609+ html += '</div>' ;
610+ }
611+ } ) ;
612+ html += '</div>' ;
613+
614+ // Right column: IDs, errors, links
615+ html += '<div class="col-md-4">' ;
616+ if ( evt . node_id ) {
617+ html += '<div class="event-field">' ;
618+ html += '<span class="field-name">Node: </span>' ;
619+ html += '<a class="node-link" href="/viewer?node_id=' +
620+ encodeURIComponent ( evt . node_id ) +
621+ '" target="_blank">' + escapeHtml ( evt . node_id ) + '</a>' ;
622+ html += '</div>' ;
623+ }
624+ if ( evt . job_id ) {
625+ html += '<div class="event-field">' ;
626+ html += '<span class="field-name">Job ID: </span>' ;
627+ html += '<span class="field-value">' + escapeHtml ( evt . job_id ) + '</span>' ;
628+ html += '</div>' ;
629+ }
630+ if ( evt . error_type ) {
631+ html += '<div class="event-field">' ;
632+ html += '<span class="field-name">Error Type: </span>' ;
633+ html += '<span class="badge" style="background:#fd7e14">' +
634+ escapeHtml ( evt . error_type ) + '</span>' ;
635+ html += '</div>' ;
636+ }
637+ if ( evt . error_msg ) {
638+ html += '<div class="event-field">' ;
639+ html += '<span class="field-name">Error Msg: </span>' ;
640+ html += '<span class="field-value text-danger">' +
641+ escapeHtml ( evt . error_msg ) + '</span>' ;
642+ html += '</div>' ;
643+ }
644+ if ( evt . retry > 0 ) {
645+ html += '<div class="event-field">' ;
646+ html += '<span class="field-name">Retry: </span>' ;
647+ html += '<span class="field-value">#' + evt . retry + '</span>' ;
648+ html += '</div>' ;
649+ }
650+ html += '</div>' ;
651+
652+ html += '</div></div>' ;
653+ return html ;
654+ }
655+
656+ function showDetail ( title , filters ) {
657+ var container = $ ( '#detailContainer' ) ;
658+ var content = $ ( '#detailContent' ) ;
659+ var loading = $ ( '#detailLoading' ) ;
660+ $ ( '#detailTitle' ) . text ( title ) ;
661+ content . empty ( ) ;
662+ container . show ( ) ;
663+ loading . show ( ) ;
664+ $ ( '#detailCount' ) . text ( '' ) ;
665+
666+ var time = getTimeRange ( ) ;
667+ var url = apiurl + '/latest/telemetry?since=' +
668+ encodeURIComponent ( time . since ) + '&limit=50' ;
669+ for ( var key in filters ) {
670+ if ( filters [ key ] !== null && filters [ key ] !== undefined && filters [ key ] !== '' ) {
671+ url += '&' + encodeURIComponent ( key ) + '=' +
672+ encodeURIComponent ( filters [ key ] ) ;
673+ }
674+ }
675+
676+ $ . ajax ( {
677+ url : url ,
678+ method : 'GET' ,
679+ dataType : 'json'
680+ } ) . then ( function ( data ) {
681+ loading . hide ( ) ;
682+ var items = data . items || [ ] ;
683+ var total = data . total || items . length ;
684+ $ ( '#detailCount' ) . text (
685+ items . length + ' shown' +
686+ ( total > items . length ? ' of ' + total + ' total' : '' )
687+ ) ;
688+ if ( items . length === 0 ) {
689+ content . html (
690+ '<div class="text-muted text-center py-3">' +
691+ 'No events found matching these filters.</div>'
692+ ) ;
693+ return ;
694+ }
695+ items . forEach ( function ( evt ) {
696+ content . append ( renderEventCard ( evt ) ) ;
697+ } ) ;
698+ // Scroll to the detail panel
699+ container [ 0 ] . scrollIntoView ( { behavior : 'smooth' , block : 'start' } ) ;
700+ } ) . catch ( function ( err ) {
701+ loading . hide ( ) ;
702+ content . html (
703+ '<div class="alert alert-danger">Error loading events: ' +
704+ escapeHtml ( String ( err . statusText || err ) ) + '</div>'
705+ ) ;
706+ } ) ;
707+ }
708+
506709 function renderStatsTable ( tableId , data , groupField ) {
507- var tbody = $ ( '#' + tableId + ' tbody' ) ;
710+ var table = $ ( '#' + tableId ) ;
711+ table . addClass ( 'stats-clickable' ) ;
712+ var tbody = table . find ( 'tbody' ) ;
508713 tbody . empty ( ) ;
509714 if ( ! data || data . length === 0 ) {
510715 tbody . append ( '<tr><td colspan="6" class="text-muted text-center">No data available</td></tr>' ) ;
@@ -517,16 +722,22 @@ <h5><i class="bi bi-heart-pulse"></i> Runtime Health Overview</h5>
517722 var pass = row . pass || 0 ;
518723 var fail = row . fail || 0 ;
519724 var infra = row . infra_error || 0 ;
520- tbody . append (
521- '<tr>' +
522- '<td><strong>' + name + '</strong></td>' +
725+ var tr = $ (
726+ '<tr title="Click to see individual events" >' +
727+ '<td><strong>' + escapeHtml ( name ) + '</strong></td>' +
523728 '<td>' + total + '</td>' +
524729 '<td><span class="badge badge-pass bg-success">' + pass + '</span></td>' +
525730 '<td><span class="badge badge-fail bg-danger">' + fail + '</span></td>' +
526731 '<td><span class="badge badge-infra" style="background:#fd7e14">' + infra + '</span></td>' +
527732 '<td>' + healthBar ( pass , total ) + '</td>' +
528733 '</tr>'
529734 ) ;
735+ tr . on ( 'click' , function ( ) {
736+ var filters = { } ;
737+ filters [ groupField ] = name ;
738+ showDetail ( 'Events for ' + groupField + ': ' + name , filters ) ;
739+ } ) ;
740+ tbody . append ( tr ) ;
530741 } ) ;
531742 }
532743
@@ -553,37 +764,62 @@ <h5><i class="bi bi-heart-pulse"></i> Runtime Health Overview</h5>
553764 var icon = severity === 'high' ? 'bi-exclamation-octagon text-danger' :
554765 severity === 'medium' ? 'bi-exclamation-triangle text-warning' :
555766 'bi-info-circle text-success' ;
556- section . append (
557- '<div class="anomaly-alert anomaly-' + severity + '">' +
767+ var alertEl = $ (
768+ '<div class="anomaly-alert anomaly-' + severity +
769+ '" title="Click to see individual events">' +
558770 '<div class="d-flex justify-content-between align-items-center">' +
559771 '<div>' +
560772 '<i class="bi ' + icon + '"></i> ' +
561- '<strong>' + ( a . runtime || 'N/A' ) + '</strong>' +
562- ( a . device_type ? ' / ' + a . device_type : '' ) +
773+ '<strong>' + escapeHtml ( a . runtime || 'N/A' ) + '</strong>' +
774+ ( a . device_type ? ' / ' + escapeHtml ( a . device_type ) : '' ) +
563775 '</div>' +
564776 '<div>' +
565777 '<span class="badge bg-danger me-1">Fail: ' + ( a . fail_rate * 100 ) . toFixed ( 1 ) + '%</span>' +
566778 '<span class="badge" style="background:#fd7e14">Infra: ' + ( a . infra_rate * 100 ) . toFixed ( 1 ) + '%</span>' +
779+ ' <i class="bi bi-arrow-right-circle ms-2"></i>' +
567780 '</div>' +
568781 '</div>' +
569782 '<small class="text-muted">' + a . total + ' events | ' +
570783 a . fail + ' fail | ' + a . incomplete + ' incomplete | ' +
571784 a . infra_error + ' infra errors</small>' +
572785 '</div>'
573786 ) ;
787+ ( function ( anomaly ) {
788+ alertEl . on ( 'click' , function ( ) {
789+ var filters = { runtime : anomaly . runtime } ;
790+ if ( anomaly . device_type ) filters . device_type = anomaly . device_type ;
791+ var label = escapeHtml ( anomaly . runtime || 'N/A' ) ;
792+ if ( anomaly . device_type ) label += ' / ' + escapeHtml ( anomaly . device_type ) ;
793+ showDetail ( 'Anomaly events: ' + label , filters ) ;
794+ } ) ;
795+ } ) ( a ) ;
796+ section . append ( alertEl ) ;
574797 } ) ;
575798
576799 if ( errorAnomalies . length > 0 ) {
577800 section . append ( '<h6 class="mt-3 mb-2">Submission / Connectivity Errors</h6>' ) ;
578801 errorAnomalies . forEach ( function ( a ) {
579- section . append (
580- '<div class="anomaly-alert anomaly-medium">' +
802+ var alertEl = $ (
803+ '<div class="anomaly-alert anomaly-medium" title="Click to see individual events" >' +
581804 '<i class="bi bi-wifi-off text-warning"></i> ' +
582- '<strong>' + ( a . runtime || 'N/A' ) + '</strong>' +
583- ' - ' + ( a . error_type || 'unknown' ) +
805+ '<strong>' + escapeHtml ( a . runtime || 'N/A' ) + '</strong>' +
806+ ' - ' + escapeHtml ( a . error_type || 'unknown' ) +
584807 ' <span class="badge bg-warning text-dark">' + a . count + ' occurrences</span>' +
808+ ' <i class="bi bi-arrow-right-circle ms-2"></i>' +
585809 '</div>'
586810 ) ;
811+ ( function ( anomaly ) {
812+ alertEl . on ( 'click' , function ( ) {
813+ var filters = { runtime : anomaly . runtime } ;
814+ if ( anomaly . error_type ) filters . error_type = anomaly . error_type ;
815+ showDetail (
816+ 'Error events: ' + escapeHtml ( anomaly . runtime || 'N/A' ) +
817+ ' - ' + escapeHtml ( anomaly . error_type || 'unknown' ) ,
818+ filters
819+ ) ;
820+ } ) ;
821+ } ) ( a ) ;
822+ section . append ( alertEl ) ;
587823 } ) ;
588824 }
589825 }
@@ -600,15 +836,29 @@ <h5><i class="bi bi-heart-pulse"></i> Runtime Health Overview</h5>
600836 var ts = evt . ts ? new Date ( evt . ts ) . toLocaleString ( ) : 'N/A' ;
601837 var msg = evt . error_msg || '' ;
602838 if ( msg . length > 80 ) msg = msg . substring ( 0 , 80 ) + '...' ;
603- tbody . append (
604- '<tr class="error-row">' +
839+ var tr = $ (
840+ '<tr class="error-row" title="Click to see full event details" >' +
605841 '<td><small>' + ts + '</small></td>' +
606- '<td><span class="badge bg-secondary">' + ( evt . kind || '' ) + '</span></td>' +
607- '<td>' + ( evt . runtime || 'N/A' ) + '</td>' +
608- '<td><span class="badge" style="background:#fd7e14">' + ( evt . error_type || 'N/A' ) + '</span></td>' +
609- '<td><small>' + msg + '</small></td>' +
842+ '<td><span class="badge bg-secondary">' + escapeHtml ( evt . kind || '' ) + '</span></td>' +
843+ '<td>' + escapeHtml ( evt . runtime || 'N/A' ) + '</td>' +
844+ '<td><span class="badge" style="background:#fd7e14">' + escapeHtml ( evt . error_type || 'N/A' ) + '</span></td>' +
845+ '<td><small>' + escapeHtml ( msg ) + '</small></td>' +
610846 '</tr>'
611847 ) ;
848+ ( function ( event ) {
849+ tr . on ( 'click' , function ( ) {
850+ // Show this single event in the detail panel
851+ $ ( '#detailContainer' ) . show ( ) ;
852+ $ ( '#detailTitle' ) . text ( 'Event Detail' ) ;
853+ $ ( '#detailCount' ) . text ( '' ) ;
854+ $ ( '#detailLoading' ) . hide ( ) ;
855+ var content = $ ( '#detailContent' ) ;
856+ content . empty ( ) ;
857+ content . append ( renderEventCard ( event ) ) ;
858+ $ ( '#detailContainer' ) [ 0 ] . scrollIntoView ( { behavior : 'smooth' , block : 'start' } ) ;
859+ } ) ;
860+ } ) ( evt ) ;
861+ tbody . append ( tr ) ;
612862 } ) ;
613863 }
614864
@@ -737,6 +987,10 @@ <h5><i class="bi bi-heart-pulse"></i> Runtime Health Overview</h5>
737987 $ ( '#filterTree' ) . change ( loadAll ) ;
738988 $ ( '#filterKind' ) . change ( loadAll ) ;
739989 $ ( '#anomalyThreshold' ) . change ( loadAll ) ;
990+ $ ( '#detailClose' ) . click ( function ( ) {
991+ $ ( '#detailContainer' ) . hide ( ) ;
992+ $ ( '#detailContent' ) . empty ( ) ;
993+ } ) ;
740994 loadAll ( ) ;
741995 } ) ;
742996 </ script >
0 commit comments