Commit Graph

569 Commits

Author SHA1 Message Date
you dbbe280f17 Fix: don't apply time window when navigating to specific packet hash
Direct links like #/packets/HASH should always show the packet
regardless of the time window filter.
2026-03-23 00:04:03 +00:00
you c267e58418 Fix: sort regional candidates by distance to IATA center
Without sender GPS (channel texts etc), the forward pass had no
anchor and just took candidates[0] — random order. Now regional
candidates are sorted by distKm to observer IATA center before
disambiguation. Closest to region center = default pick.
2026-03-22 23:59:33 +00:00
you 19c60e6872 Fix: packet detail uses server-side resolve-hops with GPS anchor
Client-side HopResolver wasn't properly disambiguating despite
correct data. Switched detail view to use the server API directly:
/api/resolve-hops?hops=...&observer=...&originLat=...&originLon=...

Server-side resolution is battle-tested and handles regional
filtering + GPS-anchored disambiguation correctly.
2026-03-22 23:52:44 +00:00
you f4002def4c Debug: log renderDetail GPS anchor and resolved hops 2026-03-22 23:50:07 +00:00
you 5f7626ab3c Fix: detail view always re-resolves hops fresh with sender GPS
List view resolves hops without anchor (no per-packet context).
Detail view now always re-resolves with the packet's actual GPS
coordinates + observer, overwriting stale cache entries.
Removed debug logging.
2026-03-22 23:47:34 +00:00
you 55ff793e37 Debug: trace renderDetail re-resolve inputs 2026-03-22 23:46:47 +00:00
you 646afa2cf9 Debug: log HopResolver.resolve inputs to trace disambiguation 2026-03-22 23:44:53 +00:00
you 3c44b4638d Fix: pass sender lat/lon as origin anchor for hop disambiguation
ADVERT packets have GPS coordinates — use them as the forward
pass anchor so the first hop resolves to the nearest candidate
to the sender, not random pick order.
2026-03-22 23:35:10 +00:00
you 398363cc9d Packet detail: re-resolve hops with observer for regional conflicts
The general hop cache was populated without observer context,
so all conflicts showed filterMethod=none. Now renderDetail()
re-resolves hops with pkt.observer_id, getting proper regional
filtering with distances and conflict flags.
2026-03-22 23:29:46 +00:00
you 632e684029 Conflict badge: bigger clickable button with popover pane
⚠3 is now a yellow button (not tiny superscript). Clicking it
opens a popover listing all regional candidates with:
- Node name (clickable → node detail page)
- Distance from observer region center
- Truncated pubkey

Popover dismisses on outside click. Each candidate is a link
to #/nodes/PUBKEY for full details.
2026-03-22 23:25:32 +00:00
you 70db798aed Packet detail byte breakdown uses shared HopDisplay for path hops
Replaces inline conflict rendering with HopDisplay.renderHop() —
consistent regional-only tooltips everywhere.
2026-03-22 23:24:02 +00:00
you 28475722d7 Conflict tooltips show only regional candidates, ignore global noise 2026-03-22 23:22:16 +00:00
you e732d52e0e Hex Paths mode still shows conflict tooltips
hexMode flag shows raw hex prefix as display text but keeps
the full conflict tooltip, link, and warning badges.
2026-03-22 23:20:28 +00:00
you 3491fdabef Shared HopDisplay module for consistent conflict tooltips
New hop-display.js: shared renderHop() and renderPath() with
full conflict info — candidate count, regional/global flags,
distance, filter method. Tooltip shows all candidates with
details on hover.

packets.js: uses HopDisplay.renderHop() (was inline)
nodes.js: path rendering uses HopDisplay when available
style.css: .hop-current for highlighting the viewed node in paths

Consistent conflict display across packets + node detail pages.
2026-03-22 23:12:11 +00:00
you 501d685003 Fix: hoist targetNodeKey to module scope so loadNodes can access it 2026-03-22 23:05:56 +00:00
you f0243ff332 Fix: delay popup open after setView, add debug logging
Leaflet needs map to settle after setView before popup can open.
Added 500ms delay + console.warn if target marker not found.
2026-03-22 23:01:18 +00:00
you 46868b7dcd Packet detail: resolve location for channel texts + all node types
For packets without direct lat/lon (GRP_TXT, TXT_MSG):
- Look up sender by pubKey via /api/nodes/:key
- Look up sender by name via /api/nodes/search?q=name
- Show location + 📍map link when node has coordinates

