Initial commit: astrape-dashboard split out of the combined Astrape tree
The web frontend: the UI itself, the oracle, weather, station and quality renderers, and the serving daemon. The weather and station display modules were each split in two. The dataclasses went to astrape-oracle with the stores that return them; only the rendering stayed here, so the dependency runs dashboard -> oracle -> ems -> gibil with no cycle. Extracted from a single repository where the forecasting engine, the database plumbing, the hardware client, the ingest daemons and the web UI all imported each other freely. Verified that every module imports with gibil resolved from its own installed checkout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from importlib import import_module, reload
|
||||
from os import environ
|
||||
from pathlib import Path
|
||||
import json
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from astrape_ems.classes.env import EnvLoader
|
||||
|
||||
EnvLoader().load()
|
||||
|
||||
HOST = environ.get("ASTRAPE_WEB_HOST", "0.0.0.0")
|
||||
PORT = int(environ.get("ASTRAPE_WEB_PORT", "8080"))
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
WATCHED_PATHS = [
|
||||
PROJECT_ROOT / "gibil" / "classes" / "models.py",
|
||||
PROJECT_ROOT / "gibil" / "classes" / "webui.py",
|
||||
PROJECT_ROOT / "gibil" / "classes" / "weather" / "display.py",
|
||||
PROJECT_ROOT / "gibil" / "classes" / "weather" / "station_display.py",
|
||||
PROJECT_ROOT / "gibil" / "classes" / "oracle" / "display.py",
|
||||
PROJECT_ROOT / "gibil" / "classes" / "oracle" / "quality_display.py",
|
||||
PROJECT_ROOT / "gibil" / "classes" / "weather" / "store.py",
|
||||
PROJECT_ROOT / "gibil" / "classes" / "oracle" / "store.py",
|
||||
PROJECT_ROOT / "gibil" / "classes" / "oracle" / "builder.py",
|
||||
PROJECT_ROOT / "gibil" / "classes" / "oracle" / "config.py",
|
||||
PROJECT_ROOT / "gibil" / "classes" / "sigen" / "store.py",
|
||||
]
|
||||
|
||||
|
||||
class AstrapeWebHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self) -> None:
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path
|
||||
|
||||
if path in {"/", "/oracle"}:
|
||||
self._send_html(self._webui().render_page("oracle"))
|
||||
return
|
||||
|
||||
if path == "/weather":
|
||||
self._send_html(self._webui().render_page("weather"))
|
||||
return
|
||||
|
||||
if path == "/weather-station":
|
||||
self._send_html(self._webui().render_page("weather_station"))
|
||||
return
|
||||
|
||||
if path == "/quality":
|
||||
self._send_html(self._webui().render_page("quality"))
|
||||
return
|
||||
|
||||
if path == "/api/weather":
|
||||
self._send_json_text(self._webui().weather_payload())
|
||||
return
|
||||
|
||||
if path == "/api/weather-station":
|
||||
params = parse_qs(parsed.query)
|
||||
lookback_hours = self._float_param(params, "lookback_hours", 168)
|
||||
self._send_json_text(
|
||||
self._webui().weather_station_payload(
|
||||
lookback_hours=lookback_hours
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if path == "/api/oracle":
|
||||
self._send_json_text(self._webui().oracle_payload())
|
||||
return
|
||||
|
||||
if path == "/api/oracle-quality":
|
||||
params = parse_qs(parsed.query)
|
||||
lookback_hours = self._float_param(params, "lookback_hours", 168)
|
||||
self._send_json_text(
|
||||
self._webui().oracle_quality_payload(
|
||||
lookback_hours=lookback_hours
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if path == "/api/ui-version":
|
||||
self._send_json_text(json.dumps({"version": self._ui_version()}))
|
||||
return
|
||||
|
||||
self.send_error(404)
|
||||
|
||||
def log_message(self, format: str, *args: object) -> None:
|
||||
print(f"{self.address_string()} - {format % args}")
|
||||
|
||||
def _webui(self):
|
||||
models_module = import_module("gibil.classes.models")
|
||||
weather_store_module = import_module("astrape_oracle.classes.weather.store")
|
||||
sigen_store_module = import_module("astrape_ems.classes.store")
|
||||
oracle_store_module = import_module("astrape_oracle.classes.oracle_store")
|
||||
oracle_config_module = import_module("gibil.classes.forecast_config")
|
||||
oracle_builder_module = import_module("gibil.classes.oracle_builder")
|
||||
oracle_display_module = import_module("astrape_dashboard.classes.displays.oracle")
|
||||
oracle_quality_display_module = import_module(
|
||||
"astrape_dashboard.classes.displays.oracle_quality"
|
||||
)
|
||||
weather_display_module = import_module("astrape_oracle.classes.datasets")
|
||||
weather_station_display_module = import_module(
|
||||
"astrape_oracle.classes.datasets"
|
||||
)
|
||||
webui_module = import_module("astrape_dashboard.classes.webui")
|
||||
reload(models_module)
|
||||
reload(weather_display_module)
|
||||
reload(weather_station_display_module)
|
||||
reload(weather_store_module)
|
||||
reload(sigen_store_module)
|
||||
reload(oracle_store_module)
|
||||
reload(oracle_config_module)
|
||||
reload(oracle_builder_module)
|
||||
reload(oracle_display_module)
|
||||
reload(oracle_quality_display_module)
|
||||
reload(webui_module)
|
||||
return webui_module.WebUI()
|
||||
|
||||
def _float_param(
|
||||
self,
|
||||
params: dict[str, list[str]],
|
||||
key: str,
|
||||
default: float,
|
||||
) -> float:
|
||||
values = params.get(key)
|
||||
if not values:
|
||||
return default
|
||||
try:
|
||||
return float(values[0])
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
def _ui_version(self) -> str:
|
||||
mtimes = [
|
||||
str(path.stat().st_mtime_ns)
|
||||
for path in WATCHED_PATHS
|
||||
if path.exists()
|
||||
]
|
||||
return ".".join(mtimes)
|
||||
|
||||
def _send_html(self, body: str) -> None:
|
||||
encoded = body.encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(encoded)))
|
||||
self.end_headers()
|
||||
self.wfile.write(encoded)
|
||||
|
||||
def _send_json_text(self, body: str) -> None:
|
||||
encoded = body.encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Content-Length", str(len(encoded)))
|
||||
self.end_headers()
|
||||
self.wfile.write(encoded)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
server = ThreadingHTTPServer((HOST, PORT), AstrapeWebHandler)
|
||||
print(f"Astrape web UI listening on http://{HOST}:{PORT}")
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Debug script to trace resolved truth data."""
|
||||
|
||||
from astrape_ems.classes.env import EnvLoader
|
||||
from astrape_oracle.classes.weather.store import WeatherStore
|
||||
from astrape_dashboard.classes.displays.weather import WeatherDisplay
|
||||
from datetime import datetime, timezone
|
||||
|
||||
EnvLoader().load()
|
||||
|
||||
store = WeatherStore.from_env()
|
||||
dataset = store.load_display_dataset()
|
||||
|
||||
print(f"\n=== DEBUG OUTPUT ===")
|
||||
print(f"Forecast points: {len(dataset.forecast_points)}")
|
||||
print(f"Resolved truth points: {len(dataset.resolved_truth)}")
|
||||
|
||||
print(f"\nResolved truth details:")
|
||||
for i, point in enumerate(dataset.resolved_truth):
|
||||
print(f" [{i}] resolved_at={point.resolved_at}, temp={point.temperature_c}, radiation={point.shortwave_radiation_w_m2}")
|
||||
|
||||
print(f"\nAPI payload:")
|
||||
display = WeatherDisplay()
|
||||
payload = display.data_payload(dataset)
|
||||
import json
|
||||
data = json.loads(payload)
|
||||
print(f" Resolved truth in payload: {len(data['resolved_truth'])}")
|
||||
for i, point in enumerate(data['resolved_truth']):
|
||||
print(f" [{i}] resolved_at={point['resolved_at']}, temp={point['temperature_c']}")
|
||||
|
||||
print(f"\n=== END DEBUG ===\n")
|
||||
Reference in New Issue
Block a user