feat: sortable ASPA audit lists (by ASN, frequency, or name)
This commit is contained in:
parent
21450ea0eb
commit
bd90113285
@ -225,6 +225,8 @@ a{color:var(--blue);text-decoration:none;transition:color .2s}a:hover{color:var(
|
|||||||
|
|
||||||
.show-more-btn{font-size:.8rem;color:var(--blue);cursor:pointer;padding:.5rem 0;margin-top:.25rem;transition:color .2s;user-select:none}
|
.show-more-btn{font-size:.8rem;color:var(--blue);cursor:pointer;padding:.5rem 0;margin-top:.25rem;transition:color .2s;user-select:none}
|
||||||
.show-more-btn:hover{color:var(--cyan);text-decoration:underline}
|
.show-more-btn:hover{color:var(--cyan);text-decoration:underline}
|
||||||
|
.sort-toggle{font-size:.7rem;color:var(--muted);cursor:pointer;padding:.2rem .5rem;border:1px solid var(--border);border-radius:4px;transition:all .2s;user-select:none}
|
||||||
|
.sort-toggle:hover{color:var(--blue);border-color:var(--blue)}
|
||||||
|
|
||||||
/* Routing Overview - Propagation Bars */
|
/* Routing Overview - Propagation Bars */
|
||||||
.routing-stats-row{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1.25rem}
|
.routing-stats-row{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1.25rem}
|
||||||
@ -532,6 +534,44 @@ function copyToClipboard(text, btn) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderAuditList(containerId, list, sortBy, type) {
|
||||||
|
var sorted = list.slice();
|
||||||
|
if (sortBy === 'asn') sorted.sort(function(a, b) { return a.asn - b.asn; });
|
||||||
|
else if (sortBy === 'frequency') sorted.sort(function(a, b) { return (b.frequency || 0) - (a.frequency || 0); });
|
||||||
|
else if (sortBy === 'name') sorted.sort(function(a, b) { return (a.name || '').localeCompare(b.name || ''); });
|
||||||
|
|
||||||
|
var labelId = type === 'missing' ? 'missingSortLabel' : 'extraSortLabel';
|
||||||
|
var labelEl = document.getElementById(labelId);
|
||||||
|
if (labelEl) labelEl.textContent = sortBy === 'asn' ? 'by ASN' : sortBy === 'frequency' ? 'by frequency' : 'by name';
|
||||||
|
|
||||||
|
var LIMIT = 5;
|
||||||
|
var h = '';
|
||||||
|
sorted.slice(0, LIMIT).forEach(function(item) {
|
||||||
|
h += '<div class="audit-row audit-' + type + '">';
|
||||||
|
h += '<span style="font-weight:600">' + asnLink(item.asn) + '</span>';
|
||||||
|
if (item.name && item.name !== 'AS' + item.asn) h += ' <span style="color:var(--muted)">' + escHtml(item.name) + '</span>';
|
||||||
|
if (type === 'missing') h += ' <span class="badge badge-orange">seen in ' + (item.frequency_pct || 0) + '% of paths (' + (item.frequency || 0) + ')</span>';
|
||||||
|
else h += ' <span class="badge badge-cyan">not seen in any path</span>';
|
||||||
|
h += '</div>';
|
||||||
|
});
|
||||||
|
if (sorted.length > LIMIT) {
|
||||||
|
var moreId = containerId + 'More';
|
||||||
|
h += '<div id="' + moreId + '" style="display:none">';
|
||||||
|
sorted.slice(LIMIT).forEach(function(item) {
|
||||||
|
h += '<div class="audit-row audit-' + type + '">';
|
||||||
|
h += '<span style="font-weight:600">' + asnLink(item.asn) + '</span>';
|
||||||
|
if (item.name && item.name !== 'AS' + item.asn) h += ' <span style="color:var(--muted)">' + escHtml(item.name) + '</span>';
|
||||||
|
if (type === 'missing') h += ' <span class="badge badge-orange">seen in ' + (item.frequency_pct || 0) + '% of paths (' + (item.frequency || 0) + ')</span>';
|
||||||
|
else h += ' <span class="badge badge-cyan">not seen in any path</span>';
|
||||||
|
h += '</div>';
|
||||||
|
});
|
||||||
|
h += '</div>';
|
||||||
|
var remaining = sorted.length - LIMIT;
|
||||||
|
h += '<div class="show-more-btn" onclick="var el=document.getElementById(\'' + moreId + '\');if(el.style.display===\'none\'){el.style.display=\'block\';this.textContent=\'Hide ' + remaining + ' entries\';}else{el.style.display=\'none\';this.textContent=\'Show ' + remaining + ' more...\';}">Show ' + remaining + ' more...</div>';
|
||||||
|
}
|
||||||
|
document.getElementById(containerId).innerHTML = h;
|
||||||
|
}
|
||||||
|
|
||||||
async function doLookup() {
|
async function doLookup() {
|
||||||
const raw = $('asnInput').value.trim().replace(/[^0-9]/g, '');
|
const raw = $('asnInput').value.trim().replace(/[^0-9]/g, '');
|
||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
@ -1197,57 +1237,26 @@ function renderAspaDeep(d) {
|
|||||||
h += '<div><span style="font-size:.7rem;color:var(--muted);text-transform:uppercase">Completeness</span><div style="font-size:1.3rem;font-weight:700;color:' + ((audit.completeness_pct || 0) >= 80 ? 'var(--green)' : 'var(--orange)') + '">' + (audit.completeness_pct || 0) + '%</div></div>';
|
h += '<div><span style="font-size:.7rem;color:var(--muted);text-transform:uppercase">Completeness</span><div style="font-size:1.3rem;font-weight:700;color:' + ((audit.completeness_pct || 0) >= 80 ? 'var(--green)' : 'var(--orange)') + '">' + (audit.completeness_pct || 0) + '%</div></div>';
|
||||||
h += '</div>';
|
h += '</div>';
|
||||||
|
|
||||||
// Missing from ASPA (collapsible after 5)
|
// Missing from ASPA (collapsible, sortable)
|
||||||
if (audit.missing_from_aspa && audit.missing_from_aspa.length > 0) {
|
if (audit.missing_from_aspa && audit.missing_from_aspa.length > 0) {
|
||||||
var missingList = audit.missing_from_aspa;
|
window._auditMissing = audit.missing_from_aspa;
|
||||||
var LIMIT = 5;
|
window._auditMissingSort = 'frequency';
|
||||||
h += '<div style="font-size:.8rem;font-weight:600;color:var(--orange);margin:.5rem 0 .3rem">Missing from ASPA Declaration (' + missingList.length + ')</div>';
|
h += '<div style="display:flex;align-items:center;gap:.75rem;margin:.5rem 0 .3rem">';
|
||||||
missingList.slice(0, LIMIT).forEach(function(m) {
|
h += '<span style="font-size:.8rem;font-weight:600;color:var(--orange)">Missing from ASPA Declaration (' + audit.missing_from_aspa.length + ')</span>';
|
||||||
h += '<div class="audit-row audit-missing">';
|
h += '<span class="sort-toggle" onclick="window._auditMissingSort=window._auditMissingSort===\'frequency\'?\'asn\':\'frequency\';renderAuditList(\'missingListEl\',window._auditMissing,window._auditMissingSort,\'missing\')" title="Toggle sort order">Sort: <span id="missingSortLabel">by frequency</span> ↕</span>';
|
||||||
h += '<span style="font-weight:600">' + asnLink(m.asn) + '</span>';
|
|
||||||
if (m.name && m.name !== 'AS' + m.asn) h += ' <span style="color:var(--muted)">' + escHtml(m.name) + '</span>';
|
|
||||||
h += ' <span class="badge badge-orange">seen in ' + (m.frequency_pct || 0) + '% of paths (' + (m.frequency || 0) + ')</span>';
|
|
||||||
h += '</div>';
|
h += '</div>';
|
||||||
});
|
h += '<div id="missingListEl"></div>';
|
||||||
if (missingList.length > LIMIT) {
|
|
||||||
var moreId = 'missingMore' + Date.now();
|
|
||||||
h += '<div id="' + moreId + '" style="display:none">';
|
|
||||||
missingList.slice(LIMIT).forEach(function(m) {
|
|
||||||
h += '<div class="audit-row audit-missing">';
|
|
||||||
h += '<span style="font-weight:600">' + asnLink(m.asn) + '</span>';
|
|
||||||
if (m.name && m.name !== 'AS' + m.asn) h += ' <span style="color:var(--muted)">' + escHtml(m.name) + '</span>';
|
|
||||||
h += ' <span class="badge badge-orange">seen in ' + (m.frequency_pct || 0) + '% of paths (' + (m.frequency || 0) + ')</span>';
|
|
||||||
h += '</div>';
|
|
||||||
});
|
|
||||||
h += '</div>';
|
|
||||||
h += '<div class="show-more-btn" onclick="var el=document.getElementById(\'' + moreId + '\');if(el.style.display===\'none\'){el.style.display=\'block\';this.textContent=\'Hide ' + (missingList.length - LIMIT) + ' entries\';}else{el.style.display=\'none\';this.textContent=\'Show ' + (missingList.length - LIMIT) + ' more...\';}">Show ' + (missingList.length - LIMIT) + ' more...</div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extra in ASPA (collapsible after 5)
|
// Extra in ASPA (collapsible, sortable)
|
||||||
if (audit.extra_in_aspa && audit.extra_in_aspa.length > 0) {
|
if (audit.extra_in_aspa && audit.extra_in_aspa.length > 0) {
|
||||||
var extraList = audit.extra_in_aspa;
|
window._auditExtra = audit.extra_in_aspa;
|
||||||
h += '<div style="font-size:.8rem;font-weight:600;color:var(--cyan);margin:.5rem 0 .3rem">Extra in ASPA (not seen in paths) (' + extraList.length + ')</div>';
|
window._auditExtraSort = 'asn';
|
||||||
extraList.slice(0, LIMIT).forEach(function(e) {
|
h += '<div style="display:flex;align-items:center;gap:.75rem;margin:.5rem 0 .3rem">';
|
||||||
h += '<div class="audit-row audit-extra">';
|
h += '<span style="font-size:.8rem;font-weight:600;color:var(--cyan)">Extra in ASPA (not seen in paths) (' + audit.extra_in_aspa.length + ')</span>';
|
||||||
h += '<span style="font-weight:600">' + asnLink(e.asn) + '</span>';
|
h += '<span class="sort-toggle" onclick="window._auditExtraSort=window._auditExtraSort===\'asn\'?\'name\':\'asn\';renderAuditList(\'extraListEl\',window._auditExtra,window._auditExtraSort,\'extra\')" title="Toggle sort order">Sort: <span id="extraSortLabel">by ASN</span> ↕</span>';
|
||||||
if (e.name && e.name !== 'AS' + e.asn) h += ' <span style="color:var(--muted)">' + escHtml(e.name) + '</span>';
|
|
||||||
h += ' <span class="badge badge-cyan">not seen in any path</span>';
|
|
||||||
h += '</div>';
|
h += '</div>';
|
||||||
});
|
h += '<div id="extraListEl"></div>';
|
||||||
if (extraList.length > LIMIT) {
|
|
||||||
var extraMoreId = 'extraMore' + Date.now();
|
|
||||||
h += '<div id="' + extraMoreId + '" style="display:none">';
|
|
||||||
extraList.slice(LIMIT).forEach(function(e) {
|
|
||||||
h += '<div class="audit-row audit-extra">';
|
|
||||||
h += '<span style="font-weight:600">' + asnLink(e.asn) + '</span>';
|
|
||||||
if (e.name && e.name !== 'AS' + e.asn) h += ' <span style="color:var(--muted)">' + escHtml(e.name) + '</span>';
|
|
||||||
h += ' <span class="badge badge-cyan">not seen in any path</span>';
|
|
||||||
h += '</div>';
|
|
||||||
});
|
|
||||||
h += '</div>';
|
|
||||||
h += '<div class="show-more-btn" onclick="var el=document.getElementById(\'' + extraMoreId + '\');if(el.style.display===\'none\'){el.style.display=\'block\';this.textContent=\'Hide ' + (extraList.length - LIMIT) + ' entries\';}else{el.style.display=\'none\';this.textContent=\'Show ' + (extraList.length - LIMIT) + ' more...\';}">Show ' + (extraList.length - LIMIT) + ' more...</div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((!audit.missing_from_aspa || audit.missing_from_aspa.length === 0) && (!audit.extra_in_aspa || audit.extra_in_aspa.length === 0)) {
|
if ((!audit.missing_from_aspa || audit.missing_from_aspa.length === 0) && (!audit.extra_in_aspa || audit.extra_in_aspa.length === 0)) {
|
||||||
@ -1316,6 +1325,14 @@ function renderAspaDeep(d) {
|
|||||||
|
|
||||||
h += '<div style="font-size:.7rem;color:var(--dim);margin-top:.75rem">ASPA verification completed in ' + (d.meta ? d.meta.duration_ms : '?') + 'ms | ' + (d.meta ? d.meta.paths_analyzed : '?') + ' of ' + (d.meta ? d.meta.total_paths_seen : '?') + ' paths analyzed</div>';
|
h += '<div style="font-size:.7rem;color:var(--dim);margin-top:.75rem">ASPA verification completed in ' + (d.meta ? d.meta.duration_ms : '?') + 'ms | ' + (d.meta ? d.meta.paths_analyzed : '?') + ' of ' + (d.meta ? d.meta.total_paths_seen : '?') + ' paths analyzed</div>';
|
||||||
$('aspaDeepContent').innerHTML = h;
|
$('aspaDeepContent').innerHTML = h;
|
||||||
|
|
||||||
|
// Initial render of sortable audit lists
|
||||||
|
if (window._auditMissing && window._auditMissing.length > 0) {
|
||||||
|
renderAuditList('missingListEl', window._auditMissing, 'frequency', 'missing');
|
||||||
|
}
|
||||||
|
if (window._auditExtra && window._auditExtra.length > 0) {
|
||||||
|
renderAuditList('extraListEl', window._auditExtra, 'asn', 'extra');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user