Skip to content

feat(rss): add native rss support - #801

Merged
s0up4200 merged 36 commits into
developfrom
feat/native-rss-support
Jan 21, 2026
Merged

feat(rss): add native rss support#801
s0up4200 merged 36 commits into
developfrom
feat/native-rss-support

Conversation

@s0up4200

@s0up4200 s0up4200 commented Dec 16, 2025

Copy link
Copy Markdown
Collaborator

Closes #478

Relies on autobrr/go-qbittorrent#100

Summary by CodeRabbit

  • New Features

    • Full RSS management UI: sidebar/header/footer links, per‑instance feeds/folders/articles, rule editor/preview, add/edit dialogs, refresh/reprocess actions, and SSE-driven live updates.
  • API

    • New REST endpoints and SSE stream for RSS operations; responses may include optional warning fields and support updating feed URLs.
  • Frontend

    • Client hooks, types, routes, mutations and UI wiring to manage feeds, rules and articles with proper invalidation and feedback.
  • Chores

    • Dependency bump and capability flag added for feed‑URL support.

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

@s0up4200 s0up4200 added enhancement New feature or request torrent rss labels Dec 16, 2025
@coderabbitai

coderabbitai Bot commented Dec 16, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds end-to-end RSS support: backend REST handlers and SSE, SyncManager RSS proxies and qBittorrent capability flag; OpenAPI schemas; frontend types, API client, hooks and SSE client; a new authenticated /rss route, navigation entries, and a full RSS management UI. No public API removals.

Changes

Cohort / File(s) Summary
Dependency
go.mod
Updated github.com/autobrr/go-qbittorrent version.
Backend — API helpers & capabilities
internal/api/handlers/helpers.go, internal/api/handlers/capabilities.go
Added exported WarningResponse type; exposed supportsSetRSSFeedURL in instance capabilities response.
Backend — RSS REST handlers
internal/api/handlers/rss.go, internal/api/server.go
New RSSHandler, public request/response types, routes mounted under authenticated API at /rss.
Backend — RSS SSE
internal/api/handlers/rss_sse.go
New RSSSSEHandler for per-instance SSE streaming, pollers, change detection and broadcasting.
Backend — qBittorrent Sync & Client
internal/qbittorrent/sync_manager.go, internal/qbittorrent/client.go
Added SyncManager RSS proxy methods (items, folders, feeds, rules, reprocess orchestration) and qBittorrent client capability supportsSetRSSFeedURL with version gating.
Backend — OpenAPI
internal/web/swagger/openapi.yaml
Added RSS endpoints, request/response schemas (including WarningResponse), and RSS tag.
Frontend — types & API client
web/src/types/index.ts, web/src/lib/api.ts
New RSS types, request/response shapes, WarningResponse, and ApiClient methods for RSS endpoints.
Frontend — React hooks & SSE
web/src/hooks/useRSS.ts, web/src/lib/rss-events.ts
Added React Query hooks and mutations for RSS; RSSEventSource SSE client with reconnection/backoff and handlers.
Frontend — Routing & page
web/src/routes/_authenticated/rss.tsx, web/src/routeTree.gen.ts, web/src/pages/RSSPage.tsx
New authenticated /rss route wired to RSSPage UI (Feeds & Rules) with URL search-param wiring and dialogs.
Frontend — Navigation & UI
web/src/components/layout/Sidebar.tsx, web/src/components/layout/Header.tsx, web/src/components/layout/MobileFooterNav.tsx, web/src/components/ui/alert.tsx
Added "RSS" navigation entries (sidebar/header/mobile) and a warning alert variant; minor UI tweaks.
Frontend — Misc hooks
web/src/hooks/useInstanceMetadata.ts, web/src/hooks/useInstancePreferences.ts
Conditional metadata query enablement (instanceId>0); updatePreferences now accepts optional options parameter.
sequenceDiagram
    participant User
    participant UI as React UI (RSSPage)
    participant Hooks as React Query Hooks
    participant SSE as RSSEventSource (client)
    participant API as ApiClient (/api/instances/{id}/rss)
    participant Server as Backend (RSSHandler / RSSSSEHandler)
    participant SM as SyncManager
    participant QBT as qBittorrent Client

    User->>UI: Open RSS page
    UI->>Hooks: useRSSFeeds(instanceId)
    Hooks->>SSE: connect /instances/{id}/rss/events
    SSE-->>Hooks: feeds_update
    Hooks-->>UI: update cache -> re-render

    User->>UI: Add Feed (with optional folder)
    UI->>Hooks: mutate addRSSFeed(data)
    Hooks->>API: POST /instances/{id}/rss/feeds
    API->>Server: HTTP request
    Server->>SM: AddRSSFeed(ctx, id, url, path)
    SM->>QBT: client.AddRSSFeed(...)
    QBT-->>SM: success
    alt folder specified
        Server->>SM: MoveRSSItem(ctx, id, feedPath, destPath)
        SM->>QBT: client.MoveRSSItem(...)
        QBT-->>SM: moved
    end
    SM-->>Server: result (may include warning)
    Server-->>API: JSON (may include WarningResponse)
    API-->>Hooks: mutation success -> invalidate feeds
    Hooks->>API: GET /instances/{id}/rss/items
    API-->>Hooks: updated items
    Hooks-->>UI: updated feed tree
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

backend, web

Suggested reviewers

  • buroa

Poem

🐇
I hopped through branches, feeds in tow,
Folders and rules in tidy row,
I sniffed each article and gave a cheer,
Fetch, nibble, sync — RSS is here!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(rss): add native rss support' clearly and concisely summarizes the main change—adding RSS support to the project.
Linked Issues check ✅ Passed The PR comprehensively implements qBittorrent RSS features as required in issue #478, including API handlers, routes, database integration, UI components, and SSE support for real-time updates.
Out of Scope Changes check ✅ Passed All changes are scoped to RSS feature implementation. Minor updates like alert variant styling and hook parameter refactoring are supporting changes for the RSS feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/native-rss-support

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

@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: 3

🧹 Nitpick comments (7)
web/src/routes/_authenticated/rss.tsx (1)

1-56: URL‑driven RSS route state looks solid

The zod search schema, Route.useSearch/useNavigate wiring, and the way RSSPage props are derived from search params are all consistent with the rest of the router setup and give nice deep‑linkable state. If you ever see confusion from stale selections, you could optionally clear ruleName when switching to the feeds tab (and vice versa), but it’s not required for correctness.

web/src/hooks/useRSS.ts (2)

22-29: Include withData in the feeds query key to avoid cache collisions

useRSSFeeds passes withData into api.getRSSItems(instanceId, withData) but rssKeys.feeds(instanceId) only keys on instanceId. If you ever call useRSSFeeds with different withData values for the same instance, they’ll share a cache entry and you can get stale/mismatched data.

You can future‑proof this by keying on the effective flag:

-export const rssKeys = {
-  all: ["rss"] as const,
-  feeds: (instanceId: number) => [...rssKeys.all, "feeds", instanceId] as const,
+export const rssKeys = {
+  all: ["rss"] as const,
+  feeds: (instanceId: number, withData: boolean) =>
+    [...rssKeys.all, "feeds", instanceId, withData] as const,
   rules: (instanceId: number) => [...rssKeys.all, "rules", instanceId] as const,
   matching: (instanceId: number, ruleName: string) =>
     [...rssKeys.all, "matching", instanceId, ruleName] as const,
 }
@@
-export function useRSSFeeds(instanceId: number, options?: { enabled?: boolean; withData?: boolean }) {
+export function useRSSFeeds(
+  instanceId: number,
+  options?: { enabled?: boolean; withData?: boolean },
+) {
   const shouldEnable = (options?.enabled ?? true) && instanceId > 0
   const withData = options?.withData ?? true
 
   return useQuery({
-    queryKey: rssKeys.feeds(instanceId),
+    queryKey: rssKeys.feeds(instanceId, withData),
     queryFn: () => api.getRSSItems(instanceId, withData),

Also applies to: 35-45


177-221: Consider invalidating matching‑articles queries when rules change

useSetRSSRule, useRenameRSSRule, useRemoveRSSRule, and useReprocessRSSRules only invalidate rssKeys.rules(instanceId) (or feeds), but not rssKeys.matching(instanceId, ruleName). If the UI keeps a rule preview panel open that uses useRSSMatchingArticles, it may continue showing stale matches after rule edits/renames/removals or a reprocess.

Not strictly required today, but you could defensively clear those caches when you have the rule name available, e.g.:

export function useSetRSSRule(instanceId: number) {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (data: SetRSSRuleRequest) => api.setRSSRule(instanceId, data),
    onSuccess: (_result, variables) => {
      queryClient.invalidateQueries({ queryKey: rssKeys.rules(instanceId) })
+     if (variables?.name) {
+       queryClient.invalidateQueries({
+         queryKey: rssKeys.matching(instanceId, variables.name),
+       })
+     }
    },
  })
}

and similarly for rename/remove, using the old/new names as appropriate.

web/src/pages/RSSPage.tsx (3)

495-507: Hardcoded delay for query invalidation is fragile.

The 2000ms timeout is arbitrary and may not align with actual refresh completion time. If the server refresh takes longer, users see stale data; if shorter, there's unnecessary delay.

Consider invalidating immediately after the mutation succeeds, or using onSettled callback if you want to delay:

 const handleRefreshFeed = async (path: string) => {
   try {
     await refreshFeed.mutateAsync({ itemPath: path })
     toast.success("Feed refresh triggered")
-    // Invalidate to pick up changes
-    setTimeout(() => {
-      queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) })
-    }, 2000)
+    // Invalidate immediately - the server has triggered the refresh
+    queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) })
   } catch (err) {

1518-1521: Consider consistent parseInt radix usage.

While parseInt(e.target.value) || 0 works, explicitly specifying radix 10 is more defensive and consistent with other usages in this file:

-                  onChange={(e) => setIgnoreDays(parseInt(e.target.value) || 0)}
+                  onChange={(e) => setIgnoreDays(parseInt(e.target.value, 10) || 0)}

Same applies to line 1810 in EditRuleDialog.


1286-1295: Consider extracting shared form logic between Add and Edit rule dialogs.

AddRuleDialog and EditRuleDialog share approximately 90% identical code (form fields, state management, validation). Consider extracting a shared RuleForm component or custom hook to reduce duplication:

// Example shared hook
function useRuleFormState(initialRule?: RSSAutoDownloadRule) {
  const [mustContain, setMustContain] = useState(initialRule?.mustContain ?? "")
  // ... other state
  return { mustContain, setMustContain, /* ... */ }
}

This would make future maintenance easier when adding new rule fields.

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

176-263: AddFeed folder workaround has potential race condition but handles failures gracefully.

The logic to work around qBittorrent's API limitation (path param is full path, not parent folder) is sound. The theoretical race condition (concurrent feed additions) is mitigated by:

  1. Warning responses for partial failures
  2. Low likelihood in typical usage

The implementation is pragmatic. Consider adding a comment explaining why this approach is necessary:

+	// qBittorrent's AddFeed API uses 'path' as the full item path, not the parent folder.
+	// To add a feed to a specific folder, we must:
+	// 1. Add the feed to root
+	// 2. Find the newly created feed by comparing before/after state
+	// 3. Move it to the target folder
 	if targetFolder != "" {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13b40b5 and 394b93a.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • go.mod (1 hunks)
  • internal/api/handlers/helpers.go (1 hunks)
  • internal/api/handlers/rss.go (1 hunks)
  • internal/api/server.go (2 hunks)
  • internal/qbittorrent/sync_manager.go (1 hunks)
  • web/src/components/layout/Sidebar.tsx (1 hunks)
  • web/src/hooks/useRSS.ts (1 hunks)
  • web/src/lib/api.ts (2 hunks)
  • web/src/pages/RSSPage.tsx (1 hunks)
  • web/src/routeTree.gen.ts (11 hunks)
  • web/src/routes/_authenticated/rss.tsx (1 hunks)
  • web/src/types/index.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 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:

  • go.mod
📚 Learning: 2025-11-25T22:46:03.762Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 632
File: internal/backups/service.go:1401-1404
Timestamp: 2025-11-25T22:46:03.762Z
Learning: In qui's backup service (internal/backups/service.go), background torrent downloads initiated during manifest import intentionally use a fire-and-forget pattern with the shared service context (s.ctx). Per-run cancellation is not needed, as orphaned downloads completing after run deletion are considered harmless and acceptable. This design prioritizes simplicity over per-run lifecycle management for background downloads.

Applied to files:

  • go.mod
📚 Learning: 2025-12-11T08:40:01.329Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 746
File: internal/services/reannounce/service.go:480-481
Timestamp: 2025-12-11T08:40:01.329Z
Learning: In autobrr/qui's internal/services/reannounce/service.go, the hasHealthyTracker, getProblematicTrackers, and getHealthyTrackers functions intentionally match qbrr's lenient tracker health logic (skip unregistered trackers and check if any other tracker is healthy) rather than go-qbittorrent's strict isTrackerStatusOK logic (which treats unregistered as an immediate failure). For multi-tracker torrents, if one tracker is working, reannouncing won't help. The duplication of the health check logic across these three functions is acceptable as it's a simple one-liner, and extracting it would add unnecessary complexity.

Applied to files:

  • go.mod
📚 Learning: 2025-11-28T20:32:30.126Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go:209-212
Timestamp: 2025-11-28T20:32:30.126Z
Learning: Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go
Learning: The cross-seed recheck-resume worker intentionally runs for the process lifetime and keys pending entries by hash only. This is acceptable under the current constraint that background seeded-search runs operate on a single instance at a time; graceful shutdown and instanceID|hash keying are deferred by design.

Applied to files:

  • internal/api/server.go
🧬 Code graph analysis (6)
internal/api/server.go (2)
internal/api/handlers/rss.go (1)
  • NewRSSHandler (25-29)
web/src/routes/_authenticated/rss.tsx (1)
  • Route (16-19)
web/src/hooks/useRSS.ts (2)
web/src/lib/api.ts (1)
  • api (1823-1823)
web/src/types/index.ts (10)
  • AddRSSFeedRequest (1647-1651)
  • AddRSSFolderRequest (1643-1645)
  • RemoveRSSItemRequest (1668-1670)
  • MoveRSSItemRequest (1663-1666)
  • RefreshRSSItemRequest (1672-1674)
  • SetRSSFeedURLRequest (1653-1656)
  • SetRSSFeedRefreshIntervalRequest (1658-1661)
  • MarkRSSAsReadRequest (1676-1679)
  • SetRSSRuleRequest (1681-1684)
  • RenameRSSRuleRequest (1686-1688)
web/src/routes/_authenticated/rss.tsx (1)
web/src/pages/RSSPage.tsx (1)
  • RSSPage (109-436)
internal/qbittorrent/sync_manager.go (1)
web/src/types/index.ts (4)
  • RSSItems (1588-1590)
  • RSSRules (1638-1638)
  • RSSAutoDownloadRule (1618-1636)
  • RSSMatchingArticles (1640-1640)
web/src/types/index.ts (1)
internal/api/handlers/helpers.go (1)
  • WarningResponse (22-24)
internal/api/handlers/rss.go (1)
internal/api/handlers/helpers.go (3)
  • RespondError (39-43)
  • RespondJSON (27-36)
  • WarningResponse (22-24)
⏰ 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)
go.mod (1)

13-13: Prerelease version requires verification before merge.

The go-qbittorrent dependency is pinned to v1.15.0-rc1, a prerelease version. The previous stable release is v1.14.0, and no stable v1.15.0 release currently exists. Confirm that:

  1. Using a prerelease is acceptable for this codebase's stability requirements
  2. The RSS API methods referenced in this PR are available in v1.15.0-rc1
  3. No dependency conflicts exist with other direct/indirect dependencies
internal/api/handlers/helpers.go (1)

21-24: Shared WarningResponse type looks appropriate

Defining a simple exported WarningResponse aligned with the JSON contract (warning / omitempty) is fine and consistent with the existing ErrorResponse.

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

71-75: RSS sidebar entry wiring is consistent

The new "RSS" nav item reuses the existing navigation pattern (href, icon, active state) and matches the /rss authenticated route, so this integration looks good.

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

19-22: Frontend WarningResponse matches backend contract

The generic WarningResponse with optional warning aligns with the Go WarningResponse struct and gives you a reusable way to type non-fatal warning payloads.


1563-1689: RSS domain model and request types are coherent

The RSS article/feed tree, rule/torrent param models, and the various request DTOs (add/move/remove/refresh, rules, matching) line up cleanly with qBittorrent’s RSS concepts and should be sufficient for the hooks and API client.

internal/api/server.go (1)

274-275: RSS handler wiring under instance routes is consistent

Creating rssHandler from s.syncManager and mounting it under /instances/{instanceID}/rss inside the authenticated group matches the existing pattern for instance-scoped features and keeps routing coherent.

Also applies to: 444-447

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

19-20: Generated route tree entries for RSS are consistent

The added AuthenticatedRssRoute wiring (imports, update call, /rss fullPath/to entries, and /_authenticated/rss id + children) matches how other authenticated child routes are modeled in this generated file. No manual edits needed.

Also applies to: 61-65, 107-113, 121-127, 138-144, 155-161, 169-175, 185-191, 251-257, 325-337

web/src/pages/RSSPage.tsx (6)

1-97: LGTM - Imports and type definitions are well-structured.

The imports are organized logically, separating UI components, hooks, and types. The type guard isRSSFeed import from types is a good pattern for type narrowing.


109-156: LGTM - Instance selection and query setup are well-implemented.

Good use of enabled: instanceId > 0 to prevent queries when no instance is selected. The auto-selection logic properly prioritizes connected instances.


585-588: LGTM - Path separator is correctly hardcoded for qBittorrent API.

The backslash separator (\\) is correct as qBittorrent's RSS API uses this format regardless of the server's operating system.


1085-1091: Using array index as key is acceptable here.

Since the matching articles are simple strings without unique identifiers and this is a read-only preview list, using the index as key is acceptable. If performance issues arise with large lists, consider using a hash of feedUrl + title + idx.


1617-1634: LGTM - Form initialization effect is correctly implemented.

The useEffect properly initializes form state when the rule prop changes, with all field mappings including fallbacks for legacy field names (savePath, assignedCategory).


1879-1931: LGTM - Helper functions are well-implemented.

The recursive helper functions correctly handle the nested RSS items structure with proper type guards using isRSSFeed().

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

31-53: LGTM - Routes are well-organized following RESTful conventions.

The route structure cleanly separates feed/folder management, article management, and auto-download rules. Good use of appropriate HTTP methods.


106-126: LGTM - Consistent error handling pattern.

The handler follows the established pattern: parse instance ID, check disabled state, log errors with context, and return appropriate HTTP status codes.


617-625: LGTM - Instance ID parsing is robust.

The helper correctly validates the instance ID and returns appropriate errors for invalid input.

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

76-91: LGTM - RSS types are properly imported.

The new RSS-related types are imported consistently with existing patterns.


1799-1814: LGTM - Proper URL encoding for rule names.

Using encodeURIComponent(ruleName) correctly handles rule names with special characters in the URL path.


1724-1820: LGTM - RSS API methods are consistently implemented.

All new RSS methods follow the established patterns in the API client:

  • Proper HTTP methods for each operation
  • JSON body serialization
  • Type-safe return types
  • URL encoding where needed

Comment thread internal/api/handlers/rss.go Outdated
Comment thread internal/qbittorrent/sync_manager.go
Comment thread web/src/lib/api.ts Outdated

@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/pages/RSSPage.tsx (2)

498-510: Consider using mutation's onSuccess callback for cache invalidation instead of arbitrary timeout.

The 2-second timeout for cache invalidation is fragile - the refresh operation may complete faster or slower than expected. Consider leveraging React Query's built-in invalidation mechanisms or the mutation's onSettled callback for more reliable cache updates.

 const handleRefreshFeed = async (path: string) => {
   try {
     await refreshFeed.mutateAsync({ itemPath: path })
     toast.success("Feed refresh triggered")
-    // Invalidate to pick up changes
-    setTimeout(() => {
-      queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) })
-    }, 2000)
+    // The mutation hook should handle invalidation via onSuccess/onSettled
   } catch (err) {
     const message = err instanceof Error ? err.message : "Failed to refresh feed"
     toast.error(message)
   }
 }

