Skip to content

feat(orphan): add auto cleanup mode - #897

Merged
s0up4200 merged 154 commits into
mainfrom
feat/orphan-scan-auto-cleanup
Dec 31, 2025
Merged

feat(orphan): add auto cleanup mode#897
s0up4200 merged 154 commits into
mainfrom
feat/orphan-scan-auto-cleanup

Conversation

@s0up4200

@s0up4200 s0up4200 commented Dec 30, 2025

Copy link
Copy Markdown
Collaborator

Add opt-in auto-cleanup that allows scheduled orphan scans to automatically delete found orphans without manual confirmation.

Behavior:

  • Manual scans always show preview (unchanged)
  • Scheduled scans + auto-cleanup OFF: show preview (current behavior)
  • Scheduled scans + auto-cleanup ON: auto-delete after scan

Safety features:

  • Default OFF (opt-in only)
  • File count threshold (default 100): if scan finds more files than threshold, skip auto-cleanup and require manual review
  • Existing safeguards (grace period, ignore paths) still apply

Settings:

  • autoCleanupEnabled: bool (default false)
  • autoCleanupMaxFiles: int (default 100)

Also removes the requirement to enable scanning before configuring settings - users can now set up everything first, then enable.

Goes in after #889

Summary by CodeRabbit

  • New Features
    • Automatic cleanup for orphan scans with enable/disable and max-files threshold for scheduled runs.
  • UI Changes
    • Orphan Scan settings: Auto-Cleanup toggle and max-files input; schedule inputs always visible.
    • Reannounce moved to a dedicated settings dialog.
    • Tracker picker shows merged nicknames and icons.
    • Multi-select can hide check icons.
  • Improvements
    • Layout and header refinements across instance preference overviews.

✏️ Tip: You can customize this high-level summary in your review settings.

Add ability to automatically delete torrents when seeding goals are
reached (ratio or seeding time limit). Supports two delete modes:
- delete: removes torrent entry, keeps files
- deleteWithFiles: removes torrent entry and data files

Deletion only triggers when torrent is completed and has reached either
the configured ratio limit or seeding time limit.
Add new delete mode "deleteWithFilesPreserveCrossSeeds" that safely
deletes torrents and files only when no cross-seed is detected sharing
the same data. When a cross-seed is found (matching ContentPath), only
the torrent entry is removed while files are preserved.

- Add detectCrossSeeds() function using ContentPath comparison
- Add normalizePath() helper for consistent path matching
- Update deletion logic to route cross-seed mode to appropriate action
- Add 4th option to TrackerRulesPanel dropdown
- Update badge display to show "XS safe" for cross-seed mode
Add ability to automatically delete torrents that are no longer registered
with the tracker. Uses existing 60-second cached UnregisteredSet from
SyncManager.

- Add delete_unregistered column to tracker_rules table
- Add DeleteUnregistered field to model, handler, and types
- Add unregistered deletion logic to service (reuses delete modes)
- Add checkbox and badge to TrackerRulesPanel
- Include torrent name, hash, and specific reason in all deletion logs
- Log ratio/seeding time values that triggered deletion
- Add filesKept boolean for clear indication of file fate
- Consolidate cross-seed detection into single log message
- Use user-friendly summary: "removed N torrents (files kept)" vs "with files"
Previously torrents were marked in lastDeleted map before BulkAction,
blocking retry for 5 minutes even if deletion failed.

- Move deletion timestamp to after successful BulkAction
- Add hashes to failure logs for debugging
…ents

- Add queuedForDeletion set to skip torrents already queued by ratio/seeding
  limits when processing unregistered torrents
- Update DeleteMode comments to include deleteWithFilesPreserveCrossSeeds
- Move "Delete unregistered" checkbox before delete mode selector
- Disable checkbox when no delete mode is selected
- Add tooltip explaining why checkbox is disabled
- Clear deleteUnregistered when switching to "Don't delete"
- Clarify that "Don't delete" just pauses torrents
Use AlertDialog for delete confirmation, consistent with rest of codebase.
…tion

- Introduced a new option to apply rules to all trackers, allowing users to select a single pattern ("*") for broader matching.
- Updated validation to require at least one tracker or the 'Apply to all' option to be enabled.
- Adjusted the TrackerRulesPanel to include a toggle for the new feature, improving user experience in tracker management.
- Add 'Speed limits' and 'Seeding limits & auto-delete' sections
- Add OR separator between ratio and seeding time fields
- Make dialog scrollable with fixed header/footer
- Move Enabled toggle to footer alongside Cancel/Save buttons
- Constrain dialog height to 90dvh for smaller screens
… categories and tags

- Refactor TrackerRule and TrackerRulePayload to replace single category and tag fields with arrays for categories and tags.
- Implement cleanStringSlice function to handle trimming and filtering of category and tag inputs.
- Update database interactions to accommodate new array structure for categories and tags.
- Modify TrackerRulesPanel to support multi-select for categories and tags, enhancing user experience in rule management.
Add tagMatchMode field to tracker rules allowing users to choose whether
torrents must match ANY (default) or ALL of the specified tags. Categories
continue to use ANY matching since torrents can only have one category.

