Add navi-offroute service (extraction #8 — final) (#10)

* Add navi-offroute service (extraction #8 — the last one)

Faithful port of recon's /api/offroute (POST) + /api/mvum (GET) and the
runtime offroute modules into a new :8428 service. Closes the loop: after
this, navi-frontend talks only to navi-backend.

Ported: router.py (OffrouteRouter, EntryPointIndex, 4 route strategies,
in-Python MCP_Geometric least-cost path, Valhalla integration, per-request
osmium extract), mvum.py (MVUMReader over navi.db), cost.py, friction.py,
trails.py, and barriers.py (runtime BarrierReader/WildernessReader only).

NOT ported (per Phase A §3/§15): prototype.py (dead at runtime), barriers.py
build_*_raster (offline GDB→raster prep). DEM imported from shared/dem.py
(PR #9), not duplicated.

Behaviour-faithful changes: hardcoded paths/URLs → env vars; the
profile.offroute.* config (osm_pbf_path/postgis_dsn/densify_interval_m) →
dedicated env vars (router drops deployment_config). Both routes public (no
auth, matching recon). PADUS via libpq peer-auth DSN (dbname=padus) — NO
secret. Owns no DB.

15 hermetic tests (offroute validation + mocked-router shape + close-always;
fixture-SQLite MVUM roads/trails/fallback/null; admin auth + no-secrets +
probe shape). Full suite 119 passed / 1 skipped. Adds scikit-image + rasterio.

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

* navi-offroute: PR #10 review cleanups (4 faithful-port deviations)

1. trails.py — drop recon-era "Run the Phase B rasterization script"
   reference from the not-found error (confusing in navi-offroute context).
2. friction.py — add FileNotFoundError-before-rasterio-open check to
   match barriers/trails consistency.
3. mvum.py — remove dead try/except shapely import + warnings.warn at
   2 sites (shapely is a hard pyproject dep; the fallback was unreachable).
4. router.py — declare psutil in pyproject, drop the silent fallback;
   the MEMORY_LIMIT_GB safety check was silently disabled in prod.

Adds test_friction_reader_raises_file_not_found_when_missing (16 navi-offroute
tests; full suite 120 passed / 1 skipped).

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>
This commit is contained in:
malice 2026-05-22 23:30:43 -06:00 committed by GitHub
commit ae82cee46a
17 changed files with 3960 additions and 0 deletions

View file

@ -0,0 +1,30 @@
# navi-offroute — /etc/navi-backend/navi-offroute.env
# Faithful port of recon's /api/offroute + /api/mvum (extraction #8).
# NO SECRETS — PADUS uses libpq peer-auth (dbname=padus, no password); all other
# inputs are read-only paths/URLs. Owns no DB.
# Shared planet-DEM (via shared/dem.py — same file/var as navi-geo).
NAVI_DEM_PMTILES=/mnt/nas/nav/planet-dem.pmtiles
# Offroute-specific read-only data (all stay external per data-ownership rule).
NAVI_OFFROUTE_OSM_PBF=/mnt/nav/sources/idaho-latest.osm.pbf
NAVI_OFFROUTE_NAVI_DB=/mnt/nav/navi.db
NAVI_OFFROUTE_BARRIERS_TIF=/mnt/nav/worldcover/padus_barriers.tif
NAVI_OFFROUTE_WILDERNESS_TIF=/mnt/nav/worldcover/wilderness.tif
NAVI_OFFROUTE_TRAILS_TIF=/mnt/nav/worldcover/trails.tif
NAVI_OFFROUTE_FRICTION_VRT=/mnt/nav/worldcover/friction/friction_conus.vrt
# PADUS PostGIS — peer-auth DSN, NO password. libpq connects via the local
# socket as the service user (systemd User=zvx; verified zvx has padus access).
# Queries the `entry_points` routing index (a different table than navi-landclass).
NAVI_OFFROUTE_POSTGIS_DSN=dbname=padus
# Valhalla — recon-side network router (HTTP), for the on-network leg.
NAVI_OFFROUTE_VALHALLA_URL=http://localhost:8002
# Cost-grid densification interval, metres (was profile.offroute.densify_interval_m).
NAVI_OFFROUTE_DENSIFY_M=100
# HOST DEP: router.py shells out to `osmium extract` per route — the host needs
# osmium-tool installed (`sudo apt-get install osmium-tool`). admin-info reports
# its version (or 'not installed').

View file

@ -0,0 +1,33 @@
# =============================================================================
# navi-offroute — nginx integration for the navi.echo6.co vhost
#
# TWO blocks. Add INSIDE the existing
# server { server_name navi.echo6.co; ... }
# block, BEFORE the existing `location /api/ { ... }` catch-all.
#
# Both routes are PUBLIC (no forward_auth) — @public_api in Caddy already covers
# them, so NO Caddy edit (extraction #8 / Phase A §12).
#
# KEY DELTA from other navi-* blocks: `^~ /api/offroute` needs a LONG
# proxy_read_timeout (130s). Off-network routing runs an in-Python least-cost
# search + Valhalla + a per-request `osmium extract` and can take up to ~2 min;
# the frontend allows 120s (Phase A §8/§15.8). The matching gunicorn
# `--timeout 130` is set in navi-offroute.service. /api/mvum is fast (default).
# -----------------------------------------------------------------------------
location ^~ /api/offroute {
proxy_pass http://127.0.0.1:8428;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Authentik-Username $http_x_authentik_username;
proxy_read_timeout 130s; # long-running routing (see above)
add_header X-Cache-Status BYPASS;
}
location ^~ /api/mvum {
proxy_pass http://127.0.0.1:8428;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Authentik-Username $http_x_authentik_username;
proxy_read_timeout 15s;
add_header X-Cache-Status BYPASS;
}

View file

@ -0,0 +1,18 @@
[Unit]
Description=navi-offroute — off-network router + MVUM API (Echo6 navi-backend, extraction #8)
After=network-online.target
Wants=network-online.target
[Service]
# User=zvx is REQUIRED: PADUS PostGIS access uses libpq peer-auth via the local
# socket (dbname=padus, no password). zvx is verified to have padus access; a
# different user would fail to connect at runtime.
User=zvx
WorkingDirectory=/home/zvx/projects/repos/navi-backend
EnvironmentFile=/etc/navi-backend/navi-offroute.env
ExecStart=/home/zvx/projects/repos/navi-backend/.venv/bin/gunicorn 'services.navi_offroute.app:create_app()' --bind 127.0.0.1:8428 --workers 2 --timeout 130
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target