diff --git a/.env.example b/.env.example index 5c9ac1caba..e0a8794bdb 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,25 @@ # =================================== NEON_DATABASE_URL=postgresql://username:password@host.region.aws.neon.tech/database?sslmode=require +# =================================== +# OSM TRAIL DATABASE +# =================================== +# Local Docker PostGIS used by osm2pgsql during import (scratch/processing DB). +# Not exposed publicly — only used by import and migration scripts. +OSM_DATABASE_URL_LOCAL=postgresql://packrat:packrat@localhost:5433/osm + +# osm2pgsql node cache size in MB (default: 800). +# Increase for continent-scale imports to avoid disk-I/O stalls during way processing. +# Rule of thumb: ~40% of available RAM. For a 16 GB machine use 6000; for 32 GB use 12000. +# OSM_CACHE_MB=6000 + +# Managed production PostGIS (Supabase, Neon, RDS, etc.). +# Matches OSM_DATABASE_URL in the Worker (injected via Hyperdrive at runtime). +# When set, `bun run import` automatically syncs osm_ways + osm_routes here +# after a successful import. Can also be triggered manually with `bun run sync`. +# Leave unset to skip the sync step during local-only imports. +# OSM_DATABASE_URL=postgresql://user:password@host/osm?sslmode=require + # =================================== # AUTHENTICATION & SECURITY # =================================== diff --git a/apps/admin/app/dashboard/trails/page.tsx b/apps/admin/app/dashboard/trails/page.tsx new file mode 100644 index 0000000000..c1f21a4bc2 --- /dev/null +++ b/apps/admin/app/dashboard/trails/page.tsx @@ -0,0 +1,108 @@ +'use client'; + +import { Button } from '@packrat/web-ui/components/button'; +import { Input } from '@packrat/web-ui/components/input'; +import { useQuery } from '@tanstack/react-query'; +import { getTrailGeometry, type TrailGeometry } from 'admin-app/lib/api'; +import { queryKeys } from 'admin-app/lib/queryKeys'; +import dynamic from 'next/dynamic'; +import { useState } from 'react'; + +const LEADING_SLASH_RE = /^r?\//; + +const TrailMap = dynamic(() => import('admin-app/components/trail-map').then((m) => m.TrailMap), { + ssr: false, + loading: () =>
, +}); + +function MetaBadge({ label, value }: { label: string; value: string | null | undefined }) { + if (!value) return null; + return ( +
+ {label} + {value} +
+ ); +} + +export default function TrailViewerPage() { + const [input, setInput] = useState(''); + const [osmId, setOsmId] = useState(''); + + const { data, isLoading, isError, error } = useQuery({ + queryKey: queryKeys.osm.trail(osmId), + queryFn: () => getTrailGeometry(osmId), + enabled: osmId.length > 0, + retry: false, + }); + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + const trimmed = input.trim().replace(LEADING_SLASH_RE, ''); + if (trimmed) setOsmId(trimmed); + } + + return ( +
+
+

Trail Viewer

+

+ Enter an OSM relation ID to visualise its geometry and verify stitching. +

+
+ +
+ setInput(e.target.value)} + className="font-mono" + /> + +
+ + {isError && ( +

+ {error instanceof Error ? error.message : 'Failed to load trail'} +

+ )} + + {isLoading &&

Loading trail…

} + + {data && ( + <> +
+
+ Name + + {data.name ?? unnamed} + +
+ + + + + + {!data.geometry && ( + + ⚠ No geometry — stitching returned null + + )} +
+ +
+ {data.geometry ? ( + + ) : ( +
+ No geometry available for this trail +
+ )} +
+ + )} +
+ ); +} diff --git a/apps/admin/app/layout.tsx b/apps/admin/app/layout.tsx index 532c0fff6f..3726ac7874 100644 --- a/apps/admin/app/layout.tsx +++ b/apps/admin/app/layout.tsx @@ -3,6 +3,7 @@ import { QueryProvider } from 'admin-app/components/query-provider'; import { ThemeProvider } from 'admin-app/components/theme-provider'; import type { Metadata } from 'next'; import { Mona_Sans as FontSans } from 'next/font/google'; +import Script from 'next/script'; import type React from 'react'; import './globals.css'; @@ -28,6 +29,10 @@ export default function RootLayout({ }>) { return ( + + {/* Leaflet CSS loaded from CDN — the JS bundle uses webpack externals (window.L) */} + + +