Page through the received routes, on the status page and in the API

/api/routes/received now returns one page: {routes, next_after,
prev_before}. Pages are in prefix order and begin at a prefix (after= or
before=), not at a count, so a table that changes between two pages does
not make them overlap or leave a gap. A page holds whole prefixes, about
`limit` routes' worth.

The table keeps prefixes in no order, so a page is found by looking at
every prefix once and keeping the nearest few; the routes of the others
are not touched (RibTable::for_each_prefix).

The status page shows about 100 routes with first / previous / next
links; the place is kept in the page's address with the lookup.

The response of /api/routes/received was an array before: this changes
its shape.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-02 14:33:38 +02:00
1 parent f9ef781d51
commit 1f84235e87
10 files changed
+536 -67

No files matched your search

+61 -10
View File
@@ -32,6 +32,8 @@ summary:hover { text-decoration: underline; }
#config-text { border: 1px solid #99a; padding: 4px 6px; margin: 4px 0; font-size: 12px; overflow-x: auto; }
address { font-size: smaller; margin-top: 2em; color: #444; }
form.lookup { margin: 4px 0; }
.pager { margin: 4px 0; }
.pager a, .pager span { margin-right: 1em; }
form.lookup input[type=text] { font-family: monospace; }
</style>
</head>
@@ -85,10 +87,12 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
<a href="#" id="lookup-clear">show all</a>
<span id="lookup-note"></span>
</form>
<div class="pager" id="received-pager"></div>
<table class="data">
<thead><tr><th>prefix</th><th>from peer</th><th>AS path</th><th>next hop</th><th>origin</th><th>MED</th><th>local pref</th><th>communities</th><th>large communities</th><th>RPKI</th><th>ASPA</th><th>best</th></tr></thead>
<tbody id="received-body"></tbody>
</table>
<div class="pager" id="received-pager-below"></div>
<h1 id="refused">Refused routes (<span id="refused-count">...</span>)</h1>
<div class="dim">Routes that peers announced and their import policies did not take. The counts are since the daemon started; the latest refusals of each peer are kept.</div>
@@ -114,7 +118,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
const $ = id => document.getElementById(id);
const esc = s => String(s).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
const state = { status: null, peers: [], local: [], received: [], refused: [], rpki: null };
const state = { status: null, peers: [], local: [], received: [], receivedPage: {}, refused: [], rpki: null };
const DASH = '<span class="dim">-</span>';
// ---- helpers -----------------------------------------------------------
@@ -190,10 +194,29 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
// The query lives in the page's address, so that it survives a reload and
// auto-refresh, and can be passed on.
const QUERY_KEYS = ['q', 'mode', 'peer', 'best'];
// Which page of the listing is shown lives there too: the prefix the
// page comes after, or the one it comes before.
const PAGE_KEYS = ['after', 'before'];
// About this many routes to a page. A page holds whole prefixes.
const PAGE_ROUTES = 100;
function lookup() {
const here = new URLSearchParams(location.search);
return { q: (here.get('q') || '').trim(), mode: here.get('mode') || 'exact', peer: here.get('peer') || '', best: here.get('best') === '1' };
}
function pagePlace() {
const here = new URLSearchParams(location.search);
return { after: here.get('after') || '', before: here.get('before') || '' };
}
// Goes to another page: {after: prefix}, {before: prefix}, or nothing
// for the first.
function setPage(place) {
const url = new URL(location.href);
PAGE_KEYS.forEach(k => url.searchParams.delete(k));
PAGE_KEYS.forEach(k => { if (place && place[k]) url.searchParams.set(k, place[k]); });
history.replaceState(null, '', url.pathname + url.search + '#received');
renderRefreshControl();
loadReceived().catch(console.error);
}
function isLookup() { const l = lookup(); return !!(l.q || l.peer || l.best); }
// What to ask the API for: an address finds every prefix that covers it.
function receivedUrl() {
@@ -201,8 +224,10 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
if (l.q) ask.set(l.mode === 'within' ? 'within' : l.q.includes('/') ? 'prefix' : 'address', l.q);
if (l.peer) ask.set('peer', l.peer);
if (l.best) ask.set('best', 'true');
const query = ask.toString();
return '/api/routes/received' + (query ? `?${query}` : '');
const place = pagePlace();
if (place.after) ask.set('after', place.after); else if (place.before) ask.set('before', place.before);
ask.set('limit', PAGE_ROUTES);
return `/api/routes/received?${ask}`;
}
function renderPeerChoices() {
const chosen = lookup().peer;
@@ -219,7 +244,8 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
}
function setLookup(l) {
const url = new URL(location.href);
QUERY_KEYS.forEach(k => url.searchParams.delete(k));
// Another question starts at its first page.
QUERY_KEYS.concat(PAGE_KEYS).forEach(k => url.searchParams.delete(k));
if (l) {
if (l.q) url.searchParams.set('q', l.q);
if (l.q && l.mode === 'within') url.searchParams.set('mode', 'within');
@@ -244,11 +270,14 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
function renderReceived() {
const b = $('received-body'), st = state.status, looking = isLookup();
const page = state.receivedPage, paged = !!(page.next_after || page.prev_before);
const shown = paged ? `${state.received.length} on this page` : `${state.received.length}`;
$('received-count').textContent = looking
? `${state.received.length} found${st ? ` among ${st.received_routes}` : ''}`
: st && st.received_routes > state.received.length
? `showing ${state.received.length} of ${st.received_routes}, for ${st.received_prefixes} prefixes`
: `${state.received.length}${st ? `, for ${st.received_prefixes} prefixes` : ''}`;
? `${shown}${paged ? '' : ' found'}${st ? ` among ${st.received_routes}` : ''}`
: paged && st
? `${st.received_routes}, for ${st.received_prefixes} prefixes; ${shown}`
: `${shown}${st ? `, for ${st.received_prefixes} prefixes` : ''}`;
renderPager();
if (!state.received.length) {
setHTML(b, `<tr><td colspan="12" class="dim">${looking ? 'No route matches.' : 'No routes received from peers.'}</td></tr>`); return;
}
@@ -297,15 +326,30 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
// ---- data loading ------------------------------------------------------
async function loadStatus() { state.status = await getJSON('/api/status'); renderStatus(); renderLocal(); renderReceived(); }
async function loadPeers() { state.peers = await getJSON('/api/peers'); renderPeers(); }
// The way to the pages before and after this one, where there are any.
function renderPager() {
const page = state.receivedPage, place = pagePlace();
const paged = page.next_after || page.prev_before;
const link = (label, to, there) => there ? `<a href="#" data-page='${esc(JSON.stringify(to))}'>${label}</a>` : `<span class="dim">${label}</span>`;
const range = state.received.length
? `<span class="mono">${esc(state.received[0].prefix)} &ndash; ${esc(state.received[state.received.length - 1].prefix)}</span>` : '';
const html = !paged && !place.after && !place.before ? '' :
link('&laquo; first', {}, page.prev_before || (!state.received.length && (place.after || place.before))) +
link('&lsaquo; previous', { before: page.prev_before }, page.prev_before) +
link('next &rsaquo;', { after: page.next_after }, page.next_after) + range;
for (const id of ['received-pager', 'received-pager-below']) setHTML($(id), html);
}
async function loadLocal() { state.local = await getJSON('/api/routes/local'); renderLocal(); }
async function loadReceived() {
const note = $('lookup-note');
try {
state.received = await getJSON(receivedUrl());
const page = await getJSON(receivedUrl());
state.received = page.routes; state.receivedPage = page;
note.textContent = '';
} catch (e) {
// A query the server could not make sense of: say what it said.
state.received = [];
state.received = []; state.receivedPage = {};
note.innerHTML = `<span class="bad">${esc(e.message.replace(/^[^:]*: \d+ /, ''))}</span>`;
}
renderReceived();
@@ -376,6 +420,13 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
setLookup({ q: $('lookup-q').value.trim(), mode: $('lookup-mode').value, peer: $('lookup-peer').value, best: $('lookup-best').checked });
});
$('lookup-clear').addEventListener('click', e => { e.preventDefault(); setLookup(null); });
for (const id of ['received-pager', 'received-pager-below']) {
$(id).addEventListener('click', e => {
const to = e.target.closest('a[data-page]');
if (!to) return;
e.preventDefault(); setPage(JSON.parse(to.dataset.page));
});
}
loadAll().catch(e => logEvent(`<span class="bad">${esc(e.message)}</span>`));
if (REFRESH) {
connect();