Works for decrypted channel messages (sender field), direct
messages (srcPubKey), and any packet type with a resolvable sender.
2026-03-22 23:00:09 +00:00
you 537e04d0ad Fix: map node highlight uses _nodeKey instead of alt text matching
Store public_key on each Leaflet marker as _nodeKey. Match by
exact pubkey instead of fragile alt text substring search.
2026-03-22 22:55:46 +00:00
you 5273d1fd01 Map: navigate to node by pubkey from packet detail
📍map link now uses #/map?node=PUBKEY. Map centers on the node
at zoom 14 and opens its popup. No fake markers — uses the
existing node marker already on the map.
2026-03-22 22:51:15 +00:00
you 5ad498a662 Map: highlight pin when navigated from packet detail
Red circle marker with tooltip at the target coordinates,
fades out after 10 seconds. Makes it obvious where the
packet location is on the map.
2026-03-22 22:47:01 +00:00
you 0974e7b15b Fix: observer detail packet links use hash, not ID
Was linking to #/packet/ID (wrong route + observation ID).
Now links to #/packets/HASH (correct route + packet hash).
2026-03-22 22:44:00 +00:00
you b11722d854 Location link points to our own map, not Google Maps
Packet detail 📍map link now navigates to #/map?lat=X&lon=Y&zoom=12.
Map page reads lat/lon/zoom from URL query params to center on
the linked location.
2026-03-22 22:43:29 +00:00
you 25c85aeeb2 Fix: packet-detail page loads observers before rendering
Direct navigation to #/packet/ID skipped loadObservers(), so
obsName() fell through to raw hex pubkey. Now loads observers
first.
2026-03-22 22:42:38 +00:00
you a2dd96812d Packet detail: show location for ADVERTs and nodes with lat/lon
Shows coordinates with Google Maps link for packets that have
lat/lon in decoded payload (ADVERTs, known nodes). Includes
node name when available.
2026-03-22 22:41:35 +00:00
you 13460cfc93 Fix: move /api/iata-coords route after app initialization
Route was at line 16 before express app was created — caused
'Cannot access app before initialization' crash on startup.
2026-03-22 22:26:15 +00:00
you 935ed6ef85 Show observer IATA regions in packet, node, and live detail views
- packets.js: obsName() now shows IATA code next to observer name, e.g. 'EW-SFC-DR01 (SFO)'
- packets.js: hop conflicts in field table show distance (e.g. '37km')
- nodes.js: both full and sidebar detail views show 'Regions: SJC, OAK, SFO' badges and per-observer IATA
- live.js: node detail panel shows regions in 'Heard By' heading and per-observer IATA
- server.js: /api/nodes/:pubkey/health now returns iata field for each observer
- Bump cache busters
2026-03-22 22:21:01 +00:00
you 70c67d8551 Client-side regional hop filtering (#117)
HopResolver now mirrors server-side layered regional filtering:
- init() accepts observers list + IATA coords
- resolve() accepts observerId, looks up IATA, filters candidates
  by haversine distance (300km radius) to IATA center
- Candidates include regional, filterMethod, distKm fields
- Packet detail view passes observer_id to resolve()

New endpoint: GET /api/iata-coords returns airport coordinates
for client-side use.

Fixes: conflict badges showing "0 conflicts" in packet detail
because client-side resolver had no regional filtering.
2026-03-22 22:18:01 +00:00
you c1d789b5d7 Regional hop filtering: layered geo + observer approach (#117)
Layer 1 (GPS, bridge-proof): Nodes with lat/lon are checked via
haversine distance to the observer IATA center. Only nodes within
300km are considered regional. Bridged WA nodes appearing in SJC
MQTT feeds are correctly rejected because their GPS coords are
1100km+ from SJC.

Layer 2 (observer-based, fallback): Nodes without GPS fall back to
_advertByObserver index — were they seen by a regional observer?
Less precise but still useful for nodes that never sent ADVERTs
with coordinates.

Layer 3: Global fallback, flagged.

New module: iata-coords.js with 60+ IATA airport coordinates +
haversine distance function.

API response now includes filterMethod (geo/observer/none) and
distKm per conflict candidate.

Tests: 22 unit tests (haversine, boundaries, cross-regional
collision sim, layered fallback, bridge rejection).
2026-03-22 22:09:43 +00:00
you 4cc1ad7b34 Fix #117: Regional filtering for repeater ID resolution
1-byte (and 2-byte) hop IDs match many nodes globally. Previously
resolve-hops picked candidates from anywhere, causing cross-regional
false paths (e.g. Eugene packet showing Vancouver repeaters).

Fix: Use observer IATA to determine packet region. Filter candidates
to nodes seen by observers in the same IATA region via the existing
_advertByObserver index. Fall back to global only if zero regional
candidates exist (flagged as globalFallback).

API changes to /api/resolve-hops response:
- conflicts[]: all candidates with regional flag per hop
- totalGlobal/totalRegional: candidate counts
- globalFallback: true when no regional candidates found
- region: packet IATA region in top-level response

UI changes:
- Conflict count badge (⚠3) instead of bare ⚠
- Tooltip shows regional vs global candidates
- Unreliable hops shown with strikethrough + opacity
- Global fallback hops shown with red dashed underline
2026-03-22 21:38:24 +00:00
you b7a29d4849 feat: add built-in IATA-to-city mapping for region dropdown (#116)
Add window.IATA_CITIES with ~150 common airport codes covering US, Canada,
Europe, Asia, Oceania, South America, and Africa. The region filter now
falls back to this mapping when no user-configured label exists, so region
dropdowns show friendly city names out of the box.

Closes #116
2026-03-22 21:22:23 +00:00
you d474d0b427 fix: region dropdown layout for longer city name labels (#116)
- Add width: max-content to dropdown menu for auto-sizing
- Add overflow ellipsis + max-width on dropdown items for very long labels
- Checkboxes already flex-shrink: 0, no text wrapping with white-space: nowrap
2026-03-22 21:21:39 +00:00
you 2609a26605 Audio Lab: click any note row to play it individually
Each note in the sequence table has a ▶ button and the whole row
is clickable. Plays a single oscillator with the correct envelope,
filter, and frequency for that note. Highlights the corresponding
hex byte, table row, and byte bar while it plays.

Also added MeshAudio.getContext() accessor for audio lab to create
individual notes without duplicating AudioContext.
2026-03-22 19:41:00 +00:00
you 0934d8bbb6 Audio Lab: fix highlight timing vs speed setting
computeMapping was applying speedMult on top of BPM that already
included it (setBPM(baseBPM * speedMult)). Double-multiplication
made highlights run at wrong speed. BPM already encodes speed.
2026-03-22 19:39:18 +00:00
you 2fd0d3e07b Audio Lab: show WHY each parameter has its value
Sound Mapping: 3-column table (Parameter | Value | Formula/Reason)
Note Sequence: payload index + duration/gap derivation formulas
2026-03-22 19:36:07 +00:00
you 7c1132b7cf Audio Lab: real-time playback highlighting
As each note plays, highlights sync across all three views:
- Hex dump: current byte pulses red
- Note table: current row highlights blue
- Byte visualizer: current bar glows and scales up

Timing derived from note duration + gap (same values the voice
module uses), scheduled via setTimeout in parallel with audio.
Clears previous note before highlighting next. Auto-clears at end.
2026-03-22 19:27:35 +00:00
you f523d4f3c4 Audio Lab page (Milestone 1): Packet Jukebox
New #/audio-lab page for understanding and debugging audio sonification.

Server: GET /api/audio-lab/buckets — returns representative packets
bucketed by type (up to 8 per type spanning size range).

Client: Left sidebar with collapsible type sections, right panel with:
- Controls: Play, Loop, Speed (0.25x-4x), BPM, Volume, Voice select
- Packet Data: type, sizes, hops, obs count, hex dump with sampled
  bytes highlighted
- Sound Mapping: computed instrument, scale, filter, volume, voices,
  pan — shows exactly why it sounds the way it does
- Note Sequence: table of sampled bytes → MIDI → freq → duration → gap
- Byte Visualizer: bar chart of payload bytes, sampled ones colored

Enables MeshAudio automatically on first play. Mobile responsive.
2026-03-22 19:19:45 +00:00
you edbaddbd37 Audio Workbench: expand M2 parameter overrides — envelope, filter, limiter, timing 2026-03-22 19:00:48 +00:00
you d5e6481d9b Audio: eliminate pops — proper envelope + per-packet limiter
Three pop sources fixed:
1. setValueAtTime(0) at note start — oscillator starting at exact zero
   causes click. Now starts at 0.0001 with exponentialRamp up.
2. setValueAtTime at noteEnd jumping to sustain level — removed.
   Decay ramp flows naturally into setTargetAtTime release (smooth
   exponential decay, no discontinuities).
3. No amplitude limiting — multiple overlapping packets could spike.
   Added DynamicsCompressor as limiter per packet chain (-6dB
   threshold, 12:1 ratio, 1ms attack).

Also: 20ms lookahead (was 10ms) gives scheduler more headroom.
2026-03-22 18:58:33 +00:00
you 1450bc928b Packets page: O(1) hash dedup via Map index
packets.find(g => g.hash === h) was O(n) and could race with
loadPackets replacing the array. hashIndex Map stays in sync —
rebuilt on API fetch, updated on WS insert. Prevents duplicate
rows for same hash in grouped mode.
2026-03-22 18:55:22 +00:00
you c7f12c72b9 Audio: log2 voice scaling (up to 8), wider detune spread
1 obs = solo, 3 = duo, 8 = trio, 15 = quartet, 30 = quintet, 60 = sextet.
Wider detune (±8, ±13, ±18...) so stacked voices shimmer instead
of sounding like the same oscillator copied.
2026-03-22 18:45:47 +00:00
you 2688f3e63a Audio: pass consolidated observation_count to sonifyPacket
Realistic mode buffers observations then fires once — but was
passing the first packet (obs_count=1). Now passes consolidated
packet with obs_count=packets.length so the voice module gets
the real count for volume + chord voicing.
2026-03-22 18:44:45 +00:00
you 90bd9e12e5 Fix realistic mode: all WS broadcasts include hash + raw_hex
Secondary broadcast paths (ADVERT, GRP_TXT, TXT_MSG, TRACE, API)
were missing hash field. Without hash, realistic mode's buffer
check (if pkt.hash) failed and packets fell through to
animatePacket individually — causing duplicate feed items and
duplicate sonification.

Also added missing addFeedItem call in animateRealisticPropagation
so the feed shows consolidated entries in realistic mode.
2026-03-22 18:37:53 +00:00
you 4e8b1b2584 Audio: fix pop/crackle — exponential ramps + longer release tail
Linear gain ramps + osc.stop() too close to release end caused
waveform discontinuities. Switched to exponentialRamp (natural
decay curve), 0.0001 floor (-80dB), 50ms extra headroom before
oscillator stop.
2026-03-22 18:31:36 +00:00
you 288c1b048b CI: skip deploy on markdown, docs, LICENSE, gitignore changes 2026-03-22 18:28:56 +00:00
you 85ecddd92c Audio Workbench plan — packet jukebox, parameter overrides, A/B comparison 2026-03-22 18:26:49 +00:00
you d0b02b7070 Remove legacy playSound/🔇 button — MeshAudio is the only audio system now 2026-03-22 18:11:55 +00:00
you 08255aeba5 Audio: "Tap to enable audio" overlay when context is suspended
Instead of silently dropping notes or hoping gesture listeners fire,
show a clear overlay on first packet if AudioContext is suspended.
One tap resumes context and removes overlay. Standard pattern used
by every browser game/music site.
2026-03-22 17:31:23 +00:00
you 51df14521b Audio: create context eagerly on restore, gesture listener as fallback 2026-03-22 17:27:54 +00:00
you f3572c646a Audio: unlock on first user gesture after restore
When audio was previously enabled, registers one-shot click/touch/key
listener to init AudioContext on first interaction. Any tap on the
page is enough — no need to toggle the checkbox.
2026-03-22 17:27:00 +00:00
you 0dbe5fd229 Audio: fix inconsistent init — lazy AudioContext, no premature creation
restore() was creating AudioContext without user gesture (on page load
when volume was saved), causing browser to permanently suspend it.
Now restore() only sets flags; AudioContext created lazily on first
sonifyPacket() call or setEnabled() click. Pending volume applied
when context is finally created.
2026-03-22 17:25:42 +00:00