diff --git a/public/index.html b/public/index.html
index 56f06adb..c954721e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -84,7 +84,7 @@
-
+
diff --git a/public/packets.js b/public/packets.js
index 23809051..dba51322 100644
--- a/public/packets.js
+++ b/public/packets.js
@@ -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 @@
`;
// Child rows (loaded async when expanded)
if (isExpanded && p._children) {
+ const sortMode = groupSortModes[p.hash] || SORT_OBSERVER;
+ const obsLabel = sortMode === SORT_OBSERVER ? 'Observer' : 'Observer';
+ const pathLabel = sortMode === SORT_PATH_LENGTH ? 'Path length' : 'Path length';
+ html += `
| Sort: ${obsLabel} · ${pathLabel} |
`;
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();