mirror of
https://github.com/lynchaos/ashvale-station.git
synced 2026-09-12 12:47:49 +00:00
Settings tab
Everything that was previously a curl command now has a surface: surroundings, site geometry, the matrix, the psychrometric flag, and the maintenance actions. Changes persist to data/state/settings.json, not config.yaml. That file is hand-annotated and hand-edited per station, and rewriting it from an API would destroy the comments and risk clobbering something the owner set. The overlay is applied last in load_config, so a dashboard change beats both the file and the environment, and deleting the overlay reverts everything. Written atomically via a temp file so a crash cannot truncate it. Every field applies live. A settings page that needs a restart is one people stop trusting, so site geometry is re-read per sample, the compensator flag is set on the live object, and the display picks up its rate the next frame. Two deliberate frictions. Selecting a surroundings pill only stages it: nothing is recorded until you press the button, because that writes a discontinuity marker and queues a retrain. And changing altitude or the psychrometric flag says outright that the stored history is now inconsistent and offers the re-derive, rather than leaving a silent mismatch. Verified in a browser: pills stage and apply, the toggle round-trips, re-derive ran 6201 rows in 0.29 s from the button, all six tabs report zero scrollbars and zero clipping, zero console errors.
This commit is contained in:
+102
-1
@@ -34,7 +34,7 @@ from fastapi.responses import HTMLResponse, StreamingResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .config import CONFIG
|
||||
from .config import CONFIG, load_overrides, save_overrides
|
||||
from .dashboard import DASHBOARD_HTML
|
||||
from .features import FEATURE_NAMES
|
||||
from .led import LedDisplay
|
||||
@@ -128,6 +128,19 @@ class CalibrationIn(BaseModel):
|
||||
"covariance, returning to the configured prior")
|
||||
|
||||
|
||||
class SettingsIn(BaseModel):
|
||||
"""Every field optional: the UI sends only what changed."""
|
||||
environment: Optional[str] = None
|
||||
enclosure: Optional[str] = None
|
||||
note: str = ""
|
||||
altitude_m: Optional[float] = Field(None, ge=-430, le=9000)
|
||||
latitude: Optional[float] = Field(None, ge=-90, le=90)
|
||||
longitude: Optional[float] = Field(None, ge=-180, le=180)
|
||||
hum_psychrometric: Optional[bool] = None
|
||||
led_enabled: Optional[bool] = None
|
||||
led_fps: Optional[float] = Field(None, ge=4, le=30)
|
||||
|
||||
|
||||
class EnvironmentIn(BaseModel):
|
||||
environment: Optional[str] = Field(None, description="indoor | sheltered | outdoor")
|
||||
enclosure: Optional[str] = Field(None, description="closed | ventilated | open")
|
||||
@@ -624,6 +637,94 @@ def environment(body: EnvironmentIn) -> Dict:
|
||||
return _clean(_st().set_environment(body.environment, body.enclosure, body.note))
|
||||
|
||||
|
||||
@app.get("/api/settings")
|
||||
def get_settings() -> Dict:
|
||||
st = _st()
|
||||
return _clean({
|
||||
"site": {"environment": CONFIG.site.environment,
|
||||
"enclosure": CONFIG.site.enclosure,
|
||||
"altitude_m": CONFIG.site.altitude_m,
|
||||
"latitude": CONFIG.site.latitude,
|
||||
"longitude": CONFIG.site.longitude,
|
||||
"timezone": CONFIG.site.timezone,
|
||||
"name": CONFIG.site.name},
|
||||
"sensor": {"hum_psychrometric": CONFIG.sensor.hum_psychrometric,
|
||||
"cpu_heat_k": round(st.tracker.compensator.k, 4),
|
||||
"hum_offset": round(st.tracker.hum_compensator.offset, 3)},
|
||||
"server": {"led_enabled": CONFIG.server.led_enabled,
|
||||
"led_fps": CONFIG.server.led_fps},
|
||||
"options": {
|
||||
"environment": ["indoor", "sheltered", "outdoor"],
|
||||
"enclosure": ["closed", "ventilated", "open"],
|
||||
},
|
||||
"overrides": load_overrides(CONFIG),
|
||||
})
|
||||
|
||||
|
||||
@app.post("/api/settings")
|
||||
def post_settings(body: SettingsIn) -> Dict:
|
||||
"""Apply settings live and persist them to the overlay.
|
||||
|
||||
Everything here takes effect without a restart, because a settings page that
|
||||
needs one is a settings page people stop trusting. Site geometry is read per
|
||||
sample, the compensator flag is a field on a live object, and the display
|
||||
reads its own rate each frame.
|
||||
"""
|
||||
st = _st()
|
||||
patch: Dict[str, Dict] = {}
|
||||
applied, needs_recompute = [], False
|
||||
|
||||
if body.environment or body.enclosure:
|
||||
r = st.set_environment(body.environment, body.enclosure, body.note)
|
||||
if r.get("changed"):
|
||||
applied.append(r["detail"])
|
||||
patch.setdefault("site", {}).update(
|
||||
{"environment": CONFIG.site.environment,
|
||||
"enclosure": CONFIG.site.enclosure})
|
||||
|
||||
for name, value in (("altitude_m", body.altitude_m),
|
||||
("latitude", body.latitude),
|
||||
("longitude", body.longitude)):
|
||||
if value is not None and value != getattr(CONFIG.site, name):
|
||||
applied.append(f"{name} {getattr(CONFIG.site, name)} -> {value}")
|
||||
setattr(CONFIG.site, name, float(value))
|
||||
patch.setdefault("site", {})[name] = float(value)
|
||||
# Altitude feeds the sea-level reduction on every stored row, so the
|
||||
# history is now inconsistent with the new value until re-derived.
|
||||
needs_recompute = needs_recompute or name == "altitude_m"
|
||||
|
||||
if body.hum_psychrometric is not None and \
|
||||
body.hum_psychrometric != CONFIG.sensor.hum_psychrometric:
|
||||
CONFIG.sensor.hum_psychrometric = bool(body.hum_psychrometric)
|
||||
st.tracker.hum_compensator.psychrometric = bool(body.hum_psychrometric)
|
||||
patch.setdefault("sensor", {})["hum_psychrometric"] = bool(body.hum_psychrometric)
|
||||
applied.append(f"psychrometric correction {'on' if body.hum_psychrometric else 'off'}")
|
||||
needs_recompute = True
|
||||
|
||||
if body.led_enabled is not None and body.led_enabled != CONFIG.server.led_enabled:
|
||||
CONFIG.server.led_enabled = bool(body.led_enabled)
|
||||
patch.setdefault("server", {})["led_enabled"] = bool(body.led_enabled)
|
||||
if display is not None:
|
||||
display.enabled = bool(body.led_enabled)
|
||||
if not body.led_enabled:
|
||||
st.board.clear()
|
||||
applied.append(f"matrix {'on' if body.led_enabled else 'off'}")
|
||||
|
||||
if body.led_fps is not None and body.led_fps != CONFIG.server.led_fps:
|
||||
CONFIG.server.led_fps = float(body.led_fps)
|
||||
patch.setdefault("server", {})["led_fps"] = float(body.led_fps)
|
||||
if display is not None:
|
||||
display.fps = float(body.led_fps)
|
||||
applied.append(f"matrix {body.led_fps:g} fps")
|
||||
|
||||
if patch:
|
||||
save_overrides(CONFIG, patch)
|
||||
st.store.log_event("settings", "info", "; ".join(applied))
|
||||
|
||||
return _clean({"applied": applied, "changed": bool(applied),
|
||||
"needs_recompute": needs_recompute})
|
||||
|
||||
|
||||
@app.get("/api/status")
|
||||
def status() -> Dict:
|
||||
st = _st()
|
||||
|
||||
Reference in New Issue
Block a user