Alternatively, if the backend refresh is asynchronous and doesn't return updated data, consider polling or using a more sophisticated approach with refetchInterval temporarily enabled.


1676-1692: Missing dependency warning in useEffect.

The useEffect only has [rule] as a dependency but doesn't need additional ones since the setters are stable. However, this pattern could cause issues if the dialog is opened with the same rule twice after external changes. Consider adding open to dependencies or resetting form when dialog opens.

The current implementation works correctly because the rule prop changes when editing different rules. The React linter might warn about this, but the behavior is intentional since the setters from useState are stable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 394b93a and 24768bc.

📒 Files selected for processing (2)
  • internal/web/swagger/openapi.yaml (3 hunks)
  • web/src/pages/RSSPage.tsx (1 hunks)
🔇 Additional comments (21)
internal/web/swagger/openapi.yaml (7)

3209-3265: LGTM! RSS items endpoints are well-defined.

The RSS items endpoints follow established patterns with proper request/response schemas, appropriate HTTP methods, and consistent error handling (400, 409, 500 responses).


3267-3328: LGTM! Move and refresh endpoints follow REST conventions.

The move and refresh operations correctly use POST methods for action-oriented operations with appropriate request body schemas.


3330-3400: LGTM! Folder and feed creation endpoints are properly defined.

The folder and feed creation endpoints correctly return 201 status codes. The feed endpoint appropriately includes the WarningResponse schema for partial success scenarios.


3402-3503: LGTM! Feed URL, interval, and article read endpoints are well-structured.

These endpoints properly handle feed modifications with appropriate PUT methods for updates and POST for marking articles as read.


3505-3669: LGTM! RSS rules management endpoints are comprehensive.

The rules endpoints provide full CRUD operations plus reprocess, rename, and preview capabilities. The response schema using additionalProperties for the rules map is appropriate for the key-value structure.


4892-5030: LGTM! RSS schemas are well-defined with appropriate types.

The schema definitions properly model the RSS data structures:

  • RSSItems uses recursive oneOf for nested feeds/folders
  • RSSFeed and RSSArticle capture all relevant feed metadata
  • RSSAutoDownloadRule includes both new torrentParams and legacy fields for backward compatibility
  • RSSRuleTorrentParams comprehensively covers qBittorrent's torrent parameters

5053-5054: LGTM! RSS tag added to the tags section.

The new RSS tag is properly added with a descriptive description for grouping the RSS-related endpoints.

web/src/pages/RSSPage.tsx (14)

1-104: LGTM! Imports and type definitions are well-organized.

The imports are logically grouped (React, UI libraries, custom hooks, components, types) and the RSSPageProps interface is properly typed with appropriate callback signatures.


126-135: LGTM! Auto-selection effect is properly implemented.

The effect correctly auto-selects the first connected instance (or falls back to the first instance) when no selection exists. Dependencies are correctly specified.


150-160: LGTM! Query hooks are properly configured with conditional enabling.

The queries are correctly disabled when instanceId is 0, preventing unnecessary API calls before an instance is selected.


248-266: LGTM! Main layout structure is clean and responsive.

The header layout with title, instance selector, and settings popover is well-structured with appropriate responsive behavior.


602-730: LGTM! FeedTree component handles recursive structure well.

The recursive component properly distinguishes between feeds and folders using isRSSFeed, maintains expansion state locally, and renders appropriate UI with actions for each item type.


742-826: LGTM! ArticlesPanel and ArticleRow provide clean article display.

The components handle empty states, date formatting, read/unread visual distinction, and external link actions appropriately. The rel="noopener noreferrer" attributes on external links are correctly applied for security.


843-950: LGTM! RulesTab component properly manages rule operations.

The component handles loading states, empty states, rule toggling, editing, and removal with appropriate toast feedback. The preview sheet integration for matching articles is well-implemented.


966-1105: LGTM! RuleCard provides excellent visual representation of rules.

The component effectively displays rule configuration with:

  • Color-coded badges for match/exclude patterns
  • Status badges for regex, smart filter, and enabled state
  • Metadata display with appropriate icons
  • Clear visual feedback for selected state

1118-1164: LGTM! RulePreviewSheet shows matching articles effectively.

The component properly handles loading, empty states, and displays matching articles grouped by feed URL with appropriate scrolling behavior.


1179-1273: LGTM! AddFeedDialog handles form submission correctly.

The dialog properly validates required fields, resets form state on success, and appropriately distinguishes between success and warning responses from the API.


1285-1338: LGTM! AddFolderDialog is simple and functional.

The dialog provides clear guidance about folder path format and handles form submission correctly.


1353-1631: LGTM! AddRuleDialog provides comprehensive rule configuration.

The dialog covers all necessary rule configuration options with proper form state management, validation, and reset functionality. The use of sentinel values for empty/default selections is appropriate.


1937-2001: LGTM! Helper functions correctly traverse recursive RSS structure.

The helper functions properly use the isRSSFeed type guard and handle the recursive feed/folder structure. The backslash path separator matches qBittorrent's convention.


2007-2091: LGTM! RssSettingsPopover provides convenient settings access.

The popover offers quick access to RSS settings with appropriate input controls and disabled states during updates.

… state

- Remove empty ItemPath rejection in RefreshItem handler to allow
  refreshing all feeds with empty path
- Preserve original RSS auto-downloading preference in ReprocessRSSRules
  by fetching state before toggle and restoring if it was disabled
- Fix addRSSFeed return type to include undefined for empty responses

@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: 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 24768bc and de0c795.

📒 Files selected for processing (3)
  • internal/api/handlers/rss.go (1 hunks)
  • internal/qbittorrent/sync_manager.go (1 hunks)
  • web/src/lib/api.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
internal/api/handlers/rss.go (2)
internal/qbittorrent/sync_manager.go (1)
  • SyncManager (159-182)
internal/api/handlers/helpers.go (3)
  • RespondError (39-43)
  • RespondJSON (27-36)
  • WarningResponse (22-24)
internal/qbittorrent/sync_manager.go (1)
web/src/types/index.ts (4)
  • RSSItems (1588-1590)
  • RSSRules (1638-1638)
  • RSSAutoDownloadRule (1618-1636)
  • RSSMatchingArticles (1640-1640)
🔇 Additional comments (11)
internal/qbittorrent/sync_manager.go (2)

4803-4941: RSS proxy methods are consistent, minimal, and correctly layered

The new RSS methods correctly follow the existing SyncManager pattern: acquire client once, add useful error context, and delegate to the qBittorrent client APIs without extra state. No issues spotted here.


4943-4977: ReprocessRSSRules now preserves original auto-download setting

This implementation fixes the prior behavior by reading AppPreferences, forcing a false -> true transition to trigger processing, and restoring the original disabled state when appropriate. Error handling is straightforward and avoids silently changing user preferences.

internal/api/handlers/rss.go (7)

32-54: RSS routes wiring is clear and aligned with SyncManager surface

Route registration cleanly groups item, article, and rule operations under /rss and matches the SyncManager methods one-for-one, which should keep server/client wiring straightforward.


107-157: GetItems/AddFolder handlers follow existing patterns and look correct

Instance ID parsing, JSON decoding, validation, and error handling for GetItems and AddFolder are consistent with other handlers in this package. No issues spotted.


279-337: Feed URL and refresh-interval handlers are straightforward

SetFeedURL and SetFeedRefreshInterval validate required fields, delegate to the SyncManager, and surface errors consistently. No additional validation seems necessary at this layer.


339-397: MoveItem/RemoveItem validation and delegation look correct

Both handlers enforce required identifiers (itemPath / path), forward to the SyncManager, and use respondIfInstanceDisabled for capability/instance errors. Behavior is in line with the rest of the API.


399-456: RefreshItem now correctly supports “refresh all” semantics

