Observer detail, home health timeline, and traces all called
.toFixed() on snr/rssi values that may be strings from the DB.
Wrapping in Number() matches what live.js already does.
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.
When new data arrived, toggleHeatmap() destroyed and recreated the
heat layer, causing a brief flash at full opacity before the CSS
opacity was applied via setTimeout. Now reuses the existing layer
via setLatLngs() for data updates, and hooks the 'add' event for
immediate opacity on first creation. No more flash.
All 12 E2E tests pass locally.
- Heat checkbox persists in localStorage across reload
- Heat checkbox is clickable (not disabled)
- Live page heat disabled when ghosts/matrix mode active
- Heatmap opacity value persists and applies to canvas
4 new tests, all verified locally before push.
The previous implementation used L.heatLayer's minOpacity which only
controlled the opacity of the coolest (blue) gradient stops. Now sets
CSS opacity on the canvas element directly, affecting all gradient
colors uniformly. Closes#119 properly.
Backend-only change: ~1 min (unit tests, skip Playwright/coverage)
Frontend-only change: ~2-5 min (E2E + coverage, skip backend suite)
Both changed: full suite (~14 min)
CI/test infra changed: full suite (safety net)
Detects changed files via git diff HEAD~1, runs appropriate suite.
The page.evaluate() calls corrupting localStorage and firing fake events
caused page error-reloads, losing accumulated coverage. Reverting to
the 42% version which was the actual high water mark.
- Full test file list with all 12+ test files
- Feature development workflow: write code → write tests → run locally → push
- Playwright defaults to localhost:3000, NEVER prod
- Coverage infrastructure explained (Istanbul instrument → Playwright → nyc)
- ARM testing notes (basic tests work, heavy coverage use CI)
- 4 new pitfalls from today's session
Exercise every major code path across all frontend files:
app.js: all routes, bad routes, hashchange, theme toggle x4,
hamburger menu, favorites dropdown, global search, Ctrl+K,
apiPerf(), timeAgo/truncate/routeTypeName utils
nodes.js: sort every column (both directions), every role tab,
every status filter, cycle all Last Heard options, click rows
for side pane, navigate to detail page, copy URL, show all
paths, node analytics day buttons (1/7/30/365), scroll target
packets.js: 12 filter expressions including bad ones, cycle all
time windows, group by hash toggle, My Nodes toggle, observer
menu, type filter menu, hash input, node filter, observer sort,
column toggle menu, hex hash toggle, pause button, resize handle,
deep-link to packet hash
map.js: all role checkboxes toggle, clusters/heatmap/neighbors/
hash labels toggles, cycle Last Heard, status filter buttons,
jump buttons, markers, zoom controls, dark mode tile swap
analytics.js: all 9 tabs clicked, deep-link to each tab via URL,
observer selector on topology, navigate rows on collisions/
subpaths, sortable headers on nodes tab, region filter
customize.js: all 5 tabs, all preset themes, branding text inputs,
theme color inputs, node color inputs, type color inputs, reset
buttons, home tab fields (hero, journey steps, checklist, links),
export tab, reset preview/user theme
live.js: VCR pause/speed/missed/prompt buttons, all visualization
toggles (heat/ghost/realistic/favorites/matrix/rain), audio
toggle + BPM slider, timeline click, resize event
channels.js: click rows, navigate to specific channel
observers.js: click rows, navigate to detail, cycle days select
traces.js: click rows
perf.js: refresh + reset buttons
home.js: both chooser paths, search + suggest, my-node cards,
health/packets buttons, remove buttons, toggle level, timeline
Also exercises packet-filter parser and region-filter directly.
- Install nyc for Istanbul instrumentation
- Add scripts/instrument-frontend.sh to instrument public/*.js
- Add scripts/collect-frontend-coverage.js to extract window.__coverage__
- Add scripts/combined-coverage.sh for combined server+frontend coverage
- Make server.js serve public-instrumented/ when COVERAGE=1 is set
- Add test:full-coverage npm script
- Add public-instrumented/ and .nyc_output/ to .gitignore
Tests now run in the test job, not after deploy. Spins up server.js
on port 13581, runs Playwright against it, kills it after.
If E2E fails, deploy is blocked — broken code never reaches prod.
BASE_URL env var makes the test configurable.
test-server-routes.js destructures { cache, pktStore, db } but these
weren't in module.exports. Also adds require.main guard so server
doesn't listen when imported by tests.
8 smoke tests against prod after deployment completes.
Uses Playwright bundled Chromium on x86 runner.
Falls back to CHROMIUM_PATH env var for other architectures.
Proof of concept: bare Playwright (not @playwright/test) running 8 critical
flow tests against analyzer.00id.net:
- Home page, nodes, map, packets, node detail, theme customizer, dark mode, analytics
- Uses system Chromium on ARM (Playwright bundled binary doesn't work on musl)
- Not added to test-all.sh or CI yet — POC only
- Run with: node test-e2e-playwright.js
Badges show: 'tests: 844/844 passed' and 'coverage: 76%'
Updated automatically by CI after each run via .badges/ JSON files.
Color: green >80%, yellow >60%, red <60%.
decodeEncryptedPayload: dest(1)+src(1)+MAC(2) per PAYLOAD_VER_1
decodeAck: dest(1)+src(1)+ack_hash(4)
decodeAnonReq: dest(1)+pubkey(32)+MAC(2)
decodePath: dest(1)+src(1)+MAC(2)+data
Source: firmware/src/Mesh.cpp lines 129-130, MeshCore.h CIPHER_MAC_SIZE=2
Golden fixture tests need updating to match correct output.