feat: global Docker health indicator and graceful degradation
- GET /api/health endpoint returning Docker connectivity status - Sidebar shows Docker connection dot (green=connected, red=disconnected) - Stale scanner returns store-only results when Docker is unavailable - Polls health every 30s
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// getHealth handles GET /api/health.
|
||||
// Returns connectivity status for Docker and other services.
|
||||
func (s *Server) getHealth(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dockerOK := false
|
||||
if s.docker != nil {
|
||||
if err := s.docker.Ping(ctx); err == nil {
|
||||
dockerOK = true
|
||||
}
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]any{
|
||||
"docker": dockerOK,
|
||||
})
|
||||
}
|
||||
@@ -130,6 +130,7 @@ func (s *Server) Router() chi.Router {
|
||||
r.Use(auth.Middleware(s.localAuth))
|
||||
|
||||
// Read-only endpoints (any authenticated user).
|
||||
r.Get("/health", s.getHealth)
|
||||
r.Get("/auth/me", s.currentUser)
|
||||
r.Get("/projects", s.listProjects)
|
||||
r.Route("/projects/{id}", func(r chi.Router) {
|
||||
|
||||
@@ -216,7 +216,9 @@ func (s *Scanner) FindStaleInstances(ctx context.Context) ([]StaleInstance, erro
|
||||
|
||||
containers, err := s.docker.ListContainers(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list docker containers: %w", err)
|
||||
// Docker unavailable — fall back to store-only detection (no live state).
|
||||
slog.Warn("stale scanner: docker unavailable, using store status only", "error", err)
|
||||
containers = nil
|
||||
}
|
||||
|
||||
containerStateByInstanceID := make(map[string]string, len(containers))
|
||||
|
||||
Reference in New Issue
Block a user