navi/backend/README.md
malice d644741c75 Add navi-admin service (extraction #7) (#7)
* Add navi-admin service (extraction #7)

Net-new fleet admin aggregator on :8427 — no port from recon (recon has no
/api/admin route; Phase A §3). Three @require_auth routes:
  GET /api/admin/fleet            fan-out to all 6 navi-* /api/admin/<svc>/info
                                  + recon /api/health, merged; never 5xx
                                  (failures land in errors[])
  GET /api/admin/recon/info       recon /api/health wrapped in the info shape
  GET /api/admin/navi-admin/info  self-describe

Fan-out forwards the caller's X-Authentik-Username so the @require_auth
upstreams accept it; per-service admin endpoints stay localhost-only (this is
the single edge-exposed admin surface). Service discovery: hardcoded list in
fleet.py (Option B). No secrets, no DB.

Deploy artifacts (NOT applied here): navi-admin.env.example, systemd unit,
nginx ^~ /api/admin snippet, and deploy/caddy notes for the @authed_api edit
(first Caddy change since #2).

12 hermetic tests (fleet happy-path, per-service timeout/500 → errors[],
auth-header forwarding, recon-down degraded-not-5xx, self-info no-secrets,
auth-required). Full monorepo suite green.

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

* PR #7 review fixes

1. Symmetric degraded-entry handling in fleet.build_fleet — every
   probed service now appears in `services` with a uniform degraded
   dict on failure (matches recon's existing pattern), AND in errors[].
   Operators see "everything I tried + which broke" consistently.
2. Catch ValueError specifically in _get_json — non-JSON 200 responses
   now surface as `error: 'invalid JSON'` instead of opaque 'ValueError'.

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

* PR #7 review fixes (round 2)

1. Unified degraded shape: wrap_recon_health calls _degraded_entry on
   failure — no more runtime.status vs runtime.recon_status asymmetry.
   Every probed service has the same shape on failure
   (runtime.status == 'unreachable'). recon-specific runtime fields
   (recon_status/recon_uptime/pipeline) remain only on the success path.
2. DRY'd git short-SHA helper into shared/git_sha.py — was duplicated in
   7 service app.py files + fleet.recon_git_sha. One implementation,
   one place to fix when behavior changes. Adds shared/tests (testpaths
   now includes "shared").

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 21:21:00 -06:00

3.7 KiB

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.

Run (local) — navi-admin (extraction #7)

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

# No secrets — read-only HTTP fan-out over localhost (see
# deploy/env/navi-admin.env.example). Owns no DB.
.venv/bin/gunicorn 'services.navi_admin.app:create_app()' \
    --bind 127.0.0.1:8427 --workers 2

navi-admin is the fleet admin front door: /api/admin/fleet fans out to every navi-* service's localhost /api/admin/<svc>/info + recon's /api/health (merged, never 5xx — failures land in errors[]); /api/admin/recon/info wraps recon's health into the uniform shape; /api/admin/navi-admin/info self-describes. All @require_auth. The per-service admin endpoints stay localhost-only; this is the single edge-exposed admin surface (needs a Caddy @authed_api edit — see deploy/caddy/navi-admin.caddy.notes.md).

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.