Commit graph

219 commits

Author SHA1 Message Date
777a311c2c
Merge pull request #6 from zvx-echo6/feat/directions-origin-row
fix(navi/ui): always render origin + destination rows in directions panel
2026-05-24 10:23:10 -06:00
Matt
ee49971da3 fix(navi/ui): always render origin + destination rows in directions panel
unifiedList previously gated the origin/destination rows behind routeStart/
routeEnd being set, so an unset endpoint had no input row. Always push origin
(first) and destination (last) with data:null when unset; LocationInput renders
its placeholder for value={null}, and the rows bind routeStart/routeEnd directly
(never item.data), so null is safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:21:49 +00:00
27a0695e79
Merge pull request #5 from zvx-echo6/feat/auto-mode-frontend
feat(navi/ui): Auto mode chip + selected_mode badge
2026-05-24 03:02:29 -06:00
Matt
89e8cff8ce feat(navi/ui): Auto mode chip + selected_mode badge
Relabel the auto chip to "Auto" (Zap icon) and the vehicle chip to "Drive";
render an "Auto chose <mode>" badge below the travel-mode row when the backend
returns selected_mode. Update routeMode/requestOffroute docs to list auto.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 09:00:47 +00:00
f72a05bcb7
Merge pull request #4 from zvx-echo6/feat/auto-mode-backend
feat(offroute): backend Auto mode probes [vehicle,atv,mtb,foot]
2026-05-24 02:56:23 -06:00
Matt
c1f71ccdc5 feat(offroute): backend Auto mode probes [vehicle,atv,mtb,foot]
When mode="auto", OffrouteRouter._route_auto() probes AUTO_MODE_PRIORITY
(vehicle -> atv -> mtb -> foot) and returns the first mode whose network can
serve the route, tagging the result with selected_mode. route() already errors
when a mode cannot reach an endpoint, so the first status==ok is the most
road-capable feasible mode. Adds 4 isolation tests (route monkeypatched).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 08:53:23 +00:00
ae44286be3 Merge navi-backend into backend/ subdir
Combines zvx-echo6/navi-backend into this repo as a monorepo. Backend code now lives under backend/; frontend was retroactively relocated under frontend/ via git-filter-repo. Full commit history of both sides is preserved with original authors, dates, and messages — only file paths in commits were rewritten (so SHAs differ from the originals).

