mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-08-26 17:31:37 +00:00
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>
59 lines
1.7 KiB
Python
59 lines
1.7 KiB
Python
"""navi-config Flask application factory + gunicorn entry.
|
|
|
|
Gunicorn entry:
|
|
gunicorn 'services.navi_config.app:create_app()' --bind 127.0.0.1:8422 --workers 2
|
|
"""
|
|
import subprocess
|
|
import time
|
|
|
|
from flask import Flask
|
|
|
|
from . import config_route, admin
|
|
from .config_loader import reset_cache
|
|
|
|
|
|
def _git_sha():
|
|
"""Short git SHA of the working tree at startup, or 'unknown' off-repo."""
|
|
try:
|
|
sha = subprocess.check_output(
|
|
['git', 'rev-parse', '--short', 'HEAD'],
|
|
stderr=subprocess.DEVNULL,
|
|
text=True,
|
|
).strip()
|
|
return sha or 'unknown'
|
|
except Exception:
|
|
return 'unknown'
|
|
|
|
|
|
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 (each worker
|
|
# is its own process); they are indicative, not cluster-wide totals.
|
|
app.config['VERSION'] = _git_sha()
|
|
app.config['METRICS'] = {
|
|
'start_time': time.time(),
|
|
'request_count': 0,
|
|
'last_error_at': None,
|
|
}
|
|
|
|
# Fresh profile load per app instance — each gunicorn worker (and each test)
|
|
# picks up the current RECON_PROFILE / NAVI_CONFIG_PROFILES_DIR env.
|
|
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(config_route.bp)
|
|
app.register_blueprint(admin.bp)
|
|
return app
|