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.
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.
Added global window.TYPE_COLORS in roles.js. Live.js and audio-lab.js
now reference the global. Customizer shows packet type colors with
emoji + descriptions. Changes sync to TYPE_COLORS in real-time.
Saved/restored via localStorage alongside node colors.
Changing node colors in the customizer now updates both ROLE_COLORS
(used for badges, labels) and ROLE_STYLE (used for map markers).
Also fixed localStorage restore to sync both objects.
- 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
Export tab now has two sections:
- "My Preferences" — save colors to browser localStorage, auto-applied
on every page load. Personal to you, no server changes.
- "Admin Export" — download config.json for server deployment,
applies to all users.
User theme auto-loads on DOMContentLoaded, overriding CSS variables
and node colors from localStorage.
Click 🎨 in nav to toggle a floating panel. Stays open as you
navigate between pages — tweak colors, check packets, tweak more.
Draggable by header. Close with ✕. Preview persists everywhere.
Each color picker now has a description underneath:
- Accent: 'Active nav tab, buttons, links, selected rows, badges'
- Status Green: 'Healthy nodes, online indicators, good SNR'
- Repeater: 'Infrastructure nodes — map markers, packet path badges'
etc.
Preview was reverting on destroy (page leave). Now CSS variable
overrides stay active until explicit reset, so you can navigate
to packets/map/etc and see your color changes.
New page at #/customize with 5 tabs:
- Branding: site name, tagline, logo/favicon URLs
- Theme Colors: color pickers for all CSS variables with live preview
- Node Colors: per-role color pickers with dot previews
- Home Page: editable hero, steps, checklist, footer links
- Export: JSON diff output, copy/download buttons
Only exports values that differ from defaults. Self-contained CSS.
Mobile responsive, dark mode compatible.
Add GET /api/config/theme endpoint serving branding, theme colors,
node colors, and home page content from config.json with sensible
defaults so unconfigured instances look identical to before.
Client-side (app.js):
- Fetch theme config on page load, before first render
- Override CSS variables from theme.* on document root
- Override ROLE_COLORS/ROLE_STYLE from nodeColors.*
- Replace nav brand text, logo, favicon from branding.*
- Store config in window.SITE_CONFIG for other pages
Home page (home.js):
- Hero title/subtitle from config.home
- Steps and checklist from config.home
- Footer links from config.home.footerLinks
- Chooser welcome text uses configured siteName
Config example updated with all available theme options.
No default appearance changes — all overrides are optional.
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.
Per-observer resolve in the WS handler made it async, which
broke the debounce callback (unhandled promise + race conditions).
Live packets now render immediately with global cache. Per-observer
resolution happens on initial load and packet detail only.
New packets arriving via WebSocket were only getting global
resolution. Now ambiguous hops in WS batches also get per-observer
server-side resolution before rendering.
Ambiguous hops in the list now get resolved per-observer via
batch server API calls. Cache uses observer-scoped keys
(hop:observerId) so the same 1-byte prefix shows different
names depending on which observer saw the packet.
Flow: global resolve first (fast, covers unambiguous hops),
then batch per-observer resolve for ambiguous ones only.
When packet doesn't have lat/lon directly (channel messages, DMs),
look up sender node from DB by pubkey or name. Use that GPS as
the origin anchor for hop disambiguation. We've seen ADVERTs from
these senders — use that known location.
Without sender GPS (channel texts etc), the forward pass had no
anchor and just took candidates[0] — random order. Now regional
candidates are sorted by distKm to observer IATA center before
disambiguation. Closest to region center = default pick.
Client-side HopResolver wasn't properly disambiguating despite
correct data. Switched detail view to use the server API directly:
/api/resolve-hops?hops=...&observer=...&originLat=...&originLon=...
Server-side resolution is battle-tested and handles regional
filtering + GPS-anchored disambiguation correctly.
List view resolves hops without anchor (no per-packet context).
Detail view now always re-resolves with the packet's actual GPS
coordinates + observer, overwriting stale cache entries.
Removed debug logging.
ADVERT packets have GPS coordinates — use them as the forward
pass anchor so the first hop resolves to the nearest candidate
to the sender, not random pick order.
The general hop cache was populated without observer context,
so all conflicts showed filterMethod=none. Now renderDetail()
re-resolves hops with pkt.observer_id, getting proper regional
filtering with distances and conflict flags.
⚠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.
For packets without direct lat/lon (GRP_TXT, TXT_MSG):
- Look up sender by pubKey via /api/nodes/:key
- Look up sender by name via /api/nodes/search?q=name
- Show location + 📍map link when node has coordinates
Works for decrypted channel messages (sender field), direct
messages (srcPubKey), and any packet type with a resolvable sender.
📍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.
- packets.js: obsName() now shows IATA code next to observer name, e.g. 'EW-SFC-DR01 (SFO)'
- packets.js: hop conflicts in field table show distance (e.g. '37km')
- nodes.js: both full and sidebar detail views show 'Regions: SJC, OAK, SFO' badges and per-observer IATA
- live.js: node detail panel shows regions in 'Heard By' heading and per-observer IATA
- server.js: /api/nodes/:pubkey/health now returns iata field for each observer
- Bump cache busters