Co-authored-with: navi-backend@b5079fd
2026-05-24 01:20:33 -06:00
b5079fd192 fix: include navi-offroute (8428) in navi-admin fleet list
navi-offroute (:8428, extraction #8) was missing from fleet.py's SERVICES list,
so it never appeared in /api/admin/fleet. Add the one (name, port) row — the
single source of truth that build_fleet, dependency_summaries, and the
self-info fanned_services all derive from. 8427/navi-admin is the aggregator
itself (self-poll), correctly not in its own fan-out.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:17:23 -06:00
65911d320c decouple: move /api/wiki-rewrite logic from recon to navi-places
PR-A of decouple #4-REWRITE — the LAST navi→recon coupling. navi-places now
decides "is this wiki article in the local Kiwix mirror?" in-process instead of
HTTP-calling recon's /api/wiki-rewrite. Intra-process swap, no nginx changes.
Mirrors decouple #4-READ (which moved wiki_index.db reads the same way).

- NEW services/navi_places/wiki_rewrite.py: verbatim port of recon's
  lib/wiki_rewrite.py. Only adjustments: setup_logging -> stdlib logging;
  KIWIX_BASE -> NAVI_KIWIX_BASE_URL env; KIWIX_PUBLIC_BASE -> NAVI_KIWIX_PUBLIC_BASE
  env; cache DB -> NAVI_WIKI_CACHE_DB (default /var/lib/navi-backend/wiki_cache.db,
  auto-created); + a reset() to match the place_cache/wiki_index per-worker pattern.
  No logic changes — same classify, same lazy hourly catalog refresh, same HEAD
  timeout, same status values (local/public/original), same cache semantics.
- place_detail.py: _enrich_wiki_links_via_http -> _enrich_wiki_links; calls
  wiki_rewrite.rewrite_wiki_link(tag,value) (TUPLE) and unpacks it, replacing
  the dict-returning HTTP client. Import + docstrings updated.
- app.py: wiki_rewrite.reset() per worker/test (alongside place_cache/wiki_index).
- DELETE services/navi_places/wiki_rewrite_client.py (HTTP shim dead).
- tests: the 2 wiki-rewrite tests now monkeypatch the local
  wiki_rewrite.rewrite_wiki_link (tuple) instead of the deleted client.

Recon's endpoint stays live but unused until PR-B (safe co-existence).

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:12:27 -06:00
8b59284158 decouple: read wiki_index.db directly in navi-places (drop /api/wiki-enrich HTTP)
PR-A of decouple #4-READ. navi-places now reads its own wiki_index.db directly
(NAVI_WIKI_INDEX_DB) instead of HTTP-calling recon's /api/wiki-enrich — same
pattern it already uses for place_cache.db. The 2.1GB DB was copied to
/var/lib/navi-backend/wiki_index.db out-of-band (5,061,763 rows verified).

- NEW services/navi_places/wiki_index.py: verbatim port of recon's
  lookup_wiki_index + _get_wiki_index_db, reading NAVI_WIKI_INDEX_DB, mirroring
  place_cache.py's db_path()/lazy-conn/reset() pattern. Returns the same
  {wiki_summary, wiki_population, wiki_url, wikivoyage_url} shape /api/wiki-enrich
  did, so it's a drop-in for the HTTP client.
- place_detail.py: _enrich_with_wiki_via_http -> _enrich_with_wiki_index; call
  wiki_index.lookup() instead of wiki_client.enrich_via_recon(); docstrings.
- app.py: wiki_index.reset() per worker/test (alongside place_cache.reset_cache()).
- admin.py: drop the recon-wiki-enrich dependency probe; add NAVI_WIKI_INDEX_DB
  env + a read-only filesystem entry. (recon-wiki-rewrite probe kept — separate
  decouple.)
- DELETE wiki_client.py (fully replaced).
- test_place.py: convert the wiki test from a monkeypatched HTTP client to a
  hermetic tmp wiki_index.db.

Internal localhost migration — no nginx/edge involvement. recon's /api/wiki-enrich
stays live until PR-B (deploy PR-A first so nothing calls the route after removal).

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 19:17:53 -06:00
d0e357a3bb decouple: add scripts/overture_import.py (relocating from recon)
PR-A of the overture-import relocation. The Overture Places ETL moves from
recon (where it produced data nothing in recon consumes) to navi-backend (the
side that owns the consumer, navi-places). Additive: recon's copy stays live
until PR-B; this just establishes the navi-side copy + deps + docs.

- scripts/overture_import.py: verbatim port of recon's script (recon master
  879df84). The ONLY non-verbatim change is the line-8 docstring usage hint,
  swapped from `/opt/recon` + venv to the navi-backend path + .venv.
- pyproject.toml: add `duckdb>=1.5` (recon runs 1.5.2; psycopg2-binary already
  present). It's the only new dep.
- scripts/README.md: document the manual-only trigger + invocation.

Source release is pinned in-code: OVERTURE_RELEASE = '2026-04-15.0'.
No tests (ETL; none on recon either). Per cleanup #29, the script has no lib/
imports — fully self-contained (stdlib + duckdb + psycopg2).

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:49:32 -06:00
05b614a22f decouple: add /api/auth/whoami to navi-admin (preparing recon migration)
PR-A of the 2-PR whoami migration. Net-new, additive endpoint in navi-admin
matching recon's existing handler shape exactly. Recon's handler stays live in
this PR; once nginx routes /api/auth/whoami to :8427 (out-of-band) and recon's
handler is removed (PR-B), navi-admin is the sole owner.

- New services/navi_admin/auth_route.py with its own blueprint (navi_admin_auth):
  GET /api/auth/whoami reads X-Authentik-Username, returns {authenticated,
  username}. NOT @require_auth — it's the "am I logged in?" check, must answer
  the unauthenticated case (mirrors recon).
