mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-04-26 13:57:21 +00:00
Compare commits
5 Commits
fix-ismobi
...
v3.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1cb84b596 | ||
|
|
711889c823 | ||
|
|
738d5fef39 | ||
|
|
8e6fc9602f | ||
|
|
e2556eaaff |
@@ -1344,6 +1344,20 @@ func (s *PacketStore) MaxTransmissionID() int {
|
||||
return maxID
|
||||
}
|
||||
|
||||
// MaxObservationID returns the highest observation ID in the store.
|
||||
func (s *PacketStore) MaxObservationID() int {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
maxID := 0
|
||||
for id := range s.byObsID {
|
||||
if id > maxID {
|
||||
maxID = id
|
||||
}
|
||||
}
|
||||
return maxID
|
||||
}
|
||||
|
||||
// --- Internal filter/query helpers ---
|
||||
|
||||
// filterPackets applies PacketQuery filters to the in-memory packet list.
|
||||
|
||||
@@ -166,6 +166,17 @@ func NewPoller(db *DB, hub *Hub, interval time.Duration) *Poller {
|
||||
func (p *Poller) Start() {
|
||||
lastID := p.db.GetMaxTransmissionID()
|
||||
lastObsID := p.db.GetMaxObservationID()
|
||||
// If the store already loaded data, use its max IDs as a floor.
|
||||
// This prevents replaying the entire DB when the DB query fails
|
||||
// (e.g., corrupted DB returns 0 from COALESCE).
|
||||
if p.store != nil {
|
||||
if storeMax := p.store.MaxTransmissionID(); storeMax > lastID {
|
||||
lastID = storeMax
|
||||
}
|
||||
if storeMaxObs := p.store.MaxObservationID(); storeMaxObs > lastObsID {
|
||||
lastObsID = storeMaxObs
|
||||
}
|
||||
}
|
||||
log.Printf("[poller] starting from transmission ID %d, obs ID %d, interval %v", lastID, lastObsID, p.interval)
|
||||
|
||||
ticker := time.NewTicker(p.interval)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "meshcore-analyzer",
|
||||
"version": "3.1.0",
|
||||
"version": "3.2.0",
|
||||
"description": "Community-run alternative to the closed-source `analyzer.letsmesh.net`. MQTT packet collection + open-source web analyzer for the Bay Area MeshCore mesh.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -412,6 +412,14 @@ function closeNav() {
|
||||
document.body.classList.remove('nav-open');
|
||||
var btn = document.getElementById('hamburger');
|
||||
if (btn) btn.setAttribute('aria-expanded', 'false');
|
||||
closeMoreMenu();
|
||||
}
|
||||
|
||||
function closeMoreMenu() {
|
||||
var menu = document.getElementById('navMoreMenu');
|
||||
var btn = document.getElementById('navMoreBtn');
|
||||
if (menu) menu.classList.remove('open');
|
||||
if (btn) btn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
function navigate() {
|
||||
@@ -448,6 +456,13 @@ function navigate() {
|
||||
document.querySelectorAll('.nav-link[data-route]').forEach(el => {
|
||||
el.classList.toggle('active', el.dataset.route === basePage);
|
||||
});
|
||||
// Update "More" button to show active state if a low-priority page is selected
|
||||
var moreBtn = document.getElementById('navMoreBtn');
|
||||
if (moreBtn) {
|
||||
var moreMenu = document.getElementById('navMoreMenu');
|
||||
var hasActiveMore = moreMenu && moreMenu.querySelector('.nav-link.active');
|
||||
moreBtn.classList.toggle('active', !!hasActiveMore);
|
||||
}
|
||||
|
||||
if (currentPage && pages[currentPage]?.destroy) {
|
||||
pages[currentPage].destroy();
|
||||
@@ -551,8 +566,36 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
navLinks.querySelectorAll('.nav-link').forEach(link => {
|
||||
link.addEventListener('click', closeNav);
|
||||
});
|
||||
|
||||
// --- "More" dropdown (tablet Priority+ nav) ---
|
||||
const navMoreBtn = document.getElementById('navMoreBtn');
|
||||
const navMoreMenu = document.getElementById('navMoreMenu');
|
||||
if (navMoreBtn && navMoreMenu) {
|
||||
// Build More menu dynamically from non-priority nav links (DRY)
|
||||
navMoreMenu.innerHTML = '';
|
||||
document.querySelectorAll('.nav-links a:not([data-priority="high"])').forEach(function(link) {
|
||||
var clone = link.cloneNode(true);
|
||||
clone.setAttribute('role', 'menuitem');
|
||||
clone.addEventListener('click', closeMoreMenu);
|
||||
navMoreMenu.appendChild(clone);
|
||||
});
|
||||
navMoreBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const opening = !navMoreMenu.classList.contains('open');
|
||||
navMoreMenu.classList.toggle('open');
|
||||
navMoreBtn.setAttribute('aria-expanded', String(opening));
|
||||
if (opening) {
|
||||
var firstLink = navMoreMenu.querySelector('.nav-link');
|
||||
if (firstLink) firstLink.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && navLinks.classList.contains('open')) closeNav();
|
||||
if (e.key === 'Escape') {
|
||||
if (navMoreMenu && navMoreMenu.classList.contains('open')) closeMoreMenu();
|
||||
if (navLinks.classList.contains('open')) closeNav();
|
||||
}
|
||||
});
|
||||
document.addEventListener('click', (e) => {
|
||||
if (navLinks.classList.contains('open') &&
|
||||
@@ -560,6 +603,11 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
!hamburger.contains(e.target)) {
|
||||
closeNav();
|
||||
}
|
||||
if (navMoreMenu && navMoreMenu.classList.contains('open') &&
|
||||
!navMoreMenu.contains(e.target) &&
|
||||
!navMoreBtn.contains(e.target)) {
|
||||
closeMoreMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// --- Favorites dropdown ---
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
<meta name="twitter:title" content="CoreScope">
|
||||
<meta name="twitter:description" content="Real-time MeshCore LoRa mesh network analyzer — live packet visualization, node tracking, channel decryption, and route analysis.">
|
||||
<meta name="twitter:image" content="https://raw.githubusercontent.com/Kpa-clawbot/corescope/master/public/og-image.png">
|
||||
<link rel="stylesheet" href="style.css?v=1775023114">
|
||||
<link rel="stylesheet" href="home.css?v=1775023114">
|
||||
<link rel="stylesheet" href="live.css?v=1775023114">
|
||||
<link rel="stylesheet" href="style.css?v=1775022775">
|
||||
<link rel="stylesheet" href="home.css?v=1775022775">
|
||||
<link rel="stylesheet" href="live.css?v=1775022775">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin="anonymous">
|
||||
@@ -44,18 +44,22 @@
|
||||
<span class="live-dot" id="liveDot" title="WebSocket connected" aria-label="WebSocket connected"></span>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<a href="#/home" class="nav-link" data-route="home">Home</a>
|
||||
<a href="#/packets" class="nav-link" data-route="packets">Packets</a>
|
||||
<a href="#/map" class="nav-link" data-route="map">Map</a>
|
||||
<a href="#/live" class="nav-link" data-route="live">🔴 Live</a>
|
||||
<a href="#/home" class="nav-link" data-route="home" data-priority="high">Home</a>
|
||||
<a href="#/packets" class="nav-link" data-route="packets" data-priority="high">Packets</a>
|
||||
<a href="#/map" class="nav-link" data-route="map" data-priority="high">Map</a>
|
||||
<a href="#/live" class="nav-link" data-route="live" data-priority="high">🔴 Live</a>
|
||||
<a href="#/channels" class="nav-link" data-route="channels">Channels</a>
|
||||
<a href="#/nodes" class="nav-link" data-route="nodes">Nodes</a>
|
||||
<a href="#/nodes" class="nav-link" data-route="nodes" data-priority="high">Nodes</a>
|
||||
<a href="#/traces" class="nav-link" data-route="traces">Traces</a>
|
||||
<a href="#/observers" class="nav-link" data-route="observers">Observers</a>
|
||||
<a href="#/analytics" class="nav-link" data-route="analytics">Analytics</a>
|
||||
<a href="#/perf" class="nav-link" data-route="perf">⚡ Perf</a>
|
||||
<a href="#/audio-lab" class="nav-link" data-route="audio-lab">🎵 Lab</a>
|
||||
</div>
|
||||
<div class="nav-more-wrap">
|
||||
<button class="nav-btn nav-more-btn" id="navMoreBtn" aria-haspopup="true" aria-expanded="false" aria-controls="navMoreMenu" title="More pages">More ▾</button>
|
||||
<div class="nav-more-menu" id="navMoreMenu" role="menu"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<div class="nav-stats" id="navStats" title="Live stats"></div>
|
||||
@@ -81,30 +85,30 @@
|
||||
<main id="app" role="main"></main>
|
||||
|
||||
<script src="vendor/qrcode.js"></script>
|
||||
<script src="roles.js?v=1775023114"></script>
|
||||
<script src="customize.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1775023114"></script>
|
||||
<script src="hop-resolver.js?v=1775023114"></script>
|
||||
<script src="hop-display.js?v=1775023114"></script>
|
||||
<script src="app.js?v=1775023114"></script>
|
||||
<script src="home.js?v=1775023114"></script>
|
||||
<script src="packet-filter.js?v=1775023114"></script>
|
||||
<script src="packets.js?v=1775023114"></script>
|
||||
<script src="geo-filter-overlay.js?v=1775023114"></script>
|
||||
<script src="map.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1775023114" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="roles.js?v=1775022775"></script>
|
||||
<script src="customize.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1775022775"></script>
|
||||
<script src="hop-resolver.js?v=1775022775"></script>
|
||||
<script src="hop-display.js?v=1775022775"></script>
|
||||
<script src="app.js?v=1775022775"></script>
|
||||
<script src="home.js?v=1775022775"></script>
|
||||
<script src="packet-filter.js?v=1775022775"></script>
|
||||
<script src="packets.js?v=1775022775"></script>
|
||||
<script src="geo-filter-overlay.js?v=1775022775"></script>
|
||||
<script src="map.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1775022775" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -324,6 +324,13 @@
|
||||
|
||||
// Check if new packets pass current filters
|
||||
const filtered = newPkts.filter(p => {
|
||||
// Respect time window filter — drop packets outside the selected window
|
||||
const windowMin = savedTimeWindowMin;
|
||||
if (windowMin > 0) {
|
||||
const cutoff = new Date(Date.now() - windowMin * 60000).toISOString();
|
||||
const pktTime = p.latest || p.timestamp || p.first_seen;
|
||||
if (pktTime && pktTime < cutoff) return false;
|
||||
}
|
||||
if (filters.type) { const types = filters.type.split(',').map(Number); if (!types.includes(p.payload_type)) return false; }
|
||||
if (filters.observer) { const obsSet = new Set(filters.observer.split(',')); if (!obsSet.has(p.observer_id)) return false; }
|
||||
if (filters.hash && p.hash !== filters.hash) return false;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
--nav-bg2: #1a1a2e;
|
||||
--nav-text: #ffffff;
|
||||
--nav-text-muted: #cbd5e1;
|
||||
--nav-active-bg: rgba(74, 158, 255, 0.15);
|
||||
--accent: #4a9eff;
|
||||
--geo-filter-color: #3b82f6;
|
||||
--status-green: #22c55e;
|
||||
@@ -128,7 +129,7 @@ a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible
|
||||
.nav-link.active {
|
||||
color: var(--nav-text);
|
||||
border-bottom-color: transparent;
|
||||
background: rgba(74, 158, 255, 0.15);
|
||||
background: var(--nav-active-bg);
|
||||
border-radius: 6px;
|
||||
margin: 4px 0;
|
||||
padding: 10px 12px;
|
||||
@@ -837,6 +838,22 @@ button.ch-item.selected { background: var(--selected-bg); }
|
||||
|
||||
/* === Hamburger (hidden on desktop) === */
|
||||
.hamburger { display: none; }
|
||||
/* "More" button (hidden on desktop) */
|
||||
.nav-more-wrap { display: none; position: relative; }
|
||||
.nav-more-btn { display: inline-flex; }
|
||||
.nav-more-menu {
|
||||
display: none; position: absolute; top: calc(var(--top-nav-h, 52px) - 4px); right: 0;
|
||||
background: var(--nav-bg); border: 1px solid var(--border); border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15); flex-direction: column;
|
||||
min-width: 160px; padding: 4px 0; z-index: 1200;
|
||||
}
|
||||
.nav-more-menu.open { display: flex; }
|
||||
.nav-more-menu .nav-link {
|
||||
padding: 10px 16px; border-bottom: none; border-radius: 0; margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nav-more-menu .nav-link:hover { background: var(--nav-bg2); color: var(--nav-text); }
|
||||
.nav-more-menu .nav-link.active { background: var(--nav-active-bg); }
|
||||
/* Ensure nav stays above Leaflet map */
|
||||
.nav-links.open { z-index: 1100; }
|
||||
#map-wrap .leaflet-container { z-index: 1; }
|
||||
@@ -851,18 +868,31 @@ button.ch-item.selected { background: var(--selected-bg); }
|
||||
.map-controls { width: 180px; font-size: 12px; }
|
||||
}
|
||||
|
||||
/* === Responsive — Hamburger nav (≤1023px) === */
|
||||
@media (max-width: 1023px) {
|
||||
/* === Responsive — Tablet Priority+ nav (768–1023px) === */
|
||||
@media (min-width: 768px) and (max-width: 1023px) {
|
||||
.nav-links { display: flex !important; flex-direction: row; gap: 2px; }
|
||||
.nav-links a:not([data-priority="high"]) { display: none; }
|
||||
.nav-more-wrap { display: flex; align-items: center; }
|
||||
.hamburger { display: none; }
|
||||
.nav-link { padding: 14px 8px; font-size: 13px; }
|
||||
.nav-links a[data-priority="high"] { order: -1; }
|
||||
.nav-link.active { background: var(--nav-active-bg); border-radius: 6px; margin: 4px 0; padding: 10px 8px; }
|
||||
}
|
||||
|
||||
/* === Responsive — Hamburger nav (<768px) === */
|
||||
@media (max-width: 767px) {
|
||||
.hamburger { display: inline-flex; }
|
||||
.nav-more-wrap { display: none !important; }
|
||||
.nav-links {
|
||||
display: none; position: absolute; top: 52px; left: 0; right: 0;
|
||||
background: var(--nav-bg); flex-direction: column; padding: 8px 0;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.4); z-index: 1100;
|
||||
max-height: calc(100dvh - 52px); overflow-y: auto;
|
||||
}
|
||||
.nav-links a:not([data-priority="high"]) { display: flex; }
|
||||
.nav-links.open { display: flex; }
|
||||
.nav-link { padding: 12px 20px; border-bottom: none; }
|
||||
.nav-link.active { background: rgba(74,158,255,0.15); border-radius: 0; margin: 0; padding: 12px 20px; }
|
||||
.nav-link.active { background: var(--nav-active-bg); border-radius: 0; margin: 0; padding: 12px 20px; }
|
||||
.nav-left { gap: 12px; }
|
||||
body.nav-open { overflow: hidden; }
|
||||
}
|
||||
|
||||
@@ -365,13 +365,12 @@ async function run() {
|
||||
await test('Packets page loads with filter', async () => {
|
||||
// Ensure desktop viewport and broad time window so fixture timestamps are included.
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await page.goto(BASE, { waitUntil: 'domcontentloaded' });
|
||||
// Set time window BEFORE packets.js IIFE re-executes (525600 min ≈ 1 year)
|
||||
// Set time window BEFORE packets.js IIFE re-executes (525600 min ≈ 1 year).
|
||||
// Navigate to the packets URL then reload — avoids about:blank cross-origin issues
|
||||
// that can prevent the SPA from fully initializing within the timeout.
|
||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
|
||||
await page.evaluate(() => localStorage.setItem('meshcore-time-window', '525600'));
|
||||
// Navigate away so next goto is a full page load (not a same-document hash change).
|
||||
// This guarantees scripts re-execute and packets.js IIFE reads the new localStorage.
|
||||
await page.goto('about:blank');
|
||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'load' });
|
||||
await page.reload({ waitUntil: 'load' });
|
||||
await page.waitForSelector('table tbody tr', { timeout: 15000 });
|
||||
const rowsBefore = await page.$$('table tbody tr');
|
||||
assert(rowsBefore.length > 0, 'No packets visible');
|
||||
@@ -387,11 +386,8 @@ async function run() {
|
||||
});
|
||||
|
||||
await test('Packets initial fetch honors persisted time window', async () => {
|
||||
// Navigate to base first to get same-origin context for localStorage
|
||||
await page.goto(BASE, { waitUntil: 'domcontentloaded' });
|
||||
// Set persisted time window to 60 min and reload so the IIFE reads it
|
||||
await page.evaluate(() => localStorage.setItem('meshcore-time-window', '60'));
|
||||
// Navigate away so next goto is a full page load
|
||||
await page.goto('about:blank');
|
||||
|
||||
const packetsRequestPromise = page.waitForRequest((req) => {
|
||||
try {
|
||||
@@ -402,8 +398,8 @@ async function run() {
|
||||
}
|
||||
}, { timeout: 10000 });
|
||||
|
||||
// Full navigation from about:blank — scripts re-execute, IIFE reads localStorage
|
||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'load' });
|
||||
// Full reload on the packets page — scripts re-execute, IIFE reads localStorage
|
||||
await page.reload({ waitUntil: 'load' });
|
||||
await page.waitForSelector('#fTimeWindow', { timeout: 10000 });
|
||||
const timeWindowValue = await page.$eval('#fTimeWindow', (el) => el.value);
|
||||
assert(timeWindowValue === '60', `Expected time window dropdown to restore 60, got ${timeWindowValue}`);
|
||||
@@ -428,10 +424,8 @@ async function run() {
|
||||
// Test: Packets groupByHash toggle changes view
|
||||
await test('Packets groupByHash toggle works', async () => {
|
||||
// Restore wide time window — previous test set it to 60 min which excludes fixture data
|
||||
await page.goto(BASE, { waitUntil: 'domcontentloaded' });
|
||||
await page.evaluate(() => localStorage.setItem('meshcore-time-window', '525600'));
|
||||
await page.goto('about:blank');
|
||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'load' });
|
||||
await page.reload({ waitUntil: 'load' });
|
||||
await page.waitForSelector('table tbody tr', { timeout: 15000 });
|
||||
const groupBtn = await page.$('#fGroup');
|
||||
assert(groupBtn, 'Group by hash button (#fGroup) not found');
|
||||
|
||||
Reference in New Issue
Block a user