"""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 (with HTTP range requests, which the map's PMTiles basemap needs), 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()