From 53aab8c4bcd3d05954726a9a09258e0cd38483ab Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 27 Feb 2026 20:05:14 +0000 Subject: [PATCH 1/2] Add web optional dependency and update docs Flask is now declared under [project.optional-dependencies] as the `web` extra, so `pip install osm-geojson[web]` installs both the CLI and Flask. The `dev` extra includes Flask too so the test/dev venv gets everything. Update README and AGENTS.md accordingly. Co-Authored-By: Claude Sonnet 4.6 --- AGENTS.md | 4 ++-- README.md | 4 ++-- pyproject.toml | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 950e309..56a43bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,10 +81,10 @@ cd web flask --app app.py run ``` -The web dependencies (Flask) are not in `pyproject.toml`; install separately: +Install the package with the `web` extra to include Flask: ``` -pip install flask +pip install -e ".[web]" ``` ## Type checking diff --git a/README.md b/README.md index 9ee0234..c25e469 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,8 @@ routes as GeoJSON. #### Running the dev server ``` -cd web -flask --app app.py run +pip install -e ".[web]" +flask --app web/app.py run ``` Open `http://127.0.0.1:5000`. diff --git a/pyproject.toml b/pyproject.toml index 45ed064..cb66129 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,15 @@ dependencies = [ ] [project.optional-dependencies] +web = [ + "flask>=3.0", +] dev = [ "mypy", "pytest", "responses", "types-requests", + "flask>=3.0", ] [project.scripts] From 17df83d04368ed86bc0305dab5999256fddea69d Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 27 Feb 2026 20:31:01 +0000 Subject: [PATCH 2/2] Use url_for() for all internal URLs; fix ProxyPass compatibility All hardcoded paths (/load, /docs, /, /api/route/ etc.) are replaced with url_for() in templates, so Flask's APPLICATION_ROOT and ProxyFix generate correct URLs regardless of mount path. For app.js (a static file), inject a URLS object from the template alongside RELATION_ID: URLS.routeApi, .segmentApi, .routeMasterApi, .routePage Each is generated with url_for(..., relation_id=0)[:-1] to give a prefix that JS appends relation IDs to. The popstate handler now strips the URLS.routePage prefix instead of matching a hardcoded leading slash. Co-Authored-By: Claude Sonnet 4.6 --- web/static/app.js | 19 +++++++++++-------- web/templates/api.html | 4 ++-- web/templates/index.html | 14 ++++++++++---- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/web/static/app.js b/web/static/app.js index 8bb7913..78c3b97 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -313,7 +313,7 @@ document.getElementById('slot-to').addEventListener('click', () => setActiveSlot function navigateTo(id, e) { if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return; e.preventDefault(); - history.pushState(null, '', `/${id}`); + history.pushState(null, '', URLS.routePage + id); loadRoute(id); } @@ -325,7 +325,7 @@ function navigateTo(id, e) { */ async function loadRoute(relationId) { try { - const resp = await fetch(`/api/route/${relationId}`); + const resp = await fetch(URLS.routeApi + relationId); const data = await resp.json(); if (!resp.ok) { if (data.error === 'is_route_master') { @@ -374,7 +374,7 @@ async function loadRoute(relationId) { dirPanel.classList.remove('d-none'); for (const dir of data.other_directions) { const a = document.createElement('a'); - a.href = `/${dir.id}`; + a.href = URLS.routePage + dir.id; a.className = 'stop-item d-block text-decoration-none'; a.textContent = dir.name; a.addEventListener('click', (e) => navigateTo(dir.id, e)); @@ -396,7 +396,7 @@ async function loadSegment() { const rid = currentRelationId; const params = new URLSearchParams({ from: selectedFrom, to: selectedTo, stops: '1' }); try { - const resp = await fetch(`/api/segment/${rid}?${params}`); + const resp = await fetch(URLS.segmentApi + rid + '?' + params); const data = await resp.json(); if (!resp.ok) { showError(data.message || 'Error loading segment.'); @@ -415,7 +415,7 @@ async function loadSegment() { */ async function loadRouteMaster(relationId) { try { - const resp = await fetch(`/api/route_master/${relationId}`); + const resp = await fetch(URLS.routeMasterApi + relationId); const data = await resp.json(); if (!resp.ok) { showError(data.message || `Error loading route master ${relationId}.`); @@ -463,7 +463,7 @@ async function loadRouteMaster(relationId) { `background:${colour};flex-shrink:0`; const a = document.createElement('a'); - a.href = `/${route.id}`; + a.href = URLS.routePage + route.id; a.className = 'text-decoration-none text-reset flex-grow-1'; a.textContent = route.name; a.addEventListener('click', (e) => navigateTo(route.id, e)); @@ -486,8 +486,11 @@ async function loadRouteMaster(relationId) { // ── Init ─────────────────────────────────────────────────────────────────── window.addEventListener('popstate', () => { - const match = location.pathname.match(/^\/(\d+)$/); - if (match) loadRoute(parseInt(match[1], 10)); + const prefix = URLS.routePage; + if (location.pathname.startsWith(prefix)) { + const id = parseInt(location.pathname.slice(prefix.length), 10); + if (id) loadRoute(id); + } }); if (RELATION_ID) { diff --git a/web/templates/api.html b/web/templates/api.html index 74bf442..40a35a1 100644 --- a/web/templates/api.html +++ b/web/templates/api.html @@ -17,8 +17,8 @@
diff --git a/web/templates/index.html b/web/templates/index.html index a1f86f6..96170b9 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -12,8 +12,8 @@
@@ -23,7 +23,7 @@