Allowing an empty, trimmed itemPath and passing it through to RefreshRSSItem enables the “Refresh All” UI flow without returning 400, while still validating JSON shape and logging failures with context. This addresses the previous backend/frontend mismatch cleanly.


458-613: RSS rules and reprocess handlers are consistent and well-scoped

Rule CRUD, preview, and ReprocessRules all reuse common instance parsing, validate path params/body as needed, and delegate to the corresponding SyncManager APIs. The comments around ReprocessRules match the SyncManager semantics (toggling auto-downloading while preserving original state).


617-625: parseRSSInstanceID helper is simple and robust

Centralizing instance ID parsing and validation here (including rejecting non-positive IDs) keeps the individual handlers focused and avoids repeated parsing logic.

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

75-91: RSS types are correctly surfaced from shared type definitions

Importing RSSItems, rules/matching types, request payloads, and WarningResponse here keeps the API client aligned with the shared frontend types module and avoids ad-hoc shapes.


1723-1819: RSS API client methods align with backend routes and handle empty responses

The new RSS methods mirror the server routes (/instances/{id}/rss/...) and use the appropriate request/response types. In particular, addRSSFeed returning Promise<WarningResponse | undefined> matches the request() helper’s behavior for empty bodies, so callers can safely handle the optional warning payload.

Comment thread internal/api/handlers/rss.go
When adding a feed to a folder, if the initial GetRSSItems call fails,
the before/after diff would treat all feeds as "new" and potentially
move a random existing feed. Now we track whether we have a valid
baseline and skip the diff comparison entirely if not, falling through
to the "could not identify feed" warning path.
- Remove duplicate parseRSSInstanceID, use existing parseInstanceID
- Use SetRSSAutoDownloadingEnabledCtx for proper context propagation
- Add error handling callbacks to enable RSS/auto-download buttons
- Update go-qbittorrent dependency with new Ctx variants

@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

♻️ Duplicate comments (1)
internal/api/handlers/rss.go (1)

401-428: RefreshItem: empty itemPath now correctly supports “refresh all”

Allowing an empty, trimmed itemPath to flow through to RefreshRSSItem aligns with the frontend’s { itemPath: "" } “Refresh All” behavior. Trimming whitespace but not rejecting the empty string is a good balance between validation and flexibility.

This resolves the earlier 400‑on‑empty issue while keeping the handler simple.

🧹 Nitpick comments (7)
web/src/pages/RSSPage.tsx (6)

105-147: Instance selection: avoid using 0 as a synthetic selected value in the UI control

Right now instanceId falls back to 0, and that value is fed into the <Select> even when no instance is actually selected. This means the select value can be "0" while there is no matching SelectItem, which is a small mismatch between internal state and the control.

You can keep using 0 as the sentinel for query enabling, but drive the select from selectedInstanceId instead so the UI reflects “no selection” as an empty value:

-  const instanceId = selectedInstanceId ?? 0
+  const instanceId = selectedInstanceId ?? 0-  const renderInstanceSelector = () => (
-    <Select value={instanceId?.toString() ?? ""} onValueChange={handleInstanceSelection}>
+  const renderInstanceSelector = () => (
+    <Select value={selectedInstanceId?.toString() ?? ""} onValueChange={handleInstanceSelection}>

This keeps the UX cleaner without affecting the query logic.


319-403: Refresh / reprocess actions are wired cleanly, but consider centralizing toasts and invalidation

The feed refresh and rules reprocess buttons correctly call their respective mutations, reflect pending state, and show user‑friendly toasts. One small follow‑up for maintainability would be to move the common mutate + toast pattern into thin helpers/hooks (e.g. useRefreshAllFeedsAction, useReprocessRulesAction) to avoid duplicating onSuccess/onError blocks as usage grows, but the current implementation is functionally fine.


966-1115: RuleCard: rich summary UI with sensible parsing

Parsing mustContain / mustNotContain into individual terms, rendering them as color‑coded pills, and surfacing status badges (regex, smart filter, active/disabled) all make the card very readable. Category/save‑path metadata and last‑match timestamp round it out nicely.

As an optional improvement, you could use the shared useDateTimeFormatters hook for lastMatch to keep date formatting consistent with the rest of the app, but the current toLocaleDateString() is perfectly serviceable.


1180-1283: AddFeedDialog: consider slightly more defensive number parsing for refresh interval

The dialog validates URL, resets state on success, and supports optional folder selection from getFolderPaths. For refreshInterval, you currently do:

onChange={(e) =>
  setRefreshInterval(e.target.value ? parseInt(e.target.value, 10) : undefined)
}

This works in practice, but if the browser ever surfaces a transient non‑numeric string from the number input, parseInt could yield NaN and propagate into state.

You can make this more robust with a small tweak:

-              value={refreshInterval ?? ""}
-              onChange={(e) =>
-                setRefreshInterval(e.target.value ? parseInt(e.target.value, 10) : undefined)
-              }
+              value={refreshInterval ?? ""}
+              onChange={(e) => {
+                const raw = e.target.value.trim()
+                if (raw === "") {
+                  setRefreshInterval(undefined)
+                  return
+                }
+                const parsed = Number(raw)
+                setRefreshInterval(Number.isNaN(parsed) ? undefined : parsed)
+              }}

Not critical, but it makes the controlled input more resilient.


1354-1641: AddRuleDialog: new torrent params (tags, ignoreDays, contentLayout, addStopped) are wired correctly

The dialog captures all the core rule fields plus the extended torrent params, builds a qbt.RSSAutoDownloadRule‑shaped object, and normalizes optional values to undefined when empty—important for keeping the JSON minimal and letting qBittorrent defaults apply.

A couple of minor polish ideas:

  • ignoreDays handling (parseInt(...) || 0) forces 0 whenever the field is cleared; if you ever want “unset” semantics distinct from 0, you may want to distinguish empty vs numeric explicitly (similar to the refreshInterval suggestion).
  • A large number of inputs live in one component; extracting a few logical sub‑sections (e.g. “Match criteria”, “Feeds”, “Torrent options”) into small presentational components would reduce cognitive load.

Both are non‑blocking.


2017-2100: RssSettingsPopover: live preference editing works, but fires a mutation on every keystroke

The popover gives a nice compact control surface for RSS refresh/max‑articles and auto‑download flags, and it reuses updatePreferences consistently.

One thing to keep in mind is that both numeric inputs call updatePreferences on every onChange, which will trigger a network request per keystroke. If this proves noisy in practice, you might consider switching to onBlur or adding a small debounce so preference updates happen less frequently while typing. Behavior is otherwise correct.

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

597-615: ReprocessRules: handler matches new SyncManager behavior; consider updating the doc comment

The handler now simply calls h.syncManager.ReprocessRSSRules, which itself toggles auto‑downloading to reprocess unread articles while restoring the original auto‑download preference.

The function comment still says “It does this by toggling auto‑downloading off then on.” To avoid confusion, you might update it to reflect that the original state is restored afterwards, e.g.:

-// It does this by toggling auto-downloading off then on.
+// It does this by forcing a false→true toggle to trigger processing,
+// while preserving the user's original auto-downloading setting.

Behavior itself is correct.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81df579 and 47a004a.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • go.mod (1 hunks)
  • internal/api/handlers/rss.go (1 hunks)
  • internal/qbittorrent/sync_manager.go (1 hunks)
  • web/src/pages/RSSPage.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • go.mod
🧰 Additional context used
🧬 Code graph analysis (2)
web/src/pages/RSSPage.tsx (25)
web/src/hooks/usePersistedInstanceSelection.ts (1)
  • usePersistedInstanceSelection (8-35)
web/src/hooks/useRSS.ts (10)
  • useRSSFeeds (35-45)
  • useRSSRules (47-56)
  • useRefreshRSSFeed (121-133)
  • useRemoveRSSItem (99-108)
  • useMarkRSSAsRead (162-171)
  • useSetRSSRule (177-186)
  • useRemoveRSSRule (200-209)
  • useRSSMatchingArticles (58-71)
  • useAddRSSFeed (77-86)
  • useAddRSSFolder (88-97)
web/src/hooks/useInstancePreferences.ts (1)
  • useInstancePreferences (15-81)
web/src/hooks/useInstanceMetadata.ts (1)
  • useInstanceMetadata (21-43)
web/src/components/ui/select.tsx (5)
  • Select (180-180)
  • SelectTrigger (188-188)
  • SelectValue (189-189)
  • SelectContent (181-181)
  • SelectItem (183-183)
web/src/components/ui/card.tsx (5)
  • Card (90-90)
  • CardHeader (91-91)
  • CardTitle (93-93)
  • CardDescription (95-95)
  • CardContent (96-96)
web/src/components/ui/alert.tsx (2)
  • Alert (64-64)
  • AlertDescription (64-64)
web/src/components/ui/button.tsx (1)
  • Button (64-64)
web/src/components/ui/tabs.tsx (4)
  • Tabs (69-69)
  • TabsList (69-69)
  • TabsTrigger (69-69)
  • TabsContent (69-69)
web/src/components/ui/file-tree.tsx (1)
  • Folder (431-431)
web/src/components/ui/tooltip.tsx (3)
  • Tooltip (184-184)
  • TooltipTrigger (184-184)
  • TooltipContent (184-184)
web/src/types/index.ts (6)
  • RSSItems (1588-1590)
  • isRSSFeed (1593-1595)
  • RSSFeed (1576-1585)
  • RSSArticle (1565-1574)
  • RSSAutoDownloadRule (1618-1636)
  • Category (434-437)
web/src/components/ui/dropdown-menu.tsx (5)
  • DropdownMenu (245-245)
  • DropdownMenuTrigger (247-247)
  • DropdownMenuContent (248-248)
  • DropdownMenuItem (251-251)
  • DropdownMenuSeparator (255-255)
web/src/hooks/useDateTimeFormatters.ts (1)
  • useDateTimeFormatters (13-52)
web/src/components/ui/switch.tsx (1)
  • Switch (34-34)
web/src/components/ui/badge.tsx (1)
  • Badge (51-51)
web/src/components/ui/sheet.tsx (5)
  • Sheet (138-138)
  • SheetContent (141-141)
  • SheetHeader (142-142)
  • SheetTitle (144-144)
  • SheetDescription (145-145)
web/src/components/ui/scroll-area.tsx (1)
  • ScrollArea (61-61)
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/ui/label.tsx (1)
  • Label (29-29)
web/src/components/ui/input.tsx (1)
  • Input (26-26)
web/src/components/ui/separator.tsx (1)
  • Separator (33-33)
web/src/lib/category-utils.ts (2)
  • buildCategorySelectOptions (9-33)
  • buildTagSelectOptions (36-42)
web/src/components/ui/multi-select.tsx (1)
  • MultiSelect (26-170)
web/src/components/ui/popover.tsx (3)
  • Popover (34-34)
  • PopoverTrigger (34-34)
  • PopoverContent (34-34)
internal/qbittorrent/sync_manager.go (1)
web/src/types/index.ts (4)
  • RSSItems (1588-1590)
  • RSSRules (1638-1638)
  • RSSAutoDownloadRule (1618-1636)
  • RSSMatchingArticles (1640-1640)
⏰ 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)
web/src/pages/RSSPage.tsx (9)

268-317: RSS preferences banners and mutation usage look solid

The processing / auto‑download banners correctly derive state from preferences, optimistically enable via updatePreferences, and surface feedback with toasts. Using enabled: instanceId > 0 on useInstancePreferences avoids unnecessary calls when no instance is selected.

No changes needed here.


454-597: FeedsTab: behavior and edge cases are handled well

FeedsTab cleanly handles loading, empty‑state, and normal rendering. Selection is safely derived with findFeedByPath, unread counts are computed from the tree, and “mark all as read” defers to the backend. The queryClient.invalidateQueries after a short delay on refresh is a pragmatic way to pick up feed changes without over‑eager polling.

No issues here.


603-740: FeedTree: recursive rendering and item actions are correct and robust

The recursive FeedTree correctly constructs item paths with \\, distinguishes feeds vs folders via isRSSFeed, tracks expanded folders with a Set, and protects row clicks from being hijacked by the dropdown buttons.

The “Remove” and “Refresh” callbacks are passed down consistently, and unread counts per feed/folder are computed via helpers. This looks good.


746-836: ArticlesPanel / ArticleRow: mark‑as‑read and link handling are sensible

ArticlesPanel correctly short‑circuits on empty feeds, passes per‑article handlers, and wraps the markAsRead mutation in error handling. ArticleRow avoids rendering the external link when it equals the torrent URL and guards the date formatting.

Given React 19’s handling of javascript: URLs, the direct <a href={...}> usage is acceptable here. No changes needed.


842-960: RulesTab: editing and preview orchestration is clean

Rules are sorted for deterministic ordering, toggling uses SetRSSRule with a full rule clone, and edit/delete actions correctly maintain selectedRuleName and editingRule state while keeping dialogs and sheets in sync.

This is a solid wiring of the matching‑articles sheet and edit dialog around the underlying rules data.


1121-1173: RulePreviewSheet: conditional fetching and empty‑state UX are solid

useRSSMatchingArticles is correctly enabled only when ruleName is truthy, and the sheet handles loading, non‑empty, and empty states cleanly with a scrollable list per feed URL.

No issues here.


1289-1347: AddFolderDialog: UX and validation are straightforward

Folder creation validates non‑empty path, gives clear feedback via toasts, and resets/closing behavior is correct. The guidance text about using backslashes for nesting matches how paths are built elsewhere.

No changes needed.


1658-1941: EditRuleDialog: initialization from existing rule and submission shape look good

The useEffect correctly hydrates local state from the provided rule, including legacy fields (savePath, assignedCategory) and the newer torrentParams. On submit you spread the original rule and carefully override only the edited fields, which avoids accidentally dropping backend‑only properties.

The feeds, category, tags, ignoreDays, contentLayout, and addStopped controls mirror AddRuleDialog as expected; any future validation tweaks can likely be shared between the two.

No changes needed here.


1947-2011: Helper functions: path traversal and folder/url extraction align with the RSS tree model

findFeedByPath, countFeeds, countUnreadArticles, getFolderPaths, and getFeedUrls all honor the RSSItems recursive shape and the \ separator convention. Early returns on missing paths and checks via isRSSFeed keep them safe against malformed trees.

Looks good.

internal/qbittorrent/sync_manager.go (2)

4801-4941: RSS proxy methods: consistent error handling and context usage

All the new RSS methods (GetRSSItems, AddRSSFolder, AddRSSFeed, etc.) consistently obtain the client via GetClient(ctx, instanceID), wrap errors with context, and delegate to the underlying *Ctx methods. They don’t unnecessarily touch the sync manager or cache, which is appropriate since these calls are already relatively cheap and stateful.

This looks correct and aligned with the rest of the SyncManager API surface.


4943-4977: ReprocessRSSRules: preserves original auto‑download state

The implementation correctly:

  • Reads the current AppPreferences from the go-qbittorrent client.
  • Forces a false → true transition to trigger qBittorrent's internal RSS processing.
  • Restores the original auto‑download flag if it was previously disabled.

The field name RssAutoDownloadingEnabled and method SetRSSAutoDownloadingEnabledCtx are correct per the go-qbittorrent library (v1.15.0-rc1.0.20251216201705-279083efefc3).

internal/api/handlers/rss.go (8)

18-52: RSSHandler wiring and route surface are coherent

The RSSHandler struct plus NewRSSHandler and Routes method give a clear, centered place for all RSS endpoints. Route shapes (/items, /feeds, /folders, /rules/...) are consistent and map one‑to‑one onto the SyncManager methods, which should make it straightforward to keep the OpenAPI spec and frontend client in sync.

No changes needed here.


56-102: Request types are well‑shaped and closely match the underlying qBittorrent API

The various request DTOs (AddFolderRequest, AddFeedRequest, SetFeedURLRequest, MoveItemRequest, etc.) are minimal and typed in a way that mirrors the qBittorrent contract (e.g. RefreshInterval as int64, Rule as qbt.RSSAutoDownloadRule). This should keep marshalling straightforward and prevent impedance mismatches between layers.

Looks good.


157-279: AddFeed: baseline/diff+move logic is careful; baseline failure is handled safely

The sequence:

  1. Optionally fetch a baseline GetRSSItems (with hasBaseline guard).
  2. Add the feed to root (empty path).
  3. Fetch items again and diff against existingNames to locate a new name whose JSON has a url.
  4. Move that feed into the requested folder, or return a 201 with a warning if it can’t be found or moved,

is a reasonable workaround for qBittorrent’s “full path” semantics on RSS paths.

The earlier edge case where a failed baseline could cause any “new” root item to be moved is now avoided by only doing diff‑based detection when hasBaseline is true; on failure, you fall back to adding to root with a clear warning. Nice fix.


281-399: SetFeedURL / SetFeedRefreshInterval / MoveItem / RemoveItem have consistent validation and error paths

These handlers:

  • Parse instanceID uniformly via parseInstanceID.
  • Validate required fields (Path, ItemPath) and return 400s on missing data.
  • Delegate to the appropriate SyncManager method and use respondIfInstanceDisabled before logging and returning a 500.

The patterns are consistent and will be easy to maintain.


430-458: MarkAsRead: validates required path and cleanly delegates per‑article vs all‑articles

MarkAsRead enforces a non‑empty ItemPath and passes both itemPath and (optional) articleId through to the SyncManager. This matches the intended semantics of “mark all articles in feed as read” when ArticleID is empty and “mark a specific article” when it’s present, assuming the underlying client follows qBittorrent’s API.

No changes needed.


460-508: SetRule: rule creation/update API shape looks correct

The handler ensures Name is non‑empty, then forwards the qbt.RSSAutoDownloadRule to the SyncManager. Returning 201 on success is appropriate for both create and upsert semantics here, since from a client perspective the effect is “rule now exists with this configuration.”

Looks good.


510-569: RenameRule and RemoveRule: parameter validation and logging are straightforward

Both endpoints:

  • Require a ruleName path parameter, with clear 400s when missing.
  • Decode payload (for rename) and validate new name.
  • Delegate to SyncManager with consistent respondIfInstanceDisabled handling and structured logging.

No issues here.


571-595: GetMatchingArticles: preview semantics are simple and correctly scoped

The handler pulls ruleName from the path, validates it, and returns the RSSMatchingArticles map from SyncManager. Using a GET here is appropriate since the endpoint is read‑only and purely for preview.

Looks good.

@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: 3

🧹 Nitpick comments (5)
internal/api/handlers/rss_sse.go (3)

155-170: Nested lock acquisition may cause issues if ordering changes.

removeClient holds h.mu (Lock) then calls stopPollerLocked which acquires h.pollerMu. While this currently works, the naming stopPollerLocked suggests it expects the caller to already hold a lock, which is misleading—it actually acquires its own lock.

Consider renaming to stopPoller for clarity, or restructuring to avoid nested lock acquisition patterns that could become deadlock-prone if the code evolves.


68-85: SSE headers set after potential error response.

If parseInstanceID returns an error, it likely writes an error response to w. The subsequent w.Header().Set() calls at lines 75-78 would then have no effect (headers already sent). While this doesn't cause a crash, the code flow is misleading.

The current structure is fine since return exits early, but consider adding a comment clarifying that parseInstanceID handles the error response internally.


212-233: Consider making poll interval configurable.

The 5-second poll interval is hardcoded. For production deployments with varying network conditions or backend load, a configurable interval would provide operational flexibility.

+const defaultRSSPollInterval = 5 * time.Second
+
 func (h *RSSSSEHandler) pollLoop(ctx context.Context, instanceID int) {
 	log.Debug().Int("instanceID", instanceID).Msg("RSS SSE poll loop started")
 
 	var lastItems []byte
 
 	// Initial poll
 	lastItems = h.pollAndBroadcast(ctx, instanceID, lastItems)
 
-	ticker := time.NewTicker(5 * time.Second)
+	ticker := time.NewTicker(defaultRSSPollInterval)
 	defer ticker.Stop()
web/src/lib/rss-events.ts (1)

111-132: Consider notifying onDisconnected when connection fails before reconnect.

When scheduleReconnect closes the existing connection, it doesn't call onDisconnected. This means the UI won't know the connection is temporarily down during the reconnection window.

   private scheduleReconnect(): void {
     if (this.reconnectAttempts >= this.maxReconnectAttempts) {
       console.error("RSS SSE max reconnection attempts reached")
+      this.handlers.onDisconnected?.()
       return
     }
 
     // Close existing connection if any
     if (this.eventSource) {
       this.eventSource.close()
       this.eventSource = null
     }
web/src/hooks/useRSS.ts (1)

44-53: Redundant invalidateQueries after setQueryData.

setQueryData already updates the cache. The subsequent invalidateQueries with refetchType: "none" marks the query as stale but doesn't trigger a refetch. Since staleTime: Infinity is set and SSE handles updates, this call appears unnecessary.

   const handleFeedsUpdate = useCallback(
     (data: FeedsUpdatePayload) => {
       if (data.instanceId === instanceId) {
-        // Set the data and invalidate to ensure re-render
         queryClient.setQueryData(rssKeys.feeds(instanceId), data.items)
-        queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId), refetchType: "none" })
       }
     },
     [instanceId, queryClient]
   )

