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.
Replace duplicated function definitions in server.js with imports from
server-helpers.js. Functions replaced: loadConfigFile, loadThemeFile,
buildHealthConfig, getHealthMs, isHashSizeFlipFlop, computeContentHash,
geoDist, deriveHashtagChannelKey, buildBreakdown, updateHashSizeForPacket,
rebuildHashSizeMap, requireApiKey, CONFIG_PATHS, THEME_PATHS.
disambiguateHops kept in server.js due to behavioral differences in the
distance sanity check (server version nulls lat/lon on unreliable hops
and adds ambiguous field in output mapping).
server.js: 3201 → 3001 lines (-200 lines, -224 deletions/+24 insertions)
All tests pass (unit, e2e, frontend).
Added test job that runs unit tests + integration tests + coverage
before deploy. Deploy job depends on test job passing.
If any test fails, deploy is blocked.
npm test: all tests + coverage summary
npm run test:unit: fast unit tests only
npm run test:coverage: full suite + HTML report in coverage/
Baseline: 37% statements, 42% branches, 54% functions
Fixed e2e channels crash (undefined .length on null)
Cloned meshcore-dev/MeshCore to firmware/ (gitignored).
AGENTS.md now mandates reading firmware source before implementing
anything protocol-related. Lists key files to check.
Derived from git history analysis: 4.3x fix ratio, 12 reverts, 7 cache
buster regressions, 21 commits for hash size, 6 for QR overlay.
Rules: test before push, bump cache busters, verify API shape, plan
before implementing, one commit per change, understand before fixing.
renderRows() in nodes.js and three places in map.js were using only
n.last_seen to compute active/stale status, ignoring the more recent
n.last_heard from in-memory packets. This caused nodes that were recently
heard but had an old DB last_seen to incorrectly show as stale.
Also adds 29 unit tests for the aging system (getNodeStatus,
getStatusInfo, getStatusTooltip, threshold values).
Keep the 📍map link in the Location metadata row (goes to app map).
Remove the redundant 📍 Map pill in the hex breakdown (went to Google Maps).
One link, one style.
Was making N API calls per observer for ambiguous hops on every page load,
plus another per packet detail view. All hop resolution now uses the
client-side HopResolver which already handles ambiguous prefixes.
Eliminates the main perf regression.
ReferenceError: node is not defined — was using wrong variable name.
Verified with 37 tests covering: firmware type names, aliases, route,
numeric ops, string ops, payload dot notation, hops, size, observations,
AND/OR/NOT, parentheses, and error handling.
Was using display names like 'Channel Msg' which aren't standard.
Now resolves to firmware names: GRP_TXT, TXT_MSG, REQ, ADVERT, etc.
Also accepts aliases: 'channel', 'dm', 'Channel Msg' all map to the
correct firmware name for convenience.
queryGrouped was missing route_type, snr, rssi fields. The packet filter
language couldn't filter by route/snr/rssi since grouped packets didn't
have those fields.
- Add getStatusTooltip() helper with role-aware explanations
- Tooltips on status labels in: node badges, status explanation, detail table
- Tooltips on map legend active/stale counts per role
- Native title attributes (long-press on mobile)
- Bump cache busters
Server now computes last_heard from in-memory packet store (all traffic
types) and includes it in /api/nodes response. Client prefers last_heard
over DB last_seen for display, sort, filter, and status calculation.
Fixes inconsistency where list showed '5d ago' but side pane showed
'26m ago' for the same node.
- Create getStatusInfo(), renderNodeBadges(), renderStatusExplanation(),
renderHashInconsistencyWarning() shared helpers
- Side pane (renderDetail) now uses shared helpers and shows status explanation
(was previously missing)
- Full page (loadFullNode) uses same shared helpers
- Both views now render identical status info
- Bump cache buster for nodes.js
Was fetching only 200 nodes with server-side filtering — missed nodes.
Now fetches full list once, caches it, filters by role/search/lastHeard
in the browser. Region change invalidates cache.
Two-state node freshness: Active vs Stale
- roles.js: add getNodeStatus(role, lastSeenMs) helper returning 'active'/'stale'
- Repeaters/Rooms: stale after 72h
- Companions/Sensors: stale after 24h
- Backward compat: getHealthThresholds() with degradedMs/silentMs still works
- map.js: stale markers get .marker-stale CSS class (opacity 0.35, grayscale 70%)
- Applied to both SVG shape markers and hash label markers
- makeMarkerIcon() and makeRepeaterLabelIcon() accept isStale parameter
- nodes.js: visual aging in table, side pane, and full detail
- Table: Last Seen column colored green (active) or muted (stale)
- Side pane: status shows 🟢 Active or ⚪ Stale (was 🟢/🟡/🔴)
- Full detail: Status row with role-appropriate explanation
- Stale repeaters: 'not heard for Xd — repeaters typically advertise every 12-24h'
- Stale companions: 'companions only advertise when user initiates'
- Fixed lastHeard fallback to n.last_seen when health API has no stats
- style.css: .marker-stale, .last-seen-active, .last-seen-stale classes
- All 5 columns (Name, Public Key, Role, Last Seen, Adverts) are now
sortable by clicking the column header
- Click toggles between ascending/descending sort
- Visual indicator (▲/▼) shows current sort column and direction
- Sort preference persisted to localStorage (meshcore-nodes-sort)
- Removed old Sort dropdown since headers replace it
- Client-side sorting on already-fetched data
- Default: Last Seen descending (most recent first)
- App Flags now shows human-readable type (Companion/Repeater/Room Server/Sensor)
instead of confusing individual flag names like 'chat, repeater'
- Boolean flags (location, name) shown separately after type: 'Room Server + location, name'
- Added Google Maps link on longitude row using existing detail-map-link style
ADV_TYPE_ROOM=3 (0b0011) was misread as chat+repeater because decoder
treated lower nibble as individual bits. Now correctly: type & 0x0F as
enum (0=none, 1=chat, 2=repeater, 3=room, 4=sensor).
Includes startup backfill: scans all adverts and fixes any node roles
in the DB that were incorrectly set to 'repeater' when they should be
'room'. Logs count of fixed nodes on startup.
TOC: #/analytics?tab=collisions§ion=inconsistentHashSection etc.
Back-to-top: #/analytics?tab=collisions (scrolls to top of tab)
All copyable, shareable, bookmarkable.
Sections: inconsistentHashSection, hashMatrixSection, collisionRiskSection
Use ?tab=collisions§ion=inconsistentHashSection to jump directly.
Scrolls after tab render completes (400ms delay for async content).