package api import ( "log/slog" "net/http" ) // pruneImages handles POST /api/docker/prune-images. // Only removes images that belong to Docker Watcher projects (not all system images). func (s *Server) pruneImages(w http.ResponseWriter, r *http.Request) { if s.docker == nil { respondError(w, http.StatusServiceUnavailable, "Docker is not available") return } // Collect all image references from our projects. projects, err := s.store.GetAllProjects() if err != nil { slog.Error("prune: failed to list projects", "error", err) respondError(w, http.StatusInternalServerError, "internal server error") return } // Build a set of image refs used by active instances. activeImages := make(map[string]bool) for _, p := range projects { stages, _ := s.store.GetStagesByProjectID(p.ID) for _, st := range stages { instances, _ := s.store.GetInstancesByStageID(st.ID) for _, inst := range instances { if inst.ImageTag != "" { activeImages[p.Image+":"+inst.ImageTag] = true } } } } // Collect all unique image bases from projects (without tags). projectImages := make(map[string]bool) for _, p := range projects { if p.Image != "" { projectImages[p.Image] = true } } if len(projectImages) == 0 { respondJSON(w, http.StatusOK, map[string]any{ "images_removed": 0, "space_reclaimed_mb": 0, "message": "No project images to clean up", }) return } // List all local Docker images and find ones matching our projects but not actively used. ctx := r.Context() removed := 0 var reclaimedBytes int64 for imageBase := range projectImages { // List all tags for this image. images, err := s.docker.ListImagesByRef(ctx, imageBase) if err != nil { slog.Warn("prune: list images", "image", imageBase, "error", err) continue } for _, img := range images { // Skip images that are actively used by running instances. if activeImages[img.Ref] { continue } // Remove unused image. if err := s.docker.RemoveImage(ctx, img.ID); err != nil { slog.Warn("prune: remove image", "image", img.Ref, "error", err) continue } removed++ reclaimedBytes += img.Size slog.Info("prune: removed image", "ref", img.Ref, "size_mb", img.Size/(1024*1024)) } } respondJSON(w, http.StatusOK, map[string]any{ "images_removed": removed, "space_reclaimed_mb": reclaimedBytes / (1024 * 1024), }) }