-
- Hi, {currentUser?.full_name || currentUser?.email} 👋
-
-
- Welcome back, nice to see you again!!!
-
+
+
+ {health.level === "good"
+ ? "Good"
+ : health.level === "fair"
+ ? "Fair"
+ : "Poor"}{" "}
+ ({health.score}%)
+
+ )
+}
+
+function projectCoord(
+ lat: number,
+ lon: number,
+ useGcj02: boolean,
+): [number, number] {
+ if (!useGcj02) return [lat, lon]
+ return wgs84ToGcj02(lat, lon)
+}
+
+/* ---------- map sub-components ---------- */
+
+function FitBoundsToDevices({
+ coordinates,
+}: {
+ coordinates: Array<[number, number]>
+}) {
+ const map = useMap()
+ const hasFitted = useRef(false)
+
+ useEffect(() => {
+ if (coordinates.length === 0 || hasFitted.current) return
+ hasFitted.current = true
+ if (coordinates.length === 1) {
+ map.setView(coordinates[0], 14, { animate: true })
+ return
+ }
+ map.fitBounds(coordinates, { padding: [40, 40] })
+ }, [coordinates, map])
+
+ return null
+}
+
+/** Badges-only overlay — lives inside MapContainer, no dropdown needed */
+function MapStatusBadges({
+ isLive,
+ status,
+ deviceCount,
+ disconnectedDuration,
+}: {
+ isLive: boolean
+ status: ConnectionState
+ deviceCount: number
+ disconnectedDuration: number
+}) {
+ return (
+
+
+
+ {isLive && (
+
+
+
+
+ )}
+ {!isLive && }
+ {isLive
+ ? "Live"
+ : status === "retrying"
+ ? `Reconnecting${disconnectedDuration > 0 ? ` (${disconnectedDuration}s)` : ""}`
+ : "Connecting"}
+
+
+ Devices: {deviceCount}
+
)
}
+
+/** FitBounds helper — must live inside MapContainer to call useMap() */
+function FitBoundsControl({
+ coordinates,
+}: {
+ coordinates: Array<[number, number]>
+}) {
+ const map = useMap()
+ const fitAll = () => {
+ if (!coordinates.length) return
+ if (coordinates.length === 1) {
+ map.setView(coordinates[0], 14, { animate: true })
+ } else {
+ map.fitBounds(coordinates, { padding: [40, 40] })
+ }
+ }
+ // expose via a ref so the outside button can call it
+ const ref = window as unknown as Record
void>
+ ref.__mapFitAll = fitAll
+ return null
+}
+
+/* ---------- WebSocket hook ---------- */
+
+function useTelemetryWS() {
+ const [snapshot, setSnapshot] = useState(null)
+ const [status, setStatus] = useState("connecting")
+ const [disconnectedAt, setDisconnectedAt] = useState(null)
+ const retryCount = useRef(0)
+ const lastMessageAt = useRef(0)
+
+ useEffect(() => {
+ const token = localStorage.getItem("access_token")
+ if (!token) {
+ setStatus("retrying")
+ return
+ }
+
+ let reconnectTimer: number | undefined
+ let staleTimer: number | undefined
+ let socket: WebSocket | undefined
+ let shouldReconnect = true
+
+ const STALE_TIMEOUT_MS = 10_000
+
+ const resetStaleTimer = () => {
+ if (staleTimer) window.clearTimeout(staleTimer)
+ staleTimer = window.setTimeout(() => {
+ socket?.close()
+ }, STALE_TIMEOUT_MS)
+ }
+
+ const connect = () => {
+ setStatus((prev) => (prev === "live" ? prev : "connecting"))
+ socket = new WebSocket(buildTelemetryWsUrl(token))
+
+ socket.onopen = () => {
+ setStatus("live")
+ setDisconnectedAt(null)
+ retryCount.current = 0
+ lastMessageAt.current = Date.now()
+ resetStaleTimer()
+ }
+
+ socket.onmessage = (event) => {
+ try {
+ const payload = JSON.parse(event.data) as TelemetrySnapshot
+ setSnapshot(payload)
+ setStatus("live")
+ setDisconnectedAt(null)
+ lastMessageAt.current = Date.now()
+ resetStaleTimer()
+ } catch {
+ // ignore malformed messages
+ }
+ }
+
+ socket.onclose = () => {
+ if (staleTimer) window.clearTimeout(staleTimer)
+ if (!shouldReconnect) return
+ setStatus("retrying")
+ setDisconnectedAt((prev) => prev ?? Date.now())
+ const backoffMs = Math.min(2000 * 2 ** retryCount.current, 30000)
+ retryCount.current++
+ reconnectTimer = window.setTimeout(connect, backoffMs)
+ }
+
+ socket.onerror = () => {
+ socket?.close()
+ }
+ }
+
+ connect()
+
+ return () => {
+ shouldReconnect = false
+ if (reconnectTimer) window.clearTimeout(reconnectTimer)
+ if (staleTimer) window.clearTimeout(staleTimer)
+ if (socket) socket.close()
+ }
+ }, [])
+
+ return { snapshot, status, disconnectedAt }
+}
+
+/* ---------- Dashboard ---------- */
+
+function Dashboard() {
+ const { snapshot, status, disconnectedAt } = useTelemetryWS()
+ const [baseMap, setBaseMap] = useState("amap")
+ const [activeDeviceId, setActiveDeviceId] = useState("")
+ const activeDeviceIdRef = useRef(activeDeviceId)
+ activeDeviceIdRef.current = activeDeviceId
+
+ const isGcj02 = BASE_MAPS[baseMap].gcj02
+
+ const devicesWithCoords = useMemo(
+ () =>
+ (snapshot?.devices ?? []).filter(
+ (device) =>
+ device.lat !== null &&
+ device.lon !== null &&
+ device.lat >= -90 &&
+ device.lat <= 90 &&
+ device.lon >= -180 &&
+ device.lon <= 180,
+ ),
+ [snapshot],
+ )
+
+ useEffect(() => {
+ if (!devicesWithCoords.length) {
+ if (activeDeviceIdRef.current) setActiveDeviceId("")
+ return
+ }
+ const exists = devicesWithCoords.some(
+ (d) => d.device_id === activeDeviceIdRef.current,
+ )
+ if (!exists && !activeDeviceIdRef.current) {
+ setActiveDeviceId(devicesWithCoords[0].device_id)
+ }
+ }, [devicesWithCoords])
+
+ const activeDevice = useMemo(
+ () =>
+ devicesWithCoords.find((d) => d.device_id === activeDeviceId) ??
+ devicesWithCoords[0] ??
+ null,
+ [devicesWithCoords, activeDeviceId],
+ )
+
+ const mapPoints = useMemo(
+ () =>
+ devicesWithCoords.map((d) =>
+ projectCoord(d.lat as number, d.lon as number, isGcj02),
+ ),
+ [devicesWithCoords, isGcj02],
+ )
+
+ const handleDeviceClick = useCallback((deviceId: string) => {
+ setActiveDeviceId(deviceId)
+ }, [])
+
+ const isLive = status === "live"
+
+ // Disconnected time ticker
+ const [, forceUpdate] = useState(0)
+ useEffect(() => {
+ if (!disconnectedAt) return
+ const interval = window.setInterval(() => forceUpdate((n) => n + 1), 1000)
+ return () => window.clearInterval(interval)
+ }, [disconnectedAt])
+
+ const disconnectedDuration = disconnectedAt
+ ? Math.floor((Date.now() - disconnectedAt) / 1000)
+ : 0
+
+ return (
+
+ {/* Map area — fills available space */}
+
+ {/* Controls live OUTSIDE MapContainer to avoid Leaflet z-index stacking issues */}
+
+
+
+
+
+
+ {(
+ Object.entries(BASE_MAPS) as Array<
+ [BaseMapType, (typeof BASE_MAPS)[BaseMapType]]
+ >
+ ).map(([key, provider]) => (
+ setBaseMap(key)}>
+ {provider.label} {baseMap === key && "✓"}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+ {devicesWithCoords.map((device) => {
+ const isOffline = checkIsOffline(device.last_event_time)
+ const isSelected = activeDeviceId === device.device_id
+ const [projLat, projLon] = projectCoord(
+ device.lat as number,
+ device.lon as number,
+ isGcj02,
+ )
+ const color = deviceMarkerColor(device.fix_status, isOffline)
+ const icon = buildDeviceIcon(color, isSelected, isOffline)
+
+ return (
+ handleDeviceClick(device.device_id),
+ }}
+ key={device.device_id}
+ >
+
+
+
+ {device.device_id}
+ {isOffline && (
+
+ (Offline)
+
+ )}
+
+
+ Lat:
+ {formatCoord(device.lat)}
+
+ Lon:
+ {formatCoord(device.lon)}
+
+ Alt:
+
+ {device.alt_m !== null
+ ? `${device.alt_m.toFixed(1)} m`
+ : "-"}
+
+
+ Fix:
+ {getFixStatusLabel(device.fix_status)}
+
+ Sats:
+ {device.satellites_used ?? "-"}
+
+
+ Last seen:{" "}
+ {device.last_event_time
+ ? new Date(device.last_event_time).toLocaleString()
+ : "Unknown"}
+
+
+
+
+ )
+ })}
+
+
+
+
+ {/* Selected Device Panel — placed at bottom, compact */}
+
+
+
+ Selected Device
+ {activeDevice?.signal_health &&
+ signalHealthBadge(activeDevice.signal_health)}
+
+
+ Click a marker on the map to view detailed device info
+
+
+
+
+
Device ID
+
+ {activeDevice?.device_id ?? "-"}
+
+
+
+
Latitude
+
+ {formatCoord(activeDevice?.lat ?? null)}
+
+
+
+
Longitude
+
+ {formatCoord(activeDevice?.lon ?? null)}
+
+
+
+
+
+ Altitude
+
+
+ {activeDevice?.alt_m !== null && activeDevice?.alt_m !== undefined
+ ? `${activeDevice.alt_m.toFixed(1)} m`
+ : "-"}
+
+
+
+
Fix Status
+
+ {getFixStatusLabel(activeDevice?.fix_status ?? null)}
+
+
+
+
+
+ Satellites
+
+
+ {activeDevice?.satellites_used ?? "-"}
+
+
+
+
+
+ Signal Score
+
+
+ {activeDevice?.signal_health
+ ? `${activeDevice.signal_health.score}%`
+ : "-"}
+
+
+
+
+
+ Last Seen
+
+
+ {timeAgo(activeDevice?.last_event_time ?? null)}
+
+
+
+
+
+ )
+}