- app.py: register the new blueprint (2 lines).
- test_auth.py: header-present + header-absent cases.

Kept in its own blueprint/file so admin_route.py's "all routes @require_auth"
invariant stays true. recon and nginx untouched (additive only).

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:23:55 -06:00
767818b88e decouple: drop navi-admin → recon /api/health coupling
Per Matt's directive that navi-* should not call any /api/* on recon.
navi-admin was the only navi service doing so (polling recon's /api/health
and surfacing it in /api/admin/recon/info + the /api/admin/fleet fan-out).
navi-admin is now the navi-only fleet view; recon has its own dashboard for
recon-pipeline health.

- admin_route.py: delete the /api/admin/recon/info handler; drop the recon
  config entry + RECON_HEALTH_URL/RECON_REPO_PATH env entries from
  /api/admin/navi-admin/info; refresh docstrings.
- fleet.py: remove recon constants, recon_health_url/recon_repo_path/
  recon_git_sha/wrap_recon_health, the now-unused shared.git_sha import, and
  the recon arms in build_fleet + dependency_summaries. /api/admin/fleet now
  reports only the 6 navi-* services.
- tests: drop the 2 recon/info tests + recon scaffolding; strip recon
  assertions from fleet + self-info tests. 12 relevant tests pass (10 admin
  + 2 git_sha).

shared/git_sha.py KEPT unchanged — it's a generic git_short_sha(path) helper
used by every service's create_app(), not recon-specific.

