add web ui
This commit is contained in:
149
cmd/ingestd/web.go
Normal file
149
cmd/ingestd/web.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user