Progressive enhancement chain: GeoIP auto-detect -> browser GPS ->
manual entry -> works without any location. When set, the observer
coordinates are injected into chat requests so the LLM can answer
"Where is Jupiter?" with actual azimuth/elevation from the user's
location instead of placeholder coordinates.
Backend:
- GeoIP service (MaxMind GeoLite2-City) with lazy init, private IP
filtering, and IPv4-mapped IPv6 unwrapping
- GET /api/geolocate endpoint (sync to avoid blocking event loop on
mmap I/O, rightmost X-Forwarded-For for Caddy trust chain)
- ObserverContext model on both chat endpoints with shared
_observer_prefix() helper that sanitizes label against prompt
injection
Frontend:
- Location bar between header and messages with pin icon, GPS button,
edit/clear controls, and inline manual entry parser (accepts
"40.7N 74.0W", decimal lat/lon, pg_orrery observer format)
- GeoIP auto-detect on first visit, localStorage persistence
- Observer coordinates sent with every chat request
Infrastructure:
- api-data volume for GeoIP database, Caddy handle_4 for /api/geolocate
- update_geoip.sh using MaxMind Basic auth (key stays out of ps/proc)
- Fix 28 ruff errors: E501 line length, B904 raise-from, F401 unused import
- Fix SQLAlchemy Row.count() ambiguity with tuple indexing (Pyright)
- Replace composite column notation with accessor functions in MCP tools
(topocentric/equatorial/pass_event are C-level base types, not composites)
- Fix satellite_pass: use time window (start + end) not count parameter
to match predict_passes(tle, observer, start_ts, end_ts, min_el) signature