package nodedispatch import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "evobgp/internal/store" ) // AgentHealthResult is the parsed outcome of GET /v1/agent/health on a replica. type AgentHealthResult struct { OK bool Error string LastAppliedRevisionID string LastSyncAt string } // FetchAgentHealth GETs /v1/agent/health for UI Connected/Offline status. func FetchAgentHealth(ctx context.Context, sp *store.Speaker, opts Options) AgentHealthResult { if sp == nil { return AgentHealthResult{Error: "nil speaker"} } meta := store.ParseSpeakerMeta(sp.MetaJSON) url := store.AgentHealthURL(meta) if url == "" { return AgentHealthResult{Error: "agent_domain not configured"} } secret := strings.TrimSpace(meta.AgentSecret) if secret == "" { return AgentHealthResult{Error: "agent_secret missing"} } req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return AgentHealthResult{Error: err.Error()} } req.Header.Set("Authorization", "Bearer "+secret) resp, err := opts.client().Do(req) if err != nil { return AgentHealthResult{Error: err.Error()} } defer func() { _ = resp.Body.Close() }() b, _ := io.ReadAll(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { return AgentHealthResult{ Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(b))), } } var out struct { OK bool `json:"ok"` LastAppliedRevisionID string `json:"last_applied_revision_id"` LastSyncAt string `json:"last_sync_at"` } if err := json.Unmarshal(b, &out); err != nil { return AgentHealthResult{Error: err.Error()} } res := AgentHealthResult{ OK: out.OK, LastAppliedRevisionID: strings.TrimSpace(out.LastAppliedRevisionID), LastSyncAt: strings.TrimSpace(out.LastSyncAt), } if !res.OK { res.Error = "agent reported ok=false" } return res }