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).
- 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
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
- 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.
📍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.
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.
Adds mapDefaults config option with center and zoom properties.
New /api/config/map endpoint serves the defaults. live.js and map.js
fetch the config with fallback to hardcoded Bay Area defaults.
FixesKpa-clawbot/meshcore-analyzer#115
- Show '?' with grey background for nodes with null hash_size instead of '1B'
- Add collision detection to offset overlapping repeater labels
- Draw callout lines from offset labels back to true position
- Re-deconflict labels on zoom change
- Compute hash_size from ADVERT packets in /api/nodes response
- Show colored rectangle markers with hash size (e.g. '2B') for repeaters
- Add 'Hash size labels' toggle in map controls (default ON, saved to localStorage)
- Non-repeater markers unchanged
- Observer ID is uppercase, node pubkey is lowercase — added COLLATE NOCASE
- New /api/config/regions endpoint merges config regions + observed IATAs
- map.js and packets.js fetch regions from API instead of hardcoded maps
- Removed dead 'MQTT Connected Only' checkbox (never worked)
- Added 'observer' role type with purple star marker
- Observer locations computed from average of nodes they've seen
- Observer popup with name, IATA, packets, link to detail page
- Role filter checkbox includes observers with count
All cache TTLs now read from config.json cacheTTL section (seconds).
Client fetches config on load via GET /api/config/cache.
config.example.json updated with defaults.
Edit config.json, restart server — no code changes needed to tweak TTLs.
- Map controls panel collapsible with toggle button, default collapsed on mobile (closes#16)
- jumpToRegion filters nodes by region observers instead of fitting all (closes#33)
- Detail panel Leaflet maps tracked and removed on re-selection (closes#34)
- Popup HTML uses semantic dl/dt/dd instead of table (closes#46)
- fitBounds now tracks user interaction instead of savedView const (closes#47)
- esc() replaced with safeEsc fallback for implicit global safety (closes#48)
- Checkboxes in map controls have explicit for/id association (closes#49)
- Sort columns have aria-sort attributes (closes#50)
- Filter selects have aria-label attributes (closes#51)
- Back button uses addEventListener instead of inline onclick (closes#52)
- Detail panel map height increased to 280px on desktop (closes#53)
Packets page now passes server-disambiguated full pubkeys.
Map needs bidirectional prefix match since DB nodes may have
truncated (8-char) or full (64-char) keys. Removes redundant
client-side geographic disambiguation since packets page already
resolved via /api/resolve-hops.
Route markers and polyline were on markerLayer, which gets cleared
by renderMarkers() on any filter/zoom change. Now uses dedicated
routeLayer that persists independently.
Hop hashes are 1-2 byte truncated values that don't work in URL params.
Now passes raw hops via sessionStorage; map page reads them after nodes
load and resolves via prefix match against full public keys.
- Hops in packet table are now clickable links to node detail (#/nodes/<hop>)
- Packet detail panel shows 'View route on map' link for packets with hops
- Map page reads ?highlight= query param and draws dashed route polyline
- Route shows numbered markers: green=origin, amber=hops, red=destination
- Map auto-fits to route bounds