Pure refactor, no behavior change. Moves services/navi_geo/dem.py to
shared/dem.py (verbatim logic + env override; only docstring + location
changed) and re-points navi-geo's two imports (geo_route.py, admin.py) to
`from shared.dem import ...`.
Per extraction-8-phase-a.md §5/§13.1: navi-offroute (#18) needs the same
DEMReader, so a single source of truth in shared/ beats a third copy. Second
shared/ promotion after PR #7 round-2's shared/git_sha.py; navi-offroute will
`from shared.dem import DEMReader` directly.
Adds shared/tests/test_dem.py (dem_path default + NAVI_DEM_PMTILES override).
navi-geo behavior unchanged (test_reverse_bundle mocks geo_route._DEM, agnostic
to DEMReader's location). Full suite: 104 passed / 1 skipped (+2).
Co-authored-by: zvx-echo6 <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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>
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>