From 79fcf2bdc148dd7be850eade9e33f25d8e76a713 Mon Sep 17 00:00:00 2001 From: gnezim Date: Fri, 17 Apr 2026 22:48:18 +0300 Subject: [PATCH] Fix onlineboard empty results and flights-map polyline zoom hazard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Online board: the /board endpoint treats dateFrom/dateTo as a half-open interval, so sending the same date for both yielded zero rows on routes that obviously have flights (e.g. SVO-LED). Mirror Angular's OnlineBoardApiService.getFlightsByRoute and use dateTo = date + 1 day. Flights map: two stacked problems made arcs disappear on zoom. - syncPolylines gated endpoints on map.hasLayer(marker); when syncVisibility removed a zoom-tier layer, its arc went with it. - The zoomend and toggle effects both called syncPolylines, which captured a stale closure from the first render (polylines = []) and wiped the layer. Polyline coords are geographic — Leaflet rescales them on zoom — so the rebuild was never necessary. Arcs now render once per polylines prop change and stay put through zoom and filter toggles. --- .../flights-map/components/MapCanvas.test.tsx | 23 ++++++++-------- .../flights-map/components/MapCanvas.tsx | 6 ++--- .../components/OnlineBoardSearchPage.tsx | 27 ++++++++++++++++++- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/features/flights-map/components/MapCanvas.test.tsx b/src/features/flights-map/components/MapCanvas.test.tsx index acf3cbe4..cee61dc6 100644 --- a/src/features/flights-map/components/MapCanvas.test.tsx +++ b/src/features/flights-map/components/MapCanvas.test.tsx @@ -452,7 +452,10 @@ describe("MapCanvas — polylines (C.3)", () => { expect(createdPolylines.length).toBe(0); }); - it("skips intermediate cities whose marker is not on the map", () => { + it("draws multi-hop polylines through every city in the index", () => { + // Route/domestic filtering happens upstream in filterRoutes — by the time + // a polyline reaches MapCanvas, every city on its path is expected to be + // drawn, independent of zoom-tier visibility. render( { tileUrl="t" />, ); - const map = createdMaps[0]!; - // Reset so we count only polylines drawn by the next sync cycle. - createdPolylines.length = 0; - map.setZoom(6); - map.fireZoomend(); expect(createdPolylines.length).toBe(1); }); @@ -487,7 +485,7 @@ describe("MapCanvas — polylines (C.3)", () => { expect(opts?.dashArray).toBe("4 14"); }); - it("rebuilds polylines on zoomend", () => { + it("does not rebuild polylines on zoomend (they are zoom-independent)", () => { render( { />, ); const map = createdMaps[0]!; - map.setZoom(6); - map.fireZoomend(); - const before = createdPolylines.length; + const afterInitialRender = createdPolylines.length; map.setZoom(4); map.fireZoomend(); + map.setZoom(6); + map.fireZoomend(); - expect(createdPolylines.length).toBeGreaterThan(before); + // Polyline coords are fixed by city lat/lng — Leaflet's SVG pane rescales + // them on zoom without us recomputing. Rebuilding from the zoomend + // handler also carried a stale-closure hazard. + expect(createdPolylines.length).toBe(afterInitialRender); }); it("silently skips polylines with unknown city codes", () => { diff --git a/src/features/flights-map/components/MapCanvas.tsx b/src/features/flights-map/components/MapCanvas.tsx index a8fc3c85..33ae9617 100644 --- a/src/features/flights-map/components/MapCanvas.tsx +++ b/src/features/flights-map/components/MapCanvas.tsx @@ -324,10 +324,11 @@ export const MapCanvas: FC = ({ } zoomLayersRef.current = zoomLayers; - // Rerun visibility + tooltip rules on every zoom change. + // Rerun visibility + tooltip rules on every zoom change. Polylines are + // zoom-independent (Leaflet rescales the SVG automatically) so no rebuild + // is needed here — rebuilding from a stale closure in fact wiped them. map.on("zoomend", () => { syncVisibility(); - syncPolylines(); syncTooltips(); }); @@ -441,7 +442,6 @@ export const MapCanvas: FC = ({ // --- Re-sync visibility + tooltips when toggles change --- useEffect(() => { syncVisibility(); - syncPolylines(); syncTooltips(); }, [domestic, international]); diff --git a/src/features/online-board/components/OnlineBoardSearchPage.tsx b/src/features/online-board/components/OnlineBoardSearchPage.tsx index bc13733b..1fa06a6c 100644 --- a/src/features/online-board/components/OnlineBoardSearchPage.tsx +++ b/src/features/online-board/components/OnlineBoardSearchPage.tsx @@ -61,6 +61,30 @@ function formatDateForApi(yyyymmdd: string): string { return yyyymmdd.includes("T") ? yyyymmdd : `${yyyymmdd}T00:00:00`; } +/** + * Return the yyyyMMdd date one day after `yyyymmdd`. The API treats the + * board range as a half-open interval: `dateFrom=D, dateTo=D` yields zero + * rows. Matching Angular's OnlineBoardApiService.getFlightsByRoute. + */ +function addOneDayYyyymmdd(yyyymmdd: string): string { + const iso = yyyymmdd.includes("T") ? yyyymmdd.split("T")[0]! : yyyymmdd; + let year: number, month: number, day: number; + if (iso.includes("-")) { + const [y, m, d] = iso.split("-"); + year = Number(y); month = Number(m) - 1; day = Number(d); + } else { + year = Number(iso.slice(0, 4)); + month = Number(iso.slice(4, 6)) - 1; + day = Number(iso.slice(6, 8)); + } + const dt = new Date(year, month, day); + dt.setDate(dt.getDate() + 1); + const y = dt.getFullYear().toString(); + const m = (dt.getMonth() + 1).toString().padStart(2, "0"); + const d = dt.getDate().toString().padStart(2, "0"); + return `${y}${m}${d}`; +} + /** * Convert parsed online board URL params into API search params. * The API expects dateFrom/dateTo in yyyy-MM-ddT00:00:00 format. @@ -69,9 +93,10 @@ function toSearchParams( params: OnlineBoardSearchPageProps["params"], ): SearchFlightsParams { const apiDate = formatDateForApi(params.date); + const apiDateTo = formatDateForApi(addOneDayYyyymmdd(params.date)); const base: SearchFlightsParams = { dateFrom: apiDate, - dateTo: apiDate, + dateTo: apiDateTo, }; switch (params.type) {