If you need to notify observers of the update, setQueryData already triggers re-renders for subscribed components.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 47a004a and fa96975.

📒 Files selected for processing (5)
  • internal/api/handlers/rss_sse.go (1 hunks)
  • internal/api/server.go (2 hunks)
  • web/src/hooks/useRSS.ts (1 hunks)
  • web/src/lib/rss-events.ts (1 hunks)
  • web/src/pages/RSSPage.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/api/server.go
  • web/src/pages/RSSPage.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
internal/api/handlers/rss_sse.go (3)
internal/qbittorrent/sync_manager.go (1)
  • SyncManager (159-182)
internal/api/handlers/helpers.go (1)
  • RespondError (39-43)
web/src/lib/rss-events.ts (1)
  • FeedsUpdatePayload (22-26)
web/src/lib/rss-events.ts (3)
internal/api/handlers/rss_sse.go (1)
  • FeedsUpdatePayload (52-56)
web/src/types/index.ts (1)
  • RSSItems (1588-1590)
web/src/lib/base-url.ts (1)
  • getApiBaseUrl (17-21)
web/src/hooks/useRSS.ts (2)
web/src/lib/rss-events.ts (2)
  • RSSEventSource (35-137)
  • FeedsUpdatePayload (22-26)
web/src/types/index.ts (10)
  • AddRSSFeedRequest (1647-1651)
  • AddRSSFolderRequest (1643-1645)
  • RemoveRSSItemRequest (1668-1670)
  • MoveRSSItemRequest (1663-1666)
  • RefreshRSSItemRequest (1672-1674)
  • SetRSSFeedURLRequest (1653-1656)
  • SetRSSFeedRefreshIntervalRequest (1658-1661)
  • MarkRSSAsReadRequest (1676-1679)
  • SetRSSRuleRequest (1681-1684)
  • RenameRSSRuleRequest (1686-1688)
⏰ 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/api/handlers/rss_sse.go (1)

235-266: LGTM: Change detection via byte comparison is efficient.

The approach of serializing RSS items to bytes and comparing with the previous payload is a clean way to detect changes without complex diffing logic. The json.RawMessage type preserves the original JSON for the payload.

web/src/lib/rss-events.ts (2)

50-92: LGTM: Robust connection management with proper cleanup.

The connection logic correctly:

  • Disconnects any existing connection before creating a new one
  • Resets isIntentionalClose flag
  • Resets reconnect attempts on successful connection (line 62)
  • Handles JSON parsing errors gracefully

94-109: LGTM: Clean disconnect implementation.

The disconnect method properly:

  • Sets intentional close flag first to prevent reconnection
  • Clears any pending reconnect timer
  • Closes the EventSource and notifies handlers
  • Resets reconnect attempts
web/src/hooks/useRSS.ts (3)

55-79: LGTM: Proper SSE lifecycle management in useEffect.

The effect correctly:

  • Creates the EventSource only when enabled
  • Connects immediately after creation
  • Stores reference for potential future use
  • Cleans up on unmount or dependency change

The dependency array is complete with instanceId, shouldEnable, and handleFeedsUpdate.


81-89: LGTM: Smart caching strategy with SSE.

Setting staleTime: Infinity is the correct approach when SSE handles data freshness. The initial fetch populates the cache, and subsequent updates come through the event stream.


24-31: LGTM: Well-structured query keys.

The hierarchical query key pattern enables granular cache invalidation and follows React Query best practices.

Comment thread internal/api/handlers/rss_sse.go
Comment thread internal/api/handlers/rss_sse.go
Comment thread web/src/hooks/useRSS.ts
Document the /api/instances/{instanceID}/rss/events endpoint for
Server-Sent Events streaming of real-time RSS feed updates.

@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 (6)
internal/web/swagger/openapi.yaml (6)

3210-3231: Consider documenting SSE event format.

The SSE endpoint definition is functional, but the schema description is generic. Consider adding a description of the event types and payload structure that clients can expect from this stream (e.g., feed updates, article additions, rule matches) to improve API discoverability.


3425-3493: Consider using PATCH instead of PUT for partial updates.

The endpoints at /rss/feeds/url and /rss/feeds/interval use PUT but only update a single field. Per REST conventions, PUT should replace the entire resource while PATCH should be used for partial updates. Since these operations modify only one property (url or refreshInterval), PATCH would be more semantically appropriate.

Apply this pattern for both endpoints:

- /api/instances/{instanceID}/rss/feeds/url:
-   put:
+ /api/instances/{instanceID}/rss/feeds/url:
+   patch:
- /api/instances/{instanceID}/rss/feeds/interval:
-   put:
+ /api/instances/{instanceID}/rss/feeds/interval:
+   patch:

3552-3582: Clarify response behavior for create vs update operations.

The POST /rss/rules endpoint description states it can "Create a new rule or update an existing one" but always returns 201 Created. This is inconsistent with HTTP semantics where:

  • 201 (Created) should be returned when a new resource is created
  • 200 (OK) should be returned when an existing resource is updated

Consider either:

  1. Splitting this into separate POST (create) and PUT (update) endpoints
  2. Returning 200 when updating an existing rule and 201 when creating a new one
  3. Documenting that 201 will always be returned regardless of create/update

4922-4968: Consider marking required fields in RSS schemas.

The RSSFeed and RSSArticle schemas don't specify required fields, making all properties implicitly optional. Consider adding required arrays to indicate which fields are guaranteed to be present. For example:

  • RSSFeed: url is likely always required
  • RSSArticle: id, title, and date are likely always required

This improves type safety and helps client implementations understand which fields they can rely on.

Example:

 RSSFeed:
   type: object
+  required:
+    - url
   properties:
     url:
 RSSArticle:
   type: object
+  required:
+    - id
+    - title
+    - date
   properties:

5001-5010: Use OpenAPI deprecation markers for legacy fields.

The legacy fields (savePath, assignedCategory, addPaused) are documented with description text, but OpenAPI 3.1 provides a deprecated: true property that tools can use to warn developers. This provides better machine-readable deprecation information.

Apply this pattern:

 savePath:
   type: string
+  deprecated: true
   description: Legacy field
 assignedCategory:
   type: string
+  deprecated: true
   description: Legacy field
 addPaused:
   type: boolean
   nullable: true
+  deprecated: true
   description: Legacy field

5012-5054: Consider adding validation constraints to numeric fields.

The RSSRuleTorrentParams schema has several numeric fields (upload_limit, download_limit, seeding_time_limit, etc.) without validation constraints. Adding minimum: 0 or minimum: -1 (if -1 means "unlimited") would help catch invalid values early and improve API documentation.

Example:

 upload_limit:
   type: integer
+  minimum: -1
+  description: Upload limit in bytes/sec (-1 for unlimited)
 download_limit:
   type: integer
+  minimum: -1
+  description: Download limit in bytes/sec (-1 for unlimited)
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa96975 and 681ddf9.

