From e6b81db52019e6acfe037d3837eb69b811e48e0e Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 20 Apr 2026 23:35:39 +0000 Subject: [PATCH] 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 --- config/profiles/home.yaml | 31 +++++++++++++++++++++ config/profiles/minimal_pi.yaml | 31 +++++++++++++++++++++ config/profiles/regional_pi.yaml | 31 +++++++++++++++++++++ lib/api.py | 10 +++++++ lib/deployment_config.py | 46 ++++++++++++++++++++++++++++++++ 5 files changed, 149 insertions(+) create mode 100644 config/profiles/home.yaml create mode 100644 config/profiles/minimal_pi.yaml create mode 100644 config/profiles/regional_pi.yaml create mode 100644 lib/deployment_config.py diff --git a/config/profiles/home.yaml b/config/profiles/home.yaml new file mode 100644 index 0000000..e4eb3de --- /dev/null +++ b/config/profiles/home.yaml @@ -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 diff --git a/config/profiles/minimal_pi.yaml b/config/profiles/minimal_pi.yaml new file mode 100644 index 0000000..5e26b59 --- /dev/null +++ b/config/profiles/minimal_pi.yaml @@ -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 diff --git a/config/profiles/regional_pi.yaml b/config/profiles/regional_pi.yaml new file mode 100644 index 0000000..e2b469c --- /dev/null +++ b/config/profiles/regional_pi.yaml @@ -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 diff --git a/lib/api.py b/lib/api.py index 7c54fe8..297a680 100644 --- a/lib/api.py +++ b/lib/api.py @@ -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 .status import StatusDB +from .deployment_config import get_deployment_config logger = setup_logging('recon.api') @@ -1165,6 +1166,15 @@ def api_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') def api_health(): """Health check endpoint for monitoring.""" diff --git a/lib/deployment_config.py b/lib/deployment_config.py new file mode 100644 index 0000000..978b8a0 --- /dev/null +++ b/lib/deployment_config.py @@ -0,0 +1,46 @@ +""" +Deployment profile loader. + +Reads RECON_PROFILE env var (default: "home"), loads the matching YAML +from config/profiles/.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()