## Summary
Fixes#534 — mobile filter dropdown doesn't expand on packets page.
## Root Cause
CSS specificity battle in the mobile media query. The hide rule uses
`:not()` pseudo-classes which add specificity:
```css
/* Higher specificity due to :not() */
.filter-bar > *:not(.filter-toggle-btn):not(.col-toggle-wrap) { display: none; }
/* Lower specificity — loses even with .filters-expanded */
.filter-bar.filters-expanded > * { display: inline-flex; }
```
The JS toggle correctly adds/removes `.filters-expanded`, but the CSS
expanded rule could never win.
## Fix
Match the `:not()` selectors in the expanded rule so `.filters-expanded`
makes it strictly more specific:
```css
.filter-bar.filters-expanded > *:not(.filter-toggle-btn):not(.col-toggle-wrap) { display: inline-flex; }
```
Added a comment explaining the specificity dependency so future devs
don't repeat this.
## Tests
Added Playwright E2E test: mobile viewport (480×800), navigates to
packets page, clicks filter toggle, verifies filter inputs become
visible.
---------
Co-authored-by: you <you@example.com>
## Summary
Part of #523 — fixes bugs 5 and 7 (bug 6 was a duplicate of bug 7).
### Bug 5: Show Neighbors button throws `window._mapSelectRefNode is not
a function`
**Root cause:** Map popup HTML used inline `onclick` calling
`window._mapSelectRefNode`, which was deleted on SPA page destroy. If a
popup persisted after navigation, clicks would throw.
**Fix:** Replaced inline `onclick` with event delegation. A
document-level click handler catches all `[data-show-neighbors]` clicks
and calls `selectReferenceNode` directly. The global
`window._mapSelectRefNode` is still exposed for existing Playwright
tests but is no longer relied upon by the UI.
### Bug 7: Blue text on dark blue background (dark mode contrast)
**Root cause:** Neighbor table cells inside `.node-detail-section` /
`.node-full-card` inherited accent/link color instead of using
`var(--text)`, making text unreadable in dark mode.
**Fix:** Added explicit `color: var(--text)` on `.node-detail-section
.data-table td` and `.node-full-card .data-table td`. Only `<a>` tags
within those cells retain `color: var(--accent)`.
### Files changed
- `public/map.js` — event delegation for Show Neighbors
- `public/style.css` — contrast fix for neighbor table cells
---------
Co-authored-by: you <you@example.com>
## Summary
Fixes the neighbor affinity graph returning empty results despite
abundant ADVERT data in the store.
**Root cause:** `extractFromNode()` in `neighbor_graph.go` only checked
for `"from_node"` and `"from"` fields in the decoded JSON, but real
ADVERT packets store the originator public key as `"pubKey"`. This meant
`fromNode` was always empty, so:
- Zero-hop edges (originator↔observer) were never created
- Originator↔path[0] edges were never created
- Only observer↔path[last] edges could be created (and only for
non-empty paths)
**Fix:** Check `"pubKey"` first in `extractFromNode()`, then fall
through to `"from_node"` and `"from"` for other packet types.
## Bugs Fixed
| Bug | Issue | Fix |
|-----|-------|-----|
| Empty graph results | #522 | `extractFromNode()` now reads `pubKey`
field from ADVERTs |
| 3-4s response time | #523 comment | Graph was rebuilding correctly
with 60s TTL cache — the slow response was due to iterating all packets
finding zero matches. With edges now being found, the cache works as
designed. |
| Incomplete visualization | #523 comment | Downstream of bug 1+2 —
fixed by fixing the builder |
| Accessibility | #523 comment | Added text-based neighbor list, dynamic
aria-label, keyboard focus CSS, dashed lines for ambiguous edges,
confidence symbols |
## Changes
- **`cmd/server/neighbor_graph.go`** — Fixed `extractFromNode()` to
check `pubKey` field (real ADVERT format)
- **`cmd/server/neighbor_graph_test.go`** — Added 2 new tests:
`TestBuildNeighborGraph_AdvertPubKeyField` (real ADVERT format) and
`TestBuildNeighborGraph_OneByteHashPrefixes` (1-byte prefix collision
scenario)
- **`public/analytics.js`** — Added accessible text-based neighbor list,
dynamic aria-label, dashed line pattern for ambiguous edges
- **`public/style.css`** — Added `:focus-visible` keyboard focus
indicator for canvas
## Testing
All Go tests pass (`go test ./... -count=1`). New tests verify the fix
prevents regression.
Fixes#523, Fixes#522
---------
Co-authored-by: you <you@example.com>
## Summary
- `BuildBreakdown` was never ported from the deleted Node.js
`decoder.js` to Go — the server has returned `breakdown: {}` since the
Go migration (commit `742ed865`), so `createColoredHexDump()` and
`buildHexLegend()` in the frontend always received an empty `ranges`
array and rendered everything as monochrome
- Implemented `BuildBreakdown()` in `decoder.go` — computes labeled byte
ranges matching the frontend's `LABEL_CLASS` map: `Header`, `Transport
Codes`, `Path Length`, `Path`, `Payload`; ADVERT packets get sub-ranges:
`PubKey`, `Timestamp`, `Signature`, `Flags`, `Latitude`, `Longitude`,
`Name`
- Wired into `handlePacketDetail` (was `struct{}{}`)
- Also adds per-section color classes to the field breakdown table
(`section-header`, `section-transport`, `section-path`,
`section-payload`) so the table rows get matching background tints
## Test plan
- [x] Open any packet detail pane — hex dump should show color-coded
sections (red header, orange path length, blue transport codes, green
path hops, yellow/colored payload)
- [x] Legend below action buttons should appear with color swatches
- [x] ADVERT packets: PubKey/Timestamp/Signature/Flags each get their
own distinct color
- [x] Field breakdown table section header rows should be tinted per
section
- [x] 8 new Go tests: all pass
Closes#329🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Priority+ Navigation Pattern for Tablet Viewports
Phase 2 of responsive nav improvements for #322.
### What this does
On **tablet viewports (768-1023px)**, implements the [Priority+
navigation
pattern](https://css-tricks.com/the-priority-plus-navigation-pattern/):
- **5 high-priority tabs** shown inline: Home, Nodes, Packets, Map, Live
- **6 low-priority tabs** collapse into a "More ▾" dropdown: Channels,
Traces, Observers, Analytics, Perf, Lab
- The "More" button highlights when a low-priority page is active
**Desktop (>=1024px)** and **mobile (<768px)** behavior is unchanged.
### Changes
| File | Change |
|------|--------|
| `public/index.html` | Added `data-priority="high"` to 5 primary nav
links; added More button + dropdown menu |
| `public/style.css` | Split ≤1023px hamburger query into tablet
Priority+ (768-1023px) and mobile hamburger (<768px); added More
dropdown styles |
| `public/app.js` | Added `closeMoreMenu()`, More button toggle,
outside-click/Escape close, active state on More button |
| Cache busters | Bumped in same commit |
### Accessibility
- `aria-haspopup="true"` and `aria-expanded` on More button
- `role="menu"` / `role="menuitem"` on dropdown
- Focus moves to first item on open
- Escape key closes dropdown
### Testing
- All 308 existing tests pass (217 frontend-helpers + 62 packet-filter +
29 aging)
- No new dependencies added
- No build step changes
### Breakpoint summary
| Viewport | Behavior |
|----------|----------|
| >= 1024px | Full horizontal nav (unchanged) |
| 768-1023px | Priority+ pattern: 5 tabs + More dropdown **← NEW** |
| < 768px | Hamburger drawer with all items (unchanged) |
---------
Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Extends the hamburger menu activation breakpoint from max-width: 640px
to max-width: 1023px, making all 11 nav items accessible on tablets and
small laptops where they were previously clipped/invisible.
Fixes#322
## Changes
### public/style.css
- New @media (max-width: 1023px) block activates the hamburger menu and
vertical drawer
- Drawer has max-height: calc(100dvh - 52px) with overflow-y: auto for
scrollability
- z-index set to 1100 (consistent with nav layer)
- ody.nav-open locks background scroll when drawer is open
- Mobile-only rules (brand-text hidden, tighter nav-right gap) remain at
640px
### public/app.js
- Extracted closeNav() helper for consistent drawer close behavior
- Hamburger toggle now adds/removes ody.nav-open class
- Drawer closes on: nav link click, Escape key, and route change (SPA
navigation)
### public/index.html
- Cache busters bumped for all CSS/JS assets
## What's NOT changed
- Desktop layout (>=1024px) is completely untouched
- No Priority+ pattern (Phase 2)
- No map layout changes (Phase 3)
- No new dependencies
## Testing
- All 308 frontend tests pass ( est-frontend-helpers.js,
est-packet-filter.js, est-aging.js)
- Visual verification: hamburger activates at <=1023px, full bar at
>=1024px
---------
Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Fixes the poor contrast in the node side pane's "Paths through this
node" section in dark mode.
## Root Cause
.node-detail-section (side pane) had no background or border — it
inherited the lighter --detail-bg (#232340) from .panel-right. The same
content on the full detail page sits inside .node-full-card which uses
the darker --card-bg (#1a1a2e) + a visible border, giving it proper
contrast.
| Context | Container | Background | Contrast |
|---------|-----------|------------|----------|
| Full detail page | .node-full-card | --card-bg (darker) | ✅ Good |
| Side pane | .node-detail-section | inherited --detail-bg (lighter) | ❌
Poor |
## Fix
Give .node-detail-section the same card treatment as .node-full-card:
`css
.node-detail-section {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
}
`
- All colors use CSS variables — no hardcoded hex values
- Both light and dark themes benefit from the card treatment
- No JS changes needed — CSS-only fix
- Cache busters bumped in the same commit
Fixes#334
Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Surfaces transport route types in the packets view by adding a **"T"
badge** next to the payload type badge for packets with
`TRANSPORT_FLOOD` (route type 0) or `TRANSPORT_DIRECT` (route type 3)
routes.
This helps mesh analysis — communities can quickly identify transported
packets and gain insights into scope usage adoption.
Closes#241
## What Changed
### Frontend (`public/`)
- **app.js**: Added `isTransportRoute(rt)` and `transportBadge(rt)`
helper functions that render a `<span class="badge
badge-transport">T</span>` badge with the full route type name as a
tooltip
- **packets.js**: Applied `transportBadge()` in all three packet row
render paths:
- Flat (ungrouped) packet rows
- Grouped packet header rows
- Grouped packet child rows
- **style.css**: Added `.badge-transport` class with amber styling and
CSS variable support (`--transport-badge-bg`, `--transport-badge-fg`)
for theme customization
### Backend (`cmd/server/`)
- **decoder_test.go**: Added 6 new tests covering:
- `TestDecodeHeader_TransportFlood` — verifies route type 0 decodes as
TRANSPORT_FLOOD
- `TestDecodeHeader_TransportDirect` — verifies route type 3 decodes as
TRANSPORT_DIRECT
- `TestDecodeHeader_Flood` — verifies route type 1 (non-transport)
decodes correctly
- `TestIsTransportRoute` — verifies the helper identifies transport vs
non-transport routes
- `TestDecodePacket_TransportFloodHasCodes` — verifies transport codes
are extracted from T_FLOOD packets
- `TestDecodePacket_FloodHasNoCodes` — verifies FLOOD packets have no
transport codes
## Visual
In the packets table Type column, transport packets now show:
```
[Channel Msg] [T] ← transport packet
[Channel Msg] ← normal flood packet
```
The "T" badge has an amber color scheme and shows the full route type
name on hover.
## Tests
- All Go tests pass (`cmd/server` and `cmd/ingestor`)
- All frontend tests pass (`test-packet-filter.js`, `test-aging.js`,
`test-frontend-helpers.js`)
- Cache busters bumped in `index.html`
---------
Co-authored-by: you <you@example.com>
Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Several features and fixes from a live deployment of the Go v3.0.0
backend.
### geo_filter — full enforcement
- **Go backend config** (`cmd/server/config.go`,
`cmd/ingestor/config.go`): added `GeoFilterConfig` struct so
`geo_filter.polygon` and `bufferKm` from `config.json` are parsed by
both the server and ingestor
- **Ingestor** (`cmd/ingestor/geo_filter.go`, `cmd/ingestor/main.go`):
ADVERT packets from nodes outside the configured polygon + buffer are
dropped *before* any DB write — no transmission, node, or observation
data is stored
- **Server API** (`cmd/server/geo_filter.go`, `cmd/server/routes.go`):
`GET /api/config/geo-filter` endpoint returns the polygon + bufferKm to
the frontend; `/api/nodes` responses filter out any out-of-area nodes
already in the DB
- **Frontend** (`public/map.js`, `public/live.js`): blue polygon overlay
(solid inner + dashed buffer zone) on Map and Live pages, toggled via
"Mesh live area" checkbox, state shared via localStorage
### Automatic DB pruning
- Add `retention.packetDays` to `config.json` to delete transmissions +
observations older than N days on a daily schedule (1 min after startup,
then every 24h). Nodes and observers are never pruned.
- `POST /api/admin/prune?days=N` for manual runs (requires `X-API-Key`
header if `apiKey` is set)
```json
"retention": {
"nodeDays": 7,
"packetDays": 30
}
```
### tools/geofilter-builder.html
Standalone HTML tool (no server needed) — open in browser, click to
place polygon points on a Leaflet map, set `bufferKm`, copy the
generated `geo_filter` JSON block into `config.json`.
### scripts/prune-nodes-outside-geo-filter.py
Utility script to clean existing out-of-area nodes from the database
(dry-run + confirm). Useful after first enabling geo_filter on a
populated DB.
### HB column in packets table
Shows the hop hash size in bytes (1–4) decoded from the path byte of
each packet's raw hex. Displayed as **HB** between Size and Type
columns, hidden on small screens.
## Test plan
- [x] ADVERT from node outside polygon is not stored (no new row in
nodes or transmissions)
- [x] `GET /api/config/geo-filter` returns polygon + bufferKm when
configured, `{polygon: null, bufferKm: 0}` when not
- [x] `/api/nodes` excludes nodes outside polygon even if present in DB
- [x] Map and Live pages show blue polygon overlay when configured;
checkbox toggles it
- [x] `retention.packetDays: 30` deletes old transmissions/observations
on startup and daily
- [x] `POST /api/admin/prune?days=30` returns `{deleted: N, days: 30}`
- [x] `tools/geofilter-builder.html` opens standalone, draws polygon,
copies valid JSON
- [x] HB column shows 1–4 for all packets in grouped and flat view
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 1/2/3-byte selector to Hash Issues analytics page
- 1-byte and 2-byte modes show 16×16 matrix with stat cards (nodes
tracked, using N-byte ID, prefix space used, prefix collisions)
- 3-byte mode shows summary stat cards instead of unrenderable grid
- Fix "Nodes tracked" to always show total node count across all modes
- Use CSS variable colours for matrix cells (light/dark mode compatible)
- Replace native title tooltips with custom styled popovers
- Hide collision risk card when 3-byte mode is selected
- Fix double-tooltip bug on mode switch via _matrixTipInit guard
- Fix tooltip persisting outside matrix grid on mouseleave
https://dev.ve7kod.ca/#/analytics
Hash Issues
---------
Co-authored-by: Jesse <your@email.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- perf.js: toFixed(1) on all ms/MB values in Go Runtime section
- style.css: white-space: nowrap on .nav-stats to prevent the · separator
from wrapping onto its own line
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#203: Live page node detail panel becomes a bottom-sheet on mobile
(width:100%, bottom:0, max-height:60vh, rounded top corners).
#204: Perf page reduces padding to 12px, perf-cards stack in 2-col
grid, tables get smaller font/padding on mobile.
#205: Nodes table hides Public Key column on mobile via .col-pubkey
class (same pattern as packets page .col-region/.col-rpt).
Cache busters bumped in index.html.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Change .traces-page max-width from 1000px to 95vw via CSS variable
(--trace-max-width) and center with margin: 0 auto
- Increase SVG path graph column spacing from 140px to 200px so nodes
and labels don't overlap
- Bump cache busters
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Style .version-badge anchor elements to use --nav-text-muted color
instead of browser-default blue. Adds hover state using --nav-text.
Works with both light and dark themes via existing CSS variables.
fixes#139
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add formatVersionBadge() that renders version, short commit hash, and
engine as a single badge in the nav stats area. Format: v2.6.0 · abc1234 [go].
Skips commit when 'unknown' or missing. Truncates commit to 7 chars.
Replaces the standalone engine badge call in updateNavStats().
8 unit tests cover all edge cases (missing fields, v-prefix dedup,
unknown commit, truncation).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Show [go] or [node] badge in the nav stats bar when /api/stats
returns an engine field. Gracefully hidden when field is absent.
- Add formatEngineBadge() to app.js (top-level, testable)
- Add .engine-badge CSS class using CSS variables
- Add 5 unit tests in test-frontend-helpers.js
- Bump cache busters
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add #/compare page that lets users select two observers and compare
which packets each sees. Fetches last 24h of packets per observer,
computes set diff client-side using O(n) Set lookups. Shows summary
cards (both/only-A/only-B), stacked bar, type breakdown, and tabbed
detail tables. URL is shareable via ?a=ID1&b=ID2 query params.
- New file: public/compare.js (comparePacketSets + page module)
- Added compare button to observers page header
- 11 new tests for comparePacketSets (87 total frontend tests)
- Cache busters bumped
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes#125
When the ✕ close button (or Escape) is pressed, the detail pane now
fully hides via display:none (CSS class 'detail-collapsed' on the
split-layout container) so the packets table expands to 100% width.
Clicking a packet row removes the class and restores the detail pane.
Previously the pane only cleared its content but kept its 420px width,
leaving a blank placeholder that wasted ~40% of screen space.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Issues fixed:
- #127: Firefox copy URL - shared copyToClipboard() with execCommand fallback
- #125: Dismiss packet detail pane - close button with keyboard support
- #124: Customize window scrollbar - flex layout fix for overflow
- #122: Last Activity stale times - use last_heard || last_seen
Test improvements:
- E2E perf: replace 19 networkidle waits, cut navigations 14->7, remove 11 sleeps
- 8 new unit tests for copyToClipboard helper (47->55 in test-frontend-helpers)
- 1 new E2E test for packet pane dismiss
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- 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)
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).
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
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.
Added --nav-text and --nav-text-muted CSS variables. All nav
selectors (.top-nav, .nav-brand, .nav-link, .nav-btn, .nav-stats)
use these instead of --text/--text-muted. Nav Text is in Basic
settings. Nav Muted Text in Advanced.
This is separate from page text because nav sits on a dark
background — page text color would be unreadable on the nav.
- 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
Changed nav brand, links, buttons from hardcoded #fff/#cbd5e1 to
var(--text) and var(--text-muted). Setting primary text color
now changes nav text too. Removed unnecessary --nav-text variable.
Badge colors were hardcoded in style.css with different values
than TYPE_COLORS, causing mismatch between customizer and actual
display. Removed all .badge-advert/.badge-grp-txt/etc color rules.
syncBadgeColors() in roles.js now generates them from TYPE_COLORS
on every page load.
Color changes dispatch theme-changed event → app.js re-navigates
to current page, rebuilding markers/rows with new colors.
Conflict badges (.hop-ambiguous, .hop-conflict-btn) now use
var(--status-yellow) so they follow the customized status color.
- Nav bar now uses CSS variables (was hardcoded gradient)
- 19 customizable colors: accent, text, backgrounds, borders,
surfaces, inputs, stripes, hover, selected, status indicators
- Light and dark mode have separate color sets
- Theme tab shows which mode you are editing
- Toggle ☀️/🌙 in nav bar to switch modes and edit the other set
- Export includes both theme and themeDark sections
- localStorage save/restore handles both modes
New color palette: deep navy (#060a13, #111c36) replacing
purple tones. Muted greens/yellows/reds for status indicators.
All functional CSS (hop conflicts, audio, matrix, region dropdown)
preserved and appended.
⚠3 is now a yellow button (not tiny superscript). Clicking it
opens a popover listing all regional candidates with:
- Node name (clickable → node detail page)
- Distance from observer region center
- Truncated pubkey
Popover dismisses on outside click. Each candidate is a link
to #/nodes/PUBKEY for full details.
New hop-display.js: shared renderHop() and renderPath() with
full conflict info — candidate count, regional/global flags,
distance, filter method. Tooltip shows all candidates with
details on hover.
packets.js: uses HopDisplay.renderHop() (was inline)
nodes.js: path rendering uses HopDisplay when available
style.css: .hop-current for highlighting the viewed node in paths
Consistent conflict display across packets + node detail pages.
1-byte (and 2-byte) hop IDs match many nodes globally. Previously
resolve-hops picked candidates from anywhere, causing cross-regional
false paths (e.g. Eugene packet showing Vancouver repeaters).
Fix: Use observer IATA to determine packet region. Filter candidates
to nodes seen by observers in the same IATA region via the existing
_advertByObserver index. Fall back to global only if zero regional
candidates exist (flagged as globalFallback).
API changes to /api/resolve-hops response:
- conflicts[]: all candidates with regional flag per hop
- totalGlobal/totalRegional: candidate counts
- globalFallback: true when no regional candidates found
- region: packet IATA region in top-level response
UI changes:
- Conflict count badge (⚠3) instead of bare ⚠
- Tooltip shows regional vs global candidates
- Unreliable hops shown with strikethrough + opacity
- Global fallback hops shown with red dashed underline
- Add width: max-content to dropdown menu for auto-sizing
- Add overflow ellipsis + max-width on dropdown items for very long labels
- Checkboxes already flex-shrink: 0, no text wrapping with white-space: nowrap