168 lines
4.2 KiB
Go
168 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"html/template"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/blakeridgway/heloha/internal/radar"
|
|
"github.com/blakeridgway/heloha/internal/server"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
)
|
|
|
|
const ingestEvery = 2 * time.Minute
|
|
|
|
var radarSites = []string{
|
|
"KTLX", // Oklahoma City, OK
|
|
"KINX", // Tulsa, OK
|
|
"KVNX", // Vance AFB, OK
|
|
"KFDR", // Frederick, OK
|
|
"KSRX", // Fort Smith, AR
|
|
"KLZK", // Little Rock, AR
|
|
"KSHV", // Shreveport, LA
|
|
"KFWS", // Fort Worth, TX
|
|
"KDYX", // Dyess AFB, TX
|
|
"KAMA", // Amarillo, TX
|
|
"KLBB", // Lubbock, TX
|
|
"KICT", // Wichita, KS
|
|
"KDDC", // Dodge City, KS
|
|
"KSGF", // Springfield, MO
|
|
"KEAX", // Kansas City, MO
|
|
}
|
|
|
|
func main() {
|
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
|
ctx := context.Background()
|
|
|
|
fetcher, err := radar.NewFetcher(ctx)
|
|
if err != nil {
|
|
logger.Error("init fetcher", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
store := server.NewRadarStore()
|
|
cache := server.NewTileCache()
|
|
|
|
ingestAll(ctx, logger, fetcher, store, cache)
|
|
|
|
go func() {
|
|
tick := time.NewTicker(ingestEvery)
|
|
defer tick.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-tick.C:
|
|
ingestAll(ctx, logger, fetcher, store, cache)
|
|
}
|
|
}
|
|
}()
|
|
|
|
indexTmpl := template.Must(template.ParseFiles("web/templates/index.html"))
|
|
|
|
r := chi.NewRouter()
|
|
r.Use(middleware.RequestID)
|
|
r.Use(middleware.RealIP)
|
|
r.Use(middleware.Recoverer)
|
|
|
|
r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("ok"))
|
|
})
|
|
|
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
indexTmpl.Execute(w, nil)
|
|
})
|
|
|
|
// Composite tile: product + frame in path (new)
|
|
r.Get("/api/v1/tile/composite/{product}/{frame}/{z}/{x}/{y}.png", server.CompositeTileHandler(store, cache))
|
|
// Legacy composite route (product=reflectivity, frame=latest)
|
|
r.Get("/api/v1/tile/composite/{z}/{x}/{y}.png", server.CompositeTileHandler(store, cache))
|
|
// Per-site tile
|
|
r.Get("/api/v1/tile/{site}/{z}/{x}/{y}.png", server.TileHandler(store, cache))
|
|
|
|
// Frame metadata for the loop player
|
|
r.Get("/api/v1/frames", server.FramesHandler(store))
|
|
|
|
// Scan time (KTLX reference)
|
|
r.Get("/api/v1/scan-time", func(w http.ResponseWriter, r *http.Request) {
|
|
p := store.GetLatest("ktlx", "reflectivity")
|
|
if p == nil {
|
|
w.Write([]byte("—"))
|
|
return
|
|
}
|
|
w.Write([]byte(p.Time.Format("15:04 UTC")))
|
|
})
|
|
|
|
srv := &http.Server{
|
|
Addr: ":8080",
|
|
Handler: r,
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 30 * time.Second,
|
|
IdleTimeout: 120 * time.Second,
|
|
}
|
|
|
|
logger.Info("starting server", "addr", srv.Addr)
|
|
if err := srv.ListenAndServe(); err != nil {
|
|
logger.Error("server exited", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// ingestAll fetches reflectivity + velocity for every site concurrently.
|
|
func ingestAll(ctx context.Context, logger *slog.Logger, f *radar.Fetcher, store *server.RadarStore, cache *server.TileCache) {
|
|
var wg sync.WaitGroup
|
|
for _, site := range radarSites {
|
|
wg.Add(2)
|
|
go func(s string) {
|
|
defer wg.Done()
|
|
if err := ingestRefl(ctx, logger, f, store, s); err != nil {
|
|
logger.Warn("refl ingest failed", "site", s, "err", err)
|
|
}
|
|
}(site)
|
|
go func(s string) {
|
|
defer wg.Done()
|
|
if err := ingestVel(ctx, logger, f, store, s); err != nil {
|
|
logger.Warn("vel ingest failed", "site", s, "err", err)
|
|
}
|
|
}(site)
|
|
}
|
|
wg.Wait()
|
|
cache.Invalidate()
|
|
logger.Info("ingest cycle complete")
|
|
}
|
|
|
|
func ingestRefl(ctx context.Context, logger *slog.Logger, f *radar.Fetcher, store *server.RadarStore, site string) error {
|
|
data, err := f.FetchLatest(ctx, site)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p, err := radar.Parse(site, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
store.Set(site, "reflectivity", p)
|
|
logger.Info("refl ok", "site", site, "time", p.Time, "radials", len(p.Radials))
|
|
return nil
|
|
}
|
|
|
|
func ingestVel(ctx context.Context, logger *slog.Logger, f *radar.Fetcher, store *server.RadarStore, site string) error {
|
|
data, err := f.FetchVelocity(ctx, site)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p, err := radar.ParseVelocity(site, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
store.Set(site, "velocity", p)
|
|
logger.Info("vel ok", "site", site, "time", p.Time)
|
|
return nil
|
|
}
|