Fix network map mobile overflow and infer MQTT hops from SNR=0 (#2)

* Fix network map mobile overflow and infer MQTT hops from SNR=0

- Add min-h-0 and flex-shrink-0 to map layout so legend/actions don't
  overflow the viewport on mobile
- Infer viaMqtt=true for traceroute hops with SNR exactly 0
- Change MQTT hop color from purple to orange across map, legend, and
  node detail badges

https://claude.ai/code/session_01Ffqq7YPCJE28uUFR88eK7C

* Revert MQTT color to purple; keep SNR=0 MQTT inference

The color change was unintended — MQTT hops should stay purple. The
SNR=0 inference in traceroute processing correctly marks those hops
as viaMqtt so they render as purple dashed lines.

https://claude.ai/code/session_01Ffqq7YPCJE28uUFR88eK7C

* Fix test setup: mock URL.createObjectURL for maplibre-gl

maplibre-gl calls URL.createObjectURL during import for its worker
setup, which doesn't exist in jsdom. Add the mock to test setup.

https://claude.ai/code/session_01Ffqq7YPCJE28uUFR88eK7C

* Run go fmt on unformatted files

https://claude.ai/code/session_01Ffqq7YPCJE28uUFR88eK7C

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Daniel Pupius
2026-03-15 23:06:20 -07:00
committed by GitHub
parent ededf5c93d
commit 26a2bba441
6 changed files with 30 additions and 18 deletions

View File

@@ -40,10 +40,10 @@ type Config struct {
ChannelKeys []string
// Statistics configuration
StatsInterval time.Duration
CacheSize int
CacheRetention time.Duration
VerboseLogging bool
StatsInterval time.Duration
CacheSize int
CacheRetention time.Duration
VerboseLogging bool
}
// getEnv retrieves an environment variable with the given prefix or returns the default value

View File

@@ -30,11 +30,11 @@ var typePriority = map[pb.PortNum]int{
pb.PortNum_TEXT_MESSAGE_COMPRESSED_APP: 5,
pb.PortNum_NEIGHBORINFO_APP: 4, // rare; protect from eviction
pb.PortNum_TRACEROUTE_APP: 3,
pb.PortNum_POSITION_APP: 3,
pb.PortNum_NODEINFO_APP: 2, // frequent; lower priority
pb.PortNum_TELEMETRY_APP: 2,
pb.PortNum_ROUTING_APP: 2,
pb.PortNum_MAP_REPORT_APP: 2,
pb.PortNum_POSITION_APP: 3,
pb.PortNum_NODEINFO_APP: 2, // frequent; lower priority
pb.PortNum_TELEMETRY_APP: 2,
pb.PortNum_ROUTING_APP: 2,
pb.PortNum_MAP_REPORT_APP: 2,
}
// defaultTypePriority applies to any port type not listed above.

View File

@@ -60,10 +60,10 @@ func TestNodeAwareCacheBasicOrdering(t *testing.T) {
func TestPressureEvictsOldestOfLowestPriority(t *testing.T) {
c := NewNodeAwareCache(3, time.Hour)
c.Add(pkt(1, 1, pb.PortNum_NODEINFO_APP)) // priority 2
c.Add(pkt(2, 1, pb.PortNum_NEIGHBORINFO_APP)) // priority 4
c.Add(pkt(3, 1, pb.PortNum_NODEINFO_APP)) // priority 2 — at cap
c.Add(pkt(4, 1, pb.PortNum_NODEINFO_APP)) // pressure: evicts oldest pri=2 (ID 1)
c.Add(pkt(1, 1, pb.PortNum_NODEINFO_APP)) // priority 2
c.Add(pkt(2, 1, pb.PortNum_NEIGHBORINFO_APP)) // priority 4
c.Add(pkt(3, 1, pb.PortNum_NODEINFO_APP)) // priority 2 — at cap
c.Add(pkt(4, 1, pb.PortNum_NODEINFO_APP)) // pressure: evicts oldest pri=2 (ID 1)
got := c.GetAll()
if len(got) != 3 {

View File

@@ -26,15 +26,15 @@ function MapPage() {
return (
<PageWrapper>
<div className="flex flex-col h-[calc(100vh-7rem)] md:h-[calc(100vh-5rem)]">
<div className="flex-1 flex flex-col">
<div className="flex-1 flex flex-col min-h-0">
<NetworkMap
fullHeight
ref={mapRef as any}
onAutoZoomChange={setAutoZoomEnabled}
showLinks={showLinks}
/>
<div className="mt-2 rounded-lg p-2 text-xs flex items-center justify-between effect-inset">
<div className="mt-2 rounded-lg p-2 text-xs flex items-center justify-between effect-inset flex-shrink-0">
<div className="flex flex-wrap gap-2">
<div className="flex items-center gap-x-3 gap-y-1 flex-wrap">
<span className={`inline-flex items-center px-2 py-0.5 rounded ${getNodeColors(ActivityLevel.RECENT, false).textClass} ${getNodeColors(ActivityLevel.RECENT, false).background}`}>

View File

@@ -156,6 +156,8 @@ function processTopology(
for (let i = 0; i < forwardPath.length - 1; i++) {
const cap = Math.min(snrTowards.length, forwardPath.length - 1);
const snr = i < cap ? snrTowards[i] / 4 : undefined;
// SNR of exactly 0 indicates an MQTT-bridged hop
const hopViaMqtt = snr === 0 || !!data.viaMqtt;
upsertObservation(
state,
forwardPath[i], // sender
@@ -163,7 +165,7 @@ function processTopology(
snr,
undefined,
"traceroute",
!!data.viaMqtt,
hopViaMqtt,
timestamp,
hopCount
);
@@ -176,6 +178,8 @@ function processTopology(
for (let i = 0; i < returnPath.length - 1; i++) {
const cap = Math.min(snrBack.length, returnPath.length - 1);
const snr = i < cap ? snrBack[i] / 4 : undefined;
// SNR of exactly 0 indicates an MQTT-bridged hop
const hopViaMqtt = snr === 0 || !!data.viaMqtt;
upsertObservation(
state,
returnPath[i],
@@ -183,7 +187,7 @@ function processTopology(
snr,
undefined,
"traceroute",
!!data.viaMqtt,
hopViaMqtt,
timestamp,
hopCount
);

View File

@@ -1,6 +1,14 @@
import '@testing-library/jest-dom';
import { vi } from 'vitest';
// Mock URL.createObjectURL (required by maplibre-gl worker setup)
if (typeof window.URL.createObjectURL === 'undefined') {
window.URL.createObjectURL = vi.fn(() => '');
}
if (typeof window.URL.revokeObjectURL === 'undefined') {
window.URL.revokeObjectURL = vi.fn();
}
// Mock for window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,