Phase 4 of docs/RUST_REWRITE.md. Since 2026-10-18 ping-collector (rust/) collects, forecasts, notifies (ntfy + Matrix) and serves the website/API; nothing in production ran the Python service code any more. Removed (~17k lines): the FastAPI app (web_api, schemas, health_check, metrics, postgres_history, forecast_history, thaiwater), the Python RID collector and CLI (water_scraper_v3, main, run.py), the notifier and Matrix alerting (notify, alerting), the multi-backend adapters (SQLite, MySQL, InfluxDB, VictoriaMetrics) with demo databases, models/validators/exceptions/ logging/rate limiter, ml/skill.py (served by Rust now), the migration tooling (api_parity, collector_parity, e2e_notify, install_collector.sh, gen_notify_cases.py, spikes/rust-forecast), Docker/compose/Grafana files, sql/, the release workflow and the docs for all of that. Their tests went with them. Kept in Python: src/ml (training, evaluation harness, export; predict.py as the reference the Rust forecaster is tested against), the HII and RID-dam clients/stores for the archive backfills (their live collectors removed), the backfill scripts, retrain.sh + timer, forecast_parity.py. - PostgreSQL only: src/config.py is database_url() from POSTGRES_* / .env; the stores lost their sqlite/mysql branches (and the 2026-08 column migration). The database tests run against a throwaway schema from TEST_DATABASE_URL (tests/conftest.py pg_url, skipped without it), as the Rust tests do; CI gets a PostgreSQL service. 100 tests (66 without a DB). - The notifier settings moved from src/notify.py into src/ml/export.py; flood_config.json regenerates byte-identical. - tests/test_dashboard.py checks every path the dashboard fetches against ping-collector's router. - Dependencies: fastapi, uvicorn, pydantic, influxdb, pymysql, psutil, schedule, sphinx, pyinstaller, mypy, pytest-asyncio dropped; python-dotenv 1.1.1 explicit (it came in through uvicorn[standard]). - Formatting contract extended to scripts/ and tests/ (reformatted once). - scripts/install.sh is the one installer (fresh host or update, in place in a checkout without touching its ownership): binary, units, uv sync, .env defaults, removes the retired water-monitor.service and any old --alert-check cron job, dry run + forecast check, restart, readiness wait. - Docs, CLAUDE.md, CONTRIBUTING.md, .env.example, Makefile, unit comments and Rust doc comments updated to the Rust service.
70 lines
2.6 KiB
Python
70 lines
2.6 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()
|