- server.js disambiguateHops() now delegates to server-helpers.js
(was a full copy of the same algorithm, ~70 lines removed)
- live.js resolveHopPositions() now delegates to shared HopResolver
(was a standalone reimplementation, ~50 lines removed)
- HopResolver.init() called when live page loads/updates node data
- Net -106 lines, same behavior, single source of truth
All unit tests pass (241). E2E 13/16 (3 pre-existing Chromium crashes).
Root cause: addFeedItem had no dedup logic — each WS message created
a new feed entry regardless of hash. Dedup only worked when the
'Realistic propagation' toggle was ON (which buffers by hash before
calling animateRealisticPropagation). Default mode called animatePacket
directly for every observation, producing duplicate feed entries.
Fix: Added feedHashMap (hash -> {element, count, pkt, addedAt}) that
tracks recent feed items by packet hash. When a packet with a known
hash arrives within 30s, the existing feed item is updated in-place:
- Observation count badge incremented
- Item flashed and moved to top of feed
- No duplicate DOM element created
Also adds data-hash attribute to feed items for testability.
Tests: 5 new Playwright tests in test-live-dedup.js covering:
- Same hash different observers → single entry
- Different hashes → separate entries
- 5 rapid sequential duplicates → single entry with count 5
- Same hash same observer → still deduplicates
- Packets without hash → not deduplicated
- Live page showHeatMap() now reads meshcore-live-heatmap-opacity from
localStorage and applies it to the canvas element (was hardcoded 0.3)
- Customizer now has two clearly labeled sliders:
🗺️ Nodes Map — controls the static map page heatmap
📡 Live Map — controls the live page heatmap
- Each uses its own localStorage key (meshcore-heatmap-opacity vs
meshcore-live-heatmap-opacity)
- Added E2E tests for live opacity persistence and dual slider existence
- 13/15 E2E tests pass locally (2 fail due to ARM chromium OOM after
heavy live page tests — CI on x64 will handle them)
Closes#119 properly this time.
Recover from stale localStorage state where heat checkbox stayed
disabled even after matrix/ghosts mode was turned off. Explicitly
sets ht.disabled = false in the else branch of matrix init.
13/13 E2E tests pass locally.
Live page liveHeatToggle now saves to localStorage (meshcore-live-heatmap).
Map page was already fixed but live page was missed.
Added E2E test that verifies persistence across reload.
13/13 E2E tests pass locally.
CSS changes:
- style.css: .live-dot.connected, .hop-global-fallback, .perf-slow, .perf-warn
now use var(--status-green/red/yellow) instead of hardcoded hex
- live.css: live recording dot uses var(--status-red), LCD text uses var(--status-green)
JS changes (analytics.js):
- Added cssVar/statusGreen/statusYellow/statusRed/accentColor/snrColor helpers
that read from CSS custom properties with hardcoded fallbacks
- Replaced ~20 hardcoded status colors in: SNR histograms, quality zones,
zone borders/patterns, SNR timeline, daily SNR bars, collision badges
(Local/Regional/Distant), distance classification, subpath map markers,
hop distance distribution, network status cards, self-loop bars
JS changes (live.js):
- Added statusGreen helper for LCD clock color
- Legend dots now read from TYPE_COLORS global instead of hardcoded hex
All colors now respond to theme customization via the customize panel.
- Move --status-green/yellow/red from home.css to style.css :root (light+dark)
- Replace hardcoded status colors in style.css (.tl-snr, .health-dot, .byop-err,
.badge-hash-*, .fav-star.on, .spark-fill) with CSS variable references
- Replace hardcoded colors in live.css (VCR mode, stat pills, fdc-link, playhead)
- Replace --primary/--bg-secondary/--text-primary/--text-secondary dead vars with
canonical --accent/--input-bg/--text/--text-muted in style.css, map.js, live.js,
traces.js, packets.js
- Fix nodes.js legend colors to use ROLE_COLORS globals instead of hardcoded hex
- Replace hardcoded hex in home.js (SNR), perf.js (indicators), map.js (accuracy
circles) with CSS variable references via getComputedStyle or var()
- Add --detail-bg to customizer (THEME_CSS_MAP, DEFAULTS, ADVANCED_KEYS, labels)
- Move font/mono out of ADVANCED_KEYS into separate Fonts section in customizer
- Remove debug console.log lines from customize.js
- Bump cache busters in index.html
theme-changed now dispatches theme-refresh event instead of
full navigate(). Map re-renders markers, packets re-renders
table rows. No teardown/rebuild, no flash.
Added global window.TYPE_COLORS in roles.js. Live.js and audio-lab.js
now reference the global. Customizer shows packet type colors with
emoji + descriptions. Changes sync to TYPE_COLORS in real-time.
Saved/restored via localStorage alongside node colors.
- 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
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.
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.
audio.js is now the core engine (context, routing, voice mgmt).
Voice modules register via MeshAudio.registerVoice(name, module).
Each module exports { name, play(ctx, master, parsed, opts) }.
Voice selector dropdown appears in audio controls.
Voices persist in localStorage. Adding a new voice = new file +
script tag. Previous voices are never lost.
v1 "constellation" extracted as audio-v1-constellation.js.
AudioContext starts suspended until user gesture — now resumes on
setEnabled(). Also added sonifyPacket to animateRealisticPropagation
which is the main code path when Realistic mode is on.
Each observer sees a different path length in reality. Extra
rain columns now randomly vary ±1 hop from the base, giving
different fall distances for visual variety.
Trail was limited to hops*30px which meant 1-hop packets showed
1 character. Now shows up to 20 visible chars at once, scrolling
through the entire packet byte array as the drop falls.
dbPacketToLive() wasn't including raw_hex from API data.
VCR replay and timeline scrub packets had no raw bytes,
so rain silently dropped them all. Now includes pkt.raw_hex
as 'raw' field. Removed debug log.
Format 2 MQTT packets (companion bridge) have no raw hex field.
Now falls back to pkt.hash, then extracts hex from decoded payload
JSON. Random bytes only as absolute last resort.
New 'Rain' toggle on live map. Each incoming packet spawns a
falling column of hex bytes from its raw data:
- Fall distance proportional to hop count (8+ hops = full screen)
- 5 second fall time for full-height drops, proportional for shorter
- Leading char: bright white with green glow
- Trail chars: green, progressively fading
- Entire column fades out in last 30% of life
- Random x position across screen width
- Canvas-rendered at 60fps (no DOM overhead)
- Works independently of Matrix mode (can combine both)
- Pre-create pool of 6 reusable markers (no create/destroy per frame)
- CSS transition: transform 80ms linear for position, opacity 200ms ease
- will-change: transform, opacity for GPU compositing
- Styles moved from inline to .matrix-char span class
- Marker positions updated via setLatLng, browser interpolates between
- Fade-out via CSS transition instead of rAF opacity loop
Revert to 8c3e2f4 if this doesn't feel better.
Replaced setInterval(40ms) with rAF + time-based interpolation.
Same 1.4s duration per hop, but buttery smooth movement.
Fade-out also uses rAF instead of setInterval.
- Hex chars: 16px white text with triple green glow (was 12px green)
- Only render every 2nd step for wider spacing between bytes
- Animation speed: 45 steps @ 50ms (was 30 @ 33ms) — ~2.3s per hop
- Trail length reduced to 6 (less clutter)
- Map brightness down 10% (1.4 -> 1.25)
- Replaced sepia+hue-rotate chain with grayscale+brightness+contrast
- Green tint via ::before (multiply) + ::after (screen) overlays
- Much brighter base map — roads/coastlines/land clearly visible
- Markers dimmed to #005f15 at 40% opacity
- DivIcon markers at 35% brightness
- Map tiles desaturated + darkened to near-black with green tint
- CRT scanline overlay with subtle flicker animation
- All node markers re-tinted to Matrix green (#00ff41)
- Feed panel: dark green background, monospace font, green text
- Controls/VCR bar: green-on-black theme
- Node detail panel: green themed
- Zoom controls, attribution: themed
- Node labels glow green
- Markers get hue-rotate filter (except matrix hex chars)
- Restores all original colors when toggled off
New toggle in live map controls: 'Matrix' - animates packet hex bytes
flowing along paths in green Matrix-style rain effect.
- Hex bytes from actual packet raw_hex data flow along each hop
- Green (#00ff41) monospace characters with neon glow/text-shadow
- Trail of 8 characters with progressive fade
- Dim green trail line underneath
- Falls back to random hex if no raw data available
- Persists toggle state to localStorage
- Works alongside existing Realistic mode