package api import ( "context" "io" "math" "net/http" "sync" "time" ) // svcHealth is the health of one upstream service, as shown on the panel. type svcHealth struct { Status string `json:"status"` // "ok" | "down" HTTPStatus int `json:"httpStatus,omitempty"` URL string `json:"url,omitempty"` // Fractional, and a pointer so an absent measurement (this process) stays // distinguishable from a genuinely fast one: neighbours sharing a Docker // network answer in well under a millisecond. LatencyMs *float64 `json:"latencyMs,omitempty"` Error string `json:"error,omitempty"` } // latencyMs is elapsed time in milliseconds, kept to one decimal. func latencyMs(d time.Duration) *float64 { ms := math.Round(float64(d.Microseconds())/100) / 10 return &ms } // healthClient is a short-timeout client for probing upstreams so a hung // dependency can't stall the status endpoint. var healthClient = &http.Client{Timeout: 4 * time.Second} // probe does a GET against url and classifies the result. func probe(ctx context.Context, url string) svcHealth { start := time.Now() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return svcHealth{Status: "down", URL: url, Error: err.Error()} } resp, err := healthClient.Do(req) lat := latencyMs(time.Since(start)) if err != nil { return svcHealth{Status: "down", URL: url, LatencyMs: lat, Error: err.Error()} } defer resp.Body.Close() _, _ = io.Copy(io.Discard, resp.Body) status := "ok" if resp.StatusCode >= 400 { status = "down" } return svcHealth{Status: status, LatencyMs: lat, HTTPStatus: resp.StatusCode, URL: url} } // GET /api/status — aggregate health of the API Server and its neighbours // (PocketBase and the Web App), probed server-side. The panel polls this so the // browser never has to reach PocketBase or the Web App directly. func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { var pbHealth, web svcHealth var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done(); pbHealth = probe(r.Context(), s.pb.BaseURL()+"/api/health") }() go func() { defer wg.Done(); web = probe(r.Context(), s.webAppURL()+"/healthz") }() wg.Wait() writeJSON(w, http.StatusOK, map[string]any{ "apiServer": map[string]any{"status": "ok"}, "pocketBase": pbHealth, "webApp": web, }) }