📒 Files selected for processing (1)
  • internal/web/swagger/openapi.yaml (3 hunks)
⏰ 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 (1)
internal/web/swagger/openapi.yaml (1)

4915-4921: LGTM!

The WarningResponse schema appropriately handles partial success scenarios where an operation completes but with warnings. This is a good pattern for operations like feed addition where non-critical issues shouldn't block the operation.

- Add sync.Once to rssSSEClient to ensure done channel is closed only once
- Copy clients slice while holding lock in broadcast to prevent race
  during iteration after lock release

@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: 1

🧹 Nitpick comments (1)
internal/api/handlers/rss_sse.go (1)

253-257: Log marshaling error for debuggability.

If json.Marshal fails when serializing the RSS items for comparison, the error is silently ignored. While unlikely for internal data structures, logging this would aid debugging if serialization issues occur.

Apply this diff:

 	// Serialize for comparison
 	currentItems, err := json.Marshal(items)
 	if err != nil {
+		log.Debug().Err(err).Int("instanceID", instanceID).Msg("RSS SSE failed to marshal items")
 		return lastItems
 	}
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 681ddf9 and da2273f.

📒 Files selected for processing (1)
  • internal/api/handlers/rss_sse.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/api/handlers/rss_sse.go (2)
internal/api/handlers/helpers.go (1)
  • RespondError (39-43)
web/src/lib/rss-events.ts (1)
  • FeedsUpdatePayload (22-26)
⏰ 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

Comment thread internal/api/handlers/rss_sse.go
…terval feature

- Introduced `SupportsSetRSSFeedURL` capability in the API response.
- Updated the RSS feed handling to allow setting the feed URL without a refresh interval.
- Removed the refresh interval parameter from the AddRSSFeed API and related frontend hooks.
- Updated OpenAPI documentation to reflect the changes in the RSS feed endpoints.
- Add search field to filter articles by title/description
- Add collapsible descriptions with clickable URLs
- Route download button through AddTorrentDialog
- Auto-expand parent folders when feed is selected on page load
- Add Save button to RSS settings (instead of auto-save on input)
- Strip HTML tags and decode entities in descriptions
- Use CSS grid for proper text truncation with action buttons

@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: 2

♻️ Duplicate comments (2)
web/src/hooks/useRSS.ts (1)

164-176: Clean up and de‑duplicate delayed invalidation in useRefreshRSSFeed

useRefreshRSSFeed schedules a setTimeout on every successful refresh without clearing it on unmount, and FeedsTab adds a second delayed invalidateQueries on top. That means each click schedules two refetches and keeps timers alive even if the UI is torn down.

You can centralize the delay in the hook and ensure it’s cleaned up on unmount:

 export function useRefreshRSSFeed(instanceId: number) {
-  const queryClient = useQueryClient()
-
-  return useMutation({
-    mutationFn: (data: RefreshRSSItemRequest) => api.refreshRSSItem(instanceId, data),
-    onSuccess: () => {
-      // Invalidate after a short delay to allow qBittorrent to process the refresh
-      setTimeout(() => {
-        queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) })
-      }, 1000)
-    },
-  })
+  const queryClient = useQueryClient()
+  const timeoutRef = useRef<number | null>(null)
+
+  useEffect(() => {
+    return () => {
+      if (timeoutRef.current != null) {
+        clearTimeout(timeoutRef.current)
+        timeoutRef.current = null
+      }
+    }
+  }, [])
+
+  return useMutation({
+    mutationFn: (data: RefreshRSSItemRequest) => api.refreshRSSItem(instanceId, data),
+    onSuccess: () => {
+      if (timeoutRef.current != null) {
+        clearTimeout(timeoutRef.current)
+      }
+      timeoutRef.current = window.setTimeout(() => {
+        queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) })
+        timeoutRef.current = null
+      }, 1000)
+    },
+  })
 }

Combined with removing the extra timeout in FeedsTab (see comment there), this keeps refresh behavior while avoiding redundant timers and refetches.

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

543-551: Avoid a second delayed invalidate after refresh

handleRefreshFeed calls refreshFeed.mutateAsync and then schedules its own 2s setTimeout to invalidateQueries, while useRefreshRSSFeed already does a delayed invalidate on success. With the hook updated to own the delay, this extra timeout just duplicates work and adds more timers:

  const handleRefreshFeed = async (path: string) => {
    try {
      await refreshFeed.mutateAsync({ itemPath: path })
      toast.success("Feed refresh triggered")
-      // Invalidate to pick up changes
-      setTimeout(() => {
-        queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) })
-      }, 2000)
    } catch (err) {
      const message = err instanceof Error ? err.message : "Failed to refresh feed"
      toast.error(message)
    }
  }

This keeps the behavior but makes refresh semantics single-sourced in the hook.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da2273f and d99263d.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (10)
  • go.mod (1 hunks)
  • internal/api/handlers/capabilities.go (2 hunks)
  • internal/api/handlers/rss.go (1 hunks)
  • internal/qbittorrent/client.go (4 hunks)
  • internal/qbittorrent/sync_manager.go (1 hunks)
  • internal/web/swagger/openapi.yaml (3 hunks)
  • web/src/hooks/useRSS.ts (1 hunks)
  • web/src/lib/api.ts (2 hunks)
  • web/src/pages/RSSPage.tsx (1 hunks)
  • web/src/types/index.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • go.mod
🧰 Additional context used
🧠 Learnings (2)
📚 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:

  • internal/qbittorrent/client.go
📚 Learning: 2025-12-03T18:11:08.682Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.

Applied to files:

  • web/src/pages/RSSPage.tsx
🧬 Code graph analysis (4)
web/src/types/index.ts (1)
internal/api/handlers/helpers.go (1)
  • WarningResponse (22-24)
internal/api/handlers/rss.go (3)
internal/qbittorrent/sync_manager.go (1)
  • SyncManager (159-182)
web/src/types/index.ts (2)
  • RSSAutoDownloadRule (1619-1637)
  • WarningResponse (20-22)
internal/api/handlers/helpers.go (3)
  • RespondError (39-43)
  • RespondJSON (27-36)
  • WarningResponse (22-24)
internal/qbittorrent/sync_manager.go (1)
web/src/types/index.ts (4)
  • RSSItems (1589-1591)
  • RSSRules (1639-1639)
  • RSSAutoDownloadRule (1619-1637)
  • RSSMatchingArticles (1641-1641)
web/src/lib/api.ts (2)
web/src/types/index.ts (12)
  • AddRSSFolderRequest (1644-1646)
  • AddRSSFeedRequest (1648-1651)
  • WarningResponse (20-22)
  • SetRSSFeedURLRequest (1653-1656)
  • MoveRSSItemRequest (1658-1661)
  • RemoveRSSItemRequest (1663-1665)
  • RefreshRSSItemRequest (1667-1669)
  • MarkRSSAsReadRequest (1671-1674)
  • RSSRules (1639-1639)
  • SetRSSRuleRequest (1676-1679)
  • RenameRSSRuleRequest (1681-1683)
  • RSSMatchingArticles (1641-1641)
internal/api/handlers/helpers.go (1)
  • WarningResponse (22-24)
⏰ 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)
internal/web/swagger/openapi.yaml (1)

3209-5015: LGTM! Comprehensive RSS API surface.

The RSS endpoints and schemas are well-structured and consistent. The API design includes:

  • Proper SSE streaming for real-time updates
  • RESTful CRUD operations for feeds, folders, and rules
  • Consistent error handling (409 for disabled instances)
  • Comprehensive schemas with recursive structures for hierarchical feed organization
  • Legacy field handling in RSSAutoDownloadRule
web/src/types/index.ts (1)

1594-1596: LGTM! Type guard implementation.

The isRSSFeed type guard correctly distinguishes between RSSFeed and RSSItems by checking for the url property, which is unique to feeds.

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

24-24: LGTM! Capability flag properly wired.

The new SupportsSetRSSFeedURL field follows the established pattern for capability flags and is correctly initialized from the client's capability method.

Also applies to: 43-43

internal/qbittorrent/client.go (3)

50-50: LGTM! Thread-safe capability field and getter.

The supportsSetRSSFeedURL field and its getter method follow the established pattern used by all other capability flags, with proper read-lock protection for thread safety.

Also applies to: 421-425


258-258: LGTM! Capability computation follows established pattern.

The RSS feed URL capability is computed using the same version comparison logic as all other capability flags in this method.


32-32: Version 2.9.1 is correct for RSS setFeedURL support.

The RSS setFeedURL endpoint was introduced with qBittorrent v4.6.0 (Web API v2.9.1), confirming the minimum version gate is accurate.

internal/qbittorrent/sync_manager.go (1)

4801-4967: RSS proxy surface and ReprocessRSSRules logic look solid

The new RSS methods correctly delegate to the underlying client with consistent error handling, and ReprocessRSSRules now preserves the original RssAutoDownloadingEnabled preference while forcing the required false→true transition to trigger reprocessing. This aligns with typical SyncManager patterns and avoids surprising permanent config changes.

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

150-272: Robust AddFeed folder handling and RefreshItem behavior

The AddFeed flow (baseline snapshot, diff, type-checking feeds via url, and clear warning fallbacks when baseline or post-add fetch fails) is carefully done and avoids moving the wrong item when GetRSSItems errors. Likewise, RefreshItem now correctly trims whitespace and allows an empty itemPath to support “Refresh All” while still delegating directly to RefreshRSSItem, which fixes the previous 400-on-refresh-all issue.

Also applies to: 364-391

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

710-717: AddTorrentDialog integration respects per-torrent semantics

Wiring AddTorrentDialog with a dropPayload containing only the article’s torrent URL (and not mutating instance-level preferences) keeps torrent-add behavior scoped to the individual RSS item, which matches the intended per-torrent semantics from earlier work on that dialog.

Based on learnings, this avoids accidentally treating RSS-triggered adds as global preference changes.

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

76-89: LGTM: RSS type imports are comprehensive and properly structured.

All imported types are used in the RSS methods below and align with the backend API schema.


1723-1776: LGTM: RSS feed management methods are well-implemented.

The methods follow existing patterns in this file, use appropriate HTTP verbs, and properly handle request/response types. The addRSSFeed method correctly returns Promise<WarningResponse | undefined> to reflect the backend's conditional response behavior.


1778-1812: LGTM: RSS rules methods properly handle URL encoding and follow RESTful patterns.

Good attention to detail encoding ruleName with encodeURIComponent at lines 1792, 1799, and 1805, which prevents issues with special characters in user-defined rule names.

Comment thread internal/api/handlers/rss.go
@s0up4200 s0up4200 added this to the v1.11.0 milestone Dec 17, 2025
- Add proper URL validation requiring http/https scheme and host
- Fix silent failures in SSE handler by adding error logging
- Change SSE poll failure log level from Debug to Warn (with context
  cancellation check to avoid noise during shutdown)
- Check and handle connected event send errors in SSE handler
- Rename stopPollerLocked to stopPoller for clarity
- Update interface{} to any for modern Go
- Make destPath optional in MoveRSSItemRequest TypeScript type
- Extract shared RuleFormFields component to eliminate ~150 lines of
  duplicated form code between Add and Edit rule dialogs

@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: 1

♻️ Duplicate comments (2)
web/src/types/index.ts (1)

1581-1590: Verify RSSFeed.uid field alignment with backend.

The uid field doesn't appear in the OpenAPI schema as noted in a previous review. Please verify whether this is intentionally client-only (for React keys or local tracking) or if it should be removed to match the backend contract.

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

165-180: Critical: Inconsistent lock ordering creates deadlock risk.

The lock acquisition order differs between removeClient and Shutdown:

  • removeClient (line 166): acquires h.mu, then calls stopPoller (line 175) which acquires h.pollerMu (line 216)
  • Shutdown (line 290): acquires h.pollerMu first, then h.mu (line 297)

This classic ABBA deadlock pattern can cause the application to hang during shutdown if a client disconnects simultaneously.

🔎 Proposed fix: call stopPoller outside the mu lock
 func (h *RSSSSEHandler) removeClient(instanceID int, client *rssSSEClient) {
+	var shouldStopPoller bool
+
 	h.mu.Lock()
-	defer h.mu.Unlock()
-
 	client.closeDone()
 	if h.clients[instanceID] != nil {
 		delete(h.clients[instanceID], client)
 		if len(h.clients[instanceID]) == 0 {
 			delete(h.clients, instanceID)
-			// Stop poller if no more clients
-			h.stopPoller(instanceID)
+			shouldStopPoller = true
 		}
 	}
+	h.mu.Unlock()
+
+	if shouldStopPoller {
+		h.stopPoller(instanceID)
+	}
 
 	log.Debug().Int("instanceID", instanceID).Msg("RSS SSE client disconnected")
 }

Also applies to: 215-224, 289-307

🧹 Nitpick comments (2)
internal/api/handlers/rss_sse.go (1)

249-286: Consider hash-based change detection for reduced allocations.

The current approach serializes the entire RSS items tree every 5 seconds for byte comparison. While correct, this creates allocation overhead on each poll cycle.

For feeds with many articles, consider caching a hash of the serialized data instead of the full byte slice for comparison.

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

543-555: Consider removing manual invalidation with SSE in place.

The 2-second timeout before invalidating queries may be unnecessary since SSE (configured with staleTime: Infinity) handles real-time updates. This could cause double-updates or race conditions where SSE data arrives before the timeout fires.

   const handleRefreshFeed = async (path: string) => {
     try {
       await refreshFeed.mutateAsync({ itemPath: path })
       toast.success("Feed refresh triggered")
-      // Invalidate to pick up changes
-      setTimeout(() => {
-        queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) })
-      }, 2000)
+      // SSE will push updates when feed data changes
     } catch (err) {
       const message = err instanceof Error ? err.message : "Failed to refresh feed"
       toast.error(message)
     }
   }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4cc9ae8 and 995930c.

