feat: configurable unused images threshold with dashboard warning
- Add image_prune_threshold_mb setting (default 1024 MB) - Add GET /api/docker/unused-images endpoint returning unused image count, size, and threshold status - Dashboard shows amber warning banner when unused project images exceed threshold - Banner links to settings page for pruning, shows count and human-readable size (MB/GB) - Threshold configurable in Docker Image Cleanup section of settings - DB migration + schema for image_prune_threshold_mb
This commit is contained in:
@@ -150,6 +150,75 @@ func sanitizeDockerLogLine(line string) string {
|
||||
return line
|
||||
}
|
||||
|
||||
// unusedImageStats handles GET /api/docker/unused-images.
|
||||
// Returns the total size of unused project images and whether the threshold is exceeded.
|
||||
func (s *Server) unusedImageStats(w http.ResponseWriter, r *http.Request) {
|
||||
if s.docker == nil {
|
||||
respondJSON(w, http.StatusOK, map[string]any{
|
||||
"total_size_mb": 0, "count": 0, "threshold_mb": 0, "exceeded": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := s.store.GetSettings()
|
||||
if err != nil {
|
||||
slog.Error("unused images: get settings", "error", err)
|
||||
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
|
||||
projects, err := s.store.GetAllProjects()
|
||||
if err != nil {
|
||||
slog.Error("unused images: list projects", "error", err)
|
||||
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
|
||||
// Build set of active image refs.
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sum unused image sizes.
|
||||
ctx := r.Context()
|
||||
var totalSize int64
|
||||
var count int
|
||||
for _, p := range projects {
|
||||
if p.Image == "" {
|
||||
continue
|
||||
}
|
||||
images, err := s.docker.ListImagesByRef(ctx, p.Image)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, img := range images {
|
||||
if !activeImages[img.Ref] {
|
||||
totalSize += img.Size
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalMB := totalSize / (1024 * 1024)
|
||||
exceeded := settings.ImagePruneThresholdMB > 0 && int(totalMB) >= settings.ImagePruneThresholdMB
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]any{
|
||||
"total_size_mb": totalMB,
|
||||
"count": count,
|
||||
"threshold_mb": settings.ImagePruneThresholdMB,
|
||||
"exceeded": exceeded,
|
||||
})
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
Reference in New Issue
Block a user