- Add TagMatchModeAny/TagMatchModeAll constants to reduce string literals
- Add tag_match_mode column via migration 037
- Update selectRule() to handle both matching modes
- Add input validation for tagMatchMode in Create/Update handlers
- Add frontend toggle (shown when 2+ tags selected)
- Auto-reset tagMatchMode to "any" when tags drop below 2
- Update RuleSummary to display "(any)" or "(all)" for multiple tags
Add database-backed activity logging for tracker rules that persists
through restarts. Records deletions (ratio/seeding/unregistered) and
errors (delete/limit failures) with tracker domain, rule info, and
detailed metadata.

- Add tracker_rule_activity table with migration
- Add TrackerRuleActivityStore with Create, ListByInstance, DeleteOlderThan, Prune
- Record activity on successful/failed deletions and limit changes
- Add API endpoints for listing and clearing activity
- Add TrackerRulesActivityOverview component on Services page
- Auto-prune entries older than 7 days (configurable)
- Include tracker domain in activity records for filtering
…vior

Reduce MaxBatchHashes from 150 to 50 to match qBittorrent's
max_concurrent_http_announces default. This ensures each batch completes
tracker announces in a single wave (~2s) rather than multiple waves,
reducing timeout risk with reverse proxies while maintaining the same
total processing time.

Add documentation explaining that libtorrent automatically sends stopped
announces with final stats when torrents are deleted, so no manual
pause/reannounce step is needed. Works identically for v1 and v2 torrents.
- Add deleteMode validation in Create/Update handlers
- Log JSON decode errors for debugging
- Return 503 when service unavailable in ApplyNow
- Log activity store errors instead of discarding
- Add cleanupStaleEntries to prevent memory growth in lastDeleted map
- Fix "Delete all" to actually delete all activity (days=0)
Add comprehensive tests for the trackerrules service covering:
- matchesTracker: pattern matching (wildcard, exact, suffix, glob, multi-pattern)
- shouldDeleteTorrent: deletion logic (progress, mode, limits, ratio, seeding time)
- detectCrossSeeds: cross-seed detection with path normalization
- selectRule: rule selection (enabled, tracker, category, tag modes)
- limitHashBatch: batch splitting edge cases
- normalizePath: path normalization
- torrentHasTag: tag matching

92 test cases covering the high-risk decision logic areas.
… labels

- Add tracker customization support (icons and display names)
- Change outcome badge from 'success' to 'Removed' for clarity
- Rename failed action labels to avoid redundancy:
  - 'Delete failed' → 'Delete' + 'Failed'
  - 'Limit failed' → 'Set limits' + 'Failed'
- Update description to 'History of torrents removed by tracker rules'
Only show event.reason for failures where it contains the actual error
message. For successes, the action badge and details line already
provide all necessary information without the redundant text.
Store deleteMode in activity details JSON so the frontend can display
contextual labels based on the configured delete mode and outcome:

- "Torrent only" - delete mode configured to keep files
- "Files kept due to cross-seeds" - cross-seeds detected, files preserved
- "With files" - files deleted as configured
- "Files kept"/"Files deleted" - legacy fallback for existing records
- Extract TrackerRuleDialog into reusable component
- Remove redundant summary section and Configure button from overview
- Add direct edit/delete actions on each rule in overview
- Use TruncatedText for rule names and release names (tooltip on truncate only)
- Add cursor-default to non-interactive badges
- Fix "All trackers" detection when pattern is "*"
- Add delete mode badge to rule preview
- Extend MultiSelect to support optional icon prop in options
- TrackerRuleDialog now shows tracker icons with display names
- Merged domains display as single option (e.g., "BTN" vs individual domains)
- Map existing rule domains to merged options when editing
- Update Services page to 2-column grid layout for rules/activity
- Expand all instance accordions by default on first load
- Update description text to "Automatic limits and deletion"
- Move rule name to details row in activity overview
…s and actions

- Restructure dialog: triggers first (seeding limits, unregistered), action last
- Rename section from 'Seeding limits & auto-delete' to 'Auto-removal'
- Clearer action labels: 'Pause torrent' vs 'Remove torrent (keep files)' etc
- Destructive red styling for removal actions in dropdown and when selected
- Always-enabled unregistered checkbox with auto-set removal action
- Add validation: block submission if unregistered enabled without removal action
- Update min values: ratio min=0.01, time min=1 (prevent invalid 0/negative)
- Update speed limits description to 'Applied continuously to matching torrents'
- Remove unused imports (DialogDescription, Tooltip)
Add a powerful condition system for tracker rules that allows users to
create complex deletion criteria using a visual query builder.

Backend:
- Add condition evaluator with AND/OR logic, nested groups, and negation
- Support string operators (equals, contains, starts/ends with, regex)
- Support numeric operators (comparisons, between ranges)
- Add preview endpoint to show affected torrents before enabling rules
- Add `deleted_condition` activity action for expression-based deletions
- Fix deletion reason tracking (prefer seeding time when both limits met)

Frontend:
- Add QueryBuilder component with drag-and-drop condition reordering
- Support 20+ torrent fields (name, size, ratio, seeding time, state, etc.)
- Add rule preview dialog showing torrents that would be deleted
- Add unregistered torrent preview support
- Add "Load more" pagination to activity overview
- Remove auto-expand behavior from accordions