📒 Files selected for processing (4)
  • internal/api/handlers/rss.go (1 hunks)
  • internal/api/handlers/rss_sse.go (1 hunks)
  • web/src/pages/RSSPage.tsx (1 hunks)
  • web/src/types/index.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/api/handlers/rss.go
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-28T20:32:30.126Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go:209-212
Timestamp: 2025-11-28T20:32:30.126Z
Learning: Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go
Learning: The cross-seed recheck-resume worker intentionally runs for the process lifetime and keys pending entries by hash only. This is acceptable under the current constraint that background seeded-search runs operate on a single instance at a time; graceful shutdown and instanceID|hash keying are deferred by design.

Applied to files:

  • internal/api/handlers/rss_sse.go
📚 Learning: 2025-12-03T18:11:08.682Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.

Applied to files:

  • web/src/pages/RSSPage.tsx
🧬 Code graph analysis (3)
internal/api/handlers/rss_sse.go (3)
internal/qbittorrent/sync_manager.go (1)
  • SyncManager (170-197)
internal/api/handlers/helpers.go (1)
  • RespondError (39-43)
web/src/lib/rss-events.ts (1)
  • FeedsUpdatePayload (22-26)
web/src/types/index.ts (1)
internal/api/handlers/helpers.go (1)
  • WarningResponse (22-24)
web/src/pages/RSSPage.tsx (3)
web/src/hooks/useRSS.ts (3)
  • useRSSFeeds (36-88)
  • useRSSRules (90-99)
  • rssKeys (24-30)
web/src/types/index.ts (4)
  • RSSItems (1593-1595)
  • isRSSFeed (1598-1600)
  • RSSFeed (1581-1590)
  • RSSArticle (1570-1579)
web/src/lib/linkUtils.tsx (1)
  • renderTextWithLinks (17-53)
⏰ 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 (10)
internal/api/handlers/rss_sse.go (1)

59-66: LGTM!

The constructor properly initializes maps to avoid nil map panics, and the overall handler structure is well-organized with clear separation of concerns.

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

1597-1600: Well-implemented type guard.

The isRSSFeed type guard correctly distinguishes feeds from folders using the url property check, enabling type-safe traversal of the hierarchical RSS structure.


19-22: LGTM!

The WarningResponse type correctly mirrors the backend WarningResponse struct, and the RSS request types are well-structured with appropriate optional fields matching the API contract.

Also applies to: 1647-1687

web/src/pages/RSSPage.tsx (7)

124-169: LGTM!

The component properly handles instance selection with auto-selection of the first connected instance, and the queries correctly guard against invalid instance IDs with enabled: instanceId > 0.


767-929: LGTM!

The recursive FeedTree component correctly handles the hierarchical structure with proper auto-expansion of parent folders and appropriate qBittorrent-specific path separators (backslash).


1017-1031: Basic HTML sanitization is acceptable for display.

The stripHtml function handles common HTML entities and tags adequately for RSS content display. For stricter security requirements, consider using a library like DOMPurify, but for trusted RSS feed content this approach is reasonable.


1119-1226: LGTM!

The RulesTab component provides good UX with proper state management for editing, toggling, and removing rules. The integration with RulePreviewSheet for matching articles preview is well-designed.


1600-1835: Good code reuse with shared form fields.

The RuleFormFields component effectively eliminates duplication between Add and Edit rule dialogs while maintaining flexibility through the idPrefix prop for unique form element IDs.


2083-2147: LGTM!

Helper functions correctly traverse the hierarchical RSS structure with proper use of the isRSSFeed type guard. The recursive implementations are clean and handle edge cases appropriately.


2153-2244: LGTM!

The settings popover properly synchronizes local state with preferences and handles input validation with sensible defaults. The save-all-at-once pattern provides a clear user experience.

Comment thread internal/api/handlers/rss_sse.go Outdated
@s0up4200 s0up4200 modified the milestones: v1.11.0, v1.12.0 Dec 18, 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: 2

🧹 Nitpick comments (3)
web/src/pages/RSSPage.tsx (3)

585-597: Consider cleanup for the setTimeout to prevent potential issues.

The setTimeout with query invalidation could execute after component unmount. While React Query handles this gracefully, consider storing the timeout ID and clearing it in a cleanup function for defensive coding.

🔎 Suggested improvement
 const handleRefreshFeed = async (path: string) => {
   try {
     await refreshFeed.mutateAsync({ itemPath: path })
     toast.success("Feed refresh triggered")
     // Invalidate to pick up changes
-    setTimeout(() => {
+    const timeoutId = setTimeout(() => {
       queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) })
     }, 2000)
+    return () => clearTimeout(timeoutId)
   } catch (err) {
     const message = err instanceof Error ? err.message : "Failed to refresh feed"
     toast.error(message)
   }
 }

Note: If using the cleanup approach, you'd need to refactor this into a useEffect or similar pattern.


1702-1729: Consider accessibility enhancements for the feed selection list.

The affected feeds checkbox list could benefit from:

  • An accessible label or role="group" with aria-label for the container
  • A "Select All/None" quick action for better UX when there are many feeds
  • Visual scrollbar styling for consistency
🔎 Suggested enhancement
 <div className="space-y-2">
   <Label>Affected Feeds</Label>
+  <div role="group" aria-label="Select RSS feeds for this rule">
     <div className="grid grid-cols-1 gap-2 max-h-32 overflow-y-auto">
       {feedUrls.map((feedUrl) => (
         <label key={feedUrl} className="flex items-center gap-2 text-sm">
           <input
             type="checkbox"
             checked={state.affectedFeeds.includes(feedUrl)}
             onChange={(e) => {
               if (e.target.checked) {
                 onChange("affectedFeeds", [...state.affectedFeeds, feedUrl])
               } else {
                 onChange(
                   "affectedFeeds",
                   state.affectedFeeds.filter((f) => f !== feedUrl)
                 )
               }
             }}
             className="rounded"
           />
           <span className="truncate">{feedUrl}</span>
         </label>
       ))}
       {feedUrls.length === 0 && (
         <p className="text-sm text-muted-foreground">No feeds available</p>
       )}
     </div>
+  </div>
 </div>

1-2242: Consider splitting this file for better maintainability.

At 2242 lines, this file could benefit from being split into smaller, focused modules. While the code is well-organized with clear section markers, separating components into individual files would improve:

  • Code navigation and searchability
  • Test isolation
  • Bundle splitting and lazy loading opportunities
  • Team collaboration (fewer merge conflicts)

Suggested structure:

  • RSSPage.tsx (main component)
  • components/FeedsTab/ (FeedsTab, FeedTree, ArticlesPanel)
  • components/RulesTab/ (RulesTab, RuleCard, RulePreviewSheet)
  • components/dialogs/ (RSS-specific dialogs)
  • lib/rss-utils.ts (helper functions)
  • components/RssSettingsPopover.tsx
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7aff392 and a8c6638.

📒 Files selected for processing (1)
  • web/src/pages/RSSPage.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
web/src/pages/RSSPage.tsx (7)
web/src/hooks/useRSS.ts (12)
  • useRSSFeeds (36-116)
  • useRSSRules (118-127)
  • useReprocessRSSRules (270-280)
  • useRefreshRSSFeed (192-204)
  • useRemoveRSSItem (170-179)
  • useMarkRSSAsRead (221-230)
  • useMoveRSSItem (181-190)
  • useSetRSSFeedURL (206-215)
  • rssKeys (24-30)
  • useSetRSSRule (236-245)
  • useRemoveRSSRule (259-268)
  • useRSSMatchingArticles (129-142)
web/src/hooks/useInstanceMetadata.ts (1)
  • useInstanceMetadata (21-44)
web/src/types/index.ts (4)
  • RSSItems (1594-1596)
  • RSSFeed (1583-1591)
  • RSSArticle (1572-1581)
  • Category (435-438)
web/src/components/torrents/AddTorrentDialog.tsx (2)
  • AddTorrentDropPayload (104-106)
  • AddTorrentDialog (164-1671)
web/src/hooks/useDateTimeFormatters.ts (1)
  • useDateTimeFormatters (13-52)
web/src/lib/linkUtils.tsx (1)
  • renderTextWithLinks (17-53)
web/src/lib/category-utils.ts (2)
  • buildCategorySelectOptions (9-33)
  • buildTagSelectOptions (36-42)
⏰ 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/amd64/v2)
  • GitHub Check: Build Docker images (linux/arm/v6)
  • GitHub Check: Build Docker images (linux/amd64)
  • GitHub Check: Build Docker images (linux/arm64)
  • GitHub Check: Build Docker images (linux/arm/v7)
  • GitHub Check: Build Docker images (linux/amd64/v3)
  • GitHub Check: Build distribution binaries
🔇 Additional comments (5)
web/src/pages/RSSPage.tsx (5)

179-189: LGTM: SSE disconnect notification logic is sound.

The use of a ref to prevent duplicate toast notifications when SSE disconnects is a good pattern. The logic correctly resets the flag when reconnected and only shows the toast on permanent disconnection.


1456-1479: LGTM: Dialog submission handlers are well-implemented.

Both AddFeedDialog and AddRuleDialog have robust submission logic with:

  • Proper validation
  • Error handling with user feedback
  • State cleanup on success
  • Appropriate closing behavior

Also applies to: 1862-1899


2077-2141: LGTM: Helper functions are well-implemented.

The helper functions correctly handle the recursive RSS feed/folder structure with:

  • Proper use of the isRSSFeed type guard
  • Safe type assertions
  • Appropriate recursion for tree traversal
  • Clear, focused responsibilities

2147-2242: LGTM: RSS settings popover implements good form state management.

The component correctly:

  • Maintains local form state separate from server state
  • Syncs from preferences when they change (Line 2161-2165)
  • Provides clear save action with feedback
  • Uses appropriate input types and validation

1060-1073: Regex-based HTML stripping is acceptable here, but consider alternatives for edge cases.

The stripHtml function uses regex to strip HTML tags and decode entities, which is a reasonable approach for extracting plain text from RSS descriptions. Since the output is rendered as text via React's default JSX binding (not dangerouslySetInnerHTML), React's automatic escaping of dynamic content provides built-in XSS protection. The main drawback is that regex parsing can miss edge cases, particularly in malformed HTML. Consider testing with malformed entity sequences or nested tags from real-world feeds to ensure robustness.

Comment thread web/src/pages/RSSPage.tsx Outdated
Comment thread web/src/pages/RSSPage.tsx Outdated

@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 (1)
web/src/pages/RSSPage.tsx (1)

585-597: Consider eliminating the setTimeout workaround.

The 2-second delay before invalidating queries is a code smell. Ideally, the backend would provide completion feedback or the mutation would handle cache invalidation automatically.

💡 Potential improvement

If the backend API returns updated data or a completion signal, you could:

  1. Return the updated feed data from the refresh endpoint
  2. Update the cache directly in the mutation's onSuccess callback
  3. Use optimistic updates to reflect the loading state immediately

Example pattern:

 const handleRefreshFeed = async (path: string) => {
   try {
     await refreshFeed.mutateAsync({ itemPath: path })
     toast.success("Feed refresh triggered")
-    // Invalidate to pick up changes
-    setTimeout(() => {
-      queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) })
-    }, 2000)
   } catch (err) {
     const message = err instanceof Error ? err.message : "Failed to refresh feed"
     toast.error(message)
   }
 }

Then handle invalidation in the mutation hook itself:

export function useRefreshRSSFeed(instanceId: number) {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: (data: RefreshRSSFeedRequest) => api.refreshRSSFeed(instanceId, data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) })
    },
  })
}
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8c6638 and ca1cbe9.

📒 Files selected for processing (4)
  • web/src/components/layout/Header.tsx
  • web/src/components/layout/MobileFooterNav.tsx
  • web/src/components/ui/alert.tsx
  • web/src/pages/RSSPage.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
web/src/components/layout/Header.tsx (2)
web/src/components/ui/dropdown-menu.tsx (1)
  • DropdownMenuItem (251-251)
pkg/gojackett/domain.go (1)
  • Rss (95-143)
web/src/pages/RSSPage.tsx (11)
web/src/hooks/useRSS.ts (4)
  • rssKeys (24-30)
  • useRSSMatchingArticles (129-142)
  • useAddRSSFeed (148-157)
  • useAddRSSFolder (159-168)
web/src/hooks/useInstanceMetadata.ts (1)
  • useInstanceMetadata (21-44)
web/src/components/ui/alert.tsx (1)
  • Alert (66-66)
web/src/components/ui/badge.tsx (1)
  • Badge (51-51)
web/src/types/index.ts (5)
  • RSSItems (1594-1596)
  • isRSSFeed (1599-1601)
  • RSSFeed (1583-1591)
  • RSSAutoDownloadRule (1624-1642)
  • Category (435-438)
web/src/hooks/useInstanceCapabilities.ts (1)
  • useInstanceCapabilities (15-34)
web/src/components/ui/input.tsx (1)
  • Input (26-26)
web/src/hooks/useDateTimeFormatters.ts (1)
  • useDateTimeFormatters (13-52)
web/src/lib/linkUtils.tsx (1)
  • renderTextWithLinks (17-53)
web/src/lib/category-utils.ts (2)
  • buildCategorySelectOptions (9-33)
  • buildTagSelectOptions (36-42)
web/src/components/ui/multi-select.tsx (1)
  • MultiSelect (26-170)
🔇 Additional comments (10)
web/src/components/layout/MobileFooterNav.tsx (1)

410-418: LGTM - RSS menu item integrated correctly.

The RSS navigation item follows the established pattern and is properly positioned in the Settings dropdown menu.

web/src/pages/RSSPage.tsx (6)

121-499: Well-structured main component with proper state management.

The RSSPage component correctly handles:

  • Instance selection with persistence
  • Auto-selection of first connected instance
  • SSE status monitoring with duplicate toast prevention
  • Conditional rendering for no-instance states
  • Clear warning banners with action buttons

809-971: Excellent recursive tree implementation with auto-expand.

The FeedTree component provides a solid navigation experience with:

  • Auto-expansion of parent folders when a deep feed is selected
  • Efficient state management using Set for expanded folders
  • Clear visual indicators for loading, errors, and unread counts
  • Proper keyboard and mouse interaction

984-1146: Articles display correctly implemented.

