CI / Test suite (push) Successful in 22s
CI / Format & lint (push) Successful in 16s
Docs / Validate documentation (push) Successful in 10s
Security / Dependency vulnerabilities (push) Successful in 1m34s
Security / Static analysis (push) Successful in 10s
Security / License report (push) Successful in 13s
src/ml/skill.py joins forecast_history (what each deployed version predicted for the 24 h peak, hourly) to water_measurements (what the river did) and reports per version: verified hours, peak MAE, bias, the persistence baseline (peak = current level), skill = 1 - MAE/persistence, and the same MAE restricted to observed peaks >= 2 m. Only forecasts whose window has elapsed with >= 75 % of hours observed count; a version needs 24 verified hours before it is compared. GET /api/forecast/skill?station_code=P.1&horizon=24 returns it (SWR cached, 15 min). The dashboard's forecast card gains a panel with a one-line verdict (current vs previous version), the per-version table, and a caveat that quiet weeks measure quiet-river accuracy only: the model is judged on flood-onset lead, which the backtests cover. EN + TH. On today's production data: hgb-v3+28b62e5 (369 h, Aug 13 - Sep 1) MAE 15.2 cm, skill -0.05; hgb-v2+f6570ac (224 h, Sep 1 - 11) MAE 12.3 cm, skill 0.36 - the "worse" v2 model scores better on a quieter fortnight, which is exactly why the panel shows the >= 2 m column and the caveat. Tests: 3, sqlite, synthetic. scripts/dev_proxy.py: DEV_PROXY_LOCAL lets a not-yet-deployed endpoint be answered from a local JSON file while everything else goes to prod.
59 lines
2.4 KiB
Python
59 lines
2.4 KiB
Python
"""Serve the working-copy dashboard locally with API calls proxied to the
|
|
live server, so browser-side changes can be checked against real data
|
|
before deploy. Usage: python scripts/dev_proxy.py [port]"""
|
|
|
|
import http.server
|
|
import os
|
|
import sys
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
UPSTREAM = "https://water.buildfor.life"
|
|
STATIC = Path(__file__).resolve().parents[1] / "src" / "static"
|
|
|
|
|
|
class Handler(http.server.BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
if self.path == "/" or self.path.startswith("/?"):
|
|
body = (STATIC / "dashboard.html").read_bytes()
|
|
self._send(200, "text/html; charset=utf-8", body)
|
|
return
|
|
# Local overrides for endpoints not yet deployed: DEV_PROXY_LOCAL=/api/x=file.json,...
|
|
for pair in filter(None, os.environ.get("DEV_PROXY_LOCAL", "").split(",")):
|
|
prefix, file = pair.split("=", 1)
|
|
if self.path.split("?")[0] == prefix:
|
|
self._send(200, "application/json", Path(file).read_bytes())
|
|
return
|
|
if self.path.startswith("/static/"):
|
|
f = STATIC / self.path[len("/static/"):].split("?")[0]
|
|
if f.is_file():
|
|
ctype = "application/json" if f.suffix in (".json", ".geojson") else "application/octet-stream"
|
|
self._send(200, ctype, f.read_bytes())
|
|
return
|
|
try:
|
|
req = urllib.request.Request(
|
|
UPSTREAM + self.path,
|
|
headers={"User-Agent": "Mozilla/5.0 (dev_proxy; +https://buildfor.life)", "Accept": "application/json"},
|
|
)
|
|
with urllib.request.urlopen(req, timeout=60) as r:
|
|
self._send(r.status, r.headers.get("Content-Type", "application/json"), r.read())
|
|
except urllib.error.HTTPError as e:
|
|
self._send(e.code, "application/json", e.read())
|
|
|
|
def _send(self, code, ctype, body):
|
|
self.send_response(code)
|
|
self.send_header("Content-Type", ctype)
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.send_header("Cache-Control", "no-store")
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def log_message(self, *a):
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8765
|
|
print(f"http://localhost:{port}/ (API -> {UPSTREAM})")
|
|
http.server.ThreadingHTTPServer(("127.0.0.1", port), Handler).serve_forever()
|