package httpapi import ( "encoding/json" "net/http" "strconv" "time" "evobgp/internal/pipeline" ) func (s *Server) resolveRevisionRetentionMinutesQuery(r *http.Request, tenantID string) (minutes int, ok bool) { if raw := r.URL.Query().Get("retention_minutes"); raw != "" { n, err := strconv.Atoi(raw) if err != nil { return 0, false } if n < pipeline.RevisionMinTTLMin || n > pipeline.RevisionMaxTTLMin { return 0, false } return n, true } return s.defaultRevisionRetentionMinutes(tenantID) } func (s *Server) defaultRevisionRetentionMinutes(tenantID string) (int, bool) { settings, err := s.store.ListGlobalSettings(tenantID) if err != nil { return pipeline.ClampRevisionRetentionMinutes(0), true } return pipeline.ClampRevisionRetentionMinutes(pipeline.RevisionRetentionMinutesFromSettings(settings)), true } func (s *Server) resolveRevisionRetentionMinutesBody(r *http.Request, tenantID string) (minutes int, ok bool) { var body struct { RetentionMinutes *int `json:"retention_minutes"` } if r.Body != nil && r.ContentLength != 0 { if err := json.NewDecoder(r.Body).Decode(&body); err != nil { return 0, false } if body.RetentionMinutes != nil { m := *body.RetentionMinutes if m < pipeline.RevisionMinTTLMin || m > pipeline.RevisionMaxTTLMin { return 0, false } return m, true } } return s.defaultRevisionRetentionMinutes(tenantID) } func (s *Server) handleRevisionPruneEstimate(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requirePerm(w, a, "bgp:operations:read") { return } minutes, valid := s.resolveRevisionRetentionMinutesQuery(r, a.TenantID) if !valid { writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "retention_minutes must be an integer in range 15..43200") return } cutoff := pipeline.RevisionCutoffFromMinutes(minutes) est, err := s.store.EstimateRevisionPrune(a.TenantID, cutoff, minutes) if err != nil { writeInternalError(w, "revision prune estimate", err) return } writeJSON(w, http.StatusOK, map[string]any{ "retention_minutes": est.RetentionMinutes, "cutoff_at": est.CutoffAt.UTC().Format(time.RFC3339Nano), "revision_count": est.RevisionCount, "prefix_row_count": est.PrefixRowCount, "orphan_snapshot_count": est.OrphanSnapshotCount, "bytes_estimate": est.BytesEstimate, }) } func (s *Server) handleRevisionPrune(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requirePerm(w, a, "bgp:operations:admin") { return } minutes, valid := s.resolveRevisionRetentionMinutesBody(r, a.TenantID) if !valid { writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "retention_minutes must be an integer in range 15..43200") return } cutoff := pipeline.RevisionCutoffFromMinutes(minutes) res, err := s.store.PruneRevisionsWithStats(a.TenantID, cutoff) if err != nil { writeInternalError(w, "revision prune", err) return } writeJSON(w, http.StatusOK, map[string]any{ "deleted_revisions": res.DeletedRevisions, "deleted_prefix_snapshots": res.DeletedPrefixSnapshots, "deleted_prefix_rows": res.DeletedPrefixRows, "bytes_estimate": res.BytesEstimate, }) }