From 66c2a25457f2e16e92ce9489b53dbb94d93d47e3 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 1 Feb 2026 19:30:33 +0000 Subject: [PATCH] Checkpoint all changes --- frontend/src/App.tsx | 384 +++++++++-- frontend/src/components/AreaPane.tsx | 243 +++++++ frontend/src/components/DataSources.tsx | 2 +- frontend/src/components/DataSourcesPage.tsx | 50 +- frontend/src/components/FAQPage.tsx | 119 ++++ frontend/src/components/Filters.tsx | 556 +++++++++++----- frontend/src/components/HomePage.tsx | 8 +- frontend/src/components/Map.tsx | 175 ++--- frontend/src/components/POIPane.tsx | 88 ++- frontend/src/components/PropertiesPane.tsx | 123 +++- frontend/src/components/ui/slider.tsx | 4 +- frontend/src/index.css | 2 +- frontend/src/types.ts | 25 + pipeline/transform/join_epc_pp.py | 4 +- server-rs/src/consts.rs | 2 +- server-rs/src/features.rs | 676 ++++++++++++++++++++ server-rs/src/filter.rs | 35 +- server-rs/src/grid_index.rs | 32 +- server-rs/src/main.rs | 178 ++++-- server-rs/src/routes/features.rs | 145 +++-- server-rs/src/routes/hexagon_stats.rs | 251 ++++++++ server-rs/src/routes/hexagons.rs | 272 +++++--- server-rs/src/routes/mod.rs | 7 +- server-rs/src/routes/parse.rs | 31 +- server-rs/src/routes/pois.rs | 55 +- server-rs/src/routes/properties.rs | 158 +++-- server-rs/src/state.rs | 23 +- server-rs/src/tests.rs | 8 +- 28 files changed, 3035 insertions(+), 621 deletions(-) create mode 100644 frontend/src/components/AreaPane.tsx create mode 100644 frontend/src/components/FAQPage.tsx create mode 100644 server-rs/src/features.rs create mode 100644 server-rs/src/routes/hexagon_stats.rs diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 24fd6fe..0de2505 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,8 +3,10 @@ import Map from './components/Map'; import Filters from './components/Filters'; import POIPane from './components/POIPane'; import { PropertiesPane } from './components/PropertiesPane'; +import AreaPane from './components/AreaPane'; import DataSources from './components/DataSources'; import DataSourcesPage from './components/DataSourcesPage'; +import FAQPage from './components/FAQPage'; import HomePage from './components/HomePage'; import type { FeatureMeta, @@ -21,18 +23,43 @@ import type { ViewState, Property, HexagonPropertiesResponse, + HexagonStatsResponse, } from './types'; type Theme = 'light' | 'dark'; const DEBOUNCE_MS = 150; const URL_DEBOUNCE_MS = 300; +const INITIAL_RETRY_MS = 1000; +const MAX_RETRY_MS = 10000; + +async function fetchWithRetry( + url: string, + onSuccess: (data: T) => void, + signal: AbortSignal +): Promise { + let delay = INITIAL_RETRY_MS; + while (!signal.aborted) { + try { + const res = await fetch(url, { signal }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const json = await res.json(); + onSuccess(json); + return; + } catch (err) { + if (signal.aborted) return; + console.error(`Failed to fetch ${url}, retrying in ${delay}ms:`, err); + await new Promise((resolve) => setTimeout(resolve, delay)); + delay = Math.min(delay * 2, MAX_RETRY_MS); + } + } +} // Detect if running through VS Code web proxy and construct API base URL function getApiBaseUrl(): string { - const { hostname, pathname, href } = window.location; + const { pathname, href } = window.location; - // Check pathname for /proxy/PORT pattern + // Check pathname for /proxy/PORT pattern (VS Code web proxy) const pathMatch = pathname.match(/^(\/proxy\/)(\d+)/); if (pathMatch) { return `${pathMatch[1]}8001`; @@ -44,12 +71,7 @@ function getApiBaseUrl(): string { return `${hrefMatch[1]}8001`; } - // If not localhost, assume we're behind a proxy and need explicit backend port - if (hostname !== 'localhost' && hostname !== '127.0.0.1') { - return '/proxy/8001'; - } - - // Local development - webpack proxies /api to :8001 + // Default: same origin (works for both local dev with webpack proxy and production) return ''; } @@ -66,7 +88,7 @@ function parseUrlState(): { viewState?: ViewState; filters?: FeatureFilters; poiCategories?: Set; - tab?: 'pois' | 'properties'; + tab?: 'pois' | 'properties' | 'area'; } { const params = new URLSearchParams(window.location.search); const result: ReturnType = {}; @@ -121,10 +143,11 @@ function parseUrlState(): { result.poiCategories = new Set(poi.split(',').filter(Boolean)); } - // Parse tab: tab=p or tab=o + // Parse tab: tab=p or tab=o or tab=a const tab = params.get('tab'); if (tab === 'p') result.tab = 'properties'; else if (tab === 'o') result.tab = 'pois'; + else if (tab === 'a') result.tab = 'area'; return result; } @@ -134,7 +157,7 @@ function stateToParams( filters: FeatureFilters, features: FeatureMeta[], selectedPOICategories: Set, - rightPaneTab: 'pois' | 'properties' + rightPaneTab: 'pois' | 'properties' | 'area' ): URLSearchParams { const params = new URLSearchParams(); @@ -170,6 +193,8 @@ function stateToParams( // Tab (only if non-default) if (rightPaneTab === 'properties') { params.set('tab', 'p'); + } else if (rightPaneTab === 'area') { + params.set('tab', 'a'); } return params; @@ -177,7 +202,7 @@ function stateToParams( // --- Header --- -type Page = 'home' | 'dashboard' | 'data-sources'; +type Page = 'home' | 'dashboard' | 'data-sources' | 'faq'; function Header({ activePage, @@ -200,9 +225,9 @@ function Header({ }, []); const tabClass = (page: Page) => - `px-3 py-1.5 rounded text-sm transition-colors ${ + `px-3 py-1.5 rounded text-sm font-medium transition-colors ${ activePage === page - ? 'bg-navy-700 font-semibold' + ? 'bg-navy-700 text-white' : 'text-warm-300 hover:bg-navy-800 hover:text-white' }`; @@ -234,16 +259,16 @@ function Header({ Narrowit -