Skip to content

Commit a628f78

Browse files
committed
Merge feat/HB-023-request-inspector: Request inspector with headers and body
2 parents e6b5aa1 + e256efb commit a628f78

1 file changed

Lines changed: 103 additions & 1 deletion

File tree

ui/assets/app.js

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,9 +320,111 @@ async function renderRequestFeed(hookId) {
320320
await loadRequests(false);
321321
}
322322

323+
// --- Request Inspector View ---
323324
async 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

Comments
 (0)