Add navi-landclass service (extraction #4) (#4)

New services/navi_landclass/ on :8424 — single blueprint, behavior-identical
port of recon's lib/landclass.py + the /api/landclass handler.

  GET /api/landclass?lat=&lon=  -> { lat, lon, classifications[], count,
    is_public, is_private, summary }; 400 on bad/out-of-range lat/lon.

db.py: faithful port of recon's PostGIS module — lazy module-level
psycopg2.pool.SimpleConnectionPool(minconn=1, maxconn=3) from PADUS_DB_* env;
the ST_Intersects query on pad_units (antimeridian filter, acres-ordered,
limit 10); all PAD-US code->label maps verbatim; graceful degradation
(returns [] when PG is unreachable, never raises/500). Adds reset_pool()
(create_app resets per worker) and probe_db() (SELECT 1) for admin health.

No filesystem state — PostGIS is external. No DB-on-disk migration; only the
5 PADUS_DB_* env vars (PADUS_DB_PASSWORD is a real secret, masked in
admin-info via mask_key; the other 4 shown plain). adds psycopg2-binary>=2.9.

Decision — DROPPED the recon `has_landclass` profile-flag gate: the frontend
already gates on its own has_landclass feature flag, and removing the
cross-service config dependency keeps navi-landclass self-contained per the
"only API" rule (the service's existence is the feature being available).

navi-geo coupling (reverse-bundle needs landclass) — per Phase A, recommend
Option B: navi-geo HTTP-calls /api/landclass and reads `.summary` (the
endpoint already returns it); no shared module. Decided when #6 lands.

Tests (8; recon had 2): point-with-coverage -> classification + decoded
labels, ocean point -> empty, bad/missing/out-of-range lat/lon -> 400, PG
down -> graceful 200 empty (not 500), format_summary unit. Full suite 46.

Deploy: systemd unit (:8424) + nginx snippet (one ^~ /api/landclass block, no
proxy_cache; /api/landclass is public so no Caddy edit — TIER 2 already
routes through nginx since extraction #2).

See ../recon_refactor/extraction-4-phase-a.md (which also corrects the handoff:
/mnt/nav/padus/ is source GIS files, NOT a runtime path — this service has no
/mnt/nav dependency, only PADUS_DB_* + PG network access).

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
malice 2026-05-22 12:08:58 -06:00 committed by GitHub
commit d08834451f
10 changed files with 593 additions and 0 deletions

View file

@ -0,0 +1,26 @@
# =============================================================================
# navi-landclass — nginx integration for the navi.echo6.co vhost
#
# ONE block. Add INSIDE the existing
# server { server_name navi.echo6.co; ... }
# block, BEFORE the existing `location /api/ { ... }` block.
#
# `^~` (the lesson from extraction #1) makes nginx skip regex evaluation when
# this is the longest prefix match, so the vhost's `location ~* \.(...)$`
# asset-regex can never shadow it. No trailing slash — `/api/landclass` is a
# single endpoint (no subpaths); the no-slash prefix matches both
# `/api/landclass` and `/api/landclass?lat=&lon=`.
#
# /api/landclass is PUBLIC (no forward_auth) — TIER 2 @public_api in Caddy,
# already routed through nginx :8440 since extraction #2. So NO Caddy edit.
#
# No proxy_cache: results vary by lat/lon and volume is tiny. `X-Cache-Status:
# BYPASS` for parity with the other navi-* blocks.
# -----------------------------------------------------------------------------
location ^~ /api/landclass {
proxy_pass http://127.0.0.1:8424;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 10s;
add_header X-Cache-Status BYPASS;
}

View file

@ -0,0 +1,15 @@
[Unit]
Description=navi-landclass — PAD-US land classification API (Echo6 navi-backend, extraction #4)
After=network-online.target
Wants=network-online.target
[Service]
User=zvx
WorkingDirectory=/home/zvx/projects/repos/navi-backend
EnvironmentFile=/etc/navi-backend/navi-landclass.env
ExecStart=/home/zvx/projects/repos/navi-backend/.venv/bin/gunicorn 'services.navi_landclass.app:create_app()' --bind 127.0.0.1:8424 --workers 2
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target