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 <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2026-02-27 20:31:01 +00:00
parent 53aab8c4bc
commit 17df83d043
3 changed files with 23 additions and 14 deletions

View file

@ -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) {