mirror of
https://github.com/dpup/meshstream.git
synced 2026-03-28 17:42:37 +01:00
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:
8
main.go
8
main.go
@@ -40,10 +40,10 @@ type Config struct {
|
|||||||
ChannelKeys []string
|
ChannelKeys []string
|
||||||
|
|
||||||
// Statistics configuration
|
// Statistics configuration
|
||||||
StatsInterval time.Duration
|
StatsInterval time.Duration
|
||||||
CacheSize int
|
CacheSize int
|
||||||
CacheRetention time.Duration
|
CacheRetention time.Duration
|
||||||
VerboseLogging bool
|
VerboseLogging bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEnv retrieves an environment variable with the given prefix or returns the default value
|
// getEnv retrieves an environment variable with the given prefix or returns the default value
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ var typePriority = map[pb.PortNum]int{
|
|||||||
pb.PortNum_TEXT_MESSAGE_COMPRESSED_APP: 5,
|
pb.PortNum_TEXT_MESSAGE_COMPRESSED_APP: 5,
|
||||||
pb.PortNum_NEIGHBORINFO_APP: 4, // rare; protect from eviction
|
pb.PortNum_NEIGHBORINFO_APP: 4, // rare; protect from eviction
|
||||||
pb.PortNum_TRACEROUTE_APP: 3,
|
pb.PortNum_TRACEROUTE_APP: 3,
|
||||||
pb.PortNum_POSITION_APP: 3,
|
pb.PortNum_POSITION_APP: 3,
|
||||||
pb.PortNum_NODEINFO_APP: 2, // frequent; lower priority
|
pb.PortNum_NODEINFO_APP: 2, // frequent; lower priority
|
||||||
pb.PortNum_TELEMETRY_APP: 2,
|
pb.PortNum_TELEMETRY_APP: 2,
|
||||||
pb.PortNum_ROUTING_APP: 2,
|
pb.PortNum_ROUTING_APP: 2,
|
||||||
pb.PortNum_MAP_REPORT_APP: 2,
|
pb.PortNum_MAP_REPORT_APP: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultTypePriority applies to any port type not listed above.
|
// defaultTypePriority applies to any port type not listed above.
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ func TestNodeAwareCacheBasicOrdering(t *testing.T) {
|
|||||||
func TestPressureEvictsOldestOfLowestPriority(t *testing.T) {
|
func TestPressureEvictsOldestOfLowestPriority(t *testing.T) {
|
||||||
c := NewNodeAwareCache(3, time.Hour)
|
c := NewNodeAwareCache(3, time.Hour)
|
||||||
|
|
||||||
c.Add(pkt(1, 1, pb.PortNum_NODEINFO_APP)) // priority 2
|
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(2, 1, pb.PortNum_NEIGHBORINFO_APP)) // priority 4
|
||||||
c.Add(pkt(3, 1, pb.PortNum_NODEINFO_APP)) // priority 2 — at cap
|
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(4, 1, pb.PortNum_NODEINFO_APP)) // pressure: evicts oldest pri=2 (ID 1)
|
||||||
|
|
||||||
got := c.GetAll()
|
got := c.GetAll()
|
||||||
if len(got) != 3 {
|
if len(got) != 3 {
|
||||||
|
|||||||
@@ -26,15 +26,15 @@ function MapPage() {
|
|||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<div className="flex flex-col h-[calc(100vh-7rem)] md:h-[calc(100vh-5rem)]">
|
<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
|
<NetworkMap
|
||||||
fullHeight
|
fullHeight
|
||||||
ref={mapRef as any}
|
ref={mapRef as any}
|
||||||
onAutoZoomChange={setAutoZoomEnabled}
|
onAutoZoomChange={setAutoZoomEnabled}
|
||||||
showLinks={showLinks}
|
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 flex-wrap gap-2">
|
||||||
<div className="flex items-center gap-x-3 gap-y-1 flex-wrap">
|
<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}`}>
|
<span className={`inline-flex items-center px-2 py-0.5 rounded ${getNodeColors(ActivityLevel.RECENT, false).textClass} ${getNodeColors(ActivityLevel.RECENT, false).background}`}>
|
||||||
|
|||||||
@@ -156,6 +156,8 @@ function processTopology(
|
|||||||
for (let i = 0; i < forwardPath.length - 1; i++) {
|
for (let i = 0; i < forwardPath.length - 1; i++) {
|
||||||
const cap = Math.min(snrTowards.length, forwardPath.length - 1);
|
const cap = Math.min(snrTowards.length, forwardPath.length - 1);
|
||||||
const snr = i < cap ? snrTowards[i] / 4 : undefined;
|
const snr = i < cap ? snrTowards[i] / 4 : undefined;
|
||||||
|
// SNR of exactly 0 indicates an MQTT-bridged hop
|
||||||
|
const hopViaMqtt = snr === 0 || !!data.viaMqtt;
|
||||||
upsertObservation(
|
upsertObservation(
|
||||||
state,
|
state,
|
||||||
forwardPath[i], // sender
|
forwardPath[i], // sender
|
||||||
@@ -163,7 +165,7 @@ function processTopology(
|
|||||||
snr,
|
snr,
|
||||||
undefined,
|
undefined,
|
||||||
"traceroute",
|
"traceroute",
|
||||||
!!data.viaMqtt,
|
hopViaMqtt,
|
||||||
timestamp,
|
timestamp,
|
||||||
hopCount
|
hopCount
|
||||||
);
|
);
|
||||||
@@ -176,6 +178,8 @@ function processTopology(
|
|||||||
for (let i = 0; i < returnPath.length - 1; i++) {
|
for (let i = 0; i < returnPath.length - 1; i++) {
|
||||||
const cap = Math.min(snrBack.length, returnPath.length - 1);
|
const cap = Math.min(snrBack.length, returnPath.length - 1);
|
||||||
const snr = i < cap ? snrBack[i] / 4 : undefined;
|
const snr = i < cap ? snrBack[i] / 4 : undefined;
|
||||||
|
// SNR of exactly 0 indicates an MQTT-bridged hop
|
||||||
|
const hopViaMqtt = snr === 0 || !!data.viaMqtt;
|
||||||
upsertObservation(
|
upsertObservation(
|
||||||
state,
|
state,
|
||||||
returnPath[i],
|
returnPath[i],
|
||||||
@@ -183,7 +187,7 @@ function processTopology(
|
|||||||
snr,
|
snr,
|
||||||
undefined,
|
undefined,
|
||||||
"traceroute",
|
"traceroute",
|
||||||
!!data.viaMqtt,
|
hopViaMqtt,
|
||||||
timestamp,
|
timestamp,
|
||||||
hopCount
|
hopCount
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { vi } from 'vitest';
|
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
|
// Mock for window.matchMedia
|
||||||
Object.defineProperty(window, 'matchMedia', {
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
writable: true,
|
writable: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user