package api import ( "context" "crypto/rand" "database/sql" "encoding/hex" "encoding/json" "log/slog" "net/http" "time" "reanimator/internal/history" "reanimator/internal/ingest" "reanimator/internal/repository/failures" "reanimator/internal/repository/registry" "reanimator/internal/repository/timeline" ) type Server struct { httpServer *http.Server cancelBg context.CancelFunc } type statusCaptureResponseWriter struct { http.ResponseWriter status int } func (w *statusCaptureResponseWriter) WriteHeader(status int) { w.status = status w.ResponseWriter.WriteHeader(status) } func (w *statusCaptureResponseWriter) Write(b []byte) (int, error) { if w.status == 0 { w.status = http.StatusOK } return w.ResponseWriter.Write(b) } func generateRequestID() string { b := make([]byte, 8) _, _ = rand.Read(b) return hex.EncodeToString(b) } func withRequestID(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := generateRequestID() w.Header().Set("X-Request-ID", id) next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), requestIDCtxKey, id))) }) } func withErrorLogging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sw := &statusCaptureResponseWriter{ResponseWriter: w} start := time.Now() defer func() { if rec := recover(); rec != nil { slog.Error("http panic", "method", r.Method, "path", r.URL.Path, "remote", r.RemoteAddr, "duration_ms", time.Since(start).Milliseconds(), "panic", rec, "request_id", requestIDFromCtx(r.Context()), ) if sw.status == 0 { writeJSON(sw, http.StatusInternalServerError, map[string]string{"error": "internal server error"}) } return } status := sw.status if status == 0 { status = http.StatusOK } if status >= 400 { slog.Warn("http response error", "method", r.Method, "path", r.URL.Path, "query", r.URL.RawQuery, "status", status, "remote", r.RemoteAddr, "duration_ms", time.Since(start).Milliseconds(), "request_id", requestIDFromCtx(r.Context()), ) } }() next.ServeHTTP(sw, r) }) } func NewServer(addr string, readTimeout, writeTimeout time.Duration, db *sql.DB) *Server { mux := http.NewServeMux() mux.HandleFunc("/health", healthHandler) cancelBg := func() {} if db != nil { bgCtx, cancel := context.WithCancel(context.Background()) cancelBg = cancel failureRepo := failures.NewFailureRepository(db) assetRepo := registry.NewAssetRepository(db) assetLogRepo := registry.NewAssetEventLogRepository(db) platformConfigRepo := registry.NewAssetPlatformConfigRepository(db) componentRepo := registry.NewComponentRepository(db) installationRepo := registry.NewInstallationRepository(db) timelineRepo := timeline.NewEventRepository(db) historySvc := history.NewService(db) historySvc.StartWorker(bgCtx) RegisterRegistryRoutes(mux, RegistryDependencies{ Assets: assetRepo, Components: componentRepo, History: historySvc, }) RegisterHistoryRoutes(mux, HistoryDependencies{ Service: historySvc, }) RegisterIngestRoutes(mux, IngestDependencies{ Service: ingest.NewService(db), }) RegisterAssetComponentRoutes(mux, AssetComponentDependencies{ Assets: assetRepo, AssetLogs: assetLogRepo, Components: componentRepo, Installations: installationRepo, Timeline: timelineRepo, History: historySvc, PlatformConfig: platformConfigRepo, }) RegisterFailureRoutes(mux, FailureDependencies{ Failures: failureRepo, Assets: assetRepo, Components: componentRepo, Installations: installationRepo, History: historySvc, }) RegisterUIRoutes(mux, UIDependencies{ Assets: assetRepo, Components: componentRepo, Installations: installationRepo, Timeline: timelineRepo, Failures: failureRepo, }) } return &Server{ httpServer: &http.Server{ Addr: addr, Handler: withErrorLogging(withRequestID(mux)), ReadTimeout: readTimeout, WriteTimeout: writeTimeout, }, cancelBg: cancelBg, } } func (s *Server) Start() error { return s.httpServer.ListenAndServe() } func (s *Server) Shutdown(ctx context.Context) error { if s.cancelBg != nil { s.cancelBg() } return s.httpServer.Shutdown(ctx) } func healthHandler(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) }