mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-03-30 17:05:58 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23caae40af | ||
|
|
f7e165cb61 | ||
|
|
d9bc9e13c0 | ||
|
|
6fd85db87f |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "meshcore-analyzer",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"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": {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<meta name="twitter:title" content="MeshCore Analyzer">
|
||||
<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/meshcore-analyzer/master/public/og-image.png">
|
||||
<link rel="stylesheet" href="style.css?v=1773963867">
|
||||
<link rel="stylesheet" href="style.css?v=1773969261">
|
||||
<link rel="stylesheet" href="home.css">
|
||||
<link rel="stylesheet" href="live.css?v=1773966856">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
@@ -78,7 +78,7 @@
|
||||
<script src="vendor/qrcode.js"></script>
|
||||
<script src="app.js?v=1774079160"></script>
|
||||
<script src="home.js?v=1774079160"></script>
|
||||
<script src="packets.js?v=1773961784"></script>
|
||||
<script src="packets.js?v=1773969349"></script>
|
||||
<script src="map.js?v=1774079160" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774079160" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1773961950" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
|
||||
@@ -278,6 +278,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-bar" id="pktFilters">
|
||||
<button class="btn filter-toggle-btn" id="filterToggleBtn">Filters ▾</button>
|
||||
<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">
|
||||
@@ -318,6 +319,13 @@
|
||||
typeSel.innerHTML += `<option value="${k}" ${String(filters.type) === k ? 'selected' : ''}>${v}</option>`;
|
||||
}
|
||||
|
||||
// Filter toggle button for mobile
|
||||
document.getElementById('filterToggleBtn').addEventListener('click', function() {
|
||||
const bar = document.getElementById('pktFilters');
|
||||
bar.classList.toggle('filters-expanded');
|
||||
this.textContent = bar.classList.contains('filters-expanded') ? 'Filters ▴' : 'Filters ▾';
|
||||
});
|
||||
|
||||
// Filter event listeners
|
||||
document.getElementById('fHash').value = filters.hash || '';
|
||||
document.getElementById('fHash').addEventListener('input', debounce((e) => { filters.hash = e.target.value || undefined; loadPackets(); }, 300));
|
||||
@@ -343,7 +351,8 @@
|
||||
{ key: 'rpt', label: 'Rpt' },
|
||||
{ key: 'details', label: 'Details' },
|
||||
];
|
||||
const defaultHidden = ['region'];
|
||||
const isMobile = window.innerWidth <= 640;
|
||||
const defaultHidden = isMobile ? ['region', 'hash', 'observer', 'path', 'rpt', 'size'] : ['region'];
|
||||
let visibleCols;
|
||||
try {
|
||||
visibleCols = JSON.parse(localStorage.getItem('packets-visible-cols'));
|
||||
@@ -633,10 +642,33 @@
|
||||
async function selectPacket(id) {
|
||||
selectedId = id;
|
||||
renderTableRows();
|
||||
const panel = document.getElementById('pktRight');
|
||||
panel.classList.remove('empty');
|
||||
panel.innerHTML = '<div class="panel-resize-handle" id="pktResizeHandle"></div><div class="text-center text-muted" style="padding:40px">Loading…</div>';
|
||||
initPanelResize();
|
||||
const isMobileNow = window.innerWidth <= 640;
|
||||
let panel;
|
||||
if (isMobileNow) {
|
||||
// Use mobile bottom sheet
|
||||
let sheet = document.getElementById('mobileDetailSheet');
|
||||
if (!sheet) {
|
||||
sheet = document.createElement('div');
|
||||
sheet.id = 'mobileDetailSheet';
|
||||
sheet.className = 'mobile-detail-sheet';
|
||||
sheet.innerHTML = '<div class="mobile-sheet-handle"></div><button class="mobile-sheet-close" id="mobileSheetClose">✕</button><div class="mobile-sheet-content"></div>';
|
||||
document.body.appendChild(sheet);
|
||||
sheet.querySelector('#mobileSheetClose').addEventListener('click', () => {
|
||||
sheet.classList.remove('open');
|
||||
});
|
||||
sheet.querySelector('.mobile-sheet-handle').addEventListener('click', () => {
|
||||
sheet.classList.remove('open');
|
||||
});
|
||||
}
|
||||
panel = sheet.querySelector('.mobile-sheet-content');
|
||||
panel.innerHTML = '<div class="text-center text-muted" style="padding:40px">Loading…</div>';
|
||||
sheet.classList.add('open');
|
||||
} else {
|
||||
panel = document.getElementById('pktRight');
|
||||
panel.classList.remove('empty');
|
||||
panel.innerHTML = '<div class="panel-resize-handle" id="pktResizeHandle"></div><div class="text-center text-muted" style="padding:40px">Loading…</div>';
|
||||
initPanelResize();
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await api(`/packets/${id}`);
|
||||
@@ -647,11 +679,11 @@
|
||||
const newHops = hops.filter(h => !(h in hopNameCache));
|
||||
if (newHops.length) await resolveHops(newHops);
|
||||
} catch {}
|
||||
panel.innerHTML = '<div class="panel-resize-handle" id="pktResizeHandle"></div>';
|
||||
panel.innerHTML = isMobileNow ? '' : '<div class="panel-resize-handle" id="pktResizeHandle"></div>';
|
||||
const content = document.createElement('div');
|
||||
panel.appendChild(content);
|
||||
renderDetail(content, data);
|
||||
initPanelResize();
|
||||
if (!isMobileNow) initPanelResize();
|
||||
} catch (e) {
|
||||
panel.innerHTML = `<div class="text-muted">Error: ${e.message}</div>`;
|
||||
}
|
||||
|
||||
@@ -811,8 +811,8 @@ button.ch-item.selected { background: var(--selected-bg); }
|
||||
|
||||
/* Layouts: stack instead of side-by-side */
|
||||
.split-layout { flex-direction: column; overflow-y: auto; }
|
||||
.panel-left { padding: 10px; flex: none; min-height: 50vh; overflow-x: auto; }
|
||||
.panel-right { width: 100%; min-width: 0; border-left: none; border-top: 1px solid var(--border); max-height: none; flex: none; }
|
||||
.panel-left { padding: 6px; flex: 1; min-height: 0; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||
.panel-right { display: none; }
|
||||
|
||||
/* Channels: Discord-style full screen toggle */
|
||||
.ch-layout { flex-direction: row; position: relative; }
|
||||
@@ -830,18 +830,21 @@ button.ch-item.selected { background: var(--selected-bg); }
|
||||
.ch-back-btn { display: flex; }
|
||||
.ch-main-header { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
/* Tables: smaller text, allow horizontal scroll */
|
||||
.data-table { font-size: 12px; }
|
||||
.data-table td { padding: 6px 6px; max-width: 120px; }
|
||||
.data-table th { padding: 6px 6px; font-size: 11px; }
|
||||
.panel-left { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||
.data-table { min-width: 500px; }
|
||||
/* Tables: smaller text for mobile */
|
||||
.data-table { font-size: 11px; min-width: 0; }
|
||||
.data-table td { padding: 5px 4px; max-width: 100px; }
|
||||
.data-table th { padding: 5px 4px; font-size: 10px; }
|
||||
.panel-left { overflow-x: auto; }
|
||||
|
||||
/* Filters: full width */
|
||||
.filter-bar { flex-direction: column; gap: 4px; }
|
||||
.filter-bar input { width: 100%; }
|
||||
.filter-bar select { width: 100%; }
|
||||
.filter-bar .btn { min-height: 44px; }
|
||||
/* Filters: collapse on mobile */
|
||||
.filter-bar { flex-direction: row; flex-wrap: wrap; gap: 4px; }
|
||||
.filter-toggle-btn { display: inline-flex !important; }
|
||||
.filter-bar > *:not(.filter-toggle-btn):not(.col-toggle-wrap) { display: none; }
|
||||
.filter-bar.filters-expanded > * { display: inline-flex; }
|
||||
.filter-bar.filters-expanded > .col-toggle-wrap { display: inline-block; }
|
||||
.filter-bar.filters-expanded input { width: 100%; }
|
||||
.filter-bar.filters-expanded select { width: 100%; }
|
||||
.filter-bar .btn { min-height: 36px; }
|
||||
.node-filter-wrap { width: 100%; }
|
||||
|
||||
/* Nodes */
|
||||
@@ -1384,3 +1387,29 @@ tr[data-hops]:hover { background: rgba(59,130,246,0.1); }
|
||||
.claimed-row { background: color-mix(in srgb, var(--accent) 8%, transparent) !important; border-left: 3px solid var(--accent); }
|
||||
.claimed-row:hover { background: color-mix(in srgb, var(--accent) 14%, transparent) !important; }
|
||||
.claimed-badge { color: var(--accent); font-size: 13px; margin-right: 2px; }
|
||||
|
||||
/* Filter toggle button — hidden on desktop */
|
||||
.filter-toggle-btn { display: none; }
|
||||
|
||||
/* Mobile detail bottom sheet */
|
||||
.mobile-detail-sheet {
|
||||
display: none;
|
||||
position: fixed; bottom: 0; left: 0; right: 0;
|
||||
max-height: 70vh; background: var(--detail-bg);
|
||||
border-top-left-radius: 16px; border-top-right-radius: 16px;
|
||||
box-shadow: 0 -4px 24px rgba(0,0,0,.3);
|
||||
z-index: 200; overflow-y: auto; padding: 8px 16px 24px;
|
||||
transform: translateY(100%); transition: transform .25s ease;
|
||||
}
|
||||
.mobile-detail-sheet.open { display: block; transform: translateY(0); }
|
||||
.mobile-sheet-handle {
|
||||
width: 40px; height: 4px; background: var(--border);
|
||||
border-radius: 2px; margin: 4px auto 8px; cursor: pointer;
|
||||
}
|
||||
.mobile-sheet-close {
|
||||
position: absolute; top: 8px; right: 12px;
|
||||
background: none; border: none; font-size: 20px;
|
||||
color: var(--text-muted); cursor: pointer; z-index: 1;
|
||||
}
|
||||
.mobile-sheet-close:hover { color: var(--text); }
|
||||
.mobile-sheet-content { padding-top: 4px; }
|
||||
|
||||
Reference in New Issue
Block a user