mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-26 00:44:02 +00:00
This commit is contained in:
+1
-1
@@ -977,7 +977,7 @@
|
||||
<h3>🛤️ Route Pattern Analysis</h3>
|
||||
<p>Click a route to see details. Most common subpaths — reveals backbone routes, bottlenecks, and preferred relay chains.</p>
|
||||
<label style="display:inline-flex;align-items:center;gap:6px;margin-bottom:12px;cursor:pointer;font-size:0.9em">
|
||||
<input type="checkbox" id="hideCollisions" ${localStorage.getItem('subpath-hide-collisions') === '1' ? 'checked' : ''}> Hide likely prefix collisions (self-loops)
|
||||
<input type="checkbox" id="hideCollisions" aria-label="Hide likely prefix collisions" ${localStorage.getItem('subpath-hide-collisions') === '1' ? 'checked' : ''}> Hide likely prefix collisions (self-loops)
|
||||
</label>
|
||||
<div class="subpath-jump-nav">
|
||||
<span>Jump to:</span>
|
||||
|
||||
@@ -394,6 +394,54 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
else navigate();
|
||||
});
|
||||
|
||||
/**
|
||||
* Reusable ARIA tab-bar initialiser.
|
||||
* Adds role="tablist" to container, role="tab" + aria-selected to each button,
|
||||
* and arrow-key navigation between tabs.
|
||||
* @param {HTMLElement} container - the tab bar element
|
||||
* @param {Function} [onChange] - optional callback(activeBtn) on tab change
|
||||
*/
|
||||
function initTabBar(container, onChange) {
|
||||
if (!container || container.getAttribute('role') === 'tablist') return;
|
||||
container.setAttribute('role', 'tablist');
|
||||
const tabs = Array.from(container.querySelectorAll('button, [data-tab], [data-obs]'));
|
||||
tabs.forEach(btn => {
|
||||
btn.setAttribute('role', 'tab');
|
||||
const isActive = btn.classList.contains('active');
|
||||
btn.setAttribute('aria-selected', String(isActive));
|
||||
btn.setAttribute('tabindex', isActive ? '0' : '-1');
|
||||
// Link to panel if aria-controls target exists
|
||||
const panelId = btn.dataset.tab || btn.dataset.obs;
|
||||
if (panelId && document.getElementById(panelId)) {
|
||||
btn.setAttribute('aria-controls', panelId);
|
||||
}
|
||||
});
|
||||
container.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[role="tab"]');
|
||||
if (!btn || !container.contains(btn)) return;
|
||||
tabs.forEach(b => { b.setAttribute('aria-selected', 'false'); b.setAttribute('tabindex', '-1'); });
|
||||
btn.setAttribute('aria-selected', 'true');
|
||||
btn.setAttribute('tabindex', '0');
|
||||
if (onChange) onChange(btn);
|
||||
});
|
||||
container.addEventListener('keydown', (e) => {
|
||||
const btn = e.target.closest('[role="tab"]');
|
||||
if (!btn) return;
|
||||
let idx = tabs.indexOf(btn), next = -1;
|
||||
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = (idx + 1) % tabs.length;
|
||||
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') next = (idx - 1 + tabs.length) % tabs.length;
|
||||
else if (e.key === 'Home') next = 0;
|
||||
else if (e.key === 'End') next = tabs.length - 1;
|
||||
if (next < 0) return;
|
||||
e.preventDefault();
|
||||
tabs.forEach(b => { b.setAttribute('aria-selected', 'false'); b.setAttribute('tabindex', '-1'); });
|
||||
tabs[next].setAttribute('aria-selected', 'true');
|
||||
tabs[next].setAttribute('tabindex', '0');
|
||||
tabs[next].focus();
|
||||
tabs[next].click();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make table columns resizable with drag handles. Widths saved to localStorage.
|
||||
* Call after table is in DOM. Re-call safe (idempotent per table).
|
||||
|
||||
+25
-3
@@ -43,12 +43,19 @@
|
||||
</div>
|
||||
</div>`;
|
||||
loadFullNode(directNode);
|
||||
// Escape to go back to nodes list
|
||||
document.addEventListener('keydown', function nodesEsc(e) {
|
||||
if (e.key === 'Escape') {
|
||||
document.removeEventListener('keydown', nodesEsc);
|
||||
location.hash = '#/nodes';
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
app.innerHTML = `<div class="nodes-page">
|
||||
<div class="nodes-topbar">
|
||||
<input type="text" class="nodes-search" id="nodeSearch" placeholder="Search nodes by name…">
|
||||
<input type="text" class="nodes-search" id="nodeSearch" placeholder="Search nodes by name…" aria-label="Search nodes by name">
|
||||
<div class="nodes-counts" id="nodeCounts"></div>
|
||||
</div>
|
||||
<div class="split-layout">
|
||||
@@ -215,7 +222,7 @@
|
||||
${TABS.map(t => `<button class="node-tab ${activeTab === t.key ? 'active' : ''}" data-tab="${t.key}">${t.label}</button>`).join('')}
|
||||
</div>
|
||||
<div class="nodes-filters">
|
||||
<select id="nodeLastHeard">
|
||||
<select id="nodeLastHeard" aria-label="Filter by last heard time">
|
||||
<option value="">Last Heard: Any</option>
|
||||
<option value="1h" ${lastHeard==='1h'?'selected':''}>1 hour</option>
|
||||
<option value="6h" ${lastHeard==='6h'?'selected':''}>6 hours</option>
|
||||
@@ -223,7 +230,7 @@
|
||||
<option value="7d" ${lastHeard==='7d'?'selected':''}>7 days</option>
|
||||
<option value="30d" ${lastHeard==='30d'?'selected':''}>30 days</option>
|
||||
</select>
|
||||
<select id="nodeSort">
|
||||
<select id="nodeSort" aria-label="Sort nodes">
|
||||
<option value="lastSeen" ${sortBy==='lastSeen'?'selected':''}>Sort: Last Seen</option>
|
||||
<option value="name" ${sortBy==='name'?'selected':''}>Sort: Name</option>
|
||||
<option value="packetCount" ${sortBy==='packetCount'?'selected':''}>Sort: Adverts</option>
|
||||
@@ -243,6 +250,8 @@
|
||||
</table>`;
|
||||
|
||||
// Tab clicks
|
||||
const nodeTabs = document.getElementById('nodeTabs');
|
||||
initTabBar(nodeTabs);
|
||||
el.querySelectorAll('.node-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => { activeTab = btn.dataset.tab; loadNodes(); });
|
||||
});
|
||||
@@ -270,6 +279,19 @@
|
||||
tbody.addEventListener('keydown', handler);
|
||||
}
|
||||
|
||||
// Escape to close node detail panel
|
||||
document.addEventListener('keydown', function nodesPanelEsc(e) {
|
||||
if (e.key === 'Escape') {
|
||||
const panel = document.getElementById('nodesRight');
|
||||
if (panel && !panel.classList.contains('empty')) {
|
||||
panel.classList.add('empty');
|
||||
panel.innerHTML = '<span>Select a node to view details</span>';
|
||||
selectedKey = null;
|
||||
renderRows();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
renderRows();
|
||||
}
|
||||
|
||||
|
||||
+17
-4
@@ -231,14 +231,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-bar" id="pktFilters">
|
||||
<input type="text" placeholder="Packet hash…" id="fHash">
|
||||
<input type="text" placeholder="Packet hash…" id="fHash" aria-label="Filter by packet hash">
|
||||
<div class="node-filter-wrap" style="position:relative">
|
||||
<input type="text" placeholder="Node name…" id="fNode" autocomplete="off" role="combobox" aria-expanded="false" aria-owns="fNodeDropdown" aria-activedescendant="" aria-autocomplete="list">
|
||||
<div class="node-filter-dropdown hidden" id="fNodeDropdown" role="listbox"></div>
|
||||
</div>
|
||||
<select id="fObserver"><option value="">All Observers</option></select>
|
||||
<select id="fRegion"><option value="">All Regions</option></select>
|
||||
<select id="fType"><option value="">All Types</option></select>
|
||||
<select id="fObserver" aria-label="Filter by observer"><option value="">All Observers</option></select>
|
||||
<select id="fRegion" aria-label="Filter by region"><option value="">All Regions</option></select>
|
||||
<select id="fType" aria-label="Filter by packet type"><option value="">All Types</option></select>
|
||||
<button class="btn ${groupByHash ? 'active' : ''}" id="fGroup">Group by Hash</button>
|
||||
</div>
|
||||
<table class="data-table" id="pktTable">
|
||||
@@ -370,6 +370,19 @@
|
||||
pktBody.addEventListener('keydown', handler);
|
||||
}
|
||||
|
||||
// Escape to close packet detail panel
|
||||
document.addEventListener('keydown', function pktEsc(e) {
|
||||
if (e.key === 'Escape') {
|
||||
const panel = document.getElementById('pktRight');
|
||||
if (panel && !panel.classList.contains('empty')) {
|
||||
panel.classList.add('empty');
|
||||
panel.innerHTML = '<div class="panel-resize-handle" id="pktResizeHandle"></div><span>Select a packet to view details</span>';
|
||||
selectedId = null;
|
||||
renderTableRows();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
renderTableRows();
|
||||
makeColumnsResizable('#pktTable', 'meshcore-pkt-col-widths');
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
<h2>🔍 Packet Trace</h2>
|
||||
</div>
|
||||
<div class="trace-search">
|
||||
<input type="text" id="traceHashInput" placeholder="Enter packet hash…" value="${urlHash}">
|
||||
<input type="text" id="traceHashInput" placeholder="Enter packet hash…" value="${urlHash}" aria-label="Packet hash to trace">
|
||||
<button class="btn-primary" id="traceBtn">Trace</button>
|
||||
</div>
|
||||
<div id="traceResults"></div>
|
||||
|
||||
Reference in New Issue
Block a user