New js/layers/quakes.js — fetches the USGS all_day GeoJSON directly (ACAO:*, no proxy), refreshes every 5 min regardless of scrub state. Quakes are time-anchored via availability: in-window quakes appear as the slider crosses their origin time, older-than-window quakes stay visible the whole window (intervals clamped to [start, stop]). Magnitude ramps amber→red, size scales with magnitude, quakes <1h old pulse (age-aware callback), labels only for M>=4.5. Deduped by feature id across refreshes, capped at 400 (smallest mag dropped first). HUD row "Earthquakes (USGS)" with count/max-mag status. Also: serve.py now sends Cache-Control: no-store on all responses (was proxy-only) so iterative js/css edits always take effect on reload — the module graph was caching stale copies during dev. Verified in browser: 223 quakes, 11 labeled, time-anchoring confirmed (187 visible at -6h → 223 at now), zero console errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
70 lines
2.9 KiB
Python
70 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
|
"""GODSIGH dev server: static files + same-origin proxy for feeds that block CORS."""
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
UPSTREAMS = {
|
|
# OpenSky pins Access-Control-Allow-Origin to its own domain, so the browser
|
|
# can't call it directly — we forward it here instead.
|
|
"opensky": "https://opensky-network.org/api/states/all",
|
|
# Celestrak sends ACAO:* today; proxied too so the app keeps working if that changes.
|
|
"celestrak": "https://celestrak.org/NORAD/elements/gp.php",
|
|
}
|
|
|
|
|
|
class Handler(SimpleHTTPRequestHandler):
|
|
def end_headers(self):
|
|
# Dev server: never let the browser cache our static assets, so edits to
|
|
# js/css always take effect on reload (module graphs cache aggressively).
|
|
self.send_header("Cache-Control", "no-store")
|
|
super().end_headers()
|
|
|
|
def do_GET(self):
|
|
if not self.path.startswith("/proxy/"):
|
|
return super().do_GET()
|
|
name, _, query = self.path[len("/proxy/"):].partition("?")
|
|
base = UPSTREAMS.get(name)
|
|
if not base:
|
|
return self.send_error(404, f"unknown upstream {name!r}")
|
|
url = base + ("?" + query if query else "")
|
|
# Forward these upstream headers so the client can show quota state.
|
|
passthrough = {}
|
|
|
|
def grab(hdrs):
|
|
for k in ("X-Rate-Limit-Remaining", "X-Rate-Limit-Limit", "X-Expires-After"):
|
|
if hdrs.get(k) is not None:
|
|
passthrough[k] = hdrs.get(k)
|
|
|
|
try:
|
|
req = urllib.request.Request(url, headers={"User-Agent": "godsigh-dev/0.1"})
|
|
with urllib.request.urlopen(req, timeout=45) as r:
|
|
body = r.read()
|
|
status, ctype = r.status, r.headers.get("Content-Type", "text/plain")
|
|
grab(r.headers)
|
|
except urllib.error.HTTPError as e:
|
|
body = e.read() or str(e).encode()
|
|
status, ctype = e.code, "text/plain"
|
|
grab(e.headers)
|
|
except Exception as e:
|
|
body, status, ctype = str(e).encode(), 502, "text/plain"
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", ctype)
|
|
self.send_header("Access-Control-Allow-Origin", "*")
|
|
self.send_header("Access-Control-Expose-Headers", "X-Rate-Limit-Remaining, X-Rate-Limit-Limit, X-Expires-After")
|
|
for k, v in passthrough.items():
|
|
self.send_header(k, v)
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def log_message(self, fmt, *args):
|
|
sys.stderr.write("%s - %s\n" % (self.address_string(), fmt % args))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8137
|
|
print(f"GODSIGH serving on http://127.0.0.1:{port}")
|
|
ThreadingHTTPServer(("127.0.0.1", port), Handler).serve_forever()
|