Skip to content

feat(tracker-rules): add per-tracker limits and services page#630

Merged
s0up4200 merged 19 commits intomainfrom
feat/tracker-rules-services-page
Nov 25, 2025
Merged

feat(tracker-rules): add per-tracker limits and services page#630
s0up4200 merged 19 commits intomainfrom
feat/tracker-rules-services-page

Conversation

@s0up4200
Copy link
Collaborator

@s0up4200 s0up4200 commented Nov 22, 2025

  • Add Tracker Rules panel to configure per-tracker speed, ratio, and seeding limits per instance.
  • Introduce a Services page that surfaces Tracker Rules alongside Tracker Reannounce for the selected instance.
  • Move Tracker Reannounce out of the Instance Preferences dialog into the Services page and refine its UX

Summary by CodeRabbit

  • New Features

    • Tracker Rules: DB migration, persistent store, full CRUD + reorder, "Apply Now", background enforcement and manual trigger; API + frontend types/endpoints added.
    • New Services page and navigation entry with Tracker Rules panel and multi-level category support.
  • UI / Navigation

    • Tracker Reannounce moved into Services and renamed Quick Retry; route-driven flows replaced tab defaults.
  • Bug Fixes / Behavior

    • Reannounce honors per-instance cooldowns, initial-wait, and clearer debounce messaging.
  • Tests

    • Tests updated for reannounce cooldowns and tracker-rule flows.
  • Documentation

    • README and feature docs updated for Tracker Rules and Quick Retry.

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

- Add Tracker Rules panel to configure per-tracker speed, ratio, and seeding limits per instance.
- Introduce a Services page that surfaces Tracker Rules alongside Tracker Reannounce for the selected instance.
- Move Tracker Reannounce out of the Instance Preferences dialog into the Services page and refine its UX (Aggressive mode copy, enable/disable behavior, instance scoping).
@s0up4200 s0up4200 added this to the v1.8.0 milestone Nov 22, 2025
@s0up4200 s0up4200 added enhancement New feature or request web torrent labels Nov 22, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 22, 2025

Walkthrough

Adds a Tracker Rules feature: DB migration and model/store, API handlers and server wiring, a background service that applies rules to torrents, frontend types/client/UI (Tracker Rules panel and Services page), and reannounce behavior/UI updates (Quick Retry and initial-wait enforcement).

Changes

Cohort / File(s) Summary
Database
internal/database/migrations/019_add_tracker_rules.sql
Creates tracker_rules table, index, and update trigger; columns for pattern, domains/limits, enabled, sort order, timestamps, and FK to instances.
Models / Store
internal/models/tracker_rule.go
Adds TrackerRule model and TrackerRuleStore with List/Get/Create/Update/Delete/Reorder, pattern normalization, domain extraction, SQL helpers, and transactional reorder.
Tracker-rule Service
internal/services/trackerrules/service.go
New background service (Config/DefaultConfig) that periodically or manually applies rules per instance: rule matching, per-torrent debounce, batched qBittorrent updates, and ApplyOnceForInstance API.
Reannounce Service
internal/services/reannounce/service.go, internal/services/reannounce/service_test.go
Makes debounce dynamic (uses Retry Interval when Quick Retry enabled), enforces InitialWaitSeconds, preserves hash casing, updates messaging and tests.
API Handlers
internal/api/handlers/tracker_rules.go
New HTTP handler exposing List/Create/Update/Delete/Reorder/Apply endpoints with payload validation, normalization, and optional service integration.
API Server Wiring & Tests
internal/api/server.go, internal/api/server_test.go, cmd/qui/main.go
Instantiates TrackerRuleStore and TrackerRuleService, exposes them via Dependencies/Server fields, registers /tracker-rules routes, and starts service in lifecycle; tests updated to include store.
Frontend Types & Client
web/src/types/index.ts, web/src/lib/api.ts
Adds TrackerRule and TrackerRuleInput types and ApiClient methods for list/create/update/delete/reorder/apply tracker rules.
Frontend UI - Tracker Rules
web/src/components/instances/preferences/TrackerRulesPanel.tsx
New React panel for listing, creating, editing, deleting, reordering, and applying tracker rules with form validation, optimistic updates, and UI components.
Services Page & Routing
web/src/pages/Services.tsx, web/src/routes/_authenticated/services.tsx, web/src/routeTree.gen.ts
Adds new Services page, route, and route-tree entries; Services renders TrackerRulesPanel and TrackerReannounceForm per selected instance.
Navigation & Layout
web/src/components/layout/Header.tsx, web/src/components/layout/MobileFooterNav.tsx, web/src/components/layout/Sidebar.tsx
Adds "Services" nav entries (Wrench icon) to header, mobile footer, and sidebar.
Preferences & Reannounce UI
web/src/components/instances/preferences/InstancePreferencesDialog.tsx, web/src/components/instances/preferences/TrackerReannounceForm.tsx
Removes reannounce tab from dialog; refactors reannounce form to persist settings flow and renames Aggressive → Quick Retry with UI/tooltips updated.
Torrent Table Navigation
web/src/components/torrents/TorrentTableOptimized.tsx
Replaces tab-driven dialog opening with route-based navigation to /services?instanceId=...; removes defaultTab usage.
Frontend helpers & UI tweaks
web/src/components/ui/multi-select.tsx, web/src/components/instances/preferences/TrackerReannounceForm.tsx
Adds Option.level for indented multi-select options; integrates hierarchical category handling and UI refinements.
Frontend deps
web/package.json
Adds @tanstack/router-cli to dependencies and devDependencies.
Docs & Tests
README.md, internal/services/reannounce/REANNOUNCE.md, internal/services/trackerrules/TRACKER_RULES.md, internal/services/reannounce/service_test.go, internal/api/server_test.go
Docs for Tracker Rules and updated reannounce docs; tests adjusted/added for Quick Retry and initial-wait behavior; server tests include tracker-rules routes.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Web as Web UI
    participant API as API Server
    participant Store as TrackerRuleStore
    participant Service as TrackerRuleService
    participant QBT as qBittorrent

    User->>Web: Open Services page / select instance
    Web->>API: GET /api/instances/{id}/tracker-rules
    API->>Store: ListByInstance(instanceID)
    Store-->>API: [TrackerRule]
    API-->>Web: 200 [TrackerRule]

    User->>Web: Create/Update rule
    Web->>API: POST/PUT /api/instances/{id}/tracker-rules
    API->>Store: Create/Update(rule)
    Store-->>API: TrackerRule
    API-->>Web: 201/200 TrackerRule

    User->>Web: Click "Apply Now"
    Web->>API: POST /api/instances/{id}/tracker-rules/apply
    API->>Service: ApplyOnceForInstance(instanceID)
    Service->>Store: ListByInstance(instanceID)
    Store-->>Service: [TrackerRule]
    Service->>QBT: Fetch torrents, compute diffs
    Service->>QBT: Apply limits (batched)
    QBT-->>Service: Success
    Service-->>API: OK
    API-->>Web: 200 OK

    rect rgba(180,230,200,0.12)
      Note over Service: Background loop periodically runs applyAll()
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus review areas:
    • internal/models/tracker_rule.go — SQL queries, NULL handling, pattern/domain normalization
    • internal/services/trackerrules/service.go — matching logic, debounce, batching, concurrency and timeouts
    • internal/api/handlers/tracker_rules.go & server wiring — validation, error mapping, dependency injection
    • Frontend ↔ backend contract — web/src/lib/api.ts, web/src/types/index.ts, TrackerRulesPanel payloads
    • Migration SQL — index/trigger semantics and FK ON DELETE behavior

