Compare commits

..

4 Commits

Author SHA1 Message Date
you
23caae40af release: v2.0.1 — mobile packets UX 2026-03-20 01:19:19 +00:00
you
f7e165cb61 merge: mobile packets improvements (v2.0.1) 2026-03-20 01:19:19 +00:00
you
d9bc9e13c0 mobile: hide hash column by default 2026-03-20 01:15:49 +00:00
you
6fd85db87f mobile packets: bottom sheet detail, collapsed filters, smaller table, fewer columns
- Mobile (≤640px) default columns: time, hash, type, details (hide region, observer, path, rpt, size)
- Detail panel hidden on mobile; tapping row opens slide-up bottom sheet (70vh max, close button, drag handle)
- Filter bar collapses to single 'Filters' toggle button on mobile
- Table font 11px, tighter padding, no min-width forcing horizontal scroll
- Panel-right completely hidden on mobile (no split layout)
2026-03-20 01:14:32 +00:00
4 changed files with 84 additions and 23 deletions

View File

@@ -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": {

View File

@@ -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>

View File

@@ -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>`;
}

View File

@@ -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; }