diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1c79c39 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +node_modules +bin +dist +data.volume +*.local* +assets diff --git a/README.md b/README.md index f862fe5..1d60067 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,8 @@ statuspage ## Requirements -- Go (recent, 1.22+ for `http.ServeMux` patterns). +- Go 1.26+ (see the `go` directive in `go.mod`; the backend uses `http.ServeMux` + method patterns, added in 1.22). - A C compiler for `mattn/go-sqlite3` (the `CGO_ENABLED=1` build). - `bun` (or `npm`) for the frontend build. @@ -64,8 +65,12 @@ Then open `http://localhost:5000`. ### Docker +Clone the repo and set up your config inside it: + ```sh -cp config.local.yaml config.local.yaml # your config goes here +git clone https://git.phc.dm.unipi.it/aziis98/statuspage +cd statuspage +cp example.config.yaml config.local.yaml # your config goes here docker compose up -d --build ``` diff --git a/server/main.go b/server/main.go index a9d2b98..2f353d0 100644 --- a/server/main.go +++ b/server/main.go @@ -7,7 +7,6 @@ import ( "os" "os/exec" "path/filepath" - "strings" "github.com/joho/godotenv" "github.com/spf13/pflag" @@ -32,22 +31,8 @@ func buildFrontend() error { 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 spaHandler() http.Handler { + return http.FileServer(http.Dir("dist")) } func main() { @@ -78,7 +63,7 @@ func main() { mux.HandleFunc("GET /api/metrics/{machine}", mm.metricsHandler) mux.HandleFunc("POST /api/refresh/{machine}", mm.refreshHandler) if !*devServer { - mux.HandleFunc("GET /", spaHandler()) + mux.Handle("GET /", spaHandler()) } log.Printf("listening on %s (mock)", *addr) if err := http.ListenAndServe(*addr, mux); err != nil { @@ -118,7 +103,7 @@ func main() { mux.HandleFunc("GET /api/metrics/{machine}", mon.metricsHandler) mux.HandleFunc("POST /api/refresh/{machine}", mon.refreshHandler) if !*devServer { - mux.HandleFunc("GET /", spaHandler()) + mux.Handle("GET /", spaHandler()) } log.Printf("listening on %s", *addr) diff --git a/server/monitor.go b/server/monitor.go index f413435..6027cde 100644 --- a/server/monitor.go +++ b/server/monitor.go @@ -31,6 +31,9 @@ const checkNA = "na" const maxIPHistory = 10 +// minimum interval between interactive refresh requests for the same machine +const refreshCooldown = 5 * time.Second + type sshResultState struct { Ok bool `json:"ok"` Result string `json:"result"` @@ -125,6 +128,9 @@ type Monitor struct { statesMu sync.RWMutex states map[string]*machineState + + refreshMu sync.Mutex + lastRefresh map[string]time.Time } func NewMonitor(cfg *Config, history *History) *Monitor { @@ -136,6 +142,7 @@ func NewMonitor(cfg *Config, history *History) *Monitor { ctx: context.Background(), history: history, states: make(map[string]*machineState, len(cfg.Machines)), + lastRefresh: make(map[string]time.Time, len(cfg.Machines)), } for _, mc := range cfg.Machines { m.states[mc.ID] = &machineState{status: statusUnknown, icmp: checkNA, tcp: checkNA} @@ -765,6 +772,17 @@ func (m *Monitor) refreshHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, "machine not found", http.StatusNotFound) return } + + m.refreshMu.Lock() + if now := time.Now(); now.Sub(m.lastRefresh[id]) < refreshCooldown { + m.refreshMu.Unlock() + http.Error(w, "refresh cooldown active", http.StatusTooManyRequests) + return + } else { + m.lastRefresh[id] = now + } + m.refreshMu.Unlock() + log.Printf("interactive refresh scheduled for %s (%s)", mc.Name, mc.Host) m.Refresh(mc) w.WriteHeader(http.StatusAccepted)