feat: route detection, hexdb API, aircraft photos, trail improvements#20
feat: route detection, hexdb API, aircraft photos, trail improvements#20
Conversation
kewonit
commented
Mar 30, 2026
- Add route detection and lookup system (route-detection.ts, route-lookup.ts, use-route-info.ts)
- Add hexdb API route for aircraft metadata
- Update flight API client and trace routes
- Improve aircraft photos, flight card, and mobile toast UI
- Update weather radar layer and map state tracker
- Trail altitude and spline improvements
- Add route detection and lookup system (route-detection.ts, route-lookup.ts, use-route-info.ts) - Add hexdb API route for aircraft metadata - Update flight API client and trace routes - Improve aircraft photos, flight card, and mobile toast UI - Update weather radar layer and map state tracker - Trail altitude and spline improvements - Add local-only copilot config to .gitignore
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR adds a multi-source route (origin/destination) resolution system for flights, introduces new server proxies for upstream services without CORS support, and improves trail rendering/animation plus several UI elements (flight card/toast, photos, radar).
Changes:
- Add route lookup + client-side departure detection and destination estimation, surfaced via a new
useRouteInfohook. - Extend flight/trace proxying and upstream selection (multi-provider flights proxy, smart trace source selection, new hexdb proxy).
- Improve trail rendering/animation behavior (altitude handling, spline/trim logic, caching keys) and refresh several UI components.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/lib/trail-spline.ts | Adjust endpoint reflection to clamp only lat/lng while keeping altitude independent. |
| src/lib/trail-altitude.ts | Clarify ground-segment filtering constant documentation. |
| src/lib/route-lookup.ts | New callsign→route lookup service with caching, dedupe, rate limiting, and formatting helpers. |
| src/lib/route-detection.ts | New client-side departure detection + destination estimation heuristics. |
| src/lib/flight-api-client.ts | Update client fallback chain to use server proxy for airplanes.live/adsb.lol + OpenSky. |
| src/hooks/use-route-info.ts | New hook combining API lookup + trace/live detection + estimation into a single route model. |
| src/components/ui/mobile-flight-toast.tsx | Display route info and refresh badges/typography. |
| src/components/ui/flight-card.tsx | Add route banner and collapsible vertical profile UI. |
| src/components/ui/aircraft-photos.tsx | Fix separator/ellipsis rendering in the header line. |
| src/components/map/weather-radar-layer.tsx | Refactor source/layer creation and add abortable fetch for radar frame updates. |
| src/components/map/map-state-tracker.tsx | Update onChange prop description to reflect actual behavior. |
| src/components/map/flight-layer-builders.ts | Refine trail color cache invalidation key to better track visible-window changes. |
| src/components/map/flight-animation-helpers.ts | Improve trail trimming + head connection, altitude smoothing, and loop-handling logic. |
| src/components/flight-tracker.tsx | Feed polled flights into departure detection and pass track into UI components. |
| src/app/api/hexdb/route.ts | New SSRF-protected proxy to hexdb.io for client consumption. |
| src/app/api/flights/trace/route.ts | Reorder trace sources and add preferred-source + fallback racing strategy. |
| src/app/api/flights/route.ts | Convert adsb.lol proxy into a multi-provider proxy with server-side rate limiting. |
| src/app/api/aircraft-photos/route.ts | Reduce upstream timeouts for photo metadata pipeline. |
| next.config.ts | Update CSP connect-src to allow client-side adsbdb route lookups. |
| .gitignore | Ignore local Copilot agent/skill/instruction files. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Introduced `trail-path-utils.ts` for trail path operations including smoothing, trimming, and fallback generation. - Updated `control-panel-settings.tsx` to increase maximum trail distance from 100 to 120. - Adjusted polling interval in `use-flights.ts` from 10s to 5s for improved data density. - Increased maximum trail distance in `use-settings.tsx` from 100 to 120 and updated default trail distance from 40 to 80. - Expanded maximum points in `use-trail-history.ts` from 55 to 120. - Modified maximum altitude for departure detection in `route-detection.ts` from 3000m to 1500m. - Changed cache description in `route-lookup.ts` from LRU to FIFO. - Implemented post-processing for stitched trail paths in `trail-stitch-postprocess.ts` to ensure trails reach aircraft position, filter NaN/Infinity coordinates, cap total path length, and smooth junctions. - Refactored `stitchHistoricalTrail` in `trail-stitching.ts` to utilize new post-processing functions for improved trail quality.
- Add server-trace-service.ts to handle fetching and processing flight trace data from various providers. - Introduce createOpenSkyCooldownMs and preferNextProvider utility functions. - Implement fetchServerTrace to manage trace fetching logic with provider preference. - Create trace-proxy-client.ts for proxying trace requests. - Establish trail-store.ts to manage flight trail state and history resolution. - Add types for trail management in types.ts. - Implement unit tests for server-trace-service and trail-store functionality.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 82 out of 84 changed files in this pull request and generated 3 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…and caching mechanisms
- Implemented `removeSuspiciousDisplayCuts` to clean up trail points by removing suspicious display cuts based on various geometric criteria. - Updated `buildTrailBasePath` to utilize the new cleanup function for both full history and live trails. - Added tests for `buildTrailDisplayGeometry` to ensure proper handling of trail segments, including cases with older loops and sharp angles. - Introduced `collapseDisplayBacktracks` to eliminate backtracks in display geometry. - Enhanced `mergeSegments` logic to improve handling of historical and live trail merging, including new continuity checks. - Updated `parseReadsbTrace` to drop older branches and impossible jumps in trace data. - Added a new test suite for merging trail segments to validate the merging logic under various scenarios. - Exposed selected envelope in the trail store to facilitate rendering of history and live boundaries.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 88 out of 90 changed files in this pull request and generated 2 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Implement tests for the useTrailHistory hook to ensure it does not ingest flights during render. - Add tests for parseFlightTrack to validate trimming of OpenSky responses to the latest plausible departure leg and dropping implausible older jumps. - Refactor useTrailHistory to utilize useEffect for handling flight ingestion. - Introduce normalizeTrackWaypoints function to streamline waypoint normalization in track parsing. - Enhance trail store to retain more live points and manage dynamic thresholds for flight movement. - Update GPU memory monitor to improve buffer data handling and type safety. - Adjust spline parameters for better curve handling in trail rendering. - Improve provider health tracking with escalated cooldowns on consecutive failures.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 101 out of 103 changed files in this pull request and generated 13 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Adjusted the calculation of timestamps in `makeArcTrail` to remove unnecessary condition. - Updated import paths in test files for consistency. - Improved dropdown state management in `ControlPanel` using `useSyncExternalStore`. - Added type imports and cleaned up unused imports in `flight-card`. - Introduced new tests for `resolveDropdownState` in `status-bar-state`. - Implemented `deriveAircraftPhotosFlags` to handle aircraft photo loading states. - Enhanced `mergeSegments` to drop suspect bootstrap points and added related tests. - Improved error handling in `fetchReadsbDirectTrack` to rethrow abort errors. - Added tests for `fetchTraceViaProxy` to ensure proper handling of non-JSON responses. - Updated `ingestLiveFlights` logic to maintain trail integrity across various scenarios.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 109 out of 111 changed files in this pull request and generated 2 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 109 out of 111 changed files in this pull request and generated 2 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 109 out of 111 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
src/app/api/flights/route.ts:166
- The timeout/abort detection checks
err instanceof DOMException && err.name === "AbortError". In Next.js route handlers, aborted fetches can also surface as anErrorwithname === "AbortError"(not necessarily aDOMException), which would incorrectly return a 502 instead of a 504. Consider checkingerr instanceof Error && err.name === "AbortError"(or a more flexibletypeof (err as any)?.name === "string") to reliably classify aborts across runtimes.
} catch (err) {
clearTimeout(timer);
const isTimeout = err instanceof DOMException && err.name === "AbortError";
return NextResponse.json(
{
error: isTimeout
? `${config.name} request timed out`
: `${config.name} request failed`,
},
{
status: isTimeout ? 504 : 502,
headers: { "Cache-Control": "no-store" },
},
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 109 out of 111 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…isibility handling
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 109 out of 111 changed files in this pull request and generated 6 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 109 out of 111 changed files in this pull request and generated 7 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
src/hooks/use-aircraft-photos.ts:1
- This effect can abort or duplicate the “phase 2” registration (JetAPI) fetch. In the
reg && !hexCachedpath, callingsetResolvedKey(normalized)triggers a rerender, which runs the effect cleanup and abortscontroller.signal—cancelling the in-flight phase-2 fetch started on the same signal. The dependency array also includescached/hexCached, which change asputCacheruns, causing extra effect reruns and potentially restarting work. A concrete fix is to (1) removecached/hexCachedfrom the effect dependencies and only depend on stable inputs (cacheKey,normalized,reg,hasIcao24), and (2) manage “phase 1/phase 2” progression with explicit state/refs so caching updates don’t retrigger/abort the fetch pipeline.
"use client";
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| controller.abort(); | ||
| }; | ||
| }, [icao24, registration]); | ||
| }, [cacheKey, cached, hasIcao24, hexCached, normalized, reg]); |
There was a problem hiding this comment.
This effect can abort or duplicate the “phase 2” registration (JetAPI) fetch. In the reg && !hexCached path, calling setResolvedKey(normalized) triggers a rerender, which runs the effect cleanup and aborts controller.signal—cancelling the in-flight phase-2 fetch started on the same signal. The dependency array also includes cached/hexCached, which change as putCache runs, causing extra effect reruns and potentially restarting work. A concrete fix is to (1) remove cached/hexCached from the effect dependencies and only depend on stable inputs (cacheKey, normalized, reg, hasIcao24), and (2) manage “phase 1/phase 2” progression with explicit state/refs so caching updates don’t retrigger/abort the fetch pipeline.
| export function flattenPathColors( | ||
| pathLength: number, | ||
| color: Color | Color[], | ||
| ): Uint8Array { | ||
| if (pathLength <= 0) { | ||
| return new Uint8Array(0); | ||
| } | ||
|
|
||
| if (Array.isArray(color[0])) { | ||
| const flattened = new Uint8Array(pathLength * 4); | ||
| let offset = 0; | ||
| const vertexColors = color as Color[]; | ||
| if (vertexColors.length !== pathLength) { | ||
| throw new Error( | ||
| "PathLayer getColor() returned vertex colors that do not match the path length", | ||
| ); | ||
| } | ||
|
|
||
| for (const vertexColor of vertexColors) { | ||
| const tuple = toColorTuple(vertexColor); | ||
| flattened[offset++] = tuple[0]; | ||
| flattened[offset++] = tuple[1]; | ||
| flattened[offset++] = tuple[2]; | ||
| flattened[offset++] = tuple[3]; | ||
| } | ||
|
|
||
| return flattened; | ||
| } | ||
|
|
||
| const renderedSegmentCount = Math.max(pathLength - 1, 0); | ||
| const flattened = new Uint8Array(renderedSegmentCount * 4); | ||
| let offset = 0; | ||
| const tuple = toColorTuple(color as Color); | ||
| for (let index = 1; index < pathLength; index += 1) { | ||
| flattened[offset++] = tuple[0]; | ||
| flattened[offset++] = tuple[1]; | ||
| flattened[offset++] = tuple[2]; | ||
| flattened[offset++] = tuple[3]; | ||
| } | ||
|
|
||
| return flattened; | ||
| } |
There was a problem hiding this comment.
The per-vertex branch returns pathLength * 4 bytes, while the uniform-color branch returns (pathLength - 1) * 4, implying inconsistent expectations about whether colors are per-vertex or per-segment/instance. Since TrailGradientPathLayer registers instanceColors as an instanced attribute, deck.gl typically expects one color per rendered segment (instance), i.e. pathLength - 1. If pathLength * 4 is written into an instanced buffer sized for segments, this can misalign subsequent objects’ colors or overflow. Consider normalizing both branches to emit per-segment colors (length pathLength - 1) by blending adjacent vertex colors (e.g., average color[i] and color[i+1]) instead of returning raw per-vertex values.
| import type { AltitudeDisplayMode } from "@/lib/altitude-display-mode"; | ||
| import { projectDisplayedAltitudeMeters } from "@/components/map/altitude-projection"; |
There was a problem hiding this comment.
This introduces a dependency from src/lib/* into src/components/* (flight-utils → components/map/altitude-projection). That can make the module graph harder to reason about and increases the chance of circular imports (and also risks pulling UI-layer code into server/runtime code paths that use flight-utils). A concrete fix is to move altitude-projection.ts into src/lib/ (or a shared src/shared/), then import it from both components/ and lib/ to keep layering consistent.
| export function altitudeToElevation( | ||
| altitude: number | null, | ||
| mode: AltitudeDisplayMode = "presentation", | ||
| ): number { | ||
| return projectDisplayedAltitudeMeters(altitude, mode); | ||
| } |
There was a problem hiding this comment.
This introduces a dependency from src/lib/* into src/components/* (flight-utils → components/map/altitude-projection). That can make the module graph harder to reason about and increases the chance of circular imports (and also risks pulling UI-layer code into server/runtime code paths that use flight-utils). A concrete fix is to move altitude-projection.ts into src/lib/ (or a shared src/shared/), then import it from both components/ and lib/ to keep layering consistent.
| destinationConfidence: "known" | "high" | "medium" | "low" | null; | ||
| /** How the route was determined */ | ||
| source: "api" | "detected" | "estimated" | "mixed" | null; | ||
| /** Whether route data is currently being fetched */ |
There was a problem hiding this comment.
The loading field docstring says “Whether route data is currently being fetched”, but the returned value is conditional on !origin && !destination. This can report loading: false even while an API lookup is still in-flight (if detected/estimated info exists). Either update the docstring to reflect “only show loading when no route info is available yet”, or change the returned loading value to match the documented meaning (and let the UI decide when to display it).
| /** Whether route data is currently being fetched */ | |
| /** Whether route data is being fetched and no route info is available yet */ |
| destination, | ||
| destinationConfidence, | ||
| source, | ||
| loading: loading && !origin && !destination, |
There was a problem hiding this comment.
The loading field docstring says “Whether route data is currently being fetched”, but the returned value is conditional on !origin && !destination. This can report loading: false even while an API lookup is still in-flight (if detected/estimated info exists). Either update the docstring to reflect “only show loading when no route info is available yet”, or change the returned loading value to match the documented meaning (and let the UI decide when to display it).
| loading: loading && !origin && !destination, | |
| loading, |
| const lastRequestTime: Record<string, number> = {}; | ||
| const rateLimitQueues: Record<string, Promise<void>> = {}; | ||
|
|
||
| async function enforceRateLimit(provider: ProviderKey): Promise<void> { | ||
| const previous = rateLimitQueues[provider] ?? Promise.resolve(); | ||
|
|
||
| const next = previous.then(async () => { | ||
| const now = Date.now(); | ||
| const last = lastRequestTime[provider] ?? 0; | ||
| const config = PROVIDERS[provider]; | ||
| const wait = Math.max(0, config.rateMs - (now - last)); | ||
| if (wait > 0) { | ||
| await new Promise((resolve) => setTimeout(resolve, wait)); | ||
| } | ||
| lastRequestTime[provider] = Date.now(); | ||
| }); | ||
|
|
||
| // Ensure the chain continues even if a previous step rejects. | ||
| rateLimitQueues[provider] = next.catch(() => {}); | ||
|
|
||
| const ADSB_LOL_BASE = "https://api.adsb.lol/v2"; | ||
| return next; | ||
| } |
There was a problem hiding this comment.
This rate limiter is in-memory and per-process, so it won’t enforce a global limit if the app runs across multiple server instances (or multiple concurrent route workers). That can still exceed upstream limits under load even though each instance behaves correctly. If this endpoint is expected to handle concurrent traffic, consider using a shared limiter (e.g., Redis-backed) or pushing the limiting responsibility to a single egress layer, and/or returning 429 to callers when the local queue depth grows past a threshold to avoid unbounded latency.