diff --git a/backend/server/adventures/views/location_view.py b/backend/server/adventures/views/location_view.py index c3206e66..9a6024e1 100644 --- a/backend/server/adventures/views/location_view.py +++ b/backend/server/adventures/views/location_view.py @@ -44,6 +44,7 @@ from .quick_add_utils import ( parse_itinerary_date, preferred_link, resolve_quick_add_collection, + resolve_quick_add_location_label, sanitize_photo_urls, sanitize_tags, ) @@ -245,12 +246,7 @@ class LocationViewSet(viewsets.ModelViewSet): phone_number = str(details.get('phone_number') or payload.get('phone_number') or '').strip() or None - location_label = ( - str(payload.get('location') or '').strip() - or str(reverse_data.get('display_name') or '').strip() - or str(details.get('formatted_address') or '').strip() - or None - ) + location_label = resolve_quick_add_location_label(payload, reverse_data, details) description = build_quick_add_description( base_description=payload.get('description'), diff --git a/backend/server/adventures/views/lodging_view.py b/backend/server/adventures/views/lodging_view.py index 1a80565d..3138e91e 100644 --- a/backend/server/adventures/views/lodging_view.py +++ b/backend/server/adventures/views/lodging_view.py @@ -21,6 +21,7 @@ from .quick_add_utils import ( parse_itinerary_date, preferred_link, resolve_quick_add_collection, + resolve_quick_add_location_label, sanitize_photo_urls, ) @@ -127,12 +128,7 @@ class LodgingViewSet(viewsets.ModelViewSet): if rating is None: rating = coerce_float(details.get('rating')) - location_label = ( - str(payload.get('location') or '').strip() - or str(reverse_data.get('display_name') or '').strip() - or str(details.get('formatted_address') or '').strip() - or None - ) + location_label = resolve_quick_add_location_label(payload, reverse_data, details) place_types = payload.get('types') if not isinstance(place_types, list) or not place_types: diff --git a/backend/server/adventures/views/quick_add_utils.py b/backend/server/adventures/views/quick_add_utils.py index 3c2582f2..af0b49c4 100644 --- a/backend/server/adventures/views/quick_add_utils.py +++ b/backend/server/adventures/views/quick_add_utils.py @@ -111,6 +111,20 @@ def build_quick_add_description(base_description, detailed_description): return description or None +def resolve_quick_add_location_label(payload, reverse_data, details): + """Prefer server reverse-geocoded display_name over raw provider address strings.""" + reverse_data = reverse_data if isinstance(reverse_data, dict) else {} + details = details if isinstance(details, dict) else {} + payload = payload if isinstance(payload, dict) else {} + + return ( + str(reverse_data.get('display_name') or '').strip() + or str(payload.get('location') or '').strip() + or str(details.get('formatted_address') or '').strip() + or None + ) + + def resolve_quick_add_collection(collection_id, validate_permissions, permission_error_message): if not collection_id: return None diff --git a/frontend/package.json b/frontend/package.json index 537caf02..65f9cc45 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "adventurelog-frontend", - "version": "0.12.0", + "version": "0.12.1", "scripts": { "dev": "vite dev", "django": "cd .. && cd backend/server && python3 manage.py runserver", diff --git a/frontend/src/lib/components/shared/PlaceQuickStart.svelte b/frontend/src/lib/components/shared/PlaceQuickStart.svelte index 1de15134..07468d93 100644 --- a/frontend/src/lib/components/shared/PlaceQuickStart.svelte +++ b/frontend/src/lib/components/shared/PlaceQuickStart.svelte @@ -6,6 +6,7 @@ import { addToast } from '$lib/toasts'; import CategoryDropdown from '../CategoryDropdown.svelte'; import type { Category } from '$lib/types'; + import { fetchFormattedLocation } from '$lib/map/places'; import SearchIcon from '~icons/mdi/magnify'; import LocationIcon from '~icons/mdi/crosshairs-gps'; @@ -348,59 +349,32 @@ async function performDetailedReverseGeocode(lat: number, lng: number) { try { - const response = await fetch(`/api/places/reverse/?lat=${lat}&lon=${lng}&format=json`); - - if (response.ok) { - const data = await response.json(); - const providerUsed = data.provider_used || data.provider || null; - locationData = { - city: data.city - ? { - name: data.city, - id: data.city_id, - visited: data.city_visited || false - } - : undefined, - region: data.region - ? { - name: data.region, - id: data.region_id, - visited: data.region_visited || false - } - : undefined, - country: data.country - ? { - name: data.country, - country_code: data.country_id, - visited: false - } - : undefined, - display_name: data.display_name, - location_name: data.location_name, - provider: providerUsed - }; - - if (selectedLocation) { - const isCoordinatePlaceholder = selectedLocation.name.startsWith('Location at '); - const shouldAutoEnrichQuickAdd = isCoordinatePlaceholder || !selectedLocation.place_id; - const resolvedLocationName = (data.location_name || '').trim(); - const resolvedDisplayName = (data.display_name || '').trim(); - - selectedLocation = { - ...selectedLocation, - name: isCoordinatePlaceholder - ? resolvedLocationName || resolvedDisplayName || selectedLocation.name - : selectedLocation.name, - location: resolvedDisplayName || `${lat.toFixed(4)}, ${lng.toFixed(4)}` - }; - searchQuery = selectedLocation.name; - - if (shouldAutoEnrichQuickAdd && resolvedLocationName) { - await enrichFromResolvedName(lat, lng, resolvedLocationName); - } - } - } else { + const formatted = await fetchFormattedLocation(lat, lng); + if (!formatted) { locationData = null; + return; + } + + locationData = formatted; + + if (selectedLocation) { + const isCoordinatePlaceholder = selectedLocation.name.startsWith('Location at '); + const shouldAutoEnrichQuickAdd = isCoordinatePlaceholder || !selectedLocation.place_id; + const resolvedLocationName = (formatted.location_name || '').trim(); + const resolvedDisplayName = (formatted.display_name || '').trim(); + + selectedLocation = { + ...selectedLocation, + name: isCoordinatePlaceholder + ? resolvedLocationName || resolvedDisplayName || selectedLocation.name + : selectedLocation.name, + location: resolvedDisplayName || `${lat.toFixed(4)}, ${lng.toFixed(4)}` + }; + searchQuery = selectedLocation.name; + + if (shouldAutoEnrichQuickAdd && resolvedLocationName) { + await enrichFromResolvedName(lat, lng, resolvedLocationName); + } } } catch (error) { console.error('Detailed reverse geocoding error:', error); @@ -519,6 +493,8 @@ } async function quickAdd() { + await ensureAdventureLogFormattedLocation(); + const prefill = buildPrefillPayload(); if (!prefill) { addToast('warning', `Please select a place or drop a pin first`); diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts index e18717b8..0a46b5e5 100644 --- a/frontend/src/lib/config.ts +++ b/frontend/src/lib/config.ts @@ -1,4 +1,4 @@ -export let appVersion = 'v0.12.1-beta-061326'; +export let appVersion = 'v0.12.1-beta-061426'; export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.12.1'; export let appTitle = 'AdventureLog'; export let copyrightYear = '2023-2026'; diff --git a/frontend/src/lib/index.server.ts b/frontend/src/lib/index.server.ts index aee1ea0e..aacdaa1f 100644 --- a/frontend/src/lib/index.server.ts +++ b/frontend/src/lib/index.server.ts @@ -1 +1 @@ -export { fetchCSRFToken, getServerEndpoint } from '$lib/server/django-proxy'; +export { backendApiUrl, fetchCSRFToken, getServerEndpoint } from '$lib/server/django-proxy'; diff --git a/frontend/src/lib/map/places.ts b/frontend/src/lib/map/places.ts index aa737861..47a76662 100644 --- a/frontend/src/lib/map/places.ts +++ b/frontend/src/lib/map/places.ts @@ -98,6 +98,73 @@ export async function enrichPlace(place: PlaceSearchResult): Promise { + try { + const response = await fetch(`/api/places/reverse/?lat=${lat}&lon=${lng}&format=json`); + if (!response.ok) { + return null; + } + + const data = await response.json(); + const providerUsed = data.provider_used || data.provider || null; + return { + city: data.city + ? { + name: data.city, + id: data.city_id, + visited: data.city_visited || false + } + : undefined, + region: data.region + ? { + name: data.region, + id: data.region_id, + visited: data.region_visited || false + } + : undefined, + country: data.country + ? { + name: data.country, + country_code: data.country_id, + visited: false + } + : undefined, + display_name: data.display_name, + location_name: data.location_name, + provider: providerUsed + }; + } catch { + return null; + } +} + +export function resolveQuickAddLocationLabel( + formatted: FormattedLocationData | null, + fallbackLocation = '' +): string { + return (formatted?.display_name || '').trim() || fallbackLocation.trim() || ''; +} + +export async function resolveQuickAddPayload(payload: QuickAddPayload): Promise { + const formatted = await fetchFormattedLocation(payload.latitude, payload.longitude); + return { + ...payload, + location: resolveQuickAddLocationLabel(formatted, payload.location) + }; +} + export type QuickAddPayload = { name: string; latitude: number; diff --git a/frontend/src/lib/server/django-proxy.ts b/frontend/src/lib/server/django-proxy.ts index 5f8cef43..38568ad5 100644 --- a/frontend/src/lib/server/django-proxy.ts +++ b/frontend/src/lib/server/django-proxy.ts @@ -22,8 +22,8 @@ type ProxyOptions = { formatSearchParam?: (search: string, method: string) => string; }; -const authTrailingSlashPaths = ['disable-password', 'mobile-qr']; -const authTrailingSlashPrefixes = ['api-keys']; +/** Allauth headless routes omit trailing slashes; custom auth routes use them. */ +const authNoTrailingSlashPrefixes = ['browser/', 'app/']; /** Headers that must not be forwarded to Node fetch (undici rejects hop-by-hop headers). */ const FORBIDDEN_PROXY_HEADERS = new Set([ @@ -60,11 +60,31 @@ function defaultFormatSearchParam(search: string, method: string, basePath: Prox } function authRequiresTrailingSlash(path: string, requireTrailingSlash: boolean): boolean { - return ( - requireTrailingSlash || - authTrailingSlashPaths.includes(path) || - authTrailingSlashPrefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)) - ); + if (authNoTrailingSlashPrefixes.some((prefix) => path.startsWith(prefix))) { + return false; + } + return requireTrailingSlash; +} + +function apiRequiresTrailingSlash(path: string, requireTrailingSlash: boolean): boolean { + if (requireTrailingSlash === false) { + return false; + } + // Preserve trailing slash for nested action paths (e.g. locations//duplicate/). + if (path.endsWith('/')) { + return false; + } + return true; +} + +/** Build a backend API URL with a trailing slash before query params. */ +export function backendApiUrl(path: string, search = ''): string { + const endpoint = getServerEndpoint(); + const normalized = path.startsWith('/') ? path : `/${path}`; + const [pathname, embeddedSearch = ''] = normalized.split('?'); + const slashPath = pathname.endsWith('/') ? pathname : `${pathname}/`; + const query = search || embeddedSearch; + return `${endpoint}${slashPath}${query ? (query.startsWith('?') ? query : `?${query}`) : ''}`; } export async function proxyToDjango( @@ -79,8 +99,8 @@ export async function proxyToDjango( const requireTrailingSlash = basePath === 'auth' - ? authRequiresTrailingSlash(path, options.requireTrailingSlash ?? false) - : (options.requireTrailingSlash ?? false); + ? authRequiresTrailingSlash(path, options.requireTrailingSlash ?? true) + : apiRequiresTrailingSlash(path, options.requireTrailingSlash ?? true); if (requireTrailingSlash && !targetUrl.endsWith('/')) { targetUrl += '/'; diff --git a/frontend/src/routes/api/[...path]/+server.ts b/frontend/src/routes/api/[...path]/+server.ts index b0029013..7857e195 100644 --- a/frontend/src/routes/api/[...path]/+server.ts +++ b/frontend/src/routes/api/[...path]/+server.ts @@ -1,17 +1,12 @@ import { proxyToDjango } from '$lib/server/django-proxy'; import type { RequestHandler } from './$types'; -export const GET: RequestHandler = (event) => - proxyToDjango(event, 'api', { requireTrailingSlash: false }); +export const GET: RequestHandler = (event) => proxyToDjango(event, 'api'); -export const POST: RequestHandler = (event) => - proxyToDjango(event, 'api', { requireTrailingSlash: true }); +export const POST: RequestHandler = (event) => proxyToDjango(event, 'api'); -export const PATCH: RequestHandler = (event) => - proxyToDjango(event, 'api', { requireTrailingSlash: true }); +export const PATCH: RequestHandler = (event) => proxyToDjango(event, 'api'); -export const PUT: RequestHandler = (event) => - proxyToDjango(event, 'api', { requireTrailingSlash: true }); +export const PUT: RequestHandler = (event) => proxyToDjango(event, 'api'); -export const DELETE: RequestHandler = (event) => - proxyToDjango(event, 'api', { requireTrailingSlash: true }); +export const DELETE: RequestHandler = (event) => proxyToDjango(event, 'api'); diff --git a/frontend/src/routes/collections/[id]/+page.server.ts b/frontend/src/routes/collections/[id]/+page.server.ts index 1ecce247..853c7c2a 100644 --- a/frontend/src/routes/collections/[id]/+page.server.ts +++ b/frontend/src/routes/collections/[id]/+page.server.ts @@ -34,9 +34,7 @@ export const load = (async (event) => { }) satisfies PageServerLoad; import type { Actions } from '@sveltejs/kit'; -import { fetchCSRFToken } from '$lib/index.server'; - -const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; +import { backendApiUrl, fetchCSRFToken } from '$lib/index.server'; export const actions: Actions = { delete: async (event) => { @@ -61,7 +59,7 @@ export const actions: Actions = { let csrfToken = await fetchCSRFToken(); - let res = await fetch(`${serverEndpoint}/api/collections/${event.params.id}`, { + let res = await fetch(backendApiUrl(`/api/collections/${event.params.id}`), { method: 'DELETE', headers: { Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, diff --git a/frontend/src/routes/locations/+page.server.ts b/frontend/src/routes/locations/+page.server.ts index fc3a8c94..1cb542a1 100755 --- a/frontend/src/routes/locations/+page.server.ts +++ b/frontend/src/routes/locations/+page.server.ts @@ -4,7 +4,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; import type { Location } from '$lib/types'; import type { Actions } from '@sveltejs/kit'; -import { fetchCSRFToken } from '$lib/index.server'; +import { backendApiUrl, fetchCSRFToken } from '$lib/index.server'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -29,7 +29,10 @@ export const load = (async (event) => { const is_visited = event.url.searchParams.get('is_visited') || 'all'; let initialFetch = await event.fetch( - `${serverEndpoint}/api/locations/filtered?types=${typeString}&order_by=${order_by}&order_direction=${order_direction}&include_collections=${include_collections}&page=${page}&is_visited=${is_visited}`, + backendApiUrl( + '/api/locations/filtered', + `types=${typeString}&order_by=${order_by}&order_direction=${order_direction}&include_collections=${include_collections}&page=${page}&is_visited=${is_visited}` + ), { headers: { Cookie: `sessionid=${event.cookies.get('sessionid')}` diff --git a/frontend/src/routes/locations/[id]/+page.server.ts b/frontend/src/routes/locations/[id]/+page.server.ts index 2c3dc9bb..ee2c3dcf 100644 --- a/frontend/src/routes/locations/[id]/+page.server.ts +++ b/frontend/src/routes/locations/[id]/+page.server.ts @@ -33,9 +33,7 @@ export const load = (async (event) => { }) satisfies PageServerLoad; import { redirect, type Actions } from '@sveltejs/kit'; -import { fetchCSRFToken } from '$lib/index.server'; - -const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; +import { backendApiUrl, fetchCSRFToken } from '$lib/index.server'; export const actions: Actions = { delete: async (event) => { @@ -54,7 +52,7 @@ export const actions: Actions = { let csrfToken = await fetchCSRFToken(); - let res = await fetch(`${serverEndpoint}/api/locations/${event.params.id}`, { + let res = await fetch(backendApiUrl(`/api/locations/${event.params.id}`), { method: 'DELETE', headers: { Referer: event.url.origin, // Include Referer header diff --git a/frontend/src/routes/map/+page.svelte b/frontend/src/routes/map/+page.svelte index 7d8d6612..56e5a27a 100644 --- a/frontend/src/routes/map/+page.svelte +++ b/frontend/src/routes/map/+page.svelte @@ -28,7 +28,8 @@ placeToQuickAddPayload, quickAddLocation, recommendationToLocationPrefill, - recommendationToQuickAddPayload + recommendationToQuickAddPayload, + resolveQuickAddPayload } from '$lib/map/places'; import MapIcon from '~icons/mdi/map'; @@ -572,7 +573,8 @@ selected.kind === 'place' ? placeToQuickAddPayload(selected.place) : recommendationToQuickAddPayload(selected.item); - const created = await quickAddLocation(payload); + const resolved = await resolveQuickAddPayload(payload); + const created = await quickAddLocation(resolved); addToast('success', $t('map.quick_add')); const newPin: Pin = { id: created.id, diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 126a520f..7b0edb35 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -3,22 +3,11 @@ import { defineConfig } from 'vite'; import { sveltekit } from '@sveltejs/kit/vite'; import Icons from 'unplugin-icons/vite'; -const backendTarget = process.env.PUBLIC_SERVER_URL ?? 'http://127.0.0.1:8000'; -const useDevProxy = !process.env.CI && process.env.VITEST !== 'true'; - export default defineConfig({ plugins: [ sveltekit(), Icons({ compiler: 'svelte' }) - ], - server: useDevProxy - ? { - proxy: { - '/api': { target: backendTarget, changeOrigin: true }, - '/auth': { target: backendTarget, changeOrigin: true } - } - } - : undefined + ] });