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:
rpotter6298
2026-08-30 14:19:09 +02:00
commit 665c941802
16 changed files with 1912 additions and 0 deletions
@@ -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")