The ArticlesPanel and ArticleRow components properly handle:

  • Date formatting using the useDateTimeFormatters hook
  • HTML content sanitization with entity decoding
  • Collapsible article details with correct Tailwind data attribute selectors
  • Link rendering with the renderTextWithLinks utility
  • Read/unread state management

1163-1434: Rules management UI is well-designed.

The rules components provide a complete management interface with:

  • Toggle, edit, and remove actions with proper state updates
  • Preview functionality showing matching articles per feed
  • Compact, informative rule cards with filter summaries
  • Proper error handling and loading states

1440-2077: Dialog components demonstrate excellent code reuse.

The dialog implementations show good practices:

  • Shared RuleFormFields component eliminates duplication between Add and Edit dialogs
  • Proper form state initialization from existing data
  • Date formatting correctly uses the useDateTimeFormatters hook
  • Consistent validation and error handling patterns
  • Clear user feedback via toast notifications

2083-2248: Helper functions and settings popover are well-implemented.

The utility functions and settings component provide:

  • Clean, pure helper functions for tree traversal and data extraction
  • Settings popover with proper state synchronization
  • Good UX with local state that syncs to preferences on save
  • Clear feedback for settings changes
web/src/components/layout/Header.tsx (1)

542-550: LGTM - RSS navigation item added correctly.

The RSS menu item follows the established pattern for dropdown items and is appropriately positioned in the navigation hierarchy.

web/src/components/ui/alert.tsx (2)

18-18: LGTM: Destructive variant styling is well-structured.

The destructive variant uses semantic color tokens and follows best practices with appropriate opacity modifiers for background and border. The dark mode variant ensures proper visibility across themes.


19-20: Change meets WCAG AA accessibility standards.

The contrast ratio for text-yellow-800 on bg-yellow-500/10 is 6.39:1, exceeding the required 4.5:1 for WCAG AA compliance. The fix is sufficient.

@s0up4200
s0up4200 changed the base branch from main to develop January 2, 2026 07:20
@netlify

netlify Bot commented Jan 3, 2026

Copy link
Copy Markdown

Deploy Preview for getqui ready!

Name Link
🔨 Latest commit fcd5bb4
🔍 Latest deploy log https://app.netlify.com/projects/getqui/deploys/695984c15432010008347b05
😎 Deploy Preview https://deploy-preview-801--getqui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@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 (5)
internal/qbittorrent/sync_manager.go (1)

5514-5548: ReprocessRSSRules correctly preserves user setting, with a small edge-case caveat

The new implementation fixes the previous behaviour by:

  • Reading RssAutoDownloadingEnabled from preferences,
  • Forcing a false -> true transition to trigger processing,
  • Restoring the original setting when it was disabled.

The only remaining edge case is if the final “restore to false” call fails: in that (rare) case a user who had auto‑downloading disabled will end up with it enabled. Since reprocessing has already run successfully at that point, you may want to treat the restore failure as a logged warning instead of a hard error, to avoid surprising persistent state changes on transient failures.

If you agree this is worth tightening, you could log the restore error and return nil so callers still treat the reprocess as successful while keeping telemetry about the failed restore.

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

397-412: InstanceCapabilities: ensure supportsSetRSSFeedURL is always defined client‑side

Adding supportsSetRSSFeedURL: boolean is a good way to feature‑gate the “edit feed URL” UI. Just make sure the backend always includes this flag (defaulting to false for older qBittorrent versions) so the frontend never has to deal with undefined here.


1922-1999: RSS models and helpers are well‑structured for the UI’s tree + rules

The RSSArticle, RSSFeed, RSSItems, and isRSSFeed type guard form a clean, recursive model for the feed/folder tree and per‑feed metadata. A couple of small observations:

  • isRSSFeed using "url" in item && typeof item.url === "string" is a pragmatic discriminator and should work fine given how you construct items.
  • RSSRuleTorrentParams / RSSAutoDownloadRule capture the modern rule shape while still preserving legacy fields (e.g. addPaused, savePath) so existing rules can round‑trip safely.

Overall this gives the UI enough structure for editing, previewing matches, and surfacing rule metadata without over‑specifying qBittorrent internals.


2000-2040: RSS request payload types line up with the server API surface

The request types (AddRSSFolderRequest, AddRSSFeedRequest, SetRSSFeedURLRequest, MoveRSSItemRequest, RemoveRSSItemRequest, RefreshRSSItemRequest, MarkRSSAsReadRequest, SetRSSRuleRequest, RenameRSSRuleRequest) are minimal and map cleanly onto the operations exposed by the new RSS handlers.

One minor detail to keep in mind: MoveRSSItemRequest.destPath being optional (with undefined meaning “move to root”) relies on the handler normalising undefined to the empty string or equivalent. As long as the handler does that consistently, these shapes look solid.

internal/web/swagger/openapi.yaml (1)

3771-4050: RSS path surface looks coherent and consistent with the rest of the API.

The new RSS endpoints (events, items, folders/feeds, articles, and rules) follow existing naming/verb patterns (/instances/{instanceID}/…), status codes, and error conventions and line up with the TS client calls (paths and payload field names all match). No functional issues from the spec side.

One minor improvement you might consider later: for POST /api/instances/{instanceID}/rss/feeds the 201 response is always described as WarningResponse, but the backend can legitimately return an empty body on full success (frontend already types this as WarningResponse | undefined). If you rely on codegen clients, it may be worth clarifying in the spec (e.g. documenting that the body may be empty / warning-only) to avoid surprises, but it’s not a blocker.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c267855 and fcd5bb4.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (11)
  • go.mod
  • internal/api/handlers/helpers.go
  • internal/api/server.go
  • internal/qbittorrent/sync_manager.go
  • internal/web/swagger/openapi.yaml
  • web/src/components/layout/Header.tsx
  • web/src/components/layout/MobileFooterNav.tsx
  • web/src/components/layout/Sidebar.tsx
  • web/src/lib/api.ts
  • web/src/routeTree.gen.ts
  • web/src/types/index.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • web/src/components/layout/Header.tsx
  • web/src/components/layout/Sidebar.tsx
  • go.mod
  • web/src/components/layout/MobileFooterNav.tsx
  • internal/api/server.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-28T18:44:10.496Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 876
File: internal/logstream/hub_test.go:188-192
Timestamp: 2025-12-28T18:44:10.496Z
Learning: In Go 1.25 (Aug 2025), use wg.Go(func()) to spawn a goroutine and automate the Add/Done lifecycle. Replace manual patterns like wg.Add(1); go func(){ defer wg.Done(); ... }() with wg.Go(func(){ ... }). Ensure the codebase builds with Go 1.25+ and apply this in relevant Go files (e.g., internal/logstream/hub_test.go). If targeting older Go versions, maintain the existing pattern.

Applied to files:

  • internal/api/handlers/helpers.go
  • internal/qbittorrent/sync_manager.go
🧬 Code graph analysis (2)
web/src/types/index.ts (1)
internal/api/handlers/helpers.go (1)
  • WarningResponse (22-24)
internal/qbittorrent/sync_manager.go (1)
web/src/types/index.ts (4)
  • RSSItems (1946-1948)
  • RSSRules (1996-1996)
  • RSSAutoDownloadRule (1976-1994)
  • RSSMatchingArticles (1998-1998)
⏰ 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)
internal/qbittorrent/sync_manager.go (1)

5382-5512: RSS SyncManager proxies are consistent and minimal

The new RSS methods cleanly mirror existing patterns: they fetch the instance client once, wrap client acquisition errors with context, and delegate to the qBittorrent RSS APIs without adding extra behaviour. No issues from a correctness or style standpoint.

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

21-24: WarningResponse type aligns with planned usage

Introducing WarningResponse as a simple { "warning": string } envelope is consistent with ErrorResponse and matches the frontend WarningResponse interface. It gives you a clear way to return 2xx responses with structured caveats without touching generic helpers.

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

18-251: Generated /rss route wiring looks consistent

The new AuthenticatedRssRoute import, route definition, and all associated entries in FileRoutesByFullPath, FileRoutesByTo, FileRoutesById, module augmentation, and AuthenticatedRouteChildren follow the same pattern as existing authenticated routes (e.g. /search, /settings). This should give you a correctly typed and navigable /rss route in the authenticated tree.

Also applies to: 321-341

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

19-22: TS WarningResponse matches backend shape

The WarningResponse interface (warning?: string) matches the new Go WarningResponse struct and gives you a generic, reusable success‑with‑caveats envelope for the client without over‑constraining payloads.

internal/web/swagger/openapi.yaml (2)

5970-6109: RSS schemas look accurate and match the client-facing types.

WarningResponse, RSSItems (recursive map), RSSFeed, RSSArticle, RSSAutoDownloadRule, and RSSRuleTorrentParams all mirror the shapes implied by the TS types and qBittorrent’s RSS structures (including nested rules and torrent params). Field naming and optionality look reasonable for the current UI usage; I don’t see any correctness issues here.


6135-6136: Good addition of dedicated RSS tag.

Tagging all new endpoints under a discrete RSS tag keeps the API surface organized and discoverable alongside existing groups (Torrents, Cross-Seed, Orphan Scan, etc.). No changes needed.

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

6-104: New type imports are well-scoped and all used.

All added imports (AddRSSFeedRequest, AddRSSFolderRequest, SetRSSFeedURLRequest, MoveRSSItemRequest, RemoveRSSItemRequest, RefreshRSSItemRequest, RenameRSSRuleRequest, MarkRSSAsReadRequest, RSSItems, RSSRules, RSSMatchingArticles, WarningResponse, WebSeed, LocalCrossSeedMatch) are consumed by the methods below and line up with the corresponding OpenAPI schemas. No dead types or obvious mismatches here.


1974-2063: RSS client methods correctly mirror the backend API and handle empty responses.

  • Paths and verbs match the OpenAPI spec: /instances/{instanceId}/rss/items, /folders, /feeds, /feeds/url, /items/move, /items/refresh, /articles/read, /rules, /rules/{ruleName}/…, and /rules/reprocess are all wired as expected.
  • Request payload shapes (AddRSSFolderRequest, AddRSSFeedRequest, SetRSSFeedURLRequest, MoveRSSItemRequest, RemoveRSSItemRequest, RefreshRSSItemRequest, MarkRSSAsReadRequest, SetRSSRuleRequest, RenameRSSRuleRequest) match the OpenAPI inline body objects.
  • addRSSFeed returning Promise<WarningResponse | undefined> is now aligned with request<T>()’s behavior for empty bodies and the backend’s “warning-only” response semantics, so callers can safely treat the result as optional.
  • Using void return types for purely side-effecting mutations (delete/move/refresh/mark-as-read/rules ops) plays nicely with the generic request<T> handling of 204/empty bodies.

I don’t see any correctness issues or missing error handling specific to these methods; they’re consistent with the rest of the ApiClient surface.

@s0up4200 s0up4200 modified the milestones: v1.12.0, v1.13.0 Jan 4, 2026

@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: 1

🤖 Fix all issues with AI agents
In `@web/src/pages/RSSPage.tsx`:
- Around line 209-233: The Select is receiving "0" when no instance is selected,
which blocks the placeholder; update the value passed to the Select in
renderInstanceSelector so it yields an empty string unless instanceId is a valid
non-zero id (e.g., use a guard that checks instanceId !== 0 && instanceId !=
null before calling toString()), and ensure handleInstanceSelection can accept
and handle an empty string as "no selection" so the placeholder shows correctly.

Comment thread web/src/pages/RSSPage.tsx

@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: 2

🤖 Fix all issues with AI agents
In `@web/src/pages/RSSPage.tsx`:
- Around line 909-915: The feed/folder row divs (the click handler using
onSelect with itemPath and isSelected) are not keyboard-accessible; update the
clickable row elements in the RSSPage rendering to either be a real interactive
element (replace the <div> with a <button> or similar) or add role="button",
tabIndex={0} and an onKeyDown handler that triggers the same onSelect(itemPath)
when Enter or Space is pressed; apply the same change to the other identical row
instance (the one referenced around lines 972-976) so both feeds and folders are
keyboard-accessible and retain the existing onClick behavior and styling.
- Around line 132-142: Validate the persisted selectedInstanceId against the
current instances inside the useEffect that auto-selects instances: if
selectedInstanceId is set but not present in instances (check by id), clear it
or replace it with a valid id (prefer a connected instance via instances.find(i
=> i.connected) or fallback to instances[0].id) and then call
setSelectedInstanceId; this prevents querying a stale localStorage id. Ensure
the logic in the useEffect (referencing selectedInstanceId, instances,
setSelectedInstanceId) handles the case where selectedInstanceId exists but is
not found in instances before returning early.

Comment thread web/src/pages/RSSPage.tsx Outdated
Comment thread web/src/pages/RSSPage.tsx
@s0up4200
s0up4200 merged commit 341092d into develop Jan 21, 2026
13 checks passed
@s0up4200
s0up4200 deleted the feat/native-rss-support branch January 21, 2026 18:54
alexlebens pushed a commit to alexlebens/infrastructure that referenced this pull request Jan 27, 2026
This PR contains the following updates:

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

---

### Release Notes

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

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

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

#### Highlights

- Native RSS support for feeds
- Cross-seed upgrades: directory scanning for data-based matching and category affix (prefix/suffix) modes
- Automations improvements: Move action support, uncategorized filter option, hasMissingFiles condition, include-cross-seeds mode, configurable FREE\_SPACE source
- UI improvements: inline pieces progress bar, clickable dashboard error counts, better mobile workflow modal
- Orphan scan safeguards for shared save paths
- External programs: new {comment} placeholder

##### Notable bug fixes:

- More stable sorting for timestamp fields and last\_activity
- Tracker icon fetching reliability and timeout cooldown handling
- Cross-seed matching fixes (anime/pack matching, size mismatch handling, V2 hash recheck)
- Orphan scan ignores .parts files and OS/NAS artifacts
- qBittorrent tracker-down matching improvements and magnet redirect handling

#### Changelog

##### New Features

