feat(navi): deployment profiles + /api/config endpoint

Add profile-driven config infrastructure:
- config/profiles/{home,regional_pi,minimal_pi}.yaml templates
- lib/deployment_config.py loader (reads RECON_PROFILE env var)
- GET /api/config returns active profile as JSON (5min cache)

Frontend reads this on startup to determine tile source, defaults,
and feature flags. No existing behavior changed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-04-20 23:35:39 +00:00
commit e6b81db520
5 changed files with 149 additions and 0 deletions

31
config/profiles/home.yaml Normal file
View file

@ -0,0 +1,31 @@
# Deployment profile: Home (VM 1130)
# Active on the main Echo6 deployment. Full stack with planet-scale NA tiles.
# Override via RECON_PROFILE env var in /etc/systemd/system/recon.service
profile: home
region_name: "North America"
tileset:
url: "/tiles/na.pmtiles"
bounds: [-168, 14, -52, 72]
max_zoom: 15
attribution: "Protomaps © OSM"
services:
geocode: "/api/geocode"
reverse: "/api/reverse"
address_book: "/api/address_book"
valhalla: "/valhalla"
features:
has_nominatim_details: false
has_kiwix_wiki: false
has_hillshade: false
has_3d_terrain: false
has_traffic_overlay: false
has_landclass: false
has_address_book_write: false
defaults:
center: [42.5736, -114.6066]
zoom: 10

View file

@ -0,0 +1,31 @@
# Deployment profile: Minimal Pi (single-state pocket deployment)
# Template for the lightest possible field kit — Idaho only.
# Override via RECON_PROFILE env var.
profile: minimal_pi
region_name: "Idaho"
tileset:
url: "/tiles/idaho.pmtiles"
bounds: [-117.5, 42.0, -111.0, 49.0]
max_zoom: 15
attribution: "Protomaps © OSM"
services:
geocode: "/api/geocode"
reverse: "/api/reverse"
address_book: "/api/address_book"
valhalla: "/valhalla"
features:
has_nominatim_details: false
has_kiwix_wiki: false
has_hillshade: false
has_3d_terrain: false
has_traffic_overlay: false
has_landclass: false
has_address_book_write: true
defaults:
center: [44.0, -114.0]
zoom: 7

View file

@ -0,0 +1,31 @@
# Deployment profile: Regional Pi (multi-state field kit)
# Template for a Raspberry Pi covering Idaho + surrounding states.
# Override via RECON_PROFILE env var.
profile: regional_pi
region_name: "Idaho + Neighbors"
tileset:
url: "/tiles/regional.pmtiles"
bounds: [-125, 40, -104, 49]
max_zoom: 15
attribution: "Protomaps © OSM"
services:
geocode: "/api/geocode"
reverse: "/api/reverse"
address_book: "/api/address_book"
valhalla: "/valhalla"
features:
has_nominatim_details: false
has_kiwix_wiki: false
has_hillshade: true
has_3d_terrain: false
has_traffic_overlay: false
has_landclass: true
has_address_book_write: true
defaults:
center: [44.0, -114.0]
zoom: 7

View file

@ -24,6 +24,7 @@ from werkzeug.utils import secure_filename
from .utils import get_config, content_hash, clean_filename_to_title, derive_source_and_category, generate_download_url, setup_logging from .utils import get_config, content_hash, clean_filename_to_title, derive_source_and_category, generate_download_url, setup_logging
from .status import StatusDB from .status import StatusDB
from .deployment_config import get_deployment_config
logger = setup_logging('recon.api') logger = setup_logging('recon.api')
@ -1165,6 +1166,15 @@ def api_knowledge_stats():
return jsonify(_cache['knowledge_stats']) return jsonify(_cache['knowledge_stats'])
@app.route('/api/config')
def api_config():
"""Return deployment profile config for frontend consumption."""
config = get_deployment_config()
resp = jsonify(config)
resp.headers['Cache-Control'] = 'public, max-age=300'
return resp
@app.route('/api/health') @app.route('/api/health')
def api_health(): def api_health():
"""Health check endpoint for monitoring.""" """Health check endpoint for monitoring."""

46
lib/deployment_config.py Normal file
View file

@ -0,0 +1,46 @@
"""
Deployment profile loader.
Reads RECON_PROFILE env var (default: "home"), loads the matching YAML
from config/profiles/<profile>.yaml, and caches the parsed dict in memory.
Provides get_deployment_config() for use by the /api/config endpoint.
"""
import os
import yaml
from .utils import setup_logging
logger = setup_logging('recon.deployment_config')
_config_cache = None
def load_deployment_config():
"""Load and cache the deployment profile. Called once at import time."""
global _config_cache
profile = os.environ.get('RECON_PROFILE', 'home')
config_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'config', 'profiles')
config_path = os.path.join(config_dir, f'{profile}.yaml')
if not os.path.exists(config_path):
raise FileNotFoundError(
f"Deployment profile '{profile}' not found at {config_path}. "
f"Available profiles: {', '.join(f.replace('.yaml','') for f in os.listdir(config_dir) if f.endswith('.yaml'))}"
)
with open(config_path, 'r') as f:
_config_cache = yaml.safe_load(f)
logger.info(f"Loaded deployment profile: {profile} ({_config_cache.get('region_name', 'unknown')})")
return _config_cache
def get_deployment_config():
"""Return the cached deployment config dict."""
if _config_cache is None:
load_deployment_config()
return _config_cache
# Load on import so startup fails fast if profile is missing
load_deployment_config()