add web ui

This commit is contained in:
2026-01-27 16:51:58 +11:00
parent 7526a7af93
commit 57a3a29914
12 changed files with 1158 additions and 23 deletions

149
cmd/ingestd/web.go Normal file
View File

@@ -0,0 +1,149 @@
package main
import (
"context"
"embed"
"encoding/json"
"errors"
"io/fs"
"log"
"net/http"
"time"
"go-weatherstation/internal/db"
"go-weatherstation/internal/providers"
)
//go:embed web/*
var webFS embed.FS
type webServer struct {
db *db.DB
site providers.Site
model string
}
type dashboardResponse struct {
GeneratedAt time.Time `json:"generated_at"`
Site string `json:"site"`
Model string `json:"model"`
OpenMeteoURL string `json:"open_meteo_url,omitempty"`
Observations []db.ObservationPoint `json:"observations"`
Forecast db.ForecastSeries `json:"forecast"`
Latest *db.ObservationPoint `json:"latest"`
}
func runWebServer(ctx context.Context, d *db.DB, site providers.Site, model, addr string) error {
sub, err := fs.Sub(webFS, "web")
if err != nil {
return err
}
ws := &webServer{
db: d,
site: site,
model: model,
}
mux := http.NewServeMux()
mux.HandleFunc("/api/dashboard", ws.handleDashboard)
mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"ok":true}`))
})
mux.Handle("/", http.FileServer(http.FS(sub)))
srv := &http.Server{
Addr: addr,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
errCh := make(chan error, 1)
go func() {
errCh <- srv.ListenAndServe()
}()
log.Printf("web ui listening addr=%s", addr)
select {
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
return nil
case err := <-errCh:
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
}
}
func (s *webServer) handleDashboard(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
rangeStr := r.URL.Query().Get("range")
if rangeStr == "" {
rangeStr = "24h"
}
bucket := r.URL.Query().Get("bucket")
if bucket == "" {
bucket = "5m"
}
if bucket != "1m" && bucket != "5m" {
http.Error(w, "invalid bucket", http.StatusBadRequest)
return
}
rangeDur, err := time.ParseDuration(rangeStr)
if err != nil || rangeDur <= 0 {
http.Error(w, "invalid range", http.StatusBadRequest)
return
}
observations, err := s.db.ObservationSeries(r.Context(), s.site.Name, bucket, int64(rangeDur.Seconds()))
if err != nil {
http.Error(w, "failed to query observations", http.StatusInternalServerError)
log.Printf("web dashboard observations error: %v", err)
return
}
latest, err := s.db.LatestObservation(r.Context(), s.site.Name)
if err != nil {
http.Error(w, "failed to query latest observation", http.StatusInternalServerError)
log.Printf("web dashboard latest error: %v", err)
return
}
forecast, err := s.db.ForecastSeriesLatest(r.Context(), s.site.Name, s.model)
if err != nil {
http.Error(w, "failed to query forecast", http.StatusInternalServerError)
log.Printf("web dashboard forecast error: %v", err)
return
}
openMeteoURL, err := providers.OpenMeteoRequestURL(s.site, s.model)
if err != nil {
log.Printf("web dashboard open-meteo url error: %v", err)
}
resp := dashboardResponse{
GeneratedAt: time.Now().UTC(),
Site: s.site.Name,
Model: s.model,
OpenMeteoURL: openMeteoURL,
Observations: observations,
Forecast: forecast,
Latest: latest,
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("web dashboard encode error: %v", err)
}
}