RECON_HEALTH_URL + RECON_REPO_PATH in /etc/navi-backend/navi-admin.env are now
dead — flagged for out-of-band post-merge cleanup.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:14:09 -06:00
ae82cee46a 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>
2026-05-22 23:30:43 -06:00
4180d3513c shared: promote dem.py to shared/ (prep for navi-offroute) (#9)
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>
2026-05-22 22:42:47 -06:00
564834a2a6 navi-geo: revert netsyms.health() wiring — was a cold-start footgun (#8)
PR #6 round-1 fixup #3 wired netsyms.health() (COUNT + DISTINCT on
35 GB) into _netsyms_fs_entry, adding >3s latency to cold admin-info
calls. navi-admin's fleet fan-out (3s timeout) caught it after #7
deploy. Reverting to the cheap _file_entry shape; deleting health()
per the no-dead-code rule (the original fallback option).

Co-authored-by: zvx-echo6 <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:38:40 -06:00
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
15cad0abf4 Add navi-geo service (extraction #6) (#6)
* Add navi-geo service (extraction #6)

Faithful port of recon's geocode/reverse family to a new :8426 service:
  GET /api/geocode?q=&limit=&lat=&lon=&zoom=   Photon-first ranked search
  GET /api/reverse?lat=&lon=                   reverse geocode (Photon)
  GET /api/reverse/<lat>/<lon>                 reverse enrichment bundle (Central)

Ported modules: geocode.py (engine), netsyms.py (address SQLite), dem.py
(planet-DEM reader), address_book.py (reader copy), and the three handlers +
four bundle helpers from netsyms_api.py. All three routes public, behaviour-
identical to recon.

Behaviour-changing edges (both pre-decided in Phase A/B, called out in the PR):
- landclass: in-process call replaced with HTTP GET to navi-landclass :8424,
  reading .summary (the same most-specific unit-name string). First navi→navi
  edge after landclass itself.
- hardcoded paths/URLs → env vars (PHOTON_URL, NAVI_NETSYMS_DB,
  NAVI_TIMEZONE_DB, NAVI_DEM_PMTILES, NAVI_ADDRESS_BOOK_YAML,
  NAVI_LANDCLASS_URL); rerank trace log opt-in (NAVI_GEO_RERANK_TRACE_LOG,
  default off — recon always wrote /tmp).

No secrets in this service: PADUS_DB_* disappears because landclass is HTTP-
delegated (Phase A §10). Address book uses Option B (shared-file read), the
same pattern navi-contacts already uses.

Bundle 9-key contract preserved exactly (name/city/county/state/country/
postal_code/timezone/landclass/elevation_m), same null-on-component-failure
semantics, same in-memory TTLCache(10_000, 86_400) per worker.

Tests: 28 passing, 1 skipped (real timezone DB, off-box). Ported the 9 recon
reverse-bundle tests + added the HTTP-landclass coupling tests + hermetic
geocode reranker/intent-classifier tests (recon's geocode_test.py was a live
smoke test). Adds usaddress/rapidfuzz/cachetools/shapely/numpy/Pillow/pmtiles
to deps.

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

* PR #6 review fixes

1. Rename geocode._setup_trace_logger → setup_trace_logger (public hook)
2. Hoist `import requests as http_requests` to module level in geo_route.py
3. Wire netsyms.health() into admin.py (enriches the netsyms filesystem entry
   with row_count/file_size_bytes/indexed_countries; no shared-builder change)
4. Fix misleading LANDCLASS_TIMEOUT_S comment (recon had no timeout)

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 20:29:22 -06:00
34df49a9cd Add navi-places service (extraction #5) (#5)
New services/navi_places/ on :8425 — the heaviest extraction. Ports recon's
/api/place family with the two wiki dependencies decoupled to HTTP.

Routes (public, mirroring recon):
  GET /api/place/<osm_type>/<int:osm_id>   (Nominatim -> Overpass fallback + enrich)
  GET /api/place/wikidata/<wikidata_id>     (Wikidata entity)
  -> 200 / 400 / 404 / 502, same response shapes as recon.

Enrichment chain (recon order): Overture (PostGIS) -> Google Places -> wiki
rewrite -> wiki index. The two wiki paths are now HTTP to recon (the 2.1 GB
wiki_index.db and Kiwix/wiki_cache stay in recon — see [[reference-echo6-edge-topology]]):
  - wiki_client.enrich_via_recon  -> recon /api/wiki-enrich  (PR #8)  [has_kiwix_wiki]
  - wiki_rewrite_client.rewrite_via_recon -> recon /api/wiki-rewrite (PR #9) [has_wiki_rewriting]
    (per-tag loop over the <=4 wiki extratags, mirroring recon's _enrich_wiki_links)
Both clients degrade gracefully (None / status 'original') on error/timeout.

Data ownership (see [[feedback-navi-backend-data-ownership]]):
  - place_cache.db migrates to /var/lib/navi-backend/place_cache.db (env
    NAVI_PLACE_CACHE_DB). place_cache.py auto-creates the FULL schema on first
    open — place_cache (incl. the google_place_id/google_data/google_fetched_at
    columns recon added by migration) + google_api_calls — so a fresh DB serves
    both cache_put and the Google daily-cap/cache. WAL, shared module conn.
  - overture stays in external PG (OVERTURE_DB_* env), verbatim port of recon's
    pool (1,3) + _pool_failed latch, with reset_pool()+probe_db() added.
  - wiki_index.db / Kiwix stay in recon, reached via the two HTTP endpoints.

Modules: place_cache.py, overture.py (verbatim+probe), google_places.py
(daily cap via env GOOGLE_PLACES_DAILY_CAP; DB via shared place_cache conn),
wiki_client.py + wiki_rewrite_client.py (HTTP, RECON_BASE_URL default
http://127.0.0.1:8420), osm_categories.py (vendored for humanize_category),
place_detail.py (orchestrator), config.py (feature flags from the vendored
profile via NAVI_PROFILES_DIR), place_route.py, admin.py, app.py.

Feature gates read from the vendored profile (config.py), matching recon:
has_overture_enrichment / has_google_places_enrichment / has_kiwix_wiki /
has_wiki_rewriting — flag off => that enricher is skipped entirely.

admin.py (§4.5): 2 secrets masked (OVERTURE_DB_PASSWORD, GOOGLE_PLACES_API_KEY);
3 dependency probes — overture-postgis (SELECT 1), recon-wiki-enrich and
recon-wiki-rewrite (GET with no params, expect HTTP 400 = route alive).

Deploy: systemd unit (:8425) + nginx snippet (^~ /api/place, no trailing slash,
no proxy_cache; public, no Caddy edit — TIER 2 already through nginx since #2).

Tests (13; recon had zero for this module): validation (400), cache hit (no
upstream), nominatim hit, nominatim-miss->overpass fallback, both-fail 502,
not-found 404, wikidata happy + invalid, overture gated-off (no PG call),
wiki-rewrite-via-http local hit + original pass-through, wiki-enrich-via-http
field merge. Full suite 59. See ../recon_refactor/extraction-5-phase-a.md,
-wiki-enrich-investigation.md, -wiki-rewrite-investigation.md, and PRs #8/#9.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 17:00:06 -06:00
0af05115e5 Gate Traffic toggle on auth.authenticated (#3)
Root cause: /api/traffic is on Caddy's @authed_api, so when logged out
MapLibre's raster tile fetches receive a 302 to the Authentik login (HTML),
which it can't decode as an image and retries on every map move — console spam
and a stuck-feeling Traffic toggle.

Fix (frontend-only; /api/traffic stays auth-gated in Caddy):
- LayerControl: the Traffic toggle is always rendered but disabled (greyed,
  "Sign in to enable traffic" tooltip) until auth has loaded AND the user is
  authenticated — mirroring Panel.jsx's contacts gating. The add-traffic apply
  effect now also requires auth.authenticated (and lists it in deps), and the
  mount init only restores saved traffic=true when authenticated.
- Teardown on session -> anonymous: an effect flips traffic:false once auth has
  loaded and the user is not authenticated, which drives the apply effect to
  removeTrafficLayer (no further tile requests).
- MapView: the style-reload re-apply (which re-adds layers from localStorage on
  theme/style changes) now also checks auth.authenticated for traffic, so it
  can't re-add the source for an anonymous session — the second add path that
  would otherwise reintroduce the 302 retry loop.
- localStorage hydration: LayerControl now subscribes via useConfig() and its
  init effect depends on [config] instead of [], so saved layer prefs hydrate
  correctly once /api/config resolves (previously, mounting before config
  loaded left toggles stuck off and never re-initialized).

Shown-but-disabled (not hidden) so logged-in users see no flicker on reload
during the brief pre-whoami window.

Tests: the navi repo has no test infrastructure (no vitest/jest); bootstrapping
is out of scope. Follow-up: seed a vitest + RTL test asserting the Traffic
toggle is disabled when !auth.authenticated.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:40:49 -06:00
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
76a88947ea Merge pull request #3 from zvx-echo6/extraction-3-navi-contacts
Add navi-contacts service (extraction #3)
2026-05-22 11:06:50 -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
4f1d481a20 Merge pull request #2 from zvx-echo6/extraction-2-panel-auth-wiring
Read auth URLs from /api/config (extraction #2 PR-C)
2026-05-22 09:44:30 -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
9d7be4bb0d Merge pull request #2 from zvx-echo6/extraction-2-profiles-in-repo
Add deployment profiles to navi-backend (decouple from /opt/recon)
2026-05-22 09:23:26 -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
f33d5f3427 navi: read auth login/logout URLs from /api/config (extraction #2 PR-C)
Panel.jsx now reads the login/logout URLs from the deployment config
(cfg.auth.login_url / cfg.auth.logout_url) instead of hardcoding them, with
the current home-profile literals kept as fallback. This completes the
extraction #2 trio:
  - PR-A (recon #6):   adds the `auth` block to the deployment profile YAMLs
  - PR-B (navi-backend #1): navi-config service serving /api/config on :8422
  - PR-C (this):       frontend consumes auth.* from /api/config

Behavior is unchanged in every case: the fallback literals are used if the
backend doesn't yet serve `auth` (older recon) or when FALLBACK_CONFIG is in
effect (offline / config API unreachable). useConfig() returns the cached
deployment config (null until loaded); the optional-chaining + ?? fallback
keeps clicks working before config loads.

Also adds the same `auth` block to src/config.js FALLBACK_CONFIG so offline
mode has working URLs.

No test added: navi has no test infrastructure today; flagged with a
TODO(navi) at the change site for when test infra lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:09:24 -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
067142373d fix: apply theme overlay config to contour layers
addContours() was using hardcoded black/white colors instead of
theme-specific overlay config. Now uses getOverlayConfig(themeId,
"contours") like other overlay layers (hillshade, publicLands, etc).

Also updates cyberpunk contours from dark purple to cyan (#1a5566 →
#3a99aa) to contrast with purple roads.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-21 21:42:41 +00:00
05ec80d2ba Merge PR #1: public-lands: filter "Unknown <agency>" PAD-US label artifacts
public-lands: filter "Unknown <agency>" PAD-US label artifacts
2026-05-20 11:24:32 -06:00
1308729ba7 public-lands: filter "Unknown <agency>" PAD-US label artifacts
PAD-US v4.0 ships many small sub-polygons whose unit_nm is literally
"Unknown <state agency>" (e.g. "Unknown Idaho Department of Lands"). The
PMTiles build maps unit_nm -> feature name, so the label layer rendered
these spurious labels next to/over the legitimate umbrella label.

Filter them out at the PUBLIC_LANDS_LABEL (symbol) layer only via a name
prefix test. Fill and line layers are untouched — the polygon geometry
still renders, just without the bogus label.

Evidence: /api/landclass at (42.619853, -114.462106) returns a 12-acre
"Unknown Idaho Department of Lands" overlapping the 1.98M-acre
"Idaho Department of Lands" umbrella.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:03:35 +00:00
b78334f520 Merge feature/offroute-ui: directions panel, multi-stop routing, drag reorder, radial menu integration 2026-05-09 15:45:32 +00:00
c9bca8ffe4 feat: drag-and-drop stop reordering and fix radial add-stop
- addIntermediateStop() now accepts optional place parameter
- Radial menu add-stop wedge uses addIntermediateStop with coordinates
- Replaced up/down chevron buttons with @dnd-kit drag-and-drop
- All rows (origin, stops, destination) can be reordered by dragging
- GripVertical drag handle on left of each row
- On drag end: first item → origin, last → destination, middle → stops

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-09 15:40:12 +00:00
a6ae996b63 fix: swap button layout and add stop reorder buttons
- Swap button now inline on origin row (not absolute positioned)
- Swap button no longer overlaps intermediate stop controls
- Added up/down chevron buttons on each intermediate stop row
- Reordering stops triggers route recalculation
- Destination row has spacer to align with origin row

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-09 15:14:08 +00:00
317bc15087 fix: separate stops[] from routeStart/routeEnd for multi-stop routing
- stops[] now contains ONLY intermediate waypoints
- routeStart and routeEnd are separate sources of truth
- addIntermediateStop() adds empty placeholder to stops[]
- updateStop() and removeStop() manage intermediate waypoints
- computeRoute() chains sequential 2-point routes for multi-stop
- DirectionsPanel renders: origin -> stops.map() -> destination
- Each intermediate stop has remove button (Trash2 icon)

Test scenarios verified:
- Origin + destination routes normally (no stops involved)
- Add Stop creates empty input between origin and destination
- Setting intermediate location triggers route recalculation
- Multiple stops can be added sequentially
- Removing a stop recalculates route without it
- Clear all returns to empty state

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-09 14:59:31 +00:00
cf77e43150 feat: wire up radial menu directions and multi-stop add button
- Radial menu "From here" now sets origin and opens directions panel
- Radial menu "To here" now sets destination, opens directions panel,
  and uses GPS as origin fallback when available
- DirectionsPanel "Add stop" button now creates intermediate stops
- Stops array initialized from routeStart/routeEnd when adding stops

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-09 08:23:38 +00:00
c714c75b4b feat: wilderness maneuvers, pick-from-map, distance formatting, place card panel
- Wilderness maneuvers render with compass arrows and cardinal directions
- Network maneuvers prefixed with transport mode (Drive/Walk/Ride)
- Distances under 1 mile show feet with commas
- Pick-from-map mode replaces auto-fill-on-focus (crosshair + toast)
- ESC cancels pick mode
- Place card slides out right during active routing
- Removed debug toasts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-09 06:09:14 +00:00
a84b4fea1d feat: improve directions panel with route legend and place card below
- Add route legend showing wilderness (dashed orange) vs road (solid blue)
- Show place card below directions panel when clicking map during routing
- Clean up error messages to be user-friendly (no offroute text)
- Legend only appears when route has wilderness segments

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-09 03:37:05 +00:00
15b429d5e9 fix: preserve click coordinates for wilderness routing
When clicking on a labeled feature (e.g., "Monument Peak"), the code
was using the feature's canonical coordinates instead of the actual
click coordinates. This caused wilderness clicks to snap to named
places that might be on roads, bypassing wilderness routing.

Fix: Always use click coordinates (e.lngLat) for routing purposes.
Feature coordinates are only used for display/detail fetching.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-08 23:08:38 +00:00
55cca4cfa2 feat: add directions panel with editable origin/destination inputs
New UX for Get Directions:
- DirectionsPanel component with two stacked input fields
- LocationInput component with autocomplete, coordinate parsing
- Swap button to flip origin/destination
- Travel mode selector (Drive default, Foot, MTB, ATV, 4x4)
- Boundary selector (only visible for non-Drive modes)
- Map click fills active input field with crosshair cursor
- Auto-route when both endpoints are filled
- X button closes directions and returns to search view

Store changes:
- directionsMode state for panel switching
- activeDirectionsField for map click targeting
- startDirections now enters directions mode with destination pre-filled

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-08 22:44:45 +00:00
736366bd47 feat: unified routing with Drive mode default and Add stop wedge
- Add Drive (auto) as default route mode, first in travel modes list
- Hide boundary mode selector when Drive mode is active
- Restore Add stop radial menu wedge with stops system integration
- Unify routing through single computeRoute() function in store
- Add coordinate parsing to SearchBar for direct lat/lon input
- Bridge stops system with routeStart/routeEnd for seamless UX
- Support 3+ stops with Valhalla optimization

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-08 21:59:10 +00:00
d1db104c19 feat: unified routing UI with wilderness + network segments
- Single routing system (removed duplicate Valhalla-only flow)
- Unified radial menu: From here, To here, Clear, Save, Measure
- Removed "Offroute" section from panel (single directions display)
- Better error messages without technical "Offroute" prefix
- ManeuverList shows wilderness + network breakdown
- PlaceCard integration for previews

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-08 19:03:44 +00:00
cf55fb1d7f docs: add traffic-intelligent routing and Idaho 511 planned features
- Append section 11 (On-Network Traffic Intelligence) to OFFROUTE-ARCHITECTURE.md
- Create navi-feature-ideas.md with planned features:
  - Traffic-aware Valhalla routing via TomTom tiles
  - Idaho 511 incident feed integration
  - ADS-B/AIS tracking
  - TAK Server + EUD integration
  - Native iOS app

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-08 07:02:00 +00:00
488738affd docs: add OFFROUTE effort-based routing architecture
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-07 23:22:48 +00:00
0d0a21751b cleanup: remove dead contour-test code and stale fallback config
- Remove contours-test.pmtiles and contours-test-10ft.pmtiles references
  (files deleted, feature flags disabled)
- Update fallback tileset URL from na.pmtiles to planet/current.pmtiles
- Remove has_contours_test and has_contours_test_10ft from fallback config
- Delete 46 .bak* files from src/

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-07 21:56:14 +00:00
e0fb484ec6 fix: use Noto Sans Medium for contour labels (Bold not in protomaps assets) 2026-05-07 14:18:18 +00:00
9c5191f446 fix: contour overlay with pmtiles fork, absolute URL, extended zoom range
- Switch to @acalcutt/maplibre-contour-pmtiles for PMTiles support
- Use absolute URL for DemSource so Web Worker can resolve path
- Extend contour thresholds from z3-z15 for full zoom coverage
- Improve line styling with zoom-dependent width
- Improve label styling with bold font and better halo

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-07 03:58:46 +00:00