Possibly related PRs

Suggested reviewers

  • buroa

Poem

🐇 I hopped through tables, rules, and routes,
I trimmed the patterns, sorted tiny bouts,
A service hums to apply with care,
UI shows panels and a wrench up there,
Hop—tracker rules now live! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title clearly summarizes the two main additions: tracker rules feature with per-tracker limits and a new services page that exposes this functionality.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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/tracker-rules-services-page

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/services/reannounce/service.go (1)

284-284: Inconsistent debounce window usage in GetMonitoredTorrents.

Line 284 uses the fixed s.cfg.DebounceWindow to determine cooldown state, but the enqueue method now uses a dynamic window (ReannounceIntervalSeconds in aggressive mode). This inconsistency will cause GetMonitoredTorrents to report incorrect cooldown states for torrents when aggressive mode is enabled.

The same helper logic used in enqueue (lines 341-350) should be applied here to determine the correct debounce window per instance settings.

 	s.jobsMu.Lock()
 	defer s.jobsMu.Unlock()
 
 	now := s.currentTime()
 	instJobs := s.j[instanceID]
+
+	// Determine debounce window matching enqueue logic
+	debounceWindow := s.cfg.DebounceWindow
+	if settings != nil && settings.Aggressive {
+		if interval := time.Duration(settings.ReannounceIntervalSeconds) * time.Second; interval > 0 {
+			debounceWindow = interval
+		}
+	}
 
 	var result []MonitoredTorrent
 	for _, torrent := range torrents {
 		if !s.torrentMeetsCriteria(torrent, settings) {
 			continue
 		}
 
 		hasProblem := s.trackerProblemDetected(torrent.Trackers)
 		waiting := s.trackersUpdating(torrent.Trackers) && !hasProblem
 		if !hasProblem && !waiting {
 			continue
 		}
 
 		hashUpper := strings.ToUpper(strings.TrimSpace(torrent.Hash))
 		if hashUpper == "" {
 			continue
 		}
 
 		state := MonitoredTorrentStateWatching
 		if instJobs != nil {
 			if job, ok := instJobs[hashUpper]; ok {
 				if job.isRunning {
 					state = MonitoredTorrentStateReannouncing
-				} else if !job.lastCompleted.IsZero() && now.Sub(job.lastCompleted) < s.cfg.DebounceWindow {
+				} else if !job.lastCompleted.IsZero() && now.Sub(job.lastCompleted) < debounceWindow {
 					state = MonitoredTorrentStateCooldown
 				}
 			}
 		}
web/src/components/instances/preferences/TrackerReannounceForm.tsx (1)

124-170: Centralized persistence looks good; copy still assumes dialog context

The new persistSettings flow with sanitizeSettings and the consolidated submit/toggle behavior is clean and reuses the existing updateInstance API correctly. Two small UX nits:

  • Both the early-return error (toast.error("Instance missing", ...)) and the fallback render ("Please close and reopen the dialog.") still mention a “dialog”, which is confusing when this form is rendered on the Services page. Consider rephrasing to something like “Please reload the page and try again.” or a more generic message.
  • Disabling monitoring persists immediately, while enabling requires hitting “Save Changes”. That asymmetry is easy to miss; you might want to either persist both directions on toggle or add a short hint near the switch/card to clarify that enabling isn’t saved until you click “Save Changes”.

Also applies to: 178-180, 286-287, 493-505

🧹 Nitpick comments (10)
internal/services/reannounce/service.go (1)

440-440: Inconsistent debounce window usage in finishJob.

Line 440 uses the fixed s.cfg.DebounceWindow to determine when to delete job records, but enqueue now uses a dynamic window in aggressive mode. This could lead to jobs being retained too long or cleaned up too soon, depending on the aggressive mode interval.

Consider using the same dynamic window calculation here for consistency. However, since finishJob doesn't have access to settings, you may need to either:

  1. Store the debounce window used in the job struct, or
  2. Pass settings to finishJob, or
  3. Accept the fixed cleanup window as a reasonable approximation
internal/database/migrations/019_add_tracker_rules.sql (1)

4-30: LGTM! Schema design is solid.

The table structure, indexes, and trigger implementation align well with the codebase patterns. The trigger correctly follows the established pattern (AFTER UPDATE with separate UPDATE statement) that doesn't cause recursion with SQLite's default settings.

Based on learnings

Consider adding CHECK constraints for data integrity:

  • is_default IN (0, 1) to enforce boolean values
  • Non-negative constraints on limit fields (e.g., upload_limit_kib >= 0)
  • A UNIQUE constraint on (instance_id, is_default) WHERE is_default = 1 if only one default rule per instance is intended

These constraints would provide additional safety but can be deferred if validation is handled at the application layer.

web/src/types/index.ts (1)

88-118: LGTM! Types align well with the backend.

The TrackerRule and TrackerRuleInput interfaces properly map the Go backend types to TypeScript with appropriate camelCase naming and nullable field handling.

Consider whether trackerPattern should be required (non-optional) in TrackerRuleInput for creation operations. Currently it's optional, which may be intentional for update operations, but if the backend requires it for creation, you might want to split this into separate TrackerRuleCreateInput and TrackerRuleUpdateInput types for better type safety. This is only a suggestion if stricter validation is desired.

internal/api/server_test.go (1)

51-56: Tracker-rules endpoints and store wiring look correct

The new tracker-rules routes are properly exempted from the documentation coverage test, and TrackerRuleStore is wired into the test Dependencies in the same style as other stores. This should keep TestAllEndpointsDocumented stable until the OpenAPI spec is updated to include these endpoints.

You might consider adding a brief TODO near undocumentedRoutes for the tracker-rules entries to remind future work to move them into the spec once the API is finalized.

Also applies to: 81-121

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

34-47: Tracker rules CRUD, validation, and summary implementation look robust

The panel wires tracker rules to the backend cleanly: rules are scoped by instanceId, sorted deterministically, and mutations (create/update/delete/reorder/apply) all invalidate the appropriate query. Validation around name and “default vs. at least one tracker” is sound, and parseTrackerDomains’ fallback to trackerPattern keeps existing rules editable even if domains weren’t previously stored.

If you want to refine UX further later, two ideas:

  • Add a small toast on successful reordering, or optimistic reordering with rollback on error, so users get feedback when moving rules.
  • Optionally guard against deleting the last isDefault rule, depending on how strictly the backend enforces a single default.

Also applies to: 55-119, 126-177, 179-417, 420-508

internal/api/handlers/tracker_rules.go (1)

210-225: Clarify ApplyNow behavior when the tracker-rule service is not configured

ApplyNow returns 202 with "status": "applied" even when h.service == nil, meaning nothing is actually applied. If a nil service can occur outside of tests, consider either:

  • Returning a 503/501 when h.service is nil, or
  • Logging a warning so it’s visible in diagnostics.

If a nil service is strictly a test-only condition, this is fine as-is.

web/src/lib/api.ts (1)

54-55: Wire tracker-rule API methods with explicit generics for type safety

The endpoints and payloads look correct and consistent with the backend. To keep type-checking as strong as possible (and consistent with the rest of this file), consider specifying the generic type parameter when calling this.request:

  async listTrackerRules(instanceId: number): Promise<TrackerRule[]> {
-    return this.request(`/instances/${instanceId}/tracker-rules`)
+    return this.request<TrackerRule[]>(`/instances/${instanceId}/tracker-rules`)
  }

  async createTrackerRule(instanceId: number, payload: TrackerRuleInput): Promise<TrackerRule> {
-    return this.request(`/instances/${instanceId}/tracker-rules`, {
+    return this.request<TrackerRule>(`/instances/${instanceId}/tracker-rules`, {
       method: "POST",
       body: JSON.stringify(payload),
     })
  }

  async updateTrackerRule(instanceId: number, ruleId: number, payload: TrackerRuleInput): Promise<TrackerRule> {
-    return this.request(`/instances/${instanceId}/tracker-rules/${ruleId}`, {
+    return this.request<TrackerRule>(`/instances/${instanceId}/tracker-rules/${ruleId}`, {
       method: "PUT",
       body: JSON.stringify(payload),
     })
  }

The delete, reorder, and apply methods are fine returning Promise<void> as-is.

Also applies to: 1222-1257

internal/services/trackerrules/service.go (1)

111-114: Guard ApplyOnceForInstance against misconfigured service dependencies

ApplyOnceForInstance calls applyForInstance without the defensive nil-checks that applyAll has (for syncManager, ruleStore, instanceStore). If a service were ever constructed with missing dependencies, a manual apply could panic even though the periodic loop safely does nothing.

You could mirror the guard from applyAll:

 func (s *Service) ApplyOnceForInstance(ctx context.Context, instanceID int) error {
-	return s.applyForInstance(ctx, instanceID)
+	if s == nil || s.syncManager == nil || s.ruleStore == nil {
+		return nil
+	}
+	return s.applyForInstance(ctx, instanceID)
 }

Adjust the behavior (e.g., return an error) to whatever you want callers/handlers to surface.

internal/models/tracker_rule.go (2)

82-147: Consider checking rows.Err() after iterating in ListByInstance

ListByInstance correctly scans rows and builds TrackerRule values, but it doesn’t check rows.Err() after the loop. Adding that check will surface any deferred driver errors:

 	for rows.Next() {
 		// scan, map to rule...
 	}
-
-	return rules, nil
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return rules, nil

215-255: Make default-clearing and insert transactional in Create

In Create, clearDefault and the INSERT run as separate statements on the base db connection, while Update uses a transaction and clearDefaultTx. Under concurrent writes for the same instance, this could briefly allow two “default” rules:

  • Goroutine A calls clearDefault, then gets preempted.
  • Goroutine B calls clearDefault and INSERT its default rule.
  • Goroutine A resumes and INSERTs another default rule.

If you want a strong invariant of at most one default per instance, consider wrapping clearDefault and INSERT in a single transaction, similar to Update:

 func (s *TrackerRuleStore) Create(ctx context.Context, rule *TrackerRule) (*TrackerRule, error) {
   // nil-check, normalize pattern, compute sortOrder...

-	if rule.IsDefault {
-		if err := s.clearDefault(ctx, rule.InstanceID); err != nil {
-			return nil, err
-		}
-	}
-
-	res, err := s.db.ExecContext(ctx, `
+	tx, err := s.db.BeginTx(ctx, nil)
+	if err != nil {
+		return nil, err
+	}
+	defer tx.Rollback()
+
+	if rule.IsDefault {
+		if err := s.clearDefaultTx(ctx, tx, rule.InstanceID); err != nil {
+			return nil, err
+		}
+	}
+
+	res, err := tx.ExecContext(ctx, `
 		INSERT INTO tracker_rules
       ...
-	`, /* args... */)
+	`, /* args... */)
 	if err != nil {
-		return nil, err
+		return nil, err
 	}
-
+	if err := tx.Commit(); err != nil {
+		return nil, err
+	}

(Then keep using Get afterwards as you already do.)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de0e00a and d339da4.

⛔ Files ignored due to path filters (1)
  • web/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • cmd/qui/main.go (5 hunks)
  • internal/api/handlers/tracker_rules.go (1 hunks)
  • internal/api/server.go (6 hunks)
  • internal/api/server_test.go (2 hunks)
  • internal/database/migrations/019_add_tracker_rules.sql (1 hunks)
  • internal/models/tracker_rule.go (1 hunks)
  • internal/services/reannounce/service.go (1 hunks)
  • internal/services/trackerrules/service.go (1 hunks)
  • web/package.json (1 hunks)
  • web/src/components/instances/preferences/InstancePreferencesDialog.tsx (2 hunks)
  • web/src/components/instances/preferences/TrackerReannounceForm.tsx (6 hunks)
  • web/src/components/instances/preferences/TrackerRulesPanel.tsx (1 hunks)
  • web/src/components/layout/Header.tsx (2 hunks)
  • web/src/components/layout/MobileFooterNav.tsx (2 hunks)
  • web/src/components/layout/Sidebar.tsx (2 hunks)
  • web/src/components/torrents/TorrentTableOptimized.tsx (3 hunks)
  • web/src/lib/api.ts (2 hunks)
  • web/src/pages/Services.tsx (1 hunks)
  • web/src/routeTree.gen.ts (11 hunks)
  • web/src/routes/_authenticated/services.tsx (1 hunks)
  • web/src/types/index.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-19T20:04:51.737Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 606
File: internal/database/migrations/016_add_instance_reannounce_settings.sql:21-27
Timestamp: 2025-11-19T20:04:51.737Z
Learning: In the autobrr/qui repository, PRAGMA recursive_triggers is never enabled in the SQLite setup. The AFTER UPDATE trigger pattern that issues a separate UPDATE statement on the same table (e.g., to update an updated_at timestamp) is used consistently across migrations and does not cause recursion with recursive_triggers disabled (the SQLite default). This pattern should remain consistent across all migrations.

Applied to files:

  • internal/database/migrations/019_add_tracker_rules.sql
🧬 Code graph analysis (13)
web/src/types/index.ts (1)
internal/models/tracker_rule.go (1)
  • TrackerRule (16-32)
internal/services/reannounce/service.go (1)
internal/models/instance_reannounce.go (1)
  • DefaultInstanceReannounceSettings (53-69)
web/src/components/instances/preferences/TrackerRulesPanel.tsx (5)
web/src/types/index.ts (2)
  • TrackerRuleInput (106-118)
  • TrackerRule (88-104)
internal/models/tracker_rule.go (1)
  • TrackerRule (16-32)
web/src/hooks/useInstanceTrackers.ts (1)
  • useInstanceTrackers (18-27)
web/src/components/ui/multi-select.tsx (2)
  • Option (9-12)
  • MultiSelect (25-161)
web/src/lib/api.ts (1)
  • api (1613-1613)
web/src/pages/Services.tsx (3)
web/src/hooks/useInstances.ts (1)
  • useInstances (10-185)
web/src/components/instances/preferences/TrackerRulesPanel.tsx (1)
  • TrackerRulesPanel (49-418)
web/src/components/instances/preferences/TrackerReannounceForm.tsx (1)
  • TrackerReannounceForm (54-577)
web/src/components/instances/preferences/TrackerReannounceForm.tsx (2)
web/src/types/index.ts (1)
  • InstanceReannounceSettings (42-55)
internal/models/instance_reannounce.go (1)
  • InstanceReannounceSettings (25-40)
web/src/routes/_authenticated/services.tsx (1)
web/src/pages/Services.tsx (1)
  • Services (18-111)
internal/services/trackerrules/service.go (4)
internal/models/instance.go (1)
  • InstanceStore (127-130)
internal/models/tracker_rule.go (2)
  • TrackerRuleStore (34-36)
  • TrackerRule (16-32)
internal/qbittorrent/sync_manager.go (1)
  • SyncManager (136-140)
web/src/types/index.ts (3)
  • Torrent (204-261)
  • TrackerRule (88-104)
  • Category (342-345)
web/src/lib/api.ts (2)
internal/models/tracker_rule.go (1)
  • TrackerRule (16-32)
web/src/types/index.ts (2)
  • TrackerRule (88-104)
  • TrackerRuleInput (106-118)
cmd/qui/main.go (2)
internal/models/tracker_rule.go (2)
  • NewTrackerRuleStore (38-40)
  • TrackerRuleStore (34-36)
internal/services/trackerrules/service.go (2)
  • NewService (50-67)
  • DefaultConfig (30-36)
internal/api/server_test.go (1)
internal/models/tracker_rule.go (2)
  • TrackerRuleStore (34-36)
  • NewTrackerRuleStore (38-40)
internal/api/handlers/tracker_rules.go (3)
internal/models/tracker_rule.go (2)
  • TrackerRuleStore (34-36)
  • TrackerRule (16-32)
internal/services/trackerrules/service.go (1)
  • Service (39-48)
internal/api/handlers/helpers.go (2)
  • RespondError (34-38)
  • RespondJSON (22-31)
internal/api/server.go (3)
internal/models/tracker_rule.go (1)
  • TrackerRuleStore (34-36)
internal/services/trackerrules/service.go (1)
  • Service (39-48)
internal/api/handlers/tracker_rules.go (1)
  • NewTrackerRuleHandler (26-31)
internal/models/tracker_rule.go (2)
web/src/types/index.ts (1)
  • TrackerRule (88-104)
internal/dbinterface/querier.go (2)
  • Querier (28-33)
  • TxQuerier (17-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (19)
internal/services/reannounce/service.go (1)

352-361: LGTM! Dynamic debounce window correctly applied.

The debounce check correctly uses the dynamic window and provides contextual reasons for skipped reannounces based on the mode.

web/src/components/layout/MobileFooterNav.tsx (1)

57-57: LGTM!

The addition of the Wrench icon and the Services menu item is clean and follows the established pattern. The placement in the dropdown menu (after Cross-Seed, before Instance Backups) makes sense for the navigation hierarchy.

Also applies to: 349-357

web/src/components/layout/Header.tsx (1)

38-38: LGTM!

The Services menu item addition is consistent with the existing navigation pattern and properly positioned in the dropdown menu hierarchy.

Also applies to: 508-516

web/package.json (1)

71-71: LGTM!

The addition of @tanstack/router-cli as a dev dependency is appropriate for route generation tooling, and the version matches the @tanstack/react-router dependency for consistency.

web/src/components/layout/Sidebar.tsx (1)

27-28: LGTM!

The Services navigation item is properly integrated with consistent icon usage and positioning that matches the other navigation components (Header, MobileFooterNav).

Also applies to: 55-59

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

8-8: LGTM!

The removal of the reannounce tab and the corresponding grid column adjustment (from 8 to 7 columns) is correct. This aligns with moving the Tracker Reannounce functionality to the new Services page.

Also applies to: 51-51

web/src/routes/_authenticated/services.tsx (1)

1-11: LGTM!

The route file follows the standard TanStack Router pattern and correctly wires the Services page component to the authenticated services path.

internal/api/server.go (1)

36-37: Tracker rules service/store wiring and routes are consistent and well-scoped

The tracker rules store/service are added to Server and Dependencies in line with existing services, and the /instances/{instanceID}/tracker-rules route group cleanly exposes list/create/update/delete/reorder/apply operations under the authenticated, per-instance namespace. This matches the handler constructor signature and the tests’ undocumented route entries.

Also applies to: 67-69, 71-95, 97-128, 263-418

cmd/qui/main.go (1)

41-42: Tracker rules integration into main service lifecycle looks solid

The tracker rules store/service are initialized from the shared DB and syncManager, started in their own cancellable context, and passed into api.NewServer alongside other services. This mirrors the existing reannounce/cross-seed wiring and should behave correctly across startup and graceful shutdown.

Also applies to: 477-478, 537-538, 551-554, 616-640

web/src/routeTree.gen.ts (1)

20-21: Generated route tree correctly exposes /services under the authenticated layout

The additions for AuthenticatedServicesRoute (imports, route update call, FileRoutes* mappings, and AuthenticatedRouteChildren) are consistent with the existing authenticated routes. Given this file is generated, no manual changes are needed.

Also applies to: 50-55, 93-135, 147-176, 218-231, 301-319

web/src/components/torrents/TorrentTableOptimized.tsx (1)

103-104: Reannounce status icon navigation to /services is wired correctly

The new useNavigate usage cleanly routes users to /services with the current instanceId in the search params, which matches how the Services page derives its selection. Stopping propagation in the click handler avoids interfering with table selection, and the icon remains gated on instance?.reannounceSettings?.enabled, so behavior stays consistent with the previous indicator semantics.

Also applies to: 876-882, 2794-2806

internal/api/handlers/tracker_rules.go (3)

47-71: Payload-to-model mapping and normalization look good

Centralizing trimming, domain normalization, and optional sortOrder in TrackerRulePayload.toModel is clean and keeps handler logic thin. This also aligns well with the TrackerRule model fields.


102-110: Validation correctly enforces name and tracker/default presence

The Name check and “at least one tracker or default” guard look sound and should prevent creation/update of unusable rules.

Also applies to: 141-149


237-263: String cleaning and domain normalization helpers are solid

cleanStringPtr and normalizeTrackerDomains nicely avoid storing empty/whitespace values and ensure deduplicated domain lists, which will keep persisted rule data tidy.

internal/services/trackerrules/service.go (3)

69-88: Service startup and loop structure look appropriate

The Start/loop pattern with a ticker, context cancellation, and dependency nil-checks in applyAll is straightforward and should behave well in long-running processes.


325-355: Tracker-domain extraction and tag matching are robust

Using ExtractDomainFromURL plus a sanitized fallback, set-aggregation, and sorted output gives predictable domain lists to match against. The tag matching helper is also straightforward and case-insensitive, which is what users will expect.

Also applies to: 373-383


172-183: Rewritten Review Comment

This review comment contains an incorrect analysis and should be dismissed. The unit conversions are actually correct.

The code properly handles unit consistency:

  1. rule.UploadLimitKiB (KiB) is multiplied by 1024 to create desired (bytes) for comparison with torrent.UpLimit
  2. The KiB value is correctly batched and passed to SetTorrentUploadLimit(limit)
  3. SetTorrentUploadLimit has a parameter explicitly named limitKBs and internally converts to bytes with limitBytes := limitKBs * 1024 (line 3691)

Passing desired (bytes) instead would be incorrect—it would cause the value to be multiplied by 1024 twice. The current implementation correctly passes KiB values to an API that expects KiB input.

Likely an incorrect or invalid review comment.

internal/models/tracker_rule.go (2)

16-32: TrackerRule model matches API/web shape well

The model fields and JSON tags line up with the frontend TrackerRule type, including optional limits and timestamps, which should keep serialization round-trips simple.


305-319: Reorder implementation is simple and instance-safe

Reordering uses a transaction and scopes the UPDATE by both id and instance_id, which is the right safety guard for cross-instance IDs. As long as callers pass the full ordered ID list per instance, this should behave predictably.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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)
internal/services/reannounce/service_test.go (2)

372-372: Consider renaming the test to reflect the updated behavior.

The test name TestServiceEnqueue_AggressiveModeSkipsDebounce is misleading after the changes. Aggressive mode no longer skips debounce entirely—it now respects a cooldown period based on ReannounceIntervalSeconds. Consider renaming to something like TestServiceEnqueue_AggressiveModeUsesRetryIntervalForCooldown to accurately describe the behavior being tested.


396-404: Test logic correctly validates the new aggressive mode cooldown behavior.

The updated test properly verifies that aggressive mode now respects a per-instance cooldown based on ReannounceIntervalSeconds. The test flow is clear:

  1. At 5 seconds after completion: job correctly debounces (5 < 7 second interval)
  2. At 15 seconds: cooldown expires, new job starts (15 > 7)

Optional: Consider adding a test case for the default behavior when ReannounceIntervalSeconds is 0 or unset. The AI summary mentions "fallback to defaults if missing," which would be good to verify explicitly:

// Test case for default retry interval when not configured
svc.settingsCache.Replace(&models.InstanceReannounceSettings{
    InstanceID: 1, 
    Aggressive: true, 
    Enabled: true,
    // ReannounceIntervalSeconds not set or 0
})
// Verify expected default cooldown behavior
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d339da4 and 8d2b3f3.

📒 Files selected for processing (1)
  • internal/services/reannounce/service_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/services/reannounce/service_test.go (2)
web/src/types/index.ts (1)
  • InstanceReannounceSettings (42-55)
internal/models/instance_reannounce.go (1)
  • InstanceReannounceSettings (25-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Build Docker images (linux/arm/v6)
  • GitHub Check: Build Docker images (linux/arm/v7)
  • GitHub Check: Build Docker images (linux/arm64)
  • GitHub Check: Build Docker images (linux/amd64/v3)
  • GitHub Check: Build Docker images (linux/amd64/v2)
  • GitHub Check: Build Docker images (linux/amd64)
  • GitHub Check: Build distribution binaries

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
web/src/pages/Services.tsx (1)

23-38: Align selection, URL param, and “active instance” messaging

selectedInstanceId is derived from all instances (including inactive), the <Select> lists all instances, but selectedInstance is looked up only in activeInstances. This means:

  • Picking an inactive instance keeps the select and instanceId URL param pointing at that inactive ID.
  • selectedInstance is undefined, so the page shows “Select an active instance to configure services” and renders no panels.

This reproduces the earlier concern about the URL/UI getting out of sync for inactive instances.

To make behavior predictable, consider one of:

  1. Only allow active instances to be selected (simpler, matches copy):
-  const activeInstances = useMemo(
-    () => (instances ?? []).filter((instance) => instance.isActive),
-    [instances]
-  )
+  const activeInstances = useMemo(
+    () => (instances ?? []).filter((instance) => instance.isActive),
+    [instances]
+  )

   const selectedInstanceId = useMemo(() => {
     const fromSearch = search.instanceId ? Number(search.instanceId) : undefined
-    const allInstances = instances ?? []
-    if (fromSearch && allInstances.some((inst) => inst.id === fromSearch)) {
+    const allActive = activeInstances
+    if (fromSearch && allActive.some((inst) => inst.id === fromSearch)) {
       return fromSearch
     }
-    if (allInstances.length > 0) {
-      return allInstances[0]?.id
+    if (allActive.length > 0) {
+      return allActive[0]?.id
     }
     return undefined
-  }, [instances, search.instanceId])
+  }, [activeInstances, search.instanceId])
-          {instances && instances.length > 0 && (
+          {activeInstances.length > 0 && (
             <Select
               value={selectedInstanceId ? String(selectedInstanceId) : undefined}
               onValueChange={handleInstanceChange}
             >
@@
-              <SelectContent>
-                {(instances ?? []).map((instance) => (
+              <SelectContent>
+                {activeInstances.map((instance) => (
  1. Or derive selectedInstance from all instances and adjust the messaging to reflect that services may be unavailable for inactive ones, ensuring the URL param, selected option, and rendered instance always refer to the same object.

Also applies to: 51-51, 63-88, 97-101

🧹 Nitpick comments (1)
internal/services/trackerrules/service.go (1)

111-115: Clarify/adjust debounce behavior for manual “Apply once” and consider nil‑safety

ApplyOnceForInstance calls applyForInstance directly, and applyForInstance uses the same SkipWithin debounce window as the periodic loop:

if ok && now.Sub(ts) < s.cfg.SkipWithin {
    continue
}
...
s.mu.Lock()
instLastApplied[torrent.Hash] = now
s.mu.Unlock()

This means a user who just edited rules and presses “Apply now” may see no changes for torrents that were processed in the last SkipWithin (default 2 minutes), even though they explicitly requested an immediate apply. The same applies if they’ve manually changed limits in qBittorrent and expect this to override right away.

Two possible improvements:

  1. Bypass debounce for manual runs
    Treat ApplyOnceForInstance as an override path:
-func (s *Service) ApplyOnceForInstance(ctx context.Context, instanceID int) error {
-	return s.applyForInstance(ctx, instanceID)
-}
+func (s *Service) ApplyOnceForInstance(ctx context.Context, instanceID int) error {
+	if s == nil {
+		return nil
+	}
+	// Option A: bypass debounce window for explicit/manual runs
+	return s.applyForInstanceWithOptions(ctx, instanceID, true)
+}

with an internal helper that optionally skips the SkipWithin check (or resets instLastApplied before the loop). That keeps the background loop gentle on qBittorrent while ensuring explicit user actions are honored immediately.

  1. Keep debounce but document it and tighten safety
    If you prefer to keep debouncing even for manual triggers, it’s still worth:
  • Adding a brief comment where ApplyOnceForInstance is used or on the API route explaining that “Apply now” is subject to the same debounce as the background worker.
  • Adding a small nil-guard for consistency with Start:
 func (s *Service) ApplyOnceForInstance(ctx context.Context, instanceID int) error {
-	return s.applyForInstance(ctx, instanceID)
+	if s == nil {
+		return nil
+	}
+	return s.applyForInstance(ctx, instanceID)
 }

This isn’t a correctness bug, but adjusting or documenting the behavior will avoid confusion when users click “Apply now” and see no effect due to the debounce window.

Also applies to: 136-167, 199-204

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d2b3f3 and c9a6fb9.

📒 Files selected for processing (4)
  • internal/api/handlers/tracker_rules.go (1 hunks)
  • internal/services/reannounce/service.go (1 hunks)
  • internal/services/trackerrules/service.go (1 hunks)
  • web/src/pages/Services.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/api/handlers/tracker_rules.go
🧰 Additional context used
🧬 Code graph analysis (2)
web/src/pages/Services.tsx (3)
web/src/hooks/useInstances.ts (1)
  • useInstances (10-185)
web/src/components/instances/preferences/TrackerRulesPanel.tsx (1)
  • TrackerRulesPanel (49-418)
web/src/components/instances/preferences/TrackerReannounceForm.tsx (1)
  • TrackerReannounceForm (54-577)
internal/services/trackerrules/service.go (3)
internal/models/tracker_rule.go (1)
  • TrackerRule (16-32)
internal/qbittorrent/sync_manager.go (1)
  • SyncManager (136-140)
web/src/types/index.ts (3)
  • Torrent (204-261)
  • TrackerRule (88-104)
  • Category (342-345)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (3)
internal/services/reannounce/service.go (1)

341-358: LGTM! Dynamic debounce window correctly implemented.

The logic correctly computes a per-instance debounce window based on aggressive mode settings and applies it consistently in the debounce check. The contextual skip reasons ("cooldown window" vs "retry interval window") improve observability.

The previous review concern about an unreachable settings == nil check has been addressed—the current code safely accesses settings.ReannounceIntervalSeconds only when isAggressive is true, which guarantees settings != nil.

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

18-22: Services page wiring and layout look solid

Instance fetching, URL search integration, and conditional rendering of TrackerRulesPanel and TrackerReannounceForm are cohesive and match the new Services route usage. Once the instance-selection behavior is tightened up, this page should be in good shape.

Also applies to: 53-61, 103-110

internal/services/trackerrules/service.go (1)

23-67: Tracker‑rules service logic, batching, and matching look solid

  • Config defaults, dependency guards in applyAll, and per-instance lastApplied tracking with sync.RWMutex are implemented carefully and avoid data races.
  • Batch construction in applyForInstance respects KiB vs bytes, only enqueues changes where limits/ratio/time differ, and uses limitHashBatch to cap batch sizes without mutating the original slices.
  • selectRule + matchesTracker + collectTrackerDomains correctly leverage qbt.Torrent.Trackers and TorrentTracker.Url fields from github.com/autobrr/go-qbittorrent for domain-based matching, with a clean fallback to IsDefault rules when no specific tracker match is found.
  • Tag/category filters and case-insensitive comparisons (torrentHasTag, EqualFold) align with how qBittorrent exposes string fields and should be robust across trackers.

No blocking issues spotted in the core rule‑application path.

Also applies to: 69-110, 239-380

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/services/reannounce/REANNOUNCE.md (2)

41-45: Update Activity Log navigation to reflect Services page move.

The Activity Log section in REANNOUNCE.md contains outdated navigation instructions. The Tracker Reannounce feature, including Activity Log, has moved from Settings > Instances to the Services page. Update lines 41-45 to reflect the new location:

Current (incorrect):

1. Go to the **Tracker Reannounce** tab.
2. Click **Activity Log**.

Corrected:

1. Go to the **Services** page.
2. Select your instance from the dropdown.
3. In the **Tracker Reannounce** section, click the **Activity Log** tab.

11-14: Update Quick Start and Activity Log navigation to reflect Services page move.

The Tracker Reannounce feature has been moved to the Services page, but the documentation at lines 11-14 and 43-44 still references the old location (Settings > Instances). Users will not find Tracker Reannounce there.

Update lines 11-14 to:

1. Go to **Services** in the main navigation.
2. Select an instance from the dropdown.
3. In the **Tracker Reannounce** section, toggle **Enabled** to turn it on.
4. Click **Save Changes**.

Update lines 43-44 to:

1. Go to **Services** and select your instance.
2. Click the **Activity Log** tab in the Tracker Reannounce section.
🧹 Nitpick comments (2)
web/src/components/instances/preferences/TrackerReannounceForm.tsx (2)

124-155: Centralized persistence looks good; consider syncing sanitized state and handling rollback on error

Consolidating update logic into persistSettings is a solid improvement and keeps submit/toggle flows consistent. One follow‑up you might consider:

  • After sanitizeSettings(nextSettings), the UI settings state is not updated with the sanitized version. If sanitize trims/clamps values, the DB and UI can temporarily diverge until the component remounts or data is re-fetched.
  • When called from handleToggleEnabled(false), the UI is already updated to “disabled” before the mutation completes. If the request fails, the UI still shows disabled while the server is not, and activityQuery also disables polling.

If you want tighter UX, you could set settings to sanitized and optionally revert the relevant bits in onError.


162-169: Disable persists immediately; consider what should happen on failure

The toggle flow (local state update, then persisting only when disabling) matches the described UX: turning off monitoring is immediate, turning it on requires Save.

One caveat: if the disable call fails, the UI still shows monitoring disabled and activityEnabled becomes false while the server may still be running reannounces. You might want to either:

  • revert enabled to true in onError, or
  • surface a stronger error hint telling the user the disable didn’t apply.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e7f5c8 and d8f8f3f.

📒 Files selected for processing (3)
  • README.md (1 hunks)
  • internal/services/reannounce/REANNOUNCE.md (2 hunks)
  • web/src/components/instances/preferences/TrackerReannounceForm.tsx (7 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-06T12:11:04.963Z
Learnt from: Audionut
Repo: autobrr/qui PR: 553
File: internal/services/crossseed/service.go:1045-1082
Timestamp: 2025-11-06T12:11:04.963Z
Learning: The autobrr/qui project uses a custom go-qbittorrent client library (github.com/autobrr/go-qbittorrent) that supports both "paused" and "stopped" parameters when adding torrents via the options map. Both parameters should be set together when controlling torrent start state, as seen in internal/services/crossseed/service.go and throughout the codebase.

Applied to files:

  • README.md
  • internal/services/reannounce/REANNOUNCE.md
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (8)
README.md (1)

38-39: Documentation updates accurately reflect the new features.

Both the Tracker Reannounce description update and the new Tracker Rules feature entry are clear, user-friendly, and consistent with the PR objectives. The descriptions concisely explain what each feature does without unnecessary jargon.

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

157-161: Form submit wiring is straightforward and correct

The submit handler cleanly prevents default and delegates to persistSettings(settings), matching the new centralized flow and the Button type="submit" usage.


205-239: Header wiring and enable toggle integration look solid

The Card/header restructuring, tooltip for “Automatic Tracker Reannounce”, and the switch wiring (checked, onCheckedChange, disabled={isUpdating}) all look consistent and should prevent double-toggles while an update is in flight.


271-308: Quick Retry copy/readability improvements look good

Renaming the section to “Quick Retry” and updating the tooltip/description to spell out the cooldown behavior against the default 2 minutes clarifies usage without changing behavior (settings.aggressive still drives it). No issues from a code perspective.


357-360: Tabs trigger layout changes are purely stylistic

Only whitespace/formatting changed here; behavior (value, disabled, classes) remains the same.


388-391: Same for tags include toggle – no functional change

The tags include TabsTrigger edits are layout-only; runtime behavior is unchanged.


419-422: Trackers include toggle edits are also cosmetic

Tracker domain TabsTrigger changes are formatting-only, with identical props and behavior.


508-515: Activity empty state visual update is safe

The dashed-border placeholder and padding changes are visual only; logic for when it appears and what it says is unchanged.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (1)
web/src/components/layout/MobileFooterNav.tsx (1)

392-400: Services dropdown entry looks good; optionally consider instance scoping

The new Services item follows the existing dropdown pattern (DropdownMenuItem + Link, icon + label) and correctly targets /services. If the Services page can accept an instance identifier (e.g., via route params or search) and you want to minimize extra taps on mobile, you could optionally pass the currently active instance from currentInstanceId here; otherwise this is fine as-is.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38e6f18 and 120d3f4.

📒 Files selected for processing (5)
  • web/src/components/layout/Header.tsx (2 hunks)
  • web/src/components/layout/MobileFooterNav.tsx (2 hunks)
  • web/src/components/layout/Sidebar.tsx (2 hunks)
  • web/src/components/torrents/TorrentTableOptimized.tsx (3 hunks)
  • web/src/types/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • web/src/components/layout/Header.tsx
  • web/src/types/index.ts
  • web/src/components/layout/Sidebar.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
web/src/components/layout/MobileFooterNav.tsx (1)
web/src/components/ui/dropdown-menu.tsx (1)
  • DropdownMenuItem (251-251)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (3)
web/src/components/layout/MobileFooterNav.tsx (1)

62-62: Wrench icon import is correctly wired to the new menu item

The added Wrench import is used in the Services dropdown entry below and fits the existing lucide-react icon pattern; no issues here.

web/src/components/torrents/TorrentTableOptimized.tsx (2)

103-103: LGTM! Router hooks properly imported and initialized.

The useNavigate and useSearch hooks from TanStack Router are correctly imported and initialized following React's Rules of Hooks.

Also applies to: 879-879


2804-2807: Verification complete—navigation implementation is correct.

The /services route is properly defined in web/src/routes/_authenticated/services.tsx, and the search parameter schema at web/src/pages/Services.tsx:14-16 correctly declares instanceId as an optional string: type ServicesSearch = { instanceId?: string }. The type conversion flow is sound: the instanceId number is converted to a string for the URL search parameter and then converted back to a number in the Services page component (line 29: Number(search.instanceId)). The navigation call is valid and type-safe.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 120d3f4 and bd8b4bb.

📒 Files selected for processing (4)
  • web/src/components/instances/preferences/TrackerReannounceForm.tsx (9 hunks)
  • web/src/components/ui/multi-select.tsx (2 hunks)
  • web/src/hooks/useInstances.ts (1 hunks)
  • web/src/pages/CrossSeedPage.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
web/src/hooks/useInstances.ts (2)
web/src/types/index.ts (1)
  • InstanceResponse (120-125)
internal/api/handlers/instances.go (1)
  • InstanceResponse (352-366)
web/src/components/instances/preferences/TrackerReannounceForm.tsx (3)
web/src/components/torrents/CategoryTree.tsx (2)
  • buildCategoryTree (50-105)
  • CategoryNode (19-27)
web/src/components/ui/multi-select.tsx (1)
  • Option (9-13)
web/src/types/index.ts (1)
  • InstanceReannounceSettings (42-55)
web/src/pages/CrossSeedPage.tsx (1)
web/src/components/torrents/CategoryTree.tsx (2)
  • buildCategoryTree (50-105)
  • CategoryNode (19-27)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (12)
web/src/components/ui/multi-select.tsx (2)

9-13: LGTM! Optional property enables hierarchical rendering.

The optional level property is a clean addition that maintains backward compatibility while enabling indented option trees.


156-161: LGTM! Indentation logic correctly implemented.

The conditional padding calculation (level × 12px) provides clear visual hierarchy for nested options.

web/src/pages/CrossSeedPage.tsx (2)

6-6: LGTM! Import statement is correct.

Consistent with similar usage in TrackerReannounceForm.tsx.


683-711: LGTM! Tree-based category handling correctly implemented.

The refactor correctly:

  • Builds a hierarchical category tree
  • Flattens it with level information for indentation
  • Preserves manually typed categories as extras with level 0

The visitNodes recursive traversal and extras filtering logic are both sound.

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

17-17: LGTM! Import statement is correct.

Consistent with the hierarchical category handling pattern used in CrossSeedPage.tsx.


89-109: LGTM! Category tree building correctly implemented.

The hierarchical category structure is correctly built and flattened with level information. Note that unlike CrossSeedPage.tsx, this implementation doesn't append manually typed extras—this appears intentional since the MultiSelect component has creatable enabled and appendUniqueValue handles custom entries.


137-168: Good refactoring! Extracted submission logic into reusable helper.

The persistSettings helper reduces code duplication and enables customizable success messages. The sanitization and error handling are appropriate.


170-173: LGTM! Simple and correct submit handler.

Properly delegates to persistSettings with appropriate default behavior.


175-182: Good UX enhancement! Immediate persistence when disabling.

The auto-save behavior when disabling monitoring provides good user feedback while allowing users to configure settings before enabling. This is a sensible UX pattern.


218-250: LGTM! Improved header layout and UX.

The enhancements provide:

  • Better responsive design with flex-row on larger screens
  • Helpful tooltip explaining the automatic reannounce feature
  • Clearer visual styling for the enable/disable toggle
  • Proper disabled state during updates

284-284: LGTM! Terminology change improves clarity.

The rename from "Aggressive Mode" to "Quick Retry" is more user-friendly and accurately describes the behavior. The change is applied consistently across labels, tooltips, and descriptions while maintaining backend compatibility with the aggressive property.

Also applies to: 303-318


521-521: LGTM! Dashed border improves empty state visual.

The dashed border styling better conveys the placeholder nature of the empty activity state.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2669fff and fb6a03c.

📒 Files selected for processing (9)
  • README.md (1 hunks)
  • internal/api/handlers/tracker_rules.go (1 hunks)
  • internal/database/migrations/019_add_tracker_rules.sql (1 hunks)
  • internal/models/tracker_rule.go (1 hunks)
  • internal/services/trackerrules/TRACKER_RULES.md (1 hunks)
  • internal/services/trackerrules/service.go (1 hunks)
  • web/src/components/instances/preferences/TrackerRulesPanel.tsx (1 hunks)
  • web/src/hooks/useInstances.ts (1 hunks)
  • web/src/types/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • web/src/hooks/useInstances.ts
  • README.md
  • web/src/types/index.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-25T11:39:54.717Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 637
File: web/src/pages/Dashboard.tsx:805-831
Timestamp: 2025-11-25T11:39:54.717Z
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/TrackerRulesPanel.tsx
📚 Learning: 2025-11-19T20:04:51.737Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 606
File: internal/database/migrations/016_add_instance_reannounce_settings.sql:21-27
Timestamp: 2025-11-19T20:04:51.737Z
Learning: In the autobrr/qui repository, PRAGMA recursive_triggers is never enabled in the SQLite setup. The AFTER UPDATE trigger pattern that issues a separate UPDATE statement on the same table (e.g., to update an updated_at timestamp) is used consistently across migrations and does not cause recursion with recursive_triggers disabled (the SQLite default). This pattern should remain consistent across all migrations.

Applied to files:

  • internal/database/migrations/019_add_tracker_rules.sql
🧬 Code graph analysis (2)
internal/api/handlers/tracker_rules.go (3)
internal/models/tracker_rule.go (2)
  • TrackerRuleStore (34-36)
  • TrackerRule (16-32)
internal/services/trackerrules/service.go (1)
  • Service (39-48)
internal/api/handlers/helpers.go (2)
  • RespondError (34-38)
  • RespondJSON (22-31)
internal/models/tracker_rule.go (1)
internal/dbinterface/querier.go (1)
  • Querier (30-35)
🪛 markdownlint-cli2 (0.18.1)
internal/services/trackerrules/TRACKER_RULES.md

51-51: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/models/tracker_rule.go (1)

291-305: Silent no-op for non-matching IDs in Reorder.

If orderedIDs contains IDs that don't exist or belong to a different instance, those UPDATEs will affect zero rows without any error. This is defensive but could mask issues where the frontend sends stale IDs.

Consider whether you want to verify all IDs were successfully updated:

 func (s *TrackerRuleStore) Reorder(ctx context.Context, instanceID int, orderedIDs []int) error {
 	tx, err := s.db.BeginTx(ctx, nil)
 	if err != nil {
 		return err
 	}
 	defer tx.Rollback()
 
 	for idx, id := range orderedIDs {
-		if _, err := tx.ExecContext(ctx, `UPDATE tracker_rules SET sort_order = ? WHERE id = ? AND instance_id = ?`, idx+1, id, instanceID); err != nil {
+		res, err := tx.ExecContext(ctx, `UPDATE tracker_rules SET sort_order = ? WHERE id = ? AND instance_id = ?`, idx+1, id, instanceID)
+		if err != nil {
 			return err
 		}
+		if rows, _ := res.RowsAffected(); rows == 0 {
+			return sql.ErrNoRows // or a custom error indicating invalid rule ID
+		}
 	}
 
 	return tx.Commit()
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb6a03c and 5d8192b.

📒 Files selected for processing (2)
  • internal/models/tracker_rule.go (1 hunks)
  • internal/services/trackerrules/TRACKER_RULES.md (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • internal/services/trackerrules/TRACKER_RULES.md
🧰 Additional context used
🧬 Code graph analysis (1)
internal/models/tracker_rule.go (2)
web/src/types/index.ts (1)
  • TrackerRule (88-104)
internal/dbinterface/querier.go (1)
  • Querier (30-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (6)
internal/models/tracker_rule.go (6)

16-40: LGTM!

The TrackerRule struct fields are well-defined with appropriate pointer types for nullable database columns, and the JSON tags align with the frontend TypeScript interface.


42-80: LGTM!

The pattern normalization logic correctly handles multiple delimiters, whitespace trimming, and deduplication while preserving insertion order.


149-204: LGTM!

The Get method correctly scans nullable columns and derives TrackerDomains. Its id-only lookup is appropriate since it's called after operations that already verify instance ownership via their WHERE clauses.


215-249: LGTM!

The Create method correctly normalizes the tracker pattern, auto-assigns sort order when not specified, and retrieves the created rule. The pattern of treating sortOrder == 0 as "auto-assign" is reasonable.


251-278: RowsAffected check is now in place.

The previous review concern about potential cross-instance information leak has been addressed. The method now correctly checks RowsAffected() and returns sql.ErrNoRows when the UPDATE matches zero rows before calling Get.


307-333: LGTM!

The nullable helper functions correctly handle pointer-to-value conversion for SQL parameters, and boolToInt appropriately handles SQLite's lack of native boolean type.

@s0up4200 s0up4200 merged commit dfad426 into main Nov 25, 2025
11 checks passed
@s0up4200 s0up4200 deleted the feat/tracker-rules-services-page branch November 25, 2025 14:13
@coderabbitai coderabbitai bot mentioned this pull request Feb 13, 2026
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