"""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] / is src/static/dashboard.html, /static/... any file in src/static (e.g. the console preview at /static/console.html), everything else the live server.""" import http.server import mimetypes import os import re 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 mimetypes.guess_type(f.name)[0] or "application/octet-stream" ) if ctype.startswith("text/"): ctype += "; charset=utf-8" body = f.read_bytes() # Byte ranges, like the server's ServeDir: PMTiles reads the # basemap in pieces m = re.fullmatch(r"bytes=(\d+)-(\d*)", self.headers.get("Range", "")) if m: start = int(m.group(1)) end = min(int(m.group(2) or len(body) - 1), len(body) - 1) extra = {"Content-Range": f"bytes {start}-{end}/{len(body)}"} self._send(206, ctype, body[start : end + 1], extra) return self._send(200, ctype, body) 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, extra=None): self.send_response(code) self.send_header("Content-Type", ctype) self.send_header("Accept-Ranges", "bytes") for name, value in (extra or {}).items(): self.send_header(name, value) 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()