Migration adds conditions JSON column to tracker_rules table.
- Add shadcn Calendar component with react-day-picker for date selection
- Add theme-aware CSS overrides for calendar to use app color variables
- Timestamp fields now use styled calendar popover instead of native input
- BETWEEN operator for timestamps shows two date pickers (From - To)
- Duration fields now include hours option (minutes, hours, days)
- Fix duration unit selection bug where unit couldn't be changed before entering value
- Add helper text to auto-removal section explaining the options
- Add previewLimit/previewOffset params to preview API endpoint
- Implement 'Load more' button in preview dialog for large result sets
- Apply ratio color coding to preview table
- Use TruncatedText for category column
…d selector

- Add deleteUnregisteredMinAge field to support minimum age filtering for
  unregistered torrent deletion (simple mode with dropdown: 1/3/7/14/30 days)
- Add IS_UNREGISTERED boolean field to query builder for advanced mode,
  allowing isUnregistered to be combined with any other conditions
- Add EvalContext to pass unregistered status to condition evaluator
- Create new Tracker field group with: Tracker, Trackers, Private,
  Unregistered, Comment
- Replace field dropdown with searchable combobox using Command component
  for better UX when selecting from many fields
- Add delete mode constants to replace magic strings
- Fix nil map access in PreviewDeleteRule when health counts are empty
Add tag action as 4th mutually exclusive action type alongside
speedLimits, pause, and delete. Implements tqm-style smart tagging:

- Three modes: full (add+remove), add-only, remove-only
- Condition-based evaluation for when to apply/remove tags
- Version-aware API: uses SetTags (qBit 5.1+) or falls back to
  AddTags/RemoveTags for older versions
- Batch activity logging with per-tag counts for adds/removes
- Startup race condition fix: skips tag evaluation when health
  data isn't available yet for IS_UNREGISTERED conditions

