Skip to content

Commit 0fba30a

Browse files
copyleftdevclaude
andcommitted
feat(dashboard): HB-022 — request feed with pagination
Implement request feed view: show captured requests per hook with method badges, path, size, source IP, and relative time. Load more pagination, hook name header with ingestion URL, breadcrumb back to hook list, empty state, and "Showing X of Y" counter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8693708 commit 0fba30a

1 file changed

Lines changed: 92 additions & 1 deletion

File tree

ui/assets/app.js

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,100 @@ async function renderHookList() {
224224
});
225225
}
226226

227+
// --- Request Feed View ---
227228
async function renderRequestFeed(hookId) {
228229
show('view-requests');
229-
$('#view-requests').innerHTML = '<p class="empty-state">Loading requests...</p>';
230+
const el = $('#view-requests');
231+
el.innerHTML = '<p class="empty-state">Loading requests...</p>';
232+
233+
const hook = await api.get('/api/hooks/' + hookId);
234+
const origin = location.origin;
235+
const ingestUrl = origin + hook.url;
236+
237+
let offset = 0;
238+
const limit = 50;
239+
240+
async function loadRequests(append) {
241+
const data = await api.get('/api/hooks/' + hookId + '/requests?limit=' + limit + '&offset=' + offset);
242+
const requests = data.requests || [];
243+
const total = data.total || 0;
244+
245+
if (!append) {
246+
let html = '<div class="breadcrumb"><a href="#/hooks">Hooks</a> / ' + escapeHtml(hook.name) + '</div>';
247+
html += '<div class="url-box" style="margin-bottom:16px;">';
248+
html += '<span style="flex:1;">' + escapeHtml(ingestUrl) + '</span>';
249+
html += '<button class="btn btn-small btn-copy-feed-url" data-url="' + escapeHtml(ingestUrl) + '">copy</button>';
250+
html += '</div>';
251+
html += '<div class="toolbar">';
252+
html += '<span class="status-text" id="feed-status"></span>';
253+
html += '</div>';
254+
html += '<div id="request-list"></div>';
255+
html += '<div id="feed-footer" style="text-align:center;margin:16px 0;"></div>';
256+
el.innerHTML = html;
257+
258+
el.querySelector('.btn-copy-feed-url').addEventListener('click', (e) => {
259+
e.stopPropagation();
260+
copyText(e.target.dataset.url);
261+
});
262+
}
263+
264+
const listEl = document.getElementById('request-list');
265+
const statusEl = document.getElementById('feed-status');
266+
const footerEl = document.getElementById('feed-footer');
267+
268+
const currentCount = listEl.querySelectorAll('.card').length + requests.length;
269+
statusEl.textContent = 'Showing ' + currentCount + ' of ' + total;
270+
271+
if (total === 0 && !append) {
272+
listEl.innerHTML = '<p class="empty-state">No requests captured yet. Send a webhook to ' + escapeHtml(ingestUrl) + '</p>';
273+
footerEl.innerHTML = '';
274+
return;
275+
}
276+
277+
let cards = '';
278+
for (const req of requests) {
279+
cards += '<div class="card card-clickable" data-hook-id="' + escapeHtml(hookId) + '" data-request-id="' + escapeHtml(req.request_id) + '">';
280+
cards += '<div class="card-header">';
281+
cards += '<span>' + methodBadge(req.method) + ' <span style="color:var(--text-muted);">' + escapeHtml(req.path) + '</span></span>';
282+
cards += '<span class="card-meta" style="margin:0;">' + timeAgo(req.received_at) + '</span>';
283+
cards += '</div>';
284+
cards += '<div class="card-meta">';
285+
cards += '<span>' + formatBytes(req.content_length) + '</span>';
286+
cards += '<span>' + escapeHtml(req.source_ip) + '</span>';
287+
cards += '</div>';
288+
cards += '</div>';
289+
}
290+
291+
if (append) {
292+
listEl.insertAdjacentHTML('beforeend', cards);
293+
} else {
294+
listEl.innerHTML = cards;
295+
}
296+
297+
// Click to view detail
298+
listEl.querySelectorAll('.card-clickable').forEach(card => {
299+
card.onclick = () => {
300+
navigate('/hooks/' + card.dataset.hookId + '/requests/' + card.dataset.requestId);
301+
};
302+
});
303+
304+
// Load more
305+
if (currentCount < total) {
306+
footerEl.innerHTML = '<button class="btn" id="btn-load-more">Load more</button>';
307+
document.getElementById('btn-load-more').addEventListener('click', async () => {
308+
offset += limit;
309+
try {
310+
await loadRequests(true);
311+
} catch (err) {
312+
toast(err.error || 'Failed to load more', 'error');
313+
}
314+
});
315+
} else {
316+
footerEl.innerHTML = '';
317+
}
318+
}
319+
320+
await loadRequests(false);
230321
}
231322

232323
async function renderRequestDetail(hookId, requestId) {

0 commit comments

Comments
 (0)