Files
statuspage/server/main.go
T

129 lines
3.4 KiB
Go

package main
import (
"context"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/joho/godotenv"
"github.com/spf13/pflag"
)
func envOr(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func buildFrontend() error {
cmd := "npm"
if _, err := exec.LookPath("npm"); err != nil {
cmd = "bun"
}
c := exec.Command(cmd, "run", "build")
c.Stdout = os.Stdout
c.Stderr = os.Stderr
log.Printf("running %s run build", cmd)
return c.Run()
}
func spaHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/api/") {
http.NotFound(w, r)
return
}
rel := filepath.Clean(strings.TrimPrefix(r.URL.Path, "/"))
if rel != "." && rel != "" {
p := filepath.Join("dist", rel)
if fi, err := os.Stat(p); err == nil && !fi.IsDir() {
http.ServeFile(w, r, p)
return
}
}
http.ServeFile(w, r, filepath.Join("dist", "index.html"))
}
}
func main() {
_ = godotenv.Load()
dev := pflag.Bool("dev", false, "run `npm run build` before serving dist/")
devServer := pflag.Bool("dev-server", false, "frontend served by the vite dev server, do not serve dist/")
mock := pflag.Bool("mock", false, "serve generated mock data instead of probing real machines")
addr := pflag.String("addr", envOr("ADDR", ":5000"), "listen address")
configPath := pflag.StringP("config", "c", envOr("CONFIG", "config.local.yaml"), "yaml config file")
dbPath := pflag.String("db", envOr("DB", "data.volume/history.db"), "sqlite history database file")
pflag.Parse()
if *dev {
if err := buildFrontend(); err != nil {
log.Fatalf("build failed: %v", err)
}
}
mux := http.NewServeMux()
if *mock {
log.Printf("mock mode: serving generated data (no real probes)")
mm := NewMockMonitor()
mux.HandleFunc("GET /api/status", mm.statusHandler)
mux.HandleFunc("GET /api/history/{machine}", mm.historyHandler)
mux.HandleFunc("GET /api/metrics/aggregate", mm.metricsAggregateHandler)
mux.HandleFunc("GET /api/metrics/{machine}", mm.metricsHandler)
mux.HandleFunc("POST /api/refresh/{machine}", mm.refreshHandler)
if !*devServer {
mux.HandleFunc("GET /", spaHandler())
}
log.Printf("listening on %s (mock)", *addr)
if err := http.ListenAndServe(*addr, mux); err != nil {
log.Fatal(err)
}
return
}
if dir := filepath.Dir(*dbPath); dir != "." {
if err := os.MkdirAll(dir, 0o755); err != nil {
log.Fatalf("creating db dir: %v", err)
}
}
cfg, err := LoadConfig(*configPath)
if err != nil {
log.Fatalf("loading config: %v", err)
}
log.Printf("loaded %d machines from %s", len(cfg.Machines), *configPath)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
history, err := NewHistory(*dbPath)
if err != nil {
log.Fatalf("opening history db: %v", err)
}
defer history.Close()
go history.pruneLoop(ctx)
mon := NewMonitor(cfg, history)
mon.Start(ctx)
mux.HandleFunc("GET /api/status", mon.statusHandler)
mux.HandleFunc("GET /api/history/{machine}", mon.historyHandler)
mux.HandleFunc("GET /api/metrics/aggregate", mon.metricsAggregateHandler)
mux.HandleFunc("GET /api/metrics/{machine}", mon.metricsHandler)
mux.HandleFunc("POST /api/refresh/{machine}", mon.refreshHandler)
if !*devServer {
mux.HandleFunc("GET /", spaHandler())
}
log.Printf("listening on %s", *addr)
if err := http.ListenAndServe(*addr, mux); err != nil {
log.Fatal(err)
}
}