package hostmode import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" ) // heartbeatResponse is what the orchestrator sends back. // Phase 1 only populates Ok. Phase 2 adds Cmd + RunID. type heartbeatResponse struct { Ok bool `json:"ok"` Cmd string `json:"cmd,omitempty"` RunID int64 `json:"run_id,omitempty"` } type hostClient struct { base string h *http.Client } func newHostClient(base string) *hostClient { return &hostClient{ base: base, h: &http.Client{Timeout: 5 * time.Second}, } } func (c *hostClient) heartbeat(ctx context.Context, mac string) (*heartbeatResponse, error) { url := fmt.Sprintf("%s/api/v1/hosts/%s/heartbeat", c.base, mac) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader([]byte(`{}`))) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") resp, err := c.h.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) return nil, fmt.Errorf("status %d: %s", resp.StatusCode, string(body)) } var out heartbeatResponse if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return nil, fmt.Errorf("decode: %w", err) } return &out, nil }