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.
- 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
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).
Added ids: node-stats, node-observers, fullPathsSection, node-packets.
Use ?section=<id> to scroll to any section on load.
e.g. #/nodes/<pubkey>?section=node-packets
Variable hash size badge and analytics links updated to use ?section=.
- Removed yellow text and redundant Status column
- Sizes Seen now uses colored badges (orange 1B, pale green 2B, bright green 3B)
- Row striping, card border/radius, accent-colored node links
- Current hash in mono with muted byte count
- Renamed 'Hash Collisions' tab to 'Hash Issues'
- New section at top: 'Inconsistent Hash Sizes' table listing all nodes
that have sent adverts with varying hash sizes
- Each node links to its detail page with ?highlight=hashsize for
per-advert hash size breakdown
- Shows current hash prefix, all sizes seen, and affected count
- Green checkmark when no inconsistencies detected
- Existing collision grid and risk table unchanged below
- Badge is now a link to the detail page with ?highlight=hashsize
- Detail page auto-scrolls to Recent Packets section
- Each advert shows its hash size badge (yellow if different from current)
- Detail page shows always-visible explanation banner (not hidden)
- Side pane badge links to detail page too
Badge shows cursor:help and clicking toggles a yellow-bordered info box
explaining the issue and suggesting firmware update. Stats row just shows
'⚠️ varies' with tooltip. Much less jarring than a dead yellow badge.
White semi-transparent square behind QR so black modules pop.
White rects in SVG already set to transparent by JS.
Same white backing in dark mode too (QR needs light bg to scan).
Tracks all hash_size values seen per node. If a node has sent adverts
with different hash sizes, flags it as hash_size_inconsistent with a
yellow ⚠️ badge on both side pane and detail page. Tooltip mentions
likely firmware bug (pre-1.14.1). Stats row shows all sizes seen.
CSS fill-opacity selectors weren't matching the QR library's output.
Now JS directly sets white rects to transparent after SVG generation.
Overlay at 70% opacity so it doesn't fight the map for attention.
Removed 'Scan with MeshCore app' label from overlay version.
- QR globally reduced (140px → 100px, overlay 64px)
- Side pane: name/badges first, then map with QR overlaid in bottom-right corner
- Removed standalone QR section from side pane — saves vertical space
- Public key shown inline below map instead of separate section
- No-location nodes still get standalone centered QR
- Full detail page QR wrap narrower (max 160px)
- Map and QR code now sit side-by-side (flex: 3/1) instead of stacked
- QR section shows truncated public key below the code
- Stats section uses a compact 2-column table with alternating row stripes
- Name/badges/actions section tightened up with less vertical spacing
- Mobile (<768px): stacks map and QR vertically
- No-location nodes: QR centered at max 240px width
- Side pane: replace '📋 URL' button with '🔍 Details' link to full detail page
- Side pane: add hash size badge next to role badge
- Full detail page: add hash size badge next to role badge
- Full detail page: add Hash Size row in stats section
- Handle null hash_size gracefully
Customizer now syncs state.home and state.branding to window.SITE_CONFIG
on every change, then dispatches hashchange to trigger page re-render.
Previously only saved to localStorage — home.js reads SITE_CONFIG.
Two bugs:
1. fetch was cached by browser — added cache: 'no-store'
2. navigate() ran before config fetch completed — moved routing
into .finally() so SITE_CONFIG is populated before any page
renders. Home page was reading SITE_CONFIG before fetch resolved,
getting undefined, falling back to hardcoded defaults.
Adds a Theme Presets section at the top of the Theme Colors tab with 8
WCAG AA-verified preset themes:
- Default: Original MeshCore blue (#4a9eff)
- Ocean: Deep blues and teals, professional
- Forest: Greens and earth tones, natural
- Sunset: Warm oranges, ambers, and reds
- Monochrome: Pure grays, no color accent, minimal
- High Contrast: WCAG AAA (7:1), bold colors, accessibility-first
- Midnight: Deep purples and indigos, elegant
- Ember: Dark warm red/orange accents, cyberpunk feel
Each theme has both light and dark variants with all 20 color keys.
High Contrast theme includes custom nodeColors and typeColors for
maximum distinguishability.
Active preset is auto-detected and highlighted. Users can select a
preset then tweak individual colors (becomes Custom).
- Dark mode: now merges theme + themeDark and applies correctly
- Added missing CSS var mappings: navText, navTextMuted, background, sectionBg, font, mono
- Fixed 'background' key mapping (was 'surface0', never matched)
- Derived vars (content-bg, card-bg) set from server config
- Type colors from server config now applied to TYPE_COLORS global
- syncBadgeColors called after type color override
- Import File button opens file picker for .json theme files
- Merges imported theme into current state, applies live preview
- Also syncs ROLE_COLORS/TYPE_COLORS globals on import
- Moved Copy/Download buttons out of collapsed details
- Raw JSON textarea now editable (was readonly)
- Server reads from theme.json (separate from config.json), hot-loads on every request
- POST /api/config/theme writes theme.json directly — no manual file editing
- GET /api/config/theme now merges: defaults → config.json → theme.json
- Also returns themeDark and typeColors (were missing from API)
- Customizer: replaced 'merge into config.json' instructions with Save/Load Server buttons
- JSON export moved to collapsible details section
- theme.json added to .gitignore (instance-specific)