Also removes dead code: lastDeleted map and 5-minute deletion
cooldown (deleted torrents can't be re-processed anyway).
- Rename internal/services/trackerrules/ to internal/services/automations/
- Rename models: TrackerRule -> Automation, TrackerRuleStore -> AutomationStore
- Rename handlers: TrackerRuleHandler -> AutomationHandler
- Update API route: /tracker-rules -> /automations
- Add migration 036 to drop old tables and create new automations tables
- Update all frontend components and types to use Automation naming
- Update README to reference Automations feature
- Removed timestamp fields from CONDITION_FIELDS and updated FIELD_GROUPS accordingly.
- Adjusted FieldType to exclude 'timestamp' and removed related operators from OPERATORS_BY_TYPE.
Make timeout configurable via Config.ApplyTimeout for large instances.
Replace defer resp.Body.Close() with httphelpers.DrainAndClose(resp)
in arr client to drain response bodies before closing, enabling proper
HTTP keep-alive connection reuse.
Add opt-in auto-cleanup that allows scheduled orphan scans to
automatically delete found orphans without manual confirmation.

Behavior:
- Manual scans always show preview (unchanged)
- Scheduled scans + auto-cleanup OFF: show preview (current behavior)
- Scheduled scans + auto-cleanup ON: auto-delete after scan

Safety features:
- Default OFF (opt-in only)
- File count threshold (default 100): if scan finds more files than
  threshold, skip auto-cleanup and require manual review
- Existing safeguards (grace period, ignore paths) still apply

Settings:
- autoCleanupEnabled: bool (default false)
- autoCleanupMaxFiles: int (default 100)

Also removes the requirement to enable scanning before configuring
settings - users can now set up everything first, then enable.
@coderabbitai

coderabbitai Bot commented Dec 30, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds auto-cleanup controls for orphan scans: two new settings (AutoCleanupEnabled, AutoCleanupMaxFiles) persisted via a DB migration, wired through models, API handlers, service logic (maybeAutoCleanup that may trigger deletions after scheduled scans), and exposed in the frontend settings UI.

Changes

Cohort / File(s) Summary
Database Schema
internal/database/migrations/045_add_orphan_scan_auto_cleanup.sql
Adds auto_cleanup_enabled (INTEGER NOT NULL DEFAULT 0) and auto_cleanup_max_files (INTEGER NOT NULL DEFAULT 100) to orphan_scan_settings.
Data Models & Persistence
internal/models/orphan_scan.go
Adds AutoCleanupEnabled bool and AutoCleanupMaxFiles int to OrphanScanSettings; updates GetSettings and UpsertSettings to read/write the new columns.
Service Types & Defaults
internal/services/orphanscan/types.go, internal/services/orphanscan/config.go
Adds the two fields to Settings and DefaultSettings (defaults: false, 100).
Service Logic
internal/services/orphanscan/service.go
Adds maybeAutoCleanup(...), invoked after scan completion; verifies run trigger (scheduled), checks enabled flag and files-found ≤ threshold (with fallback), and initiates ConfirmDeletion when conditions met.
API Handler
internal/api/handlers/orphan_scan.go
Extends OrphanScanSettingsPayload with AutoCleanupEnabled *bool and AutoCleanupMaxFiles *int; applies defaults and validates AutoCleanupMaxFiles (min 1) in upsert flow.
Frontend types & form
web/src/types/index.ts, web/src/components/instances/preferences/OrphanScanSettingsForm.tsx
Adds autoCleanupEnabled and autoCleanupMaxFiles to types and defaults; UI adds Auto-Cleanup section (toggle + conditional max-files input) and includes fields in update payload with validation.
Related UI & UX
web/src/components/instances/preferences/OrphanScanOverview.tsx, ReannounceOverview.tsx, ReannounceSettingsDialog.tsx, TrackerReannounceForm.tsx, WorkflowDialog.tsx, multi-select.tsx, Automations.tsx
Misc UI refactors: header layout and icon aliasing, new ReannounceSettingsDialog, enriched tracker options/icons, added hideCheckIcon prop to MultiSelect, and related layout/interaction tweaks.

Sequence Diagram

sequenceDiagram
    participant Scheduler
    participant Service
    participant DB
    participant Deleter

    Note over Scheduler,Service: Scheduled orphan scan run
    Scheduler->>Service: Trigger scan (scheduled)
    Service->>Service: Execute scan, collect orphans (filesFound)
    Service->>DB: Update run record (preview_ready, results)
    Service->>Service: maybeAutoCleanup(ctx, instanceID, runID, settings, filesFound)
    Service->>DB: Fetch run details (verify TriggeredBy)
    DB-->>Service: Run info (e.g., TriggeredBy="scheduled")
    Service->>Service: Check settings.AutoCleanupEnabled and threshold (AutoCleanupMaxFiles)
    alt Conditions met (scheduled && enabled && filesFound ≤ threshold)
        Service->>Deleter: Initiate ConfirmDeletion(runID, files)
        Deleter->>DB: Record deletion actions / mark files
        Note over Service,Deleter: Automatic cleanup started
    else Conditions not met
        Service->>Service: Log reason and skip auto-cleanup
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement, web

Poem

🐰 I hopped through caches, sniffed each file,
Two tiny toggles made cleanup worthwhile.
If scheduled finds only a few,
I nibble tidy — quick and true,
Hooray for neatness, hop in style! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(orphan): add auto cleanup mode' clearly summarizes the main change: introducing an auto-cleanup feature for orphan scans. It is specific, concise, and directly reflects the primary functionality addition.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/orphan-scan-auto-cleanup

📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fdd1daa and ded176a.

📒 Files selected for processing (1)
  • web/src/types/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/types/index.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Base automatically changed from feat/cross/arr-id-lookup to main December 30, 2025 23:38
…headers

Move Switch components outside AccordionTrigger to avoid invalid HTML
nesting (button inside button). Use CSS Grid layout to maintain proper
spacing with controls aligned to the right.
…splay

- Replace Sheet with Dialog for reannounce settings to match orphan scan pattern
- Show settings form even when monitoring is disabled (allow config before enabling)
- Add tracker icons and customizations to tracker domain selector
- Add hideCheckIcon prop to MultiSelect for better alignment when using icons
@s0up4200 s0up4200 changed the title feat(tracker-rules): add optional delete mode for torrents feat(orphan): add auto delete mode Dec 31, 2025
@s0up4200 s0up4200 changed the title feat(orphan): add auto delete mode feat(orphan): add auto cleanup mode Dec 31, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
web/src/components/instances/preferences/OrphanScanOverview.tsx (1)

211-223: Consider displaying auto-cleanup settings in the summary.

The settings summary displays grace period, interval, and max files, but doesn't show the new auto-cleanup settings (autoCleanupEnabled, autoCleanupMaxFiles) introduced in this PR. Since auto-cleanup can automatically delete files without confirmation, users might benefit from seeing its status at a glance in this overview.

If this information is deemed important for quick reference, consider adding it to the summary line or as a separate indicator.

web/src/components/instances/preferences/TrackerReannounceForm.tsx (1)

98-167: LGTM! Comprehensive tracker customization logic.

The tracker processing correctly handles domain merging, display name deduplication, and icon rendering. The memoization dependencies are appropriate, and the logic properly skips secondary domains to avoid duplicate entries.

Optional: Consider extracting tracker processing logic

The tracker customization mapping (lines 99-121) and processing (lines 123-167) are well-structured but complex. For maintainability, consider extracting into a separate hook or utility function:

// Example structure
function useProcessedTrackerOptions(
  trackersData: Record<string, unknown> | undefined,
  trackerCustomizations: TrackerCustomization[] | undefined,
  trackerIcons: TrackerIcon[] | undefined
): Option[]

This would improve testability and reduce cognitive load in the form component.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67002fb and 4086497.

📒 Files selected for processing (8)
  • web/src/components/instances/preferences/OrphanScanOverview.tsx
  • web/src/components/instances/preferences/OrphanScanSettingsForm.tsx
  • web/src/components/instances/preferences/ReannounceOverview.tsx
  • web/src/components/instances/preferences/ReannounceSettingsDialog.tsx
  • web/src/components/instances/preferences/TrackerReannounceForm.tsx
  • web/src/components/instances/preferences/WorkflowDialog.tsx
  • web/src/components/ui/multi-select.tsx
  • web/src/pages/Automations.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-25T11:39:54.748Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 637
File: web/src/pages/Dashboard.tsx:805-831
Timestamp: 2025-11-25T11:39:54.748Z
Learning: In web/src/pages/Dashboard.tsx, the TrackerIconImage component intentionally receives displayDomain (incognito-mapped name) instead of the real domain in incognito mode. This causes icon lookups to fail and show only fallback letters, which is desired behavior for privacy - hiding both tracker names and icons when incognito mode is enabled.

Applied to files:

  • web/src/components/instances/preferences/WorkflowDialog.tsx
  • web/src/components/instances/preferences/TrackerReannounceForm.tsx
🧬 Code graph analysis (4)
web/src/components/ui/multi-select.tsx (1)
web/src/lib/utils.ts (1)
  • cn (10-12)
web/src/components/instances/preferences/ReannounceSettingsDialog.tsx (4)
web/src/hooks/useInstances.ts (1)
  • useInstances (10-188)
web/src/components/ui/dialog.tsx (6)
  • Dialog (136-136)
  • DialogContent (138-138)
  • DialogHeader (141-141)
  • DialogTitle (144-144)
  • DialogDescription (139-139)
  • DialogFooter (140-140)
web/src/components/instances/preferences/TrackerReannounceForm.tsx (1)
  • TrackerReannounceForm (63-743)
web/src/components/ui/button.tsx (1)
  • Button (64-64)
web/src/components/instances/preferences/OrphanScanSettingsForm.tsx (6)
web/src/components/ui/separator.tsx (1)
  • Separator (33-33)
web/src/components/ui/label.tsx (1)
  • Label (29-29)
web/src/components/ui/tooltip.tsx (3)
  • Tooltip (184-184)
  • TooltipTrigger (184-184)
  • TooltipContent (184-184)
web/src/components/ui/switch.tsx (1)
  • Switch (34-34)
web/src/components/ui/input.tsx (1)
  • Input (26-26)
web/src/components/ui/button.tsx (1)
  • Button (64-64)
web/src/pages/Automations.tsx (2)
web/src/hooks/useInstances.ts (1)
  • useInstances (10-188)
web/src/components/instances/preferences/ReannounceSettingsDialog.tsx (1)
  • ReannounceSettingsDialog (27-60)
🔇 Additional comments (10)
web/src/components/instances/preferences/OrphanScanOverview.tsx (1)

151-206: LGTM! Well-structured header refactoring.

The two-column grid layout cleanly separates the accordion trigger from the toggle controls, preventing accidental expansion when users interact with the switch. The stopPropagation handler and custom chevron with group-scoped animation are implemented correctly.

web/src/components/instances/preferences/WorkflowDialog.tsx (1)

682-682: LGTM! UI polish for tracker selection.

The hideCheckIcon prop cleans up the visual presentation when tracker icons are displayed, preventing redundant check marks alongside icons.

web/src/components/ui/multi-select.tsx (1)

26-27: LGTM! Well-implemented optional feature.

The hideCheckIcon prop is properly typed, documented, and defaults to false for backward compatibility. The conditional rendering preserves existing behavior when the prop is not provided.

Also applies to: 39-39, 158-165

web/src/components/instances/preferences/ReannounceSettingsDialog.tsx (1)

1-60: LGTM! Clean dialog implementation.

The dialog properly coordinates form submission via FORM_ID, manages loading state with isUpdating, and handles the success flow by closing on completion. The structure aligns with the new dialog-based configuration pattern described in the PR.

web/src/components/instances/preferences/ReannounceOverview.tsx (1)

209-254: LGTM! Improved accordion header layout.

The grid-based restructure provides better visual separation between the title/status badges and the enable toggle/chevron. The stopPropagation on the switch prevents accidental accordion collapse, and the custom chevron rotation is properly tied to the accordion state.

web/src/components/instances/preferences/TrackerReannounceForm.tsx (1)

554-554: LGTM! Consistent with tracker selection UX.

The hideCheckIcon prop usage aligns with the WorkflowDialog tracker selection, providing a cleaner visual when icons are present.

web/src/pages/Automations.tsx (1)

9-9: LGTM! Clean migration to dialog-based configuration.

The refactor successfully decouples reannounce configuration into a dedicated dialog, simplifying the parent component. The removal of isUpdating usage is appropriate since it's now managed within ReannounceSettingsDialog. The variable rename to configureReannounceInstance improves clarity.

Also applies to: 15-15, 37-37, 61-61, 77-82

web/src/components/instances/preferences/OrphanScanSettingsForm.tsx (3)

33-34: LGTM! Well-structured auto-cleanup feature with appropriate safeguards.

The auto-cleanup implementation follows the PR's safety-first approach:

  • Defaults to disabled (opt-in)
  • File threshold with sensible default (100)
  • Conditional UI shows threshold only when enabled
  • Clear tooltips explain behavior and risks
  • Validation ensures minimum of 1 file

The feature aligns with the PR objectives and provides the necessary safety mechanisms for automated deletion.

Also applies to: 57-58, 71-72, 232-295


94-96: Verify behavioral change: toggle now requires explicit save.

The handleToggleEnabled function now only updates local state without persisting, requiring users to click "Save Changes" to commit the toggle. This differs from the typical pattern where enable/disable toggles save immediately.

Is this intentional? If so, it's consistent with the form-based approach. If not, consider restoring immediate persistence for the enabled toggle while keeping other settings requiring explicit save.

For comparison, ReannounceOverview.tsx (line 94-125) and the reannounce form's handleToggleEnabled (TrackerReannounceForm.tsx, line 254-261) both persist immediately.


140-230: LGTM! Removal of conditional rendering improves UX.

Always showing the Schedule section aligns with the PR objective: "removes the requirement to enable scanning before configuring scan-related settings, allowing users to configure settings prior to enabling scanning." This is a positive UX improvement.

@s0up4200
s0up4200 merged commit 5d124c0 into main Dec 31, 2025
11 checks passed
@s0up4200
s0up4200 deleted the feat/orphan-scan-auto-cleanup branch December 31, 2025 19:20
alexlebens pushed a commit to alexlebens/infrastructure that referenced this pull request Jan 4, 2026
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/autobrr/qui](https://github.com/autobrr/qui) | minor | `v1.11.0` → `v1.12.0` |

---

### Release Notes

<details>
<summary>autobrr/qui (ghcr.io/autobrr/qui)</summary>

### [`v1.12.0`](https://github.com/autobrr/qui/releases/tag/v1.12.0)

[Compare Source](autobrr/qui@v1.11.0...v1.12.0)

#### Changelog

##### New Features

- [`202e950`](autobrr/qui@202e950): feat(automations): Add `free_space` condition ([#&#8203;1061](autobrr/qui#1061)) ([@&#8203;Barcode-eng](https://github.com/Barcode-eng))
- [`3b106d6`](autobrr/qui@3b106d6): feat(automations): make conditions optional for non-delete rules and add drag reorder ([#&#8203;939](autobrr/qui#939)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`0684d75`](autobrr/qui@0684d75): feat(config): Add QUI\_\_OIDC\_CLIENT\_SECRET\_FILE env var ([#&#8203;841](autobrr/qui#841)) ([@&#8203;PedDavid](https://github.com/PedDavid))
- [`dac0173`](autobrr/qui@dac0173): feat(config): allow disabling tracker icon fetching ([#&#8203;823](autobrr/qui#823)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`dc10bad`](autobrr/qui@dc10bad): feat(crossseed): add cancel run and opt-in errored torrent recovery ([#&#8203;918](autobrr/qui#918)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`cd1fcc9`](autobrr/qui@cd1fcc9): feat(crossseed): add custom category option for cross-seeds ([#&#8203;907](autobrr/qui#907)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d189fe9`](autobrr/qui@d189fe9): feat(crossseed): add indexerName to webhook apply + fix category mode defaults ([#&#8203;916](autobrr/qui#916)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`03a147e`](autobrr/qui@03a147e): feat(crossseed): add option to skip recheck-required matches ([#&#8203;825](autobrr/qui#825)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`edae00a`](autobrr/qui@edae00a): feat(crossseed): add optional hardlink mode for cross-seeding ([#&#8203;849](autobrr/qui#849)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`0938436`](autobrr/qui@0938436): feat(crossseed): add source aliasing for WEB/WEB-DL/WEBRip precheck matching ([#&#8203;874](autobrr/qui#874)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`65f6129`](autobrr/qui@65f6129): feat(crossseed): show failure reasons, prune runs, and add cache cleanup ([#&#8203;923](autobrr/qui#923)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`e10fba8`](autobrr/qui@e10fba8): feat(details): torrent details panel improvements ([#&#8203;884](autobrr/qui#884)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`6921140`](autobrr/qui@6921140): feat(docs): add Docusaurus documentation site ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`6a5a66c`](autobrr/qui@6a5a66c): feat(docs): add Icon and webUI variables to the Unraid install guide ([#&#8203;942](autobrr/qui#942)) ([@&#8203;BaukeZwart](https://github.com/BaukeZwart))
- [`281fce7`](autobrr/qui@281fce7): feat(docs): add local search plugin ([#&#8203;1076](autobrr/qui#1076)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`566de08`](autobrr/qui@566de08): feat(docs): add qui logo, update readme, remove v4 flag ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`b83ac5a`](autobrr/qui@b83ac5a): feat(docs): apply minimal.css theme to Docusaurus ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`fe6a6df`](autobrr/qui@fe6a6df): feat(docs): improve documentation pages and add support page ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`62a7ad5`](autobrr/qui@62a7ad5): feat(docs): use qui favicon ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5d124c0`](autobrr/qui@5d124c0): feat(orphan): add auto cleanup mode ([#&#8203;897](autobrr/qui#897)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`3172ad9`](autobrr/qui@3172ad9): feat(settings): add log settings + live log stream ([#&#8203;876](autobrr/qui#876)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`3c1b34b`](autobrr/qui@3c1b34b): feat(torrents): add "torrent introuvable" to unregistered status ([#&#8203;836](autobrr/qui#836)) ([@&#8203;kephasdev](https://github.com/kephasdev))
- [`afe4d39`](autobrr/qui@afe4d39): feat(torrents): add tracker URL editing for single torrents ([#&#8203;848](autobrr/qui#848)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`76dedd7`](autobrr/qui@76dedd7): feat(torrents): update GeneralTabHorizontal to display limits and improve layout ([#&#8203;1078](autobrr/qui#1078)) ([@&#8203;martylukyy](https://github.com/martylukyy))
- [`6831c24`](autobrr/qui@6831c24): feat(ui): unify payment options into single dialog ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`4dcdf7f`](autobrr/qui@4dcdf7f): feat(web): add local file access indicator to instance cards ([#&#8203;911](autobrr/qui#911)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`a560e5e`](autobrr/qui@a560e5e): feat(web): compact torrent details panel ([#&#8203;833](autobrr/qui#833)) ([@&#8203;martylukyy](https://github.com/martylukyy))
- [`557e7bd`](autobrr/qui@557e7bd): feat: add issue/discussion template ([#&#8203;945](autobrr/qui#945)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`8b93719`](autobrr/qui@8b93719): feat: add workflow automation system with category actions, orphan scanner, and hardlink detection ([#&#8203;818](autobrr/qui#818)) ([@&#8203;s0up4200](https://github.com/s0up4200))

##### Bug Fixes

- [`b85ad6b`](autobrr/qui@b85ad6b): fix(automations): allow delete rules to match incomplete torrents ([#&#8203;926](autobrr/qui#926)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`ae06200`](autobrr/qui@ae06200): fix(automations): make tags field condition operators tag-aware ([#&#8203;908](autobrr/qui#908)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`ace0101`](autobrr/qui@ace0101): fix(crossseed): detect folder mismatch for bare file to folder cross-seeds ([#&#8203;846](autobrr/qui#846)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`1cc1243`](autobrr/qui@1cc1243): fix(crossseed): enforce resolution and language matching with sensible defaults ([#&#8203;855](autobrr/qui#855)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`cefb9cd`](autobrr/qui@cefb9cd): fix(crossseed): execute external program reliably after injection ([#&#8203;1083](autobrr/qui#1083)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`867e2da`](autobrr/qui@867e2da): fix(crossseed): improve matching with size validation and relaxed audio checks ([#&#8203;845](autobrr/qui#845)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`4b5079b`](autobrr/qui@4b5079b): fix(crossseed): persist custom category settings in PATCH handler ([#&#8203;913](autobrr/qui#913)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`cfbbc1f`](autobrr/qui@cfbbc1f): fix(crossseed): prevent season packs matching episodes ([#&#8203;854](autobrr/qui#854)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c7c1706`](autobrr/qui@c7c1706): fix(crossseed): reconcile interrupted runs on startup ([#&#8203;1084](autobrr/qui#1084)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`7d633bd`](autobrr/qui@7d633bd): fix(crossseed): use ContentPath for manually-managed single-file torrents ([#&#8203;832](autobrr/qui#832)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d5db761`](autobrr/qui@d5db761): fix(database): include arr\_instances in string pool cleanup + remove auto-recovery ([#&#8203;898](autobrr/qui#898)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c73ec6f`](autobrr/qui@c73ec6f): fix(database): prevent race between stmt cache access and db close ([#&#8203;840](autobrr/qui#840)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`a40b872`](autobrr/qui@a40b872): fix(db): drop legacy hardlink columns from cross\_seed\_settings ([#&#8203;912](autobrr/qui#912)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`e400af3`](autobrr/qui@e400af3): fix(db): recover wedged SQLite writer + stop cross-seed tight loop ([#&#8203;890](autobrr/qui#890)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`90e15b4`](autobrr/qui@90e15b4): fix(deps): update rls to recognize IP as iPlayer ([#&#8203;922](autobrr/qui#922)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`8e81b9f`](autobrr/qui@8e81b9f): fix(proxy): honor TLSSkipVerify for proxied requests ([#&#8203;1051](autobrr/qui#1051)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`eb2bee0`](autobrr/qui@eb2bee0): fix(security): redact sensitive URL parameters in logs ([#&#8203;853](autobrr/qui#853)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`40982bc`](autobrr/qui@40982bc): fix(themes): prevent reset on license errors, improve switch performance ([#&#8203;844](autobrr/qui#844)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`a8a32f7`](autobrr/qui@a8a32f7): fix(ui): incomplete torrents aren't "Completed: 1969-12-31" ([#&#8203;851](autobrr/qui#851)) ([@&#8203;finevan](https://github.com/finevan))
- [`5908bba`](autobrr/qui@5908bba): fix(ui): preserve category collapse state when toggling incognito mode ([#&#8203;834](autobrr/qui#834)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`25c847e`](autobrr/qui@25c847e): fix(ui): torrents with no creation metadata don't display 1969 ([#&#8203;873](autobrr/qui#873)) ([@&#8203;finevan](https://github.com/finevan))
- [`6403b6a`](autobrr/qui@6403b6a): fix(web): column filter status now matches all states in category ([#&#8203;880](autobrr/qui#880)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`eafc4e7`](autobrr/qui@eafc4e7): fix(web): make delete cross-seed check rely on content\_path matches ([#&#8203;1080](autobrr/qui#1080)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d57c749`](autobrr/qui@d57c749): fix(web): only show selection checkbox on normal view ([#&#8203;830](autobrr/qui#830)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`60338f6`](autobrr/qui@60338f6): fix(web): optimize TorrentDetailsPanel for mobile view and make tabs scrollable horizontally ([#&#8203;1066](autobrr/qui#1066)) ([@&#8203;martylukyy](https://github.com/martylukyy))
- [`aedab87`](autobrr/qui@aedab87): fix(web): speed limit input reformatting during typing ([#&#8203;881](autobrr/qui#881)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`df7f3e0`](autobrr/qui@df7f3e0): fix(web): truncate file progress percentage instead of rounding ([#&#8203;919](autobrr/qui#919)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`2fadd01`](autobrr/qui@2fadd01): fix(web): update eslint config for flat config compatibility ([#&#8203;879](autobrr/qui#879)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`721cedd`](autobrr/qui@721cedd): fix(web): use fixed heights for mobile torrent cards ([#&#8203;812](autobrr/qui#812)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`a7db605`](autobrr/qui@a7db605): fix: remove pnpm-workspace.yaml breaking CI ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c0ddc0a`](autobrr/qui@c0ddc0a): fix: use prefix matching for allowed bash commands ([@&#8203;s0up4200](https://github.com/s0up4200))

##### Other Changes

- [`fff52ce`](autobrr/qui@fff52ce): chore(ci): disable reviewer ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`7ef2a38`](autobrr/qui@7ef2a38): chore(ci): fix automated triage and deduplication workflows ([#&#8203;1057](autobrr/qui#1057)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d84910b`](autobrr/qui@d84910b): chore(docs): move Tailwind to documentation workspace only ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`37ebe05`](autobrr/qui@37ebe05): chore(docs): move netlify.toml to documentation directory ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`e25de38`](autobrr/qui@e25de38): chore(docs): remove disclaimer ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c59b809`](autobrr/qui@c59b809): chore(docs): update support sections ([#&#8203;1063](autobrr/qui#1063)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`b723523`](autobrr/qui@b723523): chore(tests): remove dead tests and optimize slow test cases ([#&#8203;842](autobrr/qui#842)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`662a1c6`](autobrr/qui@662a1c6): chore(workflows): update runners from 4vcpu to 2vcpu for all jobs ([#&#8203;859](autobrr/qui#859)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`46f2a1c`](autobrr/qui@46f2a1c): chore: clean up repo root by moving Docker, scripts, and community docs ([#&#8203;1054](autobrr/qui#1054)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`2f27c0d`](autobrr/qui@2f27c0d): chore: remove old issue templates ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`04f361a`](autobrr/qui@04f361a): ci(triage): add labeling for feature-requests-ideas discussions ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`f249c69`](autobrr/qui@f249c69): ci(triage): remove needs-triage label after applying labels ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`bdda1de`](autobrr/qui@bdda1de): ci(workflows): add self-dispatch workaround for discussion events ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`a9732a2`](autobrr/qui@a9732a2): ci(workflows): increase max-turns to 25 for Claude workflows ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d7d830d`](autobrr/qui@d7d830d): docs(README): add Buy Me a Coffee link ([#&#8203;863](autobrr/qui#863)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`266d92e`](autobrr/qui@266d92e): docs(readme): Clarify ignore pattern ([#&#8203;878](autobrr/qui#878)) ([@&#8203;quorn23](https://github.com/quorn23))
- [`9586084`](autobrr/qui@9586084): docs(readme): add banner linking to stable docs ([#&#8203;925](autobrr/qui#925)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`e36a621`](autobrr/qui@e36a621): docs(readme): use markdown link for Polar URL ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`9394676`](autobrr/qui@9394676): docs: add frontmatter titles and descriptions, remove marketing language ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`ba9d45e`](autobrr/qui@ba9d45e): docs: add local filesystem access snippet and swizzle Details component ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`4329edd`](autobrr/qui@4329edd): docs: disclaimer about unreleased features ([#&#8203;943](autobrr/qui#943)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`735d065`](autobrr/qui@735d065): docs: improve external programs, orphan scan, reverse proxy, tracker icons documentation ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`78faef2`](autobrr/qui@78faef2): docs: remove premature tip and fix stat command ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`eaad3bf`](autobrr/qui@eaad3bf): docs: update social card image in Docusaurus configuration ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`02a68e5`](autobrr/qui@02a68e5): refactor(crossseed): hardcode ignore patterns for file matching ([#&#8203;915](autobrr/qui#915)) ([@&#8203;s0up4200](https://github.com/s0up4200))

**Full Changelog**: <autobrr/qui@v1.11.0...v1.12.0>

#### Docker images

- `docker pull ghcr.io/autobrr/qui:v1.12.0`
- `docker pull ghcr.io/autobrr/qui:latest`

#### What to do next?

- Join our [Discord server](https://discord.autobrr.com/qui)

Thank you for using qui!

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi42OS4yIiwidXBkYXRlZEluVmVyIjoiNDIuNjkuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW1hZ2UiXX0=-->

Reviewed-on: https://gitea.alexlebens.dev/alexlebens/infrastructure/pulls/3060
Co-authored-by: Renovate Bot <renovate-bot@alexlebens.net>
Co-committed-by: Renovate Bot <renovate-bot@alexlebens.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant