package main import ( "fmt" "log" "net/http" "os" "strings" "time" "github.com/gin-gonic/gin" "gopkg.in/yaml.v3" ) // serviceMonitor defines one configured service endpoint to check. type serviceMonitor struct { Name string `yaml:"name"` URL string `yaml:"url"` Category string `yaml:"category"` } // appConfig contains the monitor definitions loaded from the YAML config file. type appConfig struct { Monitors []serviceMonitor `yaml:"monitors"` } // serviceStatus represents the latest observed health information for a monitor. type serviceStatus struct { Name string `json:"name"` URL string `json:"url"` Category string `json:"category"` Protocol string `json:"protocol"` Healthy bool `json:"healthy"` StatusCode int `json:"status_code"` ResponseTime string `json:"response_time"` LastChecked time.Time `json:"last_checked"` Message string `json:"message"` Details string `json:"details"` } // main starts the status server, schedules monitor refreshes, and wires routes. func main() { store := &statusStore{} hub := newWSHub() client := &http.Client{Timeout: 5 * time.Second} addr := serverAddr() tlsCertFile, tlsKeyFile, err := serverTLSFiles() if err != nil { log.Fatal(err) } theme := serverTheme() monitors, err := loadMonitors(configPath()) if err != nil { log.Fatal(err) } refreshStatuses(store, client, monitors, hub) go func() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for range ticker.C { refreshStatuses(store, client, monitors, hub) } }() router := gin.Default() router.LoadHTMLGlob("templates/*.html") router.Static("/static", "./static") router.GET("/", func(c *gin.Context) { c.HTML(http.StatusOK, "status.html", gin.H{ "title": "Status Board", "theme": theme, }) }) router.GET("/api/status", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "generated_at": time.Now(), "services": store.list(), }) }) router.GET("/ws", func(c *gin.Context) { serveWebSocket(c, hub) }) scheme := "http" if tlsCertFile != "" { scheme = "https" } log.Printf("status page listening on %s://%s", scheme, addr) if err := runServer(router, addr, tlsCertFile, tlsKeyFile); err != nil { log.Fatal(err) } } // configPath returns the monitor configuration file path. func configPath() string { if path := os.Getenv("CONFIG_PATH"); path != "" { return path } return "config.yaml" } // serverAddr returns the HTTP listen address for the status server. func serverAddr() string { if addr := os.Getenv("ADDR"); addr != "" { return addr } if port := os.Getenv("PORT"); port != "" { return ":" + port } return "127.0.0.1:8080" } // serverTLSFiles returns the configured TLS certificate and key file paths. func serverTLSFiles() (string, string, error) { certFile := os.Getenv("TLS_CERT_FILE") keyFile := os.Getenv("TLS_KEY_FILE") if certFile == "" && keyFile == "" { return "", "", nil } if certFile == "" || keyFile == "" { return "", "", fmt.Errorf("both TLS_CERT_FILE and TLS_KEY_FILE must be set to enable HTTPS") } return certFile, keyFile, nil } // serverTheme returns the normalized UI theme name. func serverTheme() string { switch strings.ToLower(os.Getenv("THEME")) { case "light": return "light" case "dark", "": return "dark" default: return "dark" } } // runServer starts the Gin server with or without TLS based on configuration. func runServer(router *gin.Engine, addr, certFile, keyFile string) error { if certFile != "" { return router.RunTLS(addr, certFile, keyFile) } return router.Run(addr) } // loadMonitors reads, validates, and returns monitor definitions from disk. func loadMonitors(path string) ([]serviceMonitor, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config %q: %w", path, err) } var cfg appConfig if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parse config %q: %w", path, err) } if len(cfg.Monitors) == 0 { return nil, fmt.Errorf("config %q does not define any monitors", path) } for i, monitor := range cfg.Monitors { if monitor.Name == "" || monitor.URL == "" || monitor.Category == "" { return nil, fmt.Errorf("config %q has an incomplete monitor at index %d", path, i) } if _, err := parseMonitorURL(monitor.URL); err != nil { return nil, fmt.Errorf("config %q has an invalid URL for monitor %q: %w", path, monitor.Name, err) } } return cfg.Monitors, nil }