177177 color : var (--status-error-text );
178178 }
179179
180+ .chip-warning {
181+ display : inline-block;
182+ padding : 4px 12px ;
183+ border-radius : 4px ;
184+ font-size : 0.9em ;
185+ font-weight : bold;
186+ background-color : var (--status-queued-bg );
187+ color : var (--status-queued-text );
188+ }
189+
180190 .error-message {
181191 background-color : var (--error-bg );
182192 color : var (--error-text );
302312 border : 1px solid var (--border-color );
303313 }
304314
315+ .patch-nav-item .patch-error {
316+ background-color : var (--status-error-bg );
317+ color : var (--status-error-text );
318+ }
319+
305320 .patch-nav-item : hover {
306321 opacity : 0.8 ;
307322 transform : translateY (-2px );
324339 font-weight : normal;
325340 font-style : italic;
326341 }
342+
343+ .patch-error-content {
344+ background-color : var (--error-bg );
345+ color : var (--error-text );
346+ border : 1px solid var (--error-border );
347+ padding : 15px ;
348+ border-radius : 4px ;
349+ font-style : italic;
350+ }
351+
352+ .collapsible-box {
353+ background-color : var (--bg-secondary );
354+ border : 1px solid var (--border-color );
355+ border-radius : 5px ;
356+ margin-bottom : 20px ;
357+ }
358+
359+ .collapsible-header {
360+ padding : 15px 20px ;
361+ cursor : pointer;
362+ display : flex;
363+ justify-content : space-between;
364+ align-items : center;
365+ user-select : none;
366+ }
367+
368+ .collapsible-header : hover {
369+ opacity : 0.8 ;
370+ }
371+
372+ .collapsible-header .title {
373+ font-weight : bold;
374+ color : var (--heading-color );
375+ }
376+
377+ .collapsible-header .toggle-icon {
378+ color : var (--text-muted );
379+ transition : transform 0.2s ease;
380+ }
381+
382+ .collapsible-header .expanded .toggle-icon {
383+ transform : rotate (90deg );
384+ }
385+
386+ .collapsible-content {
387+ display : none;
388+ padding : 0 20px 20px 20px ;
389+ }
390+
391+ .collapsible-content .expanded {
392+ display : block;
393+ }
394+
395+ .metadata-table {
396+ width : 100% ;
397+ border-collapse : collapse;
398+ }
399+
400+ .metadata-table th ,
401+ .metadata-table td {
402+ padding : 8px 12px ;
403+ text-align : left;
404+ border-bottom : 1px solid var (--border-color );
405+ }
406+
407+ .metadata-table th {
408+ font-weight : bold;
409+ color : var (--text-secondary );
410+ width : 30% ;
411+ }
412+
413+ .metadata-table td {
414+ font-family : 'Courier New' , monospace;
415+ color : var (--text-primary );
416+ word-break : break-word;
417+ }
418+
419+ .metadata-table tr : last-child th ,
420+ .metadata-table tr : last-child td {
421+ border-bottom : none;
422+ }
327423 </ style >
328424</ head >
329425< body >
@@ -392,13 +488,19 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
392488 < label > Feedback:</ label >
393489 < span id ="review-feedback "> </ span >
394490 </ div >
491+ < div class ="review-info-item " id ="public-in-info " style ="display: none; ">
492+ < label > Public in:</ label >
493+ < span id ="review-public-in "> </ span >
494+ </ div >
395495 </ div >
396496 < div id ="progress-info " style ="margin-top: 15px; display: none; ">
397497 < label style ="font-weight: bold; color: var(--text-secondary); "> Progress:</ label >
398498 < span id ="review-progress " style ="color: var(--text-primary); "> </ span >
399499 </ div >
400500 </ div >
401501
502+ < div id ="metadata-container " style ="display: none; "> </ div >
503+
402504 < div id ="message-container " style ="display: none; ">
403505 < div class ="error-message " id ="review-message "> </ div >
404506 </ div >
@@ -413,6 +515,7 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
413515 < option value ="emailed "> Emailed</ option >
414516 < option value ="false-positive "> False Positive</ option >
415517 < option value ="false-negative "> False Negative</ option >
518+ < option value ="nitpick "> Ignored (nitpick)</ option >
416519 </ select >
417520 </ div >
418521 < div class ="format-selector ">
@@ -470,6 +573,7 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
470573
471574 let currentFormat = 'inline' ; // Default to inline/Review format
472575 let inlineReviews = [ ] ; // Store inline reviews for navigation coloring
576+ let failedPatchNums = [ ] ; // Store failed patch numbers
473577 let refreshInterval = null ; // Interval for auto-refreshing in-progress reviews
474578
475579 if ( ! reviewId ) {
@@ -540,6 +644,9 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
540644 const completedCount = data . completed_patches || 0 ;
541645 document . getElementById ( 'review-patches' ) . textContent = patchCount ;
542646
647+ // Store failed patch numbers for navigation display
648+ failedPatchNums = data . failed_patch_nums || [ ] ;
649+
543650 // Progress info for in-progress reviews
544651 if ( data . status === 'in-progress' && patchCount > 0 ) {
545652 document . getElementById ( 'progress-info' ) . style . display = 'block' ;
@@ -561,7 +668,7 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
561668 // Patchwork series ID
562669 if ( data . patchwork_series_id ) {
563670 document . getElementById ( 'patchwork-info' ) . style . display = 'block' ;
564- const pwUrl = `https://patchwork.kernel.org/project/netdevbpf/list/?series=${ data . patchwork_series_id } ` ;
671+ const pwUrl = `https://patchwork.kernel.org/project/${ data . pw_project || ' netdevbpf' } /list/?series=${ data . patchwork_series_id } ` ;
565672 document . getElementById ( 'review-pw-series' ) . innerHTML = `<a href="${ pwUrl } " target="_blank" rel="noopener noreferrer">${ data . patchwork_series_id } </a>` ;
566673 }
567674
@@ -590,7 +697,8 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
590697 const feedbackLabels = {
591698 'emailed' : '📧 Emailed' ,
592699 'false-positive' : '❌ False Positive' ,
593- 'false-negative' : '⚠️ False Negative'
700+ 'false-negative' : '⚠️ False Negative' ,
701+ 'nitpick' : '💬 Ignored (nitpick)'
594702 } ;
595703
596704 // Always show feedback in info section if it exists
@@ -599,6 +707,19 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
599707 document . getElementById ( 'review-feedback' ) . textContent = feedbackLabels [ data . feedback ] || data . feedback ;
600708 }
601709
710+ // Show time until public access (for owners viewing public reviews)
711+ if ( data . public_in_hours !== undefined && data . public_in_hours > 0 ) {
712+ document . getElementById ( 'public-in-info' ) . style . display = 'block' ;
713+ const hours = data . public_in_hours ;
714+ let timeText ;
715+ if ( hours < 1 ) {
716+ timeText = `${ Math . round ( hours * 60 ) } minutes` ;
717+ } else {
718+ timeText = `${ hours . toFixed ( 1 ) } hours` ;
719+ }
720+ document . getElementById ( 'review-public-in' ) . innerHTML = `<span class="chip-warning">${ timeText } </span>` ;
721+ }
722+
602723 // Message (error or warning)
603724 if ( data . message ) {
604725 document . getElementById ( 'message-container' ) . style . display = 'block' ;
@@ -640,6 +761,8 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
640761 // Then load the current format for display
641762 loadReview ( currentFormat ) ;
642763 } ) ;
764+ // Load metadata (only with token)
765+ loadMetadata ( ) ;
643766 } else if ( data . status === 'done' || ( data . status === 'error' && patchCount > 0 ) ) {
644767 // No token - just load reviews without controls bar
645768 loadInlineForNavigation ( ) . then ( ( ) => {
@@ -672,6 +795,100 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
672795 } ) ;
673796 }
674797
798+ function loadMetadata ( ) {
799+ if ( ! token ) {
800+ return ;
801+ }
802+
803+ let url = `/api/review?id=${ encodeURIComponent ( reviewId ) } ` ;
804+ url += `&token=${ encodeURIComponent ( token ) } ` ;
805+ url += '&format=metadata' ;
806+
807+ fetch ( url )
808+ . then ( response => {
809+ if ( ! response . ok ) {
810+ throw new Error ( `HTTP ${ response . status } ` ) ;
811+ }
812+ return response . json ( ) ;
813+ } )
814+ . then ( data => {
815+ if ( data . review && data . review . length > 0 ) {
816+ displayMetadata ( data . review ) ;
817+ }
818+ } )
819+ . catch ( error => {
820+ console . error ( 'Error loading metadata:' , error ) ;
821+ } ) ;
822+ }
823+
824+ function displayMetadata ( metadataArray ) {
825+ const container = document . getElementById ( 'metadata-container' ) ;
826+
827+ // Collect all metadata from all patches
828+ const allMetadata = { } ;
829+ metadataArray . forEach ( ( metadata , index ) => {
830+ if ( metadata && metadata . trim ( ) ) {
831+ try {
832+ const parsed = JSON . parse ( metadata ) ;
833+ if ( typeof parsed === 'object' && parsed !== null ) {
834+ // Prefix keys with patch number if multiple patches
835+ if ( metadataArray . length > 1 ) {
836+ Object . entries ( parsed ) . forEach ( ( [ key , value ] ) => {
837+ allMetadata [ `Patch ${ index + 1 } : ${ key } ` ] = value ;
838+ } ) ;
839+ } else {
840+ Object . assign ( allMetadata , parsed ) ;
841+ }
842+ }
843+ } catch ( e ) {
844+ console . error ( `Error parsing metadata for patch ${ index + 1 } :` , e ) ;
845+ }
846+ }
847+ } ) ;
848+
849+ if ( Object . keys ( allMetadata ) . length === 0 ) {
850+ return ;
851+ }
852+
853+ // Create collapsible box
854+ const box = document . createElement ( 'div' ) ;
855+ box . className = 'collapsible-box' ;
856+
857+ const header = document . createElement ( 'div' ) ;
858+ header . className = 'collapsible-header' ;
859+ header . innerHTML = '<span class="title">Metadata</span><span class="toggle-icon">▶</span>' ;
860+ header . onclick = function ( ) {
861+ header . classList . toggle ( 'expanded' ) ;
862+ content . classList . toggle ( 'expanded' ) ;
863+ } ;
864+
865+ const content = document . createElement ( 'div' ) ;
866+ content . className = 'collapsible-content' ;
867+
868+ // Create table
869+ const table = document . createElement ( 'table' ) ;
870+ table . className = 'metadata-table' ;
871+
872+ Object . entries ( allMetadata ) . forEach ( ( [ key , value ] ) => {
873+ const row = document . createElement ( 'tr' ) ;
874+ const th = document . createElement ( 'th' ) ;
875+ th . textContent = key ;
876+ const td = document . createElement ( 'td' ) ;
877+ td . textContent = typeof value === 'object' ? JSON . stringify ( value ) : String ( value ) ;
878+ row . appendChild ( th ) ;
879+ row . appendChild ( td ) ;
880+ table . appendChild ( row ) ;
881+ } ) ;
882+
883+ content . appendChild ( table ) ;
884+ box . appendChild ( header ) ;
885+ box . appendChild ( content ) ;
886+
887+ container . innerHTML = '' ;
888+ container . appendChild ( box ) ;
889+ container . style . display = 'block' ;
890+ }
891+
675892 function displayReviews ( reviews ) {
676893 const container = document . getElementById ( 'reviews-container' ) ;
677894 const navContainer = document . getElementById ( 'patch-navigation' ) ;
@@ -688,6 +905,9 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
688905 navContainer . style . display = 'flex' ;
689906
690907 reviews . forEach ( ( review , index ) => {
908+ const patchNum = index + 1 ;
909+ const isFailed = failedPatchNums . includes ( patchNum ) ;
910+
691911 // Use inline reviews to determine if patch has comments (for navigation coloring)
692912 const inlineReview = inlineReviews [ index ] ;
693913 const hasCommentsInline = inlineReview !== null && inlineReview !== '' ;
@@ -696,12 +916,20 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
696916 const hasComments = review !== null && review !== '' ;
697917 const patchId = `patch-${ index } ` ;
698918
699- // Create navigation item - color based on inline format
919+ // Create navigation item - color based on error status or inline format
700920 const navItem = document . createElement ( 'a' ) ;
701- navItem . className = `patch-nav-item ${ hasCommentsInline ? 'has-comments' : 'no-comments' } ` ;
702- navItem . textContent = `${ index + 1 } ` ;
921+ if ( isFailed ) {
922+ navItem . className = 'patch-nav-item patch-error' ;
923+ navItem . title = `Patch ${ patchNum } : Review failed (timeout or error)` ;
924+ } else if ( hasCommentsInline ) {
925+ navItem . className = 'patch-nav-item has-comments' ;
926+ navItem . title = `Patch ${ patchNum } : Has review comments` ;
927+ } else {
928+ navItem . className = 'patch-nav-item no-comments' ;
929+ navItem . title = `Patch ${ patchNum } : No comments` ;
930+ }
931+ navItem . textContent = `${ patchNum } ` ;
703932 navItem . href = `#${ patchId } ` ;
704- navItem . title = hasCommentsInline ? `Patch ${ index + 1 } : Has review comments` : `Patch ${ index + 1 } : No comments` ;
705933 navContainer . appendChild ( navItem ) ;
706934
707935 // Create patch review section
@@ -713,11 +941,17 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
713941 headerDiv . className = 'patch-header' ;
714942
715943 const headerTitle = document . createElement ( 'span' ) ;
716- headerTitle . textContent = `Patch ${ index + 1 } ` ;
944+ headerTitle . textContent = `Patch ${ patchNum } ` ;
717945 headerDiv . appendChild ( headerTitle ) ;
718946
719- // Show "No review comments" based on inline format
720- if ( ! hasCommentsInline ) {
947+ // Show status in header based on error or inline format
948+ if ( isFailed ) {
949+ const errorSpan = document . createElement ( 'span' ) ;
950+ errorSpan . className = 'no-comments-inline' ;
951+ errorSpan . style . color = 'var(--status-error-text)' ;
952+ errorSpan . textContent = 'Review failed' ;
953+ headerDiv . appendChild ( errorSpan ) ;
954+ } else if ( ! hasCommentsInline ) {
721955 const noCommentsSpan = document . createElement ( 'span' ) ;
722956 noCommentsSpan . className = 'no-comments-inline' ;
723957 noCommentsSpan . textContent = 'No review comments' ;
@@ -726,8 +960,18 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
726960
727961 patchDiv . appendChild ( headerDiv ) ;
728962
729- // Only add content div if there are comments in current format
730- if ( hasComments ) {
963+ // Add content div - error message for failed patches, review content otherwise
964+ if ( isFailed ) {
965+ const contentDiv = document . createElement ( 'div' ) ;
966+ contentDiv . className = 'patch-content' ;
967+
968+ const errorDiv = document . createElement ( 'div' ) ;
969+ errorDiv . className = 'patch-error-content' ;
970+ errorDiv . textContent = 'This patch could not be reviewed due to a timeout or error. Please try resubmitting the review.' ;
971+ contentDiv . appendChild ( errorDiv ) ;
972+
973+ patchDiv . appendChild ( contentDiv ) ;
974+ } else if ( hasComments ) {
731975 const contentDiv = document . createElement ( 'div' ) ;
732976 contentDiv . className = 'patch-content' ;
733977
0 commit comments