package server import ( "context" "encoding/json" "net/http" "time" "chat-agent-system/internal/config" "chat-agent-system/internal/workspace" ) func (s *Server) handleAdminWorkspaces(w http.ResponseWriter, r *http.Request) { if _, ok := s.requireAdmin(w, r); !ok { return } s.serveAdminWorkspaces(w, r) } func (s *Server) handleAdminWorkspaceCleanup(w http.ResponseWriter, r *http.Request) { if _, ok := s.requireAdmin(w, r); !ok { return } s.serveAdminWorkspaceCleanup(w, r) } func (s *Server) workspaceRegistry() *workspace.FileRegistry { if s.wsReg != nil { return s.wsReg } if s.cfg == nil || s.cfg.DataDir == "" { return nil } s.wsReg = workspace.NewFileRegistry(s.cfg.DataDir) return s.wsReg } func (s *Server) serveAdminWorkspaces(w http.ResponseWriter, r *http.Request) { reg := s.workspaceRegistry() if reg == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "workspace registry unavailable") return } if err := reg.Discover(); err != nil { writeDomainError(w, r, http.StatusInternalServerError, err) return } list, err := reg.List() if err != nil { writeDomainError(w, r, http.StatusInternalServerError, err) return } var bytes int64 for _, rec := range list { bytes += rec.Bytes } writeJSONResponse(w, http.StatusOK, map[string]any{ "workspaces": list, "stats": map[string]any{ "count": len(list), "bytes": bytes, }, }) } func (s *Server) serveAdminWorkspaceCleanup(w http.ResponseWriter, r *http.Request) { reg := s.workspaceRegistry() if reg == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "workspace registry unavailable") return } id := r.PathValue("id") var body struct { Frozen *workspace.FrozenManifest `json:"frozen"` Receipts []workspace.RetentionReceipt `json:"receipts"` } if r.Body != nil { _ = json.NewDecoder(r.Body).Decode(&body) } if body.Frozen != nil { if err := reg.SetRetention(id, *body.Frozen, body.Receipts); err != nil { writeDomainError(w, r, http.StatusBadRequest, err) return } } cap := s.wsCap() if cap == nil && s.cfg != nil { cap = workspace.NewCapability(s.cfg.DataDir, nil) } if err := reg.CleanupOne(r.Context(), id, cap); err != nil { status := http.StatusConflict if err == workspace.ErrWorkDirNotFound { status = http.StatusNotFound } writeDomainError(w, r, status, err) return } writeJSONResponse(w, http.StatusOK, map[string]any{"deleted": id}) } func workspaceRegistryFor(cfg *config.Config) *workspace.FileRegistry { if cfg == nil || cfg.DataDir == "" { return nil } return workspace.NewFileRegistry(cfg.DataDir) } func (s *Server) startWorkspaceGC() { reg := s.workspaceRegistry() stopCh, ok := s.wsGCLoop.start() if reg == nil || s.cfg == nil || !ok { return } ttl := s.cfg.WorkspaceTTL quota := s.cfg.WorkspaceQuotaBytes go func() { ticker := time.NewTicker(time.Hour) defer ticker.Stop() for { select { case <-stopCh: return case <-ticker.C: cap := s.wsCap() if cap == nil { cap = workspace.NewCapability(s.cfg.DataDir, nil) } _, _ = reg.CollectGarbage(context.Background(), time.Now(), ttl, quota, cap) } } }() }