cfd94f905a
- Restrict CORS to localhost origins (was allow_origins=[*])
- Require valid JWT on WebSocket /ws (anonymous no longer gets admin view)
- Fix path traversal in delete_cell(): resolve() + parent check
- Validate cell_id format in /charts/download-noaa/{cell_id}
- Exclude charts/ and Cartas/ from git (keep US1GC09M world overview)
- Add NOAA ENC Portal external link in charts catalog tab
- Untrack __pycache__/, .db, .claude/ session files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
106 lines
3.7 KiB
Python
106 lines
3.7 KiB
Python
"""
|
|
Runtime settings store. Persists the settings the user changes via the
|
|
SETTINGS modal to a sidecar JSON file so they survive restarts.
|
|
|
|
The single mutable `SETTINGS` dict is the source of truth at runtime —
|
|
import it, don't copy it. Endpoints should call `apply_patch()` (which
|
|
also writes the file) rather than mutating directly.
|
|
"""
|
|
from __future__ import annotations
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from threading import Lock
|
|
|
|
_PATH = Path(__file__).parent.parent / "settings.json"
|
|
_LOCK = Lock()
|
|
|
|
DEFAULTS: dict = {
|
|
# AIS source
|
|
"ais_source": os.getenv("AIS_SOURCE", "SIMULATOR"),
|
|
"ais_serial_port": "",
|
|
"ais_baud": 38400,
|
|
"ais_net_addr": "",
|
|
# GPS (mirrored from .env at startup; the GPS hot-change endpoint also writes here)
|
|
"gps_port": os.getenv("GPS_PORT", ""),
|
|
"gps_baud": int(os.getenv("GPS_BAUD", 9600) or 9600),
|
|
# Station / antenna (purely descriptive for now; antenna position is
|
|
# exposed to the frontend so it can centre the map and run proximity
|
|
# checks against the station)
|
|
"station_name": "",
|
|
"iala_region": "B",
|
|
"antenna_lat": None,
|
|
"antenna_lon": None,
|
|
"antenna_height_m": None,
|
|
"station_notes": "",
|
|
# Alert thresholds (live — alert_engine reads these every evaluation)
|
|
"displacement_warn_m": 10.0,
|
|
"displacement_alarm_m": 15.0,
|
|
"proximity_alert_meters": int(os.getenv("PROXIMITY_ALERT_METERS", 500)),
|
|
"projection_minutes": int(os.getenv("PROJECTION_MINUTES", 10)),
|
|
"projection_radius_meters": 800,
|
|
"auto_record_trigger_m": 200.0,
|
|
# ATON battery thresholds (volts)
|
|
"battery_warn_v": 11.5,
|
|
"battery_alarm_v": 10.8,
|
|
# SMTP — operator's organisation email account that sends report emails.
|
|
# When configured, /alerts/report sends EMAIL via SMTP instead of opening
|
|
# the operator's local mail client. Leave smtp_host blank to disable.
|
|
"smtp_host": "",
|
|
"smtp_port": 587,
|
|
"smtp_user": "",
|
|
"smtp_password": "",
|
|
"smtp_from": "",
|
|
"smtp_from_name": "AidsMonitoring",
|
|
"smtp_use_tls": True,
|
|
# ── Cluster / multi-server role ──────────────────────────────────────────
|
|
# STANDALONE: single server (default)
|
|
# MASTER : central aggregator — accepts connections from slave servers
|
|
# SLAVE : field server — forwards all events to the master
|
|
"server_role": os.getenv("SERVER_ROLE", "STANDALONE"),
|
|
# URL of the master's slave WebSocket endpoint (required when SLAVE)
|
|
# Example: "ws://10.0.0.1:8000/ws/slave"
|
|
"master_url": os.getenv("MASTER_URL", ""),
|
|
# Human-readable name for this slave (shown in master's status panel)
|
|
"slave_name": os.getenv("SLAVE_NAME", ""),
|
|
}
|
|
|
|
SETTINGS: dict = dict(DEFAULTS)
|
|
|
|
|
|
def _load_from_disk() -> dict:
|
|
if not _PATH.exists():
|
|
return {}
|
|
try:
|
|
return json.loads(_PATH.read_text())
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def _flush_to_disk():
|
|
try:
|
|
_PATH.write_text(json.dumps(SETTINGS, indent=2))
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def init():
|
|
"""Call once at app startup — merges saved overrides into SETTINGS."""
|
|
SETTINGS.update(_load_from_disk())
|
|
return SETTINGS
|
|
|
|
|
|
def get_all() -> dict:
|
|
return dict(SETTINGS)
|
|
|
|
|
|
def apply_patch(patch: dict) -> dict:
|
|
"""Merge patch into SETTINGS, ignoring unknown keys, write to disk.
|
|
Returns the new full settings dict."""
|
|
with _LOCK:
|
|
for k, v in patch.items():
|
|
if k in DEFAULTS:
|
|
SETTINGS[k] = v
|
|
_flush_to_disk()
|
|
return get_all()
|