package api import ( "errors" "log/slog" "net/http" "github.com/go-chi/chi/v5" "github.com/alexei/docker-watcher/internal/events" "github.com/alexei/docker-watcher/internal/store" ) // projectRequest is the expected JSON body for creating/updating a project. type projectRequest struct { Name string `json:"name"` Registry string `json:"registry"` Image string `json:"image"` Port int `json:"port"` Healthcheck string `json:"healthcheck"` Env string `json:"env"` Volumes string `json:"volumes"` NpmAccessListID *int `json:"npm_access_list_id,omitempty"` } // listProjects handles GET /api/projects. func (s *Server) listProjects(w http.ResponseWriter, r *http.Request) { projects, err := s.store.GetAllProjects() if err != nil { slog.Error("failed to list projects", "error", err) respondError(w, http.StatusInternalServerError, "internal server error") return } respondJSON(w, http.StatusOK, projects) } // createProject handles POST /api/projects. func (s *Server) createProject(w http.ResponseWriter, r *http.Request) { var req projectRequest if !decodeJSON(w, r, &req) { return } if req.Name == "" { respondError(w, http.StatusBadRequest, "name is required") return } if req.Image == "" { respondError(w, http.StatusBadRequest, "image is required") return } if req.Env == "" { req.Env = "{}" } if req.Volumes == "" { req.Volumes = "{}" } npmAccessListID := 0 if req.NpmAccessListID != nil { npmAccessListID = *req.NpmAccessListID } project, err := s.store.CreateProject(store.Project{ Name: req.Name, Registry: req.Registry, Image: req.Image, Port: req.Port, Healthcheck: req.Healthcheck, Env: req.Env, Volumes: req.Volumes, NpmAccessListID: npmAccessListID, }) if err != nil { slog.Error("failed to create project", "error", err) respondError(w, http.StatusInternalServerError, "internal server error") return } s.eventBus.Publish(events.Event{ Type: events.EventLog, Payload: events.EventLogPayload{ Source: "admin", Severity: "info", Message: "project created: " + project.Name, }, }) respondJSON(w, http.StatusCreated, project) } // getProject handles GET /api/projects/{id}. func (s *Server) getProject(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") project, err := s.store.GetProjectByID(id) if err != nil { if errors.Is(err, store.ErrNotFound) { respondNotFound(w, "project") return } slog.Error("failed to get project", "error", err) respondError(w, http.StatusInternalServerError, "internal server error") return } // Also fetch stages for this project. stages, err := s.store.GetStagesByProjectID(id) if err != nil { slog.Error("failed to get stages", "error", err) respondError(w, http.StatusInternalServerError, "internal server error") return } respondJSON(w, http.StatusOK, map[string]any{ "project": project, "stages": stages, }) } // updateProject handles PUT /api/projects/{id}. func (s *Server) updateProject(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") existing, err := s.store.GetProjectByID(id) if err != nil { if errors.Is(err, store.ErrNotFound) { respondNotFound(w, "project") return } slog.Error("failed to get project", "error", err) respondError(w, http.StatusInternalServerError, "internal server error") return } var req projectRequest if !decodeJSON(w, r, &req) { return } // Apply updates to existing project, preserving fields not provided. updated := existing if req.Name != "" { updated.Name = req.Name } if req.Image != "" { updated.Image = req.Image } updated.Registry = req.Registry updated.Port = req.Port updated.Healthcheck = req.Healthcheck if req.Env != "" { updated.Env = req.Env } if req.Volumes != "" { updated.Volumes = req.Volumes } if req.NpmAccessListID != nil { updated.NpmAccessListID = *req.NpmAccessListID } if err := s.store.UpdateProject(updated); err != nil { slog.Error("failed to update project", "error", err) respondError(w, http.StatusInternalServerError, "internal server error") return } s.eventBus.Publish(events.Event{ Type: events.EventLog, Payload: events.EventLogPayload{ Source: "admin", Severity: "info", Message: "project updated: " + updated.Name, }, }) respondJSON(w, http.StatusOK, updated) } // deleteProject handles DELETE /api/projects/{id}. func (s *Server) deleteProject(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") // Clean up Docker containers and proxy routes before deleting the project. ctx := r.Context() stages, _ := s.store.GetStagesByProjectID(id) for _, stage := range stages { instances, _ := s.store.GetInstancesByStageID(stage.ID) for _, inst := range instances { if inst.ContainerID != "" { if err := s.docker.RemoveContainer(ctx, inst.ContainerID, true); err != nil { slog.Warn("delete project: remove container", "container", inst.ContainerID, "error", err) } } if inst.ProxyRouteID != "" { if err := s.proxyProvider.DeleteRoute(ctx, inst.ProxyRouteID); err != nil { slog.Warn("delete project: delete proxy route", "route", inst.ProxyRouteID, "error", err) } } } } if err := s.store.DeleteProject(id); err != nil { if errors.Is(err, store.ErrNotFound) { respondNotFound(w, "project") return } slog.Error("failed to delete project", "error", err) respondError(w, http.StatusInternalServerError, "internal server error") return } s.eventBus.Publish(events.Event{ Type: events.EventLog, Payload: events.EventLogPayload{ Source: "admin", Severity: "info", Message: "project deleted: " + id, }, }) respondJSON(w, http.StatusOK, map[string]string{"deleted": id}) }