@@ -320,9 +320,111 @@ async function renderRequestFeed(hookId) {
320320 await loadRequests ( false ) ;
321321}
322322
323+ // --- Request Inspector View ---
323324async function renderRequestDetail ( hookId , requestId ) {
324325 show ( 'view-detail' ) ;
325- $ ( '#view-detail' ) . innerHTML = '<p class="empty-state">Loading request...</p>' ;
326+ const el = $ ( '#view-detail' ) ;
327+ el . innerHTML = '<p class="empty-state">Loading request...</p>' ;
328+
329+ const req = await api . get ( '/api/hooks/' + hookId + '/requests/' + requestId ) ;
330+
331+ // Decode body from base64
332+ let bodyText = '' ;
333+ let bodyDisplay = '' ;
334+ let isBinary = false ;
335+ if ( ! req . body || req . body === '' ) {
336+ bodyDisplay = '<span style="color:var(--text-muted);">(empty body)</span>' ;
337+ } else {
338+ try {
339+ const raw = atob ( req . body ) ;
340+ // Check if it's valid UTF-8 text
341+ const bytes = new Uint8Array ( raw . length ) ;
342+ for ( let i = 0 ; i < raw . length ; i ++ ) bytes [ i ] = raw . charCodeAt ( i ) ;
343+ bodyText = new TextDecoder ( 'utf-8' , { fatal : true } ) . decode ( bytes ) ;
344+
345+ // Try to pretty-print JSON
346+ try {
347+ const parsed = JSON . parse ( bodyText ) ;
348+ bodyDisplay = '<pre class="code-block">' + escapeHtml ( JSON . stringify ( parsed , null , 2 ) ) + '</pre>' ;
349+ } catch {
350+ bodyDisplay = '<pre class="code-block">' + escapeHtml ( bodyText ) + '</pre>' ;
351+ }
352+ } catch {
353+ isBinary = true ;
354+ bodyDisplay = '<span style="color:var(--text-muted);">(binary payload, ' + formatBytes ( req . content_length ) + ')</span>' ;
355+ }
356+ }
357+
358+ // Format timestamp
359+ const date = new Date ( req . received_at * 1000 ) ;
360+ const timestamp = date . toISOString ( ) . replace ( 'T' , ' ' ) . replace ( / \. .* $ / , '' ) + ' UTC' ;
361+
362+ let html = '<div class="breadcrumb">' ;
363+ html += '<a href="#/hooks">Hooks</a> / ' ;
364+ html += '<a href="#/hooks/' + escapeHtml ( hookId ) + '">Requests</a> / ' ;
365+ html += escapeHtml ( requestId . substring ( 0 , 8 ) ) + '...' ;
366+ html += '</div>' ;
367+
368+ // Summary
369+ html += '<div class="card" style="margin-bottom:16px;">' ;
370+ html += '<div class="card-header">' ;
371+ html += '<span>' + methodBadge ( req . method ) + ' <span style="color:var(--text-muted);">' + escapeHtml ( req . path ) + '</span></span>' ;
372+ html += '</div>' ;
373+ html += '<div class="card-meta">' ;
374+ html += '<span>' + formatBytes ( req . content_length ) + '</span>' ;
375+ html += '<span>' + escapeHtml ( req . source_ip ) + '</span>' ;
376+ html += '<span>' + escapeHtml ( timestamp ) + '</span>' ;
377+ html += '<span>' + timeAgo ( req . received_at ) + '</span>' ;
378+ html += '</div>' ;
379+ html += '</div>' ;
380+
381+ // Headers
382+ const headers = req . headers || { } ;
383+ const headerKeys = Object . keys ( headers ) ;
384+ html += '<div class="section-header">' ;
385+ html += '<span class="section-title">Headers (' + headerKeys . length + ')</span>' ;
386+ if ( headerKeys . length > 0 ) {
387+ html += '<button class="btn btn-small" id="btn-copy-headers">copy</button>' ;
388+ }
389+ html += '</div>' ;
390+
391+ if ( headerKeys . length > 0 ) {
392+ html += '<table class="kv-table">' ;
393+ for ( const key of headerKeys . sort ( ) ) {
394+ html += '<tr><th>' + escapeHtml ( key ) + '</th><td>' + escapeHtml ( headers [ key ] ) + '</td></tr>' ;
395+ }
396+ html += '</table>' ;
397+ } else {
398+ html += '<p style="color:var(--text-muted);font-size:13px;">(no headers)</p>' ;
399+ }
400+
401+ // Body
402+ html += '<div class="section-header" style="margin-top:20px;">' ;
403+ html += '<span class="section-title">Body</span>' ;
404+ if ( bodyText && ! isBinary ) {
405+ html += '<button class="btn btn-small" id="btn-copy-body">copy</button>' ;
406+ }
407+ html += '</div>' ;
408+ html += bodyDisplay ;
409+
410+ el . innerHTML = html ;
411+
412+ // Copy headers handler
413+ const copyHeadersBtn = document . getElementById ( 'btn-copy-headers' ) ;
414+ if ( copyHeadersBtn ) {
415+ copyHeadersBtn . addEventListener ( 'click' , ( ) => {
416+ const text = headerKeys . sort ( ) . map ( k => k + ': ' + headers [ k ] ) . join ( '\n' ) ;
417+ copyText ( text ) ;
418+ } ) ;
419+ }
420+
421+ // Copy body handler
422+ const copyBodyBtn = document . getElementById ( 'btn-copy-body' ) ;
423+ if ( copyBodyBtn ) {
424+ copyBodyBtn . addEventListener ( 'click' , ( ) => {
425+ copyText ( bodyText ) ;
426+ } ) ;
427+ }
326428}
327429
328430// --- Init ---
0 commit comments