package server import ( "context" "fmt" "net/http" "sync" "time" "github.com/google/uuid" "git.wntrmute.dev/kyle/mcr/internal/gc" ) // GCLastRun records the result of the last GC run. type GCLastRun struct { StartedAt string `json:"started_at"` CompletedAt string `json:"completed_at,omitempty"` BlobsRemoved int `json:"blobs_removed"` BytesFreed int64 `json:"bytes_freed"` } // GCState tracks the current state of garbage collection. type GCState struct { mu sync.Mutex Running bool `json:"running"` LastRun *GCLastRun `json:"last_run,omitempty"` Collector *gc.Collector AuditFn AuditFunc } type gcStatusResponse struct { Running bool `json:"running"` LastRun *GCLastRun `json:"last_run,omitempty"` } type gcTriggerResponse struct { ID string `json:"id"` } // AdminTriggerGCHandler handles POST /v1/gc. func AdminTriggerGCHandler(state *GCState) http.HandlerFunc { return func(w http.ResponseWriter, _ *http.Request) { state.mu.Lock() if state.Running { state.mu.Unlock() writeAdminError(w, http.StatusConflict, "garbage collection already running") return } state.Running = true state.mu.Unlock() gcID := uuid.New().String() // Run GC asynchronously. go func() { startedAt := time.Now().UTC().Format(time.RFC3339) if state.AuditFn != nil { state.AuditFn("gc_started", "", "", "", "", map[string]string{ "gc_id": gcID, }) } var result *gc.Result var gcErr error if state.Collector != nil { result, gcErr = state.Collector.Run(context.Background()) } completedAt := time.Now().UTC().Format(time.RFC3339) state.mu.Lock() state.Running = false lastRun := &GCLastRun{ StartedAt: startedAt, CompletedAt: completedAt, } if result != nil { lastRun.BlobsRemoved = result.BlobsRemoved lastRun.BytesFreed = result.BytesFreed } state.LastRun = lastRun state.mu.Unlock() if state.AuditFn != nil && gcErr == nil { details := map[string]string{ "gc_id": gcID, } if result != nil { details["blobs_removed"] = fmt.Sprintf("%d", result.BlobsRemoved) details["bytes_freed"] = fmt.Sprintf("%d", result.BytesFreed) } state.AuditFn("gc_completed", "", "", "", "", details) } }() writeJSON(w, http.StatusAccepted, gcTriggerResponse{ID: gcID}) } } // AdminGCStatusHandler handles GET /v1/gc/status. func AdminGCStatusHandler(state *GCState) http.HandlerFunc { return func(w http.ResponseWriter, _ *http.Request) { state.mu.Lock() resp := gcStatusResponse{ Running: state.Running, LastRun: state.LastRun, } state.mu.Unlock() writeJSON(w, http.StatusOK, resp) } }