navi/backend/services/navi_contacts/app.py

52 lines
1.6 KiB
Python
Raw Permalink Normal View History

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:52:32 -06:00
"""navi-contacts Flask application factory + gunicorn entry.
Gunicorn entry:
gunicorn 'services.navi_contacts.app:create_app()' --bind 127.0.0.1:8423 --workers 2
Serves two blueprints: contacts (10 routes, auth-gated) and address_book
(2 routes, public), plus the §4.5 admin-info endpoint.
"""
import time
from flask import Flask
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
from shared.git_sha import git_short_sha
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:52:32 -06:00
from . import contacts_route, address_book_route, admin
from . import address_book as address_book_mod
def create_app():
app = Flask(__name__)
# Version + lightweight runtime metrics, read by the admin-info endpoint.
# NOTE: with gunicorn --workers 2 these counters are per-worker.
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
app.config['VERSION'] = git_short_sha()
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:52:32 -06:00
app.config['METRICS'] = {
'start_time': time.time(),
'request_count': 0,
'last_error_at': None,
}
# Fresh DB handle + address-book cache per app instance, so each gunicorn
# worker (and each test) picks up the current NAVI_CONTACTS_DB /
# NAVI_ADDRESS_BOOK_YAML env. ContactsDB auto-creates the schema on open.
contacts_route.reset_db()
address_book_mod.reset_cache()
@app.before_request
def _count_request():
app.config['METRICS']['request_count'] += 1
@app.after_request
def _track_errors(response):
if response.status_code >= 500:
app.config['METRICS']['last_error_at'] = time.strftime(
'%Y-%m-%dT%H:%M:%SZ', time.gmtime()
)
return response
app.register_blueprint(contacts_route.bp)
app.register_blueprint(address_book_route.bp)
app.register_blueprint(admin.bp)
return app