diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 67f0b89c7..739ffb658 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -15,6 +15,38 @@ import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { ErrorBoundary } from "react-error-boundary"; import { MapProvider } from "react-map-gl/maplibre"; +import { useConnections } from "@pages/Connections/useConnections.ts"; +import { useEffect, useRef } from "react"; + +function AutoConnect() { + const savedConnections = useDeviceStore((s) => s.savedConnections); + const { connect } = useConnections(); + const { selectedDeviceId } = useAppStore(); + const { getDevice } = useDeviceStore(); + const device = getDevice(selectedDeviceId); + const hasAttempted = useRef(false); + + useEffect(() => { + if (hasAttempted.current || device || savedConnections.length === 0) { + return; + } + + const target = + savedConnections.find((c) => c.isDefault && c.type === "http") || + savedConnections.find((c) => c.type === "http"); + + if (target) { + hasAttempted.current = true; + useDeviceStore + .getState() + .updateSavedConnection(target.id, { status: "disconnected" }); + void connect(target.id); + } + }, [savedConnections, device, connect]); + + return null; +} + export function App() { useTheme(); @@ -26,6 +58,7 @@ export function App() { return ( + {/* Overlay sits outside the device-conditional branch so it shows diff --git a/apps/web/src/components/Map.tsx b/apps/web/src/components/Map.tsx index 0300a96c3..c0e0fad4e 100644 --- a/apps/web/src/components/Map.tsx +++ b/apps/web/src/components/Map.tsx @@ -72,7 +72,11 @@ export const BaseMap = ({ renderWorldCopies={false} maxPitch={0} dragRotate={false} - touchZoomRotate={false} + touchZoomRotate={true} + scrollZoom={true} + doubleClickZoom={true} + minZoom={0} + maxZoom={20} initialViewState={ initialViewState ?? { zoom: 1.8, @@ -80,7 +84,11 @@ export const BaseMap = ({ longitude: 0, } } - style={{ filter: darkMode ? "brightness(0.9)" : undefined }} + style={{ + width: "100%", + height: "100%", + filter: darkMode ? "brightness(0.9)" : undefined, + }} locale={locale} interactiveLayerIds={interactiveLayerIds} onMouseMove={onMouseMove} diff --git a/apps/web/src/core/hooks/useMapFitting.ts b/apps/web/src/core/hooks/useMapFitting.ts index 0229b1b78..28e504962 100644 --- a/apps/web/src/core/hooks/useMapFitting.ts +++ b/apps/web/src/core/hooks/useMapFitting.ts @@ -6,14 +6,25 @@ import type { MapRef } from "react-map-gl/maplibre"; export function useMapFitting(map: MapRef | undefined) { const focusLngLat = useCallback( (position: LngLat) => { - if (!map) { + if (!map || !position) { return; } const [lng, lat] = position; - map.easeTo({ - center: [lng, lat], - zoom: map.getZoom(), - }); + if ( + !Number.isFinite(lng) || + !Number.isFinite(lat) || + (lng === 0 && lat === 0) + ) { + return; + } + try { + map.easeTo({ + center: [lng, lat], + zoom: map.getZoom(), + }); + } catch { + // Ignore easeTo error + } }, [map], ); @@ -25,22 +36,34 @@ export function useMapFitting(map: MapRef | undefined) { } if (nodes.length === 1 && nodes[0]) { - return focusLngLat(toLngLat(nodes[0].position)); + const pos = toLngLat(nodes[0].position); + if ( + pos && + Number.isFinite(pos[0]) && + Number.isFinite(pos[1]) && + !(pos[0] === 0 && pos[1] === 0) + ) { + return focusLngLat(pos); + } + return; } - // Build [lng, lat] coords, then let boundsFromLngLat do the turf dance const coords = nodes.map((n) => toLngLat(n.position)); const bounds = boundsFromLngLat(coords); if (!bounds) { return; } - const center = map.cameraForBounds(bounds, { - padding: { top: 10, bottom: 10, left: 10, right: 10 }, - }); + try { + const center = map.cameraForBounds(bounds, { + padding: { top: 10, bottom: 10, left: 10, right: 10 }, + }); - if (center) { - map.easeTo(center); + if (center && center.center) { + map.easeTo(center); + } + } catch { + // Ignore cameraForBounds failure and leave default view } }, [map, focusLngLat], diff --git a/apps/web/src/core/stores/deviceStore/index.ts b/apps/web/src/core/stores/deviceStore/index.ts index 8ab6381b6..d1f9807c6 100644 --- a/apps/web/src/core/stores/deviceStore/index.ts +++ b/apps/web/src/core/stores/deviceStore/index.ts @@ -816,20 +816,13 @@ const persistOptions: PersistOptions = { } draft.devices = rebuilt as unknown as Map>; - // Stale in-flight states can't survive a reload — no JS code is - // running that could complete them. Reset any persisted - // "connecting" / "configuring" / "disconnecting" entries to - // "disconnected" so the connecting overlay (which keys off - // these statuses) doesn't get stuck visible on cold boot. + // Active network connections cannot survive a browser page reload. + // Reset all saved connection statuses to "disconnected" on rehydration + // so stale "configured"/"connected"/"connecting" states do not block + // auto-reconnecting or leave the UI in a desynced state. for (const conn of draft.savedConnections) { - if ( - conn.status === "connecting" || - conn.status === "configuring" || - conn.status === "disconnecting" - ) { - conn.status = "disconnected"; - conn.error = undefined; - } + conn.status = "disconnected"; + conn.error = undefined; } }), ); diff --git a/apps/web/src/core/utils/geo.ts b/apps/web/src/core/utils/geo.ts index a3017bd94..4600e7ca2 100644 --- a/apps/web/src/core/utils/geo.ts +++ b/apps/web/src/core/utils/geo.ts @@ -1,5 +1,3 @@ -import { bbox, lineString } from "@turf/turf"; - export type LngLat = [number, number]; export type Mercator = [number, number]; export type Bounds = [[number, number], [number, number]]; @@ -28,8 +26,28 @@ export const boundsFromLngLat = (coords: LngLat[]): Bounds | undefined => { return undefined; } - const turfCoords = coords.map(([lng, lat]) => [lat, lng]); - const [minLat, minLng, maxLat, maxLng] = bbox(lineString(turfCoords)); + let minLng = Infinity; + let maxLng = -Infinity; + let minLat = Infinity; + let maxLat = -Infinity; + + for (const [lng, lat] of coords) { + if (Number.isFinite(lng) && Number.isFinite(lat)) { + if (lng < minLng) minLng = lng; + if (lng > maxLng) maxLng = lng; + if (lat < minLat) minLat = lat; + if (lat > maxLat) maxLat = lat; + } + } + + if ( + !Number.isFinite(minLng) || + !Number.isFinite(minLat) || + !Number.isFinite(maxLng) || + !Number.isFinite(maxLat) + ) { + return undefined; + } return [ [minLng, minLat], diff --git a/apps/web/src/pages/Map/index.tsx b/apps/web/src/pages/Map/index.tsx index f9d4a4bfc..712b539b4 100644 --- a/apps/web/src/pages/Map/index.tsx +++ b/apps/web/src/pages/Map/index.tsx @@ -263,7 +263,7 @@ const MapPage = () => { /> )} -
+
{myNode && hasPos(myNode?.position) && (