From c18bc8f35ebea56abeee0cd262b95d0051ad94e5 Mon Sep 17 00:00:00 2001 From: csysp Date: Fri, 13 Mar 2026 20:10:58 -0600 Subject: [PATCH 1/3] ui: remove display config panel from left HUD to declutter Removes WorldviewRightPanel render and import from page.tsx. The effects state is preserved as it continues to feed MaplibreViewer. Left HUD column now contains only the data layers panel. Co-Authored-By: Claude Sonnet 4.6 Former-commit-id: 0cdb2a60bd8436b7226866e2f4086496beed1587 --- frontend/src/app/page.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index a4d53e7..42bc001 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -5,7 +5,7 @@ import { useEffect, useState, useRef, useCallback } from "react"; import dynamic from 'next/dynamic'; import { motion } from "framer-motion"; import WorldviewLeftPanel from "@/components/WorldviewLeftPanel"; -import WorldviewRightPanel from "@/components/WorldviewRightPanel"; + import NewsFeed from "@/components/NewsFeed"; import MarketsPanel from "@/components/MarketsPanel"; import FilterPanel from "@/components/FilterPanel"; @@ -442,10 +442,7 @@ export default function Dashboard() { setSettingsOpen(true)} onLegendClick={() => setLegendOpen(true)} gibsDate={gibsDate} setGibsDate={setGibsDate} gibsOpacity={gibsOpacity} setGibsOpacity={setGibsOpacity} onEntityClick={setSelectedEntity} onFlyTo={(lat, lng) => setFlyToLocation({ lat, lng, ts: Date.now() })} /> - {/* LEFT BOTTOM - DISPLAY CONFIG */} - - - + {/* RIGHT HUD CONTAINER */} From a9d21a0bb594cf0ce9af7c5107dfd7a51bcf96a4 Mon Sep 17 00:00:00 2001 From: csysp Date: Fri, 13 Mar 2026 20:42:09 -0600 Subject: [PATCH 2/3] ui: remove display config panel + restore hideable sidebar tabs - Remove WorldviewRightPanel from left HUD (declutter) - Restore sliding sidebar animation via motion.div on both HUD containers - Left tab (LAYERS): springs to x:-360 when hidden, tab tracks edge - Right tab (INTEL): springs to x:+360 when hidden, tab tracks edge - Both use spring animation (damping:30 stiffness:250) - ChevronLeft/Right icons flip direction with open state Co-Authored-By: Claude Sonnet 4.6 Former-commit-id: 5a573165d27db1704f513ce9fd503ddc3f6892ef --- frontend/src/app/page.tsx | 51 ++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 42bc001..2abc22e 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -4,6 +4,7 @@ import { API_BASE } from "@/lib/api"; import { useEffect, useState, useRef, useCallback } from "react"; import dynamic from 'next/dynamic'; import { motion } from "framer-motion"; +import { ChevronLeft, ChevronRight } from "lucide-react"; import WorldviewLeftPanel from "@/components/WorldviewLeftPanel"; import NewsFeed from "@/components/NewsFeed"; @@ -123,6 +124,8 @@ export default function Dashboard() { // Stable reference for child components — only changes when dataVersion increments const data = dataRef.current; const [uiVisible, setUiVisible] = useState(true); + const [leftOpen, setLeftOpen] = useState(true); + const [rightOpen, setRightOpen] = useState(true); const [settingsOpen, setSettingsOpen] = useState(false); const [legendOpen, setLegendOpen] = useState(false); const [mapView, setMapView] = useState({ zoom: 2, latitude: 20 }); @@ -435,18 +438,54 @@ export default function Dashboard() {
VSR
- {/* LEFT HUD CONTAINER */} -
+ {/* LEFT HUD CONTAINER — slides off left edge when hidden */} + {/* LEFT PANEL - DATA LAYERS */} setSettingsOpen(true)} onLegendClick={() => setLegendOpen(true)} gibsDate={gibsDate} setGibsDate={setGibsDate} gibsOpacity={gibsOpacity} setGibsOpacity={setGibsOpacity} onEntityClick={setSelectedEntity} onFlyTo={(lat, lng) => setFlyToLocation({ lat, lng, ts: Date.now() })} /> + + {/* LEFT SIDEBAR TOGGLE TAB */} + + + -
+ {/* RIGHT SIDEBAR TOGGLE TAB */} + + + - {/* RIGHT HUD CONTAINER */} -
+ {/* RIGHT HUD CONTAINER — slides off right edge when hidden */} + {/* FIND / LOCATE */} @@ -502,7 +541,7 @@ export default function Dashboard() {
- + {/* BOTTOM CENTER COORDINATE / LOCATION BAR — hidden when Sentinel-2 imagery overlay is open */} {!(selectedEntity?.type === 'region_dossier' && regionDossier?.sentinel2) && Date: Fri, 13 Mar 2026 20:00:06 -0600 Subject: [PATCH 3/3] perf: wrap interpFlight/Ship/Sat in useCallback to prevent spurious re-renders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit interpFlight, interpShip, and interpSat were plain arrow functions recreated on every render. Because interpTick fires every second, TrackedFlightLabels received a new function reference every second (preventing memo bailout) and all downstream useMemos closed over these functions re-executed unnecessarily. Wrap all three in useCallback([dtSeconds]) — dtSeconds is their only reactive closure variable; interpolatePosition is a stable module-level import and does not need to be listed. Co-Authored-By: Claude Sonnet 4.6 Former-commit-id: 84c3c06407afa5c0227ac1b682cca1157498d1a5 --- frontend/src/components/MaplibreViewer.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/MaplibreViewer.tsx b/frontend/src/components/MaplibreViewer.tsx index 8e92d46..e9df432 100644 --- a/frontend/src/components/MaplibreViewer.tsx +++ b/frontend/src/components/MaplibreViewer.tsx @@ -505,29 +505,29 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele }, [interpTick]); // Helper: interpolate a flight's position if airborne and has speed+heading - const interpFlight = (f: any): [number, number] => { + const interpFlight = useCallback((f: any): [number, number] => { if (!f.speed_knots || f.speed_knots <= 0 || dtSeconds <= 0) return [f.lng, f.lat]; if (f.alt != null && f.alt <= 100) return [f.lng, f.lat]; if (dtSeconds < 1) return [f.lng, f.lat]; const heading = f.true_track || f.heading || 0; const [newLat, newLng] = interpolatePosition(f.lat, f.lng, heading, f.speed_knots, dtSeconds); return [newLng, newLat]; - }; + }, [dtSeconds]); // Helper: interpolate a ship's position using SOG + heading - const interpShip = (s: any): [number, number] => { + const interpShip = useCallback((s: any): [number, number] => { if (typeof s.sog !== 'number' || !s.sog || s.sog <= 0 || dtSeconds <= 0) return [s.lng, s.lat]; const heading = (typeof s.cog === 'number' ? s.cog : 0) || s.heading || 0; const [newLat, newLng] = interpolatePosition(s.lat, s.lng, heading, s.sog, dtSeconds); return [newLng, newLat]; - }; + }, [dtSeconds]); // Helper: interpolate a satellite's position between API updates - const interpSat = (s: any): [number, number] => { + const interpSat = useCallback((s: any): [number, number] => { if (!s.speed_knots || s.speed_knots <= 0 || dtSeconds < 1) return [s.lng, s.lat]; const [newLat, newLng] = interpolatePosition(s.lat, s.lng, s.heading || 0, s.speed_knots, dtSeconds, 0, 65); return [newLng, newLat]; - }; + }, [dtSeconds]); // Satellite GeoJSON with interpolated positions const satellitesGeoJSON = useMemo(() => {