// Package httpapi exposes the Go backend's process health and database // readiness endpoints without wiring them into the production application. package httpapi import ( "context" "encoding/json" "net/http" "time" ) const appID = "zhinian-web-studio" const defaultReadinessTimeout = 3 * time.Second // DatabaseStatus is the database configuration state exposed by health and // readiness responses. type DatabaseStatus struct { Backend string `json:"backend"` Configured bool `json:"configured"` } // Readiness is the HTTP module's seam to database configuration and probing. type Readiness interface { Status() DatabaseStatus Ready(context.Context) (DatabaseStatus, error) } type handler struct { readiness Readiness readinessTimeout time.Duration healthDetails HealthDetails healthProvider func(context.Context) HealthDetails } // HealthDetails contains runtime compatibility fields assembled by the // application. Keeping this as injected data prevents the transport layer // from coupling itself to process environment configuration. type HealthDetails struct { VisualAPIMode string `json:"visualApiMode"` EvolinkMode string `json:"evolinkMode"` SeedanceMode string `json:"seedanceMode"` BailianMode string `json:"bailianMode"` MinimaxMode string `json:"minimaxMode"` AuthMode string `json:"authMode"` Capabilities []any `json:"capabilities"` } // Option configures the HTTP handler. type Option func(*handler) // WithReadinessTimeout sets the maximum duration of a database readiness probe. func WithReadinessTimeout(timeout time.Duration) Option { return func(h *handler) { if timeout > 0 { h.readinessTimeout = timeout } } } // WithHealthDetails injects the provider, authentication, and capability // summary exposed by the TypeScript-compatible health contract. func WithHealthDetails(details HealthDetails) Option { return func(h *handler) { h.healthDetails = details if h.healthDetails.Capabilities == nil { h.healthDetails.Capabilities = []any{} } } } // WithHealthDetailsProvider evaluates mutable provider state for every health // request while preserving /api/health as a non-blocking liveness endpoint. func WithHealthDetailsProvider(provider func(context.Context) HealthDetails) Option { return func(h *handler) { h.healthProvider = provider } } // NewHandler returns the foundation health/readiness HTTP handler. func NewHandler(readiness Readiness, options ...Option) http.Handler { h := &handler{readiness: readiness, readinessTimeout: defaultReadinessTimeout, healthDetails: HealthDetails{Capabilities: []any{}}} for _, option := range options { option(h) } return h } func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/health" && r.URL.Path != "/api/ready" { http.NotFound(w, r) return } if r.Method != http.MethodGet { w.Header().Set("Allow", http.MethodGet) http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } if r.URL.Path == "/api/ready" { h.serveReady(w, r) return } status := DatabaseStatus{Backend: "invalid", Configured: false} if h.readiness != nil { status = h.readiness.Status() } details := h.healthDetails if h.healthProvider != nil { details = h.healthProvider(r.Context()) if details.Capabilities == nil { details.Capabilities = []any{} } } writeJSON(w, http.StatusOK, struct { OK bool `json:"ok"` AppID string `json:"appId"` WebOnly bool `json:"webOnly"` HealthDetails Database DatabaseStatus `json:"database"` }{ OK: true, AppID: appID, WebOnly: true, HealthDetails: details, Database: status, }) } func (h *handler) serveReady(w http.ResponseWriter, r *http.Request) { status := DatabaseStatus{Backend: "invalid", Configured: false} if h.readiness == nil { writeReadyJSON(w, http.StatusServiceUnavailable, false, status) return } ctx, cancel := context.WithTimeout(r.Context(), h.readinessTimeout) defer cancel() status, err := h.readiness.Ready(ctx) if err != nil { writeReadyJSON(w, http.StatusServiceUnavailable, false, status) return } writeReadyJSON(w, http.StatusOK, true, status) } func writeReadyJSON(w http.ResponseWriter, status int, ok bool, database DatabaseStatus) { writeJSON(w, status, struct { OK bool `json:"ok"` Database DatabaseStatus `json:"database"` }{OK: ok, Database: database}) } func writeJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(body) }