- [`9acd789`](autobrr/qui@9acd789): feat(automations): add Move action to Automations ([#&#8203;1079](autobrr/qui#1079)) ([@&#8203;Barcode-eng](https://github.com/Barcode-eng))
- [`80aaf22`](autobrr/qui@80aaf22): feat(automations): add hasMissingfiles condition ([#&#8203;1081](autobrr/qui#1081)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`f942a71`](autobrr/qui@f942a71): feat(automations): add import button to empty state ([#&#8203;1111](autobrr/qui#1111)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`6b2831e`](autobrr/qui@6b2831e): feat(automations): add include-cross-seeds mode with hardlink support, fix free-space projection ([#&#8203;1116](autobrr/qui#1116)) ([@&#8203;Barcode-eng](https://github.com/Barcode-eng))
- [`b152a6a`](autobrr/qui@b152a6a): feat(automations): add tooltips to disabled conditions ([#&#8203;1286](autobrr/qui#1286)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`f36eb76`](autobrr/qui@f36eb76): feat(automations): add uncategorized category filter option ([#&#8203;1268](autobrr/qui#1268)) ([@&#8203;aulterego](https://github.com/aulterego))
- [`d7dcb58`](autobrr/qui@d7dcb58): feat(automations): configurable FREE\_SPACE source (qBit or path) ([#&#8203;1181](autobrr/qui#1181)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d685bee`](autobrr/qui@d685bee): feat(backups): add tooltips and restore from lastest button ([#&#8203;1258](autobrr/qui#1258)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`4c5a975`](autobrr/qui@4c5a975): feat(crossseed): add category affix with prefix/suffix modes ([#&#8203;1296](autobrr/qui#1296)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`28cd96d`](autobrr/qui@28cd96d): feat(crossseed): add directory scanner for data-based matching ([#&#8203;1203](autobrr/qui#1203)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`fe84eb0`](autobrr/qui@fe84eb0): feat(dirscan): resumable scans with per-run limit ([#&#8203;1274](autobrr/qui#1274)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`1af2a12`](autobrr/qui@1af2a12): feat(dockerfile): add bash to Dockerfile ([#&#8203;903](autobrr/qui#903)) ([@&#8203;ryanwalder](https://github.com/ryanwalder))
- [`5c84c3f`](autobrr/qui@5c84c3f): feat(docs): add browser extension links and URL shorteners ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`13c5461`](autobrr/qui@13c5461): feat(linking): add option to fallback on linking failure ([#&#8203;1056](autobrr/qui#1056)) ([@&#8203;Audionut](https://github.com/Audionut))
- [`5f7031f`](autobrr/qui@5f7031f): feat(orphanscan): protect shared save paths across instances ([#&#8203;1197](autobrr/qui#1197)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`341092d`](autobrr/qui@341092d): feat(rss): add native rss support ([#&#8203;801](autobrr/qui#801)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`8d2a371`](autobrr/qui@8d2a371): feat(sort): add stable fallback sorting for timestamp fields ([#&#8203;1317](autobrr/qui#1317)) ([@&#8203;aulterego](https://github.com/aulterego))
- [`28cf4ad`](autobrr/qui@28cf4ad): feat(torrents): use custom tracker names in table ([#&#8203;1120](autobrr/qui#1120)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`e0d2bb4`](autobrr/qui@e0d2bb4): feat(ui): Use 4 column grid for dashboard instances on 2xl screens ([#&#8203;1195](autobrr/qui#1195)) ([@&#8203;ewenjo](https://github.com/ewenjo))
- [`7bc98e1`](autobrr/qui@7bc98e1): feat(ui): add inline pieces progress bar visualization ([#&#8203;1050](autobrr/qui#1050)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`0154226`](autobrr/qui@0154226): feat(web): Improve performance for torrents with large file counts ([#&#8203;1252](autobrr/qui#1252)) ([@&#8203;0rkag](https://github.com/0rkag))
- [`a91df70`](autobrr/qui@a91df70): feat(web): allow merging trackers into existing groups ([#&#8203;1175](autobrr/qui#1175)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`132c512`](autobrr/qui@132c512): feat(web): improve workflow modal mobile ([#&#8203;1302](autobrr/qui#1302)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`a7f59ab`](autobrr/qui@a7f59ab): feat(web): make dashboard error counts clickable ([#&#8203;937](autobrr/qui#937)) ([@&#8203;Gykes](https://github.com/Gykes))

##### Bug Fixes

- [`c6f86e2`](autobrr/qui@c6f86e2): fix(auth): recover cleanly behind upstream SSO ([#&#8203;1142](autobrr/qui#1142)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`0c19899`](autobrr/qui@0c19899): fix(automations): handle -1 value for incomplete torrent completion\_on ([#&#8203;1186](autobrr/qui#1186)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`59a9120`](autobrr/qui@59a9120): fix(automations): include Move action in `rulesUseCondition` ([#&#8203;1079](autobrr/qui#1079)) ([#&#8203;1287](autobrr/qui#1287)) ([@&#8203;Barcode-eng](https://github.com/Barcode-eng))
- [`618d945`](autobrr/qui@618d945): fix(automations): not being able to enter multiple tags in tag action ([#&#8203;1131](autobrr/qui#1131)) ([@&#8203;Winter](https://github.com/Winter))
- [`bffecbd`](autobrr/qui@bffecbd): fix(automations): restore preview API and add loading state ([#&#8203;1194](autobrr/qui#1194)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`9b3975b`](autobrr/qui@9b3975b): fix(automations): speed up delete hardlink-copy expansion ([#&#8203;1187](autobrr/qui#1187)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`1549bcf`](autobrr/qui@1549bcf): fix(automations): support qBittorrent global/unlimited limits ([#&#8203;1134](autobrr/qui#1134)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`e31d4aa`](autobrr/qui@e31d4aa): fix(automations): validate local access for hasMissingFiles ([#&#8203;1281](autobrr/qui#1281)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`dba3419`](autobrr/qui@dba3419): fix(crossseed): allow extra files in size mismatch check ([#&#8203;1149](autobrr/qui#1149)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`f7dced6`](autobrr/qui@f7dced6): fix(crossseed): correct match type for episode-from-pack cross-seeding ([#&#8203;1250](autobrr/qui#1250)) ([@&#8203;neelmehta247](https://github.com/neelmehta247))
- [`007d87e`](autobrr/qui@007d87e): fix(crossseed): normalize ampersand to "and" for title matching ([#&#8203;1202](autobrr/qui#1202)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`048c034`](autobrr/qui@048c034): fix(crossseed): prevent automation search stalls ([#&#8203;1272](autobrr/qui#1272)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5c48ccb`](autobrr/qui@5c48ccb): fix(crossseed): prevent false cross-seed delete warnings ([#&#8203;1148](autobrr/qui#1148)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`eda0eee`](autobrr/qui@eda0eee): fix(crossseed): reduce false negatives for anime matching ([#&#8203;1243](autobrr/qui#1243)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`8b08b87`](autobrr/qui@8b08b87): fix(crossseed): restore reuse matched category option ([#&#8203;1150](autobrr/qui#1150)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`b69682d`](autobrr/qui@b69682d): fix(crossseed): support v2 hashes for recheck ([#&#8203;1237](autobrr/qui#1237)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`8fb16c1`](autobrr/qui@8fb16c1): fix(database): remove noisy stmt cache promotion log ([#&#8203;1307](autobrr/qui#1307)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c8d068b`](autobrr/qui@c8d068b): fix(docs): correct autobrr integration documentation ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c6f4a4a`](autobrr/qui@c6f4a4a): fix(docs): use absolute URLs for extension links ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`48510e9`](autobrr/qui@48510e9): fix(instances): auth bypass persistence in instance form ([#&#8203;1219](autobrr/qui#1219)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`0b71b46`](autobrr/qui@0b71b46): fix(makefile): make fmt target only format changed files ([#&#8203;1200](autobrr/qui#1200)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`1f13b0e`](autobrr/qui@1f13b0e): fix(orphanscan): avoid duplicate inode entries in scans ([#&#8203;1212](autobrr/qui#1212)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`fecfebd`](autobrr/qui@fecfebd): fix(orphanscan): gate scans until qBittorrent settled ([#&#8203;1193](autobrr/qui#1193)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`77b2fd6`](autobrr/qui@77b2fd6): fix(orphanscan): ignore .parts files from qBittorrent partial downloads ([#&#8203;1264](autobrr/qui#1264)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`0f7e646`](autobrr/qui@0f7e646): fix(orphanscan): ignore OS/NAS/k8s artifacts ([#&#8203;1259](autobrr/qui#1259)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`b55881c`](autobrr/qui@b55881c): fix(orphanscan): reduce MaxFilesPerRun and fix accordion chevrons ([#&#8203;1095](autobrr/qui#1095)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`0cb682c`](autobrr/qui@0cb682c): fix(qbittorrent): force sync+retry in BulkAction ([#&#8203;1097](autobrr/qui#1097)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`aba9b7e`](autobrr/qui@aba9b7e): fix(qbittorrent): strip URLs before tracker-down pattern matching ([#&#8203;1224](autobrr/qui#1224)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`a60076f`](autobrr/qui@a60076f): fix(reflinktree): fall back to FICLONERANGE when FICLONE unsupported ([#&#8203;1221](autobrr/qui#1221)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d79a0db`](autobrr/qui@d79a0db): fix(search): handle magnet redirects from indexer downloads ([#&#8203;1211](autobrr/qui#1211)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`1e920c4`](autobrr/qui@1e920c4): fix(sort): truncate `last_activity` to 60s for stability ([#&#8203;1318](autobrr/qui#1318)) ([@&#8203;aulterego](https://github.com/aulterego))
- [`931d720`](autobrr/qui@931d720): fix(torznab): respect per-indexer caps limits ([#&#8203;1265](autobrr/qui#1265)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`6fe91e8`](autobrr/qui@6fe91e8): fix(trackericons): apply failure cooldown on timeouts ([#&#8203;1322](autobrr/qui#1322)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`35a50b5`](autobrr/qui@35a50b5): fix(web): add max-height to import workflow dialog ([#&#8203;1207](autobrr/qui#1207)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5bd2706`](autobrr/qui@5bd2706): fix(web): hide redundant actions dropdown ([#&#8203;1305](autobrr/qui#1305)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`307907f`](autobrr/qui@307907f): fix(web): improve Reannounce In column display ([#&#8203;1320](autobrr/qui#1320)) ([@&#8203;soggy-cr0uton](https://github.com/soggy-cr0uton))
- [`7881bdc`](autobrr/qui@7881bdc): fix(web): make Add Instance dialog scrollable on small viewports ([#&#8203;1232](autobrr/qui#1232)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`bec5433`](autobrr/qui@bec5433): fix(web): make dialogs scrollable on small viewports ([#&#8203;1233](autobrr/qui#1233)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`0e47b9e`](autobrr/qui@0e47b9e): fix(web): prevent nested scroll containers on iOS ([#&#8203;1229](autobrr/qui#1229)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`f2f0616`](autobrr/qui@f2f0616): fix(web): redirect already authenticated users to dashboard ([#&#8203;1127](autobrr/qui#1127)) ([@&#8203;burritothief](https://github.com/burritothief))
- [`7a633cf`](autobrr/qui@7a633cf): fix(web): toast message when searching then adding cross-seeds ([#&#8203;1290](autobrr/qui#1290)) ([@&#8203;rybertm](https://github.com/rybertm))
- [`da14100`](autobrr/qui@da14100): fix(web): use fixed px for sidebar width instead of rem ([#&#8203;1313](autobrr/qui#1313)) ([@&#8203;soggy-cr0uton](https://github.com/soggy-cr0uton))

##### Other Changes

- [`c93a326`](autobrr/qui@c93a326): chore(deps): bump the golang group across 1 directory with 16 updates ([#&#8203;1261](autobrr/qui#1261)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`d3de074`](autobrr/qui@d3de074): chore(docs): clarify tracker pattern necessity in documentation ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`2351097`](autobrr/qui@2351097): chore(lint): Add Linting workflow to CI ([#&#8203;1270](autobrr/qui#1270)) ([@&#8203;Barcode-eng](https://github.com/Barcode-eng))
- [`e8436e3`](autobrr/qui@e8436e3): chore(lint): update linting base branch from main to develop ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`e36afc8`](autobrr/qui@e36afc8): chore: add Ko-fi funding and pre-commit config ([#&#8203;1315](autobrr/qui#1315)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`df4375e`](autobrr/qui@df4375e): chore: update copyright headers to 2025-2026 ([#&#8203;1303](autobrr/qui#1303)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`7a6f2d1`](autobrr/qui@7a6f2d1): docs(automations): clarify tag behavior ([#&#8203;1260](autobrr/qui#1260)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`05e2028`](autobrr/qui@05e2028): refactor(build): cross platform building ([#&#8203;1060](autobrr/qui#1060)) ([@&#8203;Audionut](https://github.com/Audionut))
- [`6355f4f`](autobrr/qui@6355f4f): refactor(crosssed): require full recheck for disc based content ([#&#8203;1168](autobrr/qui#1168)) ([@&#8203;Audionut](https://github.com/Audionut))
- [`7466a98`](autobrr/qui@7466a98): refactor(filesmanager): improve logging messages for orphan cleanup process ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`ad90209`](autobrr/qui@ad90209): refactor(orphan): improve disc based content handling ([#&#8203;1167](autobrr/qui#1167)) ([@&#8203;Audionut](https://github.com/Audionut))
- [`3b85f8a`](autobrr/qui@3b85f8a): refactor(web): use TanStack Query for path autocomplete ([#&#8203;1309](autobrr/qui#1309)) ([@&#8203;soggy-cr0uton](https://github.com/soggy-cr0uton))

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

#### Docker images

- `docker pull ghcr.io/autobrr/qui:v1.13.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/3540
Co-authored-by: Renovate Bot <renovate-bot@alexlebens.net>
Co-committed-by: Renovate Bot <renovate-bot@alexlebens.net>
Zmegolaz pushed a commit to Zmegolaz/qui that referenced this pull request Mar 28, 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.

RSS Support

1 participant