// sense-orphan is a local, PostgreSQL-backed operational command. Report is // read-only; apply requires a fresh scan ID, actor and exact confirmation. package main import ( "context" "encoding/json" "errors" "flag" "fmt" "net/http" "os" "os/signal" "strings" "syscall" "yovision/sense/internal/config" "yovision/sense/internal/metrics" "yovision/sense/internal/mtx" "yovision/sense/internal/orphan" "yovision/sense/internal/store" ) func main() { if err := run(); err != nil { _, _ = fmt.Fprintln(os.Stderr, "sense-orphan failed:", err) os.Exit(1) } } func run() error { mode := flag.String("mode", "report", "report or apply") scanID := flag.String("scan-id", "", "fresh scan ID required by apply") actor := flag.String("actor", "", "operator identifier required by apply") confirm := flag.String("confirm", "", "exact confirmation: DELETE ") flag.Parse() cfg, err := config.Load() if err != nil { return err } if !strings.EqualFold(strings.TrimSpace(cfg.DatabaseDriver), store.DriverPostgres) { return errors.New("sense-orphan requires SENSE_DB_DRIVER=postgres") } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() repository, err := store.OpenPostgres(ctx, cfg.DatabaseDSN) if err != nil { return err } defer repository.Close() media, err := mtx.NewClient(cfg.MediaMTXURL, &http.Client{Timeout: cfg.ReconcileOperationTimeout}) if err != nil { return err } instanceID := cfg.InstanceID if instanceID == "" { instanceID, err = metrics.GenerateInstanceID() if err != nil { return err } } manager := orphan.New(repository, media, instanceID, metrics.New(instanceID, "operator")) encoder := json.NewEncoder(os.Stdout) switch *mode { case "report": scan, err := manager.Report(ctx) if err != nil { return err } return encoder.Encode(struct { ScanID string `json:"scan_id"` Observed int `json:"observed"` OwnedStale int `json:"owned_stale"` Unowned int `json:"unowned"` SafetyAllowed bool `json:"safety_allowed"` SafetyReason string `json:"safety_reason"` ExpiresAt string `json:"expires_at"` }{ ScanID: scan.ID, Observed: scan.ObservedCount, OwnedStale: scan.OwnedStaleCount, Unowned: scan.UnownedCount, SafetyAllowed: scan.SafetyAllowed, SafetyReason: scan.SafetyReason, ExpiresAt: scan.ExpiresAt.UTC().Format("2006-01-02T15:04:05Z07:00"), }) case "apply": result, err := manager.Apply(ctx, *scanID, *actor, *confirm) if encodeErr := encoder.Encode(result); encodeErr != nil { return encodeErr } return err default: return errors.New("mode must be report or apply") } }