feat: observation sort toggle — Observer (default) or Path length

Two sort modes for expanded packet groups:
- Observer: group by observer, earliest first, ascending time within
- Path length: shortest paths first, alphabetical observer within

Sort bar appears above expanded children with bold active mode.
This commit is contained in:
you
2026-03-21 23:31:21 +00:00
parent 9e718bd090
commit 45df19b51b
2 changed files with 53 additions and 16 deletions
+1 -1
View File
@@ -84,7 +84,7 @@
<script src="hop-resolver.js?v=1774126708"></script>
<script src="app.js?v=1774126708"></script>
<script src="home.js?v=1774042199"></script>
<script src="packets.js?v=1774134942"></script>
<script src="packets.js?v=1774135881"></script>
<script src="map.js?v=1774126708" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1774331200" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1774126708" onerror="console.error('Failed to load:', this.src)"></script>
+52 -15
View File
@@ -600,6 +600,13 @@
if (hash) selectPacket(null, hash);
else selectPacket(Number(value));
}
else if (action === 'sort-group') {
e.preventDefault();
const sortType = btn.dataset.sort;
groupSortModes[value] = sortType;
const group = packets.find(p => p.hash === value);
if (group) { sortGroupChildren(group); renderTableRows(); }
}
else if (action === 'select-observation') {
const parentHash = row.dataset.parentHash;
const group = packets.find(p => p.hash === parentHash);
@@ -692,6 +699,10 @@
</tr>`;
// Child rows (loaded async when expanded)
if (isExpanded && p._children) {
const sortMode = groupSortModes[p.hash] || SORT_OBSERVER;
const obsLabel = sortMode === SORT_OBSERVER ? '<strong>Observer</strong>' : 'Observer';
const pathLabel = sortMode === SORT_PATH_LENGTH ? '<strong>Path length</strong>' : 'Path length';
html += `<tr class="group-sort-bar"><td colspan="10" style="padding:2px 8px;font-size:0.8em;color:var(--text-muted)">Sort: <a href="#" data-action="sort-group" data-value="${p.hash}" data-sort="${SORT_OBSERVER}" style="color:var(--primary);cursor:pointer">${obsLabel}</a> · <a href="#" data-action="sort-group" data-value="${p.hash}" data-sort="${SORT_PATH_LENGTH}" style="color:var(--primary);cursor:pointer">${pathLabel}</a></td></tr>`;
for (const c of p._children) {
const typeName = payloadTypeName(c.payload_type);
const typeClass = payloadTypeColor(c.payload_type);
@@ -1237,6 +1248,45 @@
} catch {}
})();
// Observation sort modes
const SORT_OBSERVER = 'observer';
const SORT_PATH_LENGTH = 'path';
const groupSortModes = {}; // hash → sort mode
function getPathHopCount(c) {
try { return JSON.parse(c.path_json || '[]').length; } catch { return 0; }
}
function sortGroupChildren(group) {
if (!group || !group._children) return;
const mode = groupSortModes[group.hash] || SORT_OBSERVER;
if (mode === SORT_PATH_LENGTH) {
group._children.sort((a, b) => {
const lenA = getPathHopCount(a), lenB = getPathHopCount(b);
if (lenA !== lenB) return lenA - lenB;
const oA = (a.observer_name || '').toLowerCase(), oB = (b.observer_name || '').toLowerCase();
return oA < oB ? -1 : oA > oB ? 1 : 0;
});
} else {
// Default: group by observer, earliest-observer first, then ascending time within each
const earliest = {};
for (const c of group._children) {
const obs = c.observer_name || c.observer || '';
const t = c.timestamp || c.rx_at || c.created_at || '';
if (!earliest[obs] || t < earliest[obs]) earliest[obs] = t;
}
group._children.sort((a, b) => {
const oA = a.observer_name || a.observer || '', oB = b.observer_name || b.observer || '';
const eA = earliest[oA] || '', eB = earliest[oB] || '';
if (eA !== eB) return eA < eB ? -1 : 1;
if (oA !== oB) return oA < oB ? -1 : 1;
const tA = a.timestamp || a.rx_at || '', tB = b.timestamp || b.rx_at || '';
return tA < tB ? -1 : tA > tB ? 1 : 0;
});
}
}
// Global handlers
async function pktToggleGroup(hash) {
if (expandedHashes.has(hash)) {
@@ -1253,21 +1303,8 @@
if (group && data.observations) {
group._children = data.observations.map(o => ({...pkt, ...o, _isObservation: true}));
group._fetchedData = data;
// Sort: group by observer, earliest-observer first, then by time within each observer
const earliest = {};
for (const c of group._children) {
const obs = c.observer_name || c.observer || '';
const t = c.timestamp || c.rx_at || c.created_at || '';
if (!earliest[obs] || t < earliest[obs]) earliest[obs] = t;
}
group._children.sort((a, b) => {
const oA = a.observer_name || a.observer || '', oB = b.observer_name || b.observer || '';
const eA = earliest[oA] || '', eB = earliest[oB] || '';
if (eA !== eB) return eA < eB ? -1 : 1;
if (oA !== oB) return oA < oB ? -1 : 1;
const tA = a.timestamp || a.rx_at || '', tB = b.timestamp || b.rx_at || '';
return tA < tB ? -1 : tA > tB ? 1 : 0;
});
// Sort children based on current sort mode
sortGroupChildren(group);
}
// Resolve any new hops from children
const childHops = new Set();