Commit graph

7 commits

Author SHA1 Message Date
d08834451f 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>
2026-05-22 12:08:58 -06:00
Matt Johnson
bba59e9f17 Add navi-contacts service (extraction #3)
New services/navi_contacts/ on :8423 — two blueprints, behavior-identical
ports of recon's contacts + address_book code.

Routes:
  contacts (10, all @require_auth, per-user via X-Authentik-Username):
    GET/POST /api/contacts; GET /api/contacts/nearby; GET /api/contacts/deleted;
    GET/PATCH/DELETE /api/contacts/<id>;
    POST /api/contacts/<id>/restore; POST /api/contacts/<id>/restore-as;
    DELETE /api/contacts/<id>/purge
  address_book (2, public):
    GET /api/address_book/lookup?q= ; GET /api/address_book/list

Data ownership (per Matt's rule — DBs live in navi-backend territory,
auto-create on first run; only massive tilesets stay external):
  - contacts.db: env NAVI_CONTACTS_DB (default /var/lib/navi-backend/contacts.db).
    ContactsDB auto-creates the schema (table + 5 indexes incl. the partial-
    unique Home/Work index) on first open via CREATE ... IF NOT EXISTS — this
    is recon's own behavior, ported verbatim. WAL + busy_timeout=5000 preserved.
  - address_book.yaml: vendored into config/address_book.yaml (read-only, like
    the deployment profiles in extraction #2); path via NAVI_ADDRESS_BOOK_YAML.

Tests (28 new; recon had none for contacts): full ContactsDB CRUD, soft-delete/
restore/restore-as/purge, Home/Work 409 (create + restore conflict), nearby
proximity, search/category filter, per-user partitioning, auth-required, and
DB auto-create; plus address_book ported from recon's test (exact/partial/
case-insensitive/alias/miss/empty/list/hot-reload/missing-file). Full suite 38.

Timestamp fix (diverges from recon on purpose): restore_as builds updated_at
with Python's strftime. recon uses the bare '%Y-%m-%dT%H:%M:%fZ' there — but
Python's %f is microseconds-only (no seconds), so that yields malformed ISO
strings like "...T15:30:123456Z". recon's own update()/soft_delete() use the
correct '%Y-%m-%dT%H:%M:%S.%fZ'. This port uses the correct format in all three
places and adds a regression guard (strptime) in test_restore_as_relabels.
This is a PRE-EXISTING recon bug; we fix it here. The recon-side restore_as
retires with extraction #7 (Jinja /nav-i + /deleted-contacts removal), so the
recon refactor doesn't need to touch it.

Also: shared/auth.py require_auth now sets request.user_id (recon's contract —
the contacts routes read it). Backward-compatible: navi-traffic/navi-config
admin endpoints don't use it.

Deploy artifacts: systemd unit (:8423) + nginx snippet with two ^~ blocks.
NOTE the nginx prefixes are `^~ /api/contacts` and `^~ /api/address_book`
WITHOUT a trailing slash, so the bare `/api/contacts` (list/create) is matched
too — a trailing-slash prefix would miss it and fall through to recon.

See ../recon_refactor/extraction-3-phase-a.md for the route/schema/ownership
analysis (which also corrects the handoff: contacts is /opt/recon/data/
contacts.db, NOT /mnt/nav/navi.db).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:59:37 -06:00
8a9b5d3337 Merge pull request #1 from zvx-echo6/extraction-2-navi-config
Add navi-config service (extraction #2)
2026-05-22 09:28:51 -06:00
Matt Johnson
f0da1554f0 Add deployment profiles to navi-backend (extraction #2 — decouple from /opt/recon)
Vendors the three deployment profile YAMLs (home, minimal_pi, regional_pi)
into navi-backend's own repo at config/profiles/. Copied verbatim from
/opt/recon/config/profiles/ post-PR-A, so home.yaml carries the auth block and
the two Pi profiles carry the TODO(matt) logout-host note.

Architectural reason: services in this monorepo should depend on each other
only through their HTTP APIs, never each other's filesystem. navi-config
currently defaults to reading /opt/recon/config/profiles/ — a cross-service
filesystem coupling. With the profiles vendored here, navi-config will be
pointed at this repo's copy via NAVI_CONFIG_PROFILES_DIR in
/etc/navi-backend/navi-config.env at deploy time, so it no longer reaches into
recon's tree.

recon keeps its own /opt/recon/config/profiles/ for its internal use; the two
copies are independent for now. Reconciling them — ideally by having recon
HTTP-fetch /api/config from navi-config rather than re-parsing YAML — is a
tracked follow-up, out of scope for extraction #2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:23:12 -06:00
Matt Johnson
565e774864 Add navi-config service (extraction #2 PR-B)
New services/navi_config/ on :8422, mirroring recon's /api/config contract:

  - config_route.py: GET /api/config -> jsonify(get_deployment_config())
    with Cache-Control: public, max-age=300 (byte-for-byte recon's response).
  - config_loader.py: faithful port of recon lib/deployment_config.py. Reads
    RECON_PROFILE (default "home") and NAVI_CONFIG_PROFILES_DIR (default
    /opt/recon/config/profiles, so it serves the SAME files recon does during
    cutover). yaml.safe_load, module-level cache. Lazy load (vs recon's eager
    import-time load) so the module imports cleanly off-VM and a missing
    profile surfaces as HTTP 500 at request time rather than a failed import.
  - admin.py: /api/admin/navi-config/info per handoff §4.5, require_auth gated.
    env values (NAVI_CONFIG_PROFILES_DIR, RECON_PROFILE) are non-secret paths/
    names, shown as-is (no mask_key); dependencies=[]; filesystem reports the
    active profile path + exists/readable.
  - app.py: create_app() factory mirroring navi_traffic, same metrics wiring;
    resets the loader cache per instance so each worker/test reloads fresh.

Deploy artifacts: systemd unit (:8422) and an nginx snippet using
`location ^~ /api/config` (the ^~ convention from extraction #1 so the asset
.png/.css regex can't shadow it). No proxy_cache zone — the response is
already cached in-process and via Cache-Control: max-age=300; emits a literal
X-Cache-Status: BYPASS for parity with navi-traffic.

Adds PyYAML>=6 to deps. Tests (services/navi_config/tests): 200 + parsed dict,
Cache-Control header, RECON_PROFILE override, default=home, missing profile=500.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 08:25:06 -06:00
Matt Johnson
ad097432fc nginx: use ^~ prefix on /api/traffic/ to beat .png regex catch-all (fixes latent 404 bug)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 00:07:49 -06:00
Matt Johnson
dfd1d38405 Initial scaffold: navi-backend + navi-traffic (extraction #1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:26:50 -06:00