navi/backend
malice 15cad0abf4 Add navi-geo service (extraction #6) (#6)
* Add navi-geo service (extraction #6)

Faithful port of recon's geocode/reverse family to a new :8426 service:
  GET /api/geocode?q=&limit=&lat=&lon=&zoom=   Photon-first ranked search
  GET /api/reverse?lat=&lon=                   reverse geocode (Photon)
  GET /api/reverse/<lat>/<lon>                 reverse enrichment bundle (Central)

Ported modules: geocode.py (engine), netsyms.py (address SQLite), dem.py
(planet-DEM reader), address_book.py (reader copy), and the three handlers +
four bundle helpers from netsyms_api.py. All three routes public, behaviour-
identical to recon.

Behaviour-changing edges (both pre-decided in Phase A/B, called out in the PR):
- landclass: in-process call replaced with HTTP GET to navi-landclass :8424,
  reading .summary (the same most-specific unit-name string). First navi→navi
  edge after landclass itself.
- hardcoded paths/URLs → env vars (PHOTON_URL, NAVI_NETSYMS_DB,
  NAVI_TIMEZONE_DB, NAVI_DEM_PMTILES, NAVI_ADDRESS_BOOK_YAML,
  NAVI_LANDCLASS_URL); rerank trace log opt-in (NAVI_GEO_RERANK_TRACE_LOG,
  default off — recon always wrote /tmp).

No secrets in this service: PADUS_DB_* disappears because landclass is HTTP-
delegated (Phase A §10). Address book uses Option B (shared-file read), the
same pattern navi-contacts already uses.

Bundle 9-key contract preserved exactly (name/city/county/state/country/
postal_code/timezone/landclass/elevation_m), same null-on-component-failure
semantics, same in-memory TTLCache(10_000, 86_400) per worker.

Tests: 28 passing, 1 skipped (real timezone DB, off-box). Ported the 9 recon
reverse-bundle tests + added the HTTP-landclass coupling tests + hermetic
geocode reranker/intent-classifier tests (recon's geocode_test.py was a live
smoke test). Adds usaddress/rapidfuzz/cachetools/shapely/numpy/Pillow/pmtiles
to deps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* PR #6 review fixes

1. Rename geocode._setup_trace_logger → setup_trace_logger (public hook)
2. Hoist `import requests as http_requests` to module level in geo_route.py
3. Wire netsyms.health() into admin.py (enriches the netsyms filesystem entry
   with row_count/file_size_bytes/indexed_countries; no shared-builder change)
4. Fix misleading LANDCLASS_TIMEOUT_S comment (recon had no timeout)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: zvx-echo6 <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 20:29:22 -06:00
..
config Add navi-contacts service (extraction #3) 2026-05-22 10:59:37 -06:00
deploy Add navi-geo service (extraction #6) (#6) 2026-05-22 20:29:22 -06:00
services Add navi-geo service (extraction #6) (#6) 2026-05-22 20:29:22 -06:00
shared Add navi-contacts service (extraction #3) 2026-05-22 10:59:37 -06:00
.gitignore Initial scaffold: navi-backend + navi-traffic (extraction #1) 2026-05-21 22:26:50 -06:00
LICENSE Initial scaffold: navi-backend + navi-traffic (extraction #1) 2026-05-21 22:26:50 -06:00
pyproject.toml Add navi-geo service (extraction #6) (#6) 2026-05-22 20:29:22 -06:00
README.md Add navi-geo service (extraction #6) (#6) 2026-05-22 20:29:22 -06:00

navi-backend

Monorepo of small, single-responsibility HTTP services extracted from the recon codebase as part of the recon ↔ Navi decoupling project. Each service owns a slice of the /api/* surface that navi.echo6.co depends on, runs behind the existing Caddy/Authentik edge, and is fronted by the navi.echo6.co nginx vhost.

See HANDOFF-recon-navi-decoupling-v3.md for the full plan. This repo is extraction #1: navi-traffic.

Layout

navi-backend/
├── shared/                  # cross-service helpers, imported by every service
│   ├── auth.py              # get_user_id(req), require_auth decorator (Authentik header)
│   └── admin_info.py        # build_info_response(), mask_key(), time_dependency()
├── services/
│   └── navi_traffic/        # extraction #1 — TomTom traffic tile proxy (:8421)
│       ├── app.py           # Flask factory (create_app) + gunicorn entry
│       ├── traffic.py       # /api/traffic/flow/<z>/<x>/<y>.png  (ported from recon)
│       ├── admin.py         # /api/admin/navi-traffic/info  (§4.5 admin convention)
│       └── tests/
└── deploy/
    ├── systemd/navi-traffic.service
    └── nginx/navi-traffic.conf.snippet

Service directories use an underscore (navi_traffic) so they're importable Python packages; the service name stays navi-traffic (hyphen) in systemd, nginx, and the admin-info service field.

Setup

Single workspace, single virtualenv:

python -m venv .venv
.venv/bin/pip install -e .

Test

.venv/bin/pytest services/navi_traffic/tests/ -v

Run (local)

TOMTOM_API_KEY=... .venv/bin/gunicorn 'services.navi_traffic.app:create_app()' \
    --bind 127.0.0.1:8421 --workers 2

Run (local) — navi-geo (extraction #6)

.venv/bin/pytest services/navi_geo/tests/ -v

# All paths/URLs are env-overridable (see deploy/env/navi-geo.env.example).
# No secrets — landclass is HTTP-delegated to navi-landclass (:8424).
.venv/bin/gunicorn 'services.navi_geo.app:create_app()' \
    --bind 127.0.0.1:8426 --workers 2

navi-geo serves /api/geocode, /api/reverse?lat=&lon=, and the reverse enrichment bundle /api/reverse/<lat>/<lon> (Central's 9-key contract). All public. The reverse bundle fans out to Photon, the SpatiaLite timezone DB, navi-landclass (HTTP), and the planet-DEM PMTiles — each degrading to null independently, never 5xx.

The admin-info convention (§4.5)

Every service exposes GET /api/admin/<service-name>/info, gated by require_auth, returning a uniform shape: service, version (git SHA), port, config, env (names + masked values), dependencies (upstream health checks), filesystem, runtime (uptime / request count / last error). No aggregator — a future admin panel fans out to each service in parallel.