package api import ( "context" "database/sql" "encoding/json" "log" "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 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 { log.Printf("http panic method=%s path=%s remote=%s panic=%v", r.Method, r.URL.Path, r.RemoteAddr, rec) panic(rec) } status := sw.status if status == 0 { status = http.StatusOK } if status >= 400 { log.Printf("http response error method=%s path=%s raw_query=%q status=%d remote=%s duration_ms=%d", r.Method, r.URL.Path, r.URL.RawQuery, status, r.RemoteAddr, time.Since(start).Milliseconds()) } }() 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) 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, Components: componentRepo, Installations: installationRepo, Timeline: timelineRepo, History: historySvc, }) 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(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"}) }