Skip to content

feat: migrate to dodopayments - #1407

Merged
s0up4200 merged 16 commits into
developfrom
feat/dodopayments
Feb 6, 2026
Merged

feat: migrate to dodopayments#1407
s0up4200 merged 16 commits into
developfrom
feat/dodopayments

Conversation

@s0up4200

@s0up4200 s0up4200 commented Feb 4, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added Dodo Payments as a second license provider and Dodo portal/checkout links; licenses now show a provider.
  • UI/UX Updates

    • Simplified license management flow, updated copy (remove vs release), checkout steps, portal links, and improved toasts/error messages.
  • Data & Migrations

    • Added schema migration to store provider and instance IDs; migration normalization added.
  • Tests & Stability

    • New tests for provider migrations, validation, and Dodo/Polar integration scenarios.

@coderabbitai

coderabbitai Bot commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds Dodo Payments as a second license provider: new Dodo client, DB schema and model fields for provider and instance ID, repo and service changes for provider-aware activate/validate/deactivate flows, API and frontend exposure of provider, tests and migration/rename handling.

Changes

Cohort / File(s) Summary
Dodo client
internal/dodo/client.go
New HTTP client implementing Activate/Validate/Deactivate, request/response types, error mapping, and configurable options.
Service core (multi-provider)
internal/services/license/license_service.go, internal/services/license/provider.go, internal/services/license/checker.go
Service now holds a dodoClient; provider-aware activation/validation/deactivation and migration logic added; new helpers and errors; offline grace becomes configurable.
Database & repo
internal/database/migrations/057_add_license_provider_dodo.sql, internal/database/db.go, internal/database/license_repo.go, internal/database/db_test.go, internal/database/migration_renames_test.go
Adds provider and dodo_instance_id columns and migration; repo queries/inserts/updates extended to persist these fields; added UpdateLicenseProvider and centralized rollback helper; tests updated.
Models & API surface
internal/models/license.go, internal/api/handlers/licenses.go, internal/web/swagger/openapi.yaml
ProductLicense gains Provider and DodoInstanceID; API handler and OpenAPI now expose provider; handler types updated.
CLI/server wiring
cmd/qui/main.go
Creates and wires Dodo client into license service; NewLicenseService signature updated at call site.
Polar client
internal/polar/client.go
Adds Deactivate support and nuanced response/error handling for deactivation flows.
Tests
internal/services/license/license_service_dodo_regression_test.go, internal/services/license/license_service_validation_test.go, internal/api/handlers/licenses_validate_test.go
New and updated tests covering Dodo/Polar flows, migration rename behavior, validation of missing licenses, and constructor/signature adjustments.
Frontend — UI & lib
web/src/components/themes/LicenseManager.tsx, web/src/hooks/useLicense.ts, web/src/lib/api.ts, web/src/lib/dodo-constants.ts, web/src/lib/license-errors.ts, web/src/pages/Settings.tsx, web/src/routes/_authenticated/settings.tsx
Adds Dodo constants, exposes provider in API types, updates LicenseManager props and UI/flow to reference Dodo/portal logic, adjusts messages and route search params.
Repo config
.gitignore
Removes .envrc from ignore and adds local reference dump ignores (dodopayments.txt, polar.txt).

Sequence Diagram(s)

sequenceDiagram
    participant UI as Frontend
    participant API as API Handler
    participant Svc as License Service
    participant Dodo as Dodo Client
    participant Polar as Polar Client
    participant DB as Database

    rect rgba(100,150,200,0.5)
    note over UI,API: Activation request (provider-aware)
    UI->>API: POST /api/license/activate (licenseKey, username)
    API->>Svc: ActivateAndStoreLicense(licenseKey, username)
    Svc->>DB: GetLicenseByKey(licenseKey)
    alt provider == "dodo"
        Svc->>Dodo: Activate(...)
        Dodo-->>Svc: ActivateResponse / error
    else provider == "polar"
        Svc->>Polar: Activate(...)
        Polar-->>Svc: ActivateResponse / error
    else provider unknown
        Svc->>Dodo: Validate(...)
        alt Dodo valid
            Svc->>DB: UpdateLicenseProvider("dodo", instanceID)
        else
            Svc->>Polar: Validate/Activate(...)
            Polar-->>Svc: response
            Svc->>DB: UpdateLicenseProvider("polar", NULL)
        end
    end
    Svc->>DB: Store/Update license (status, provider, dodo_instance_id)
    DB-->>Svc: commit
    Svc-->>API: ProductLicense
    API-->>UI: {licenseKey, status, provider, ...}
    end
Loading
sequenceDiagram
    participant Scheduler as Scheduler
    participant Svc as License Service
    participant DB as Database
    participant Dodo as Dodo Client
    participant Polar as Polar Client

    rect rgba(150,100,200,0.5)
    note over Scheduler,Svc: Periodic refresh / validation
    Scheduler->>Svc: RefreshAllLicenses()
    Svc->>DB: GetAllLicenses()
    DB-->>Svc: licenses[]
    loop each license
        alt license.provider == "dodo"
            Svc->>Dodo: Validate(licenseKey, instanceID)
            Dodo-->>Svc: ValidateResponse
        else license.provider == "polar"
            Svc->>Polar: Validate(licenseKey)
            Polar-->>Svc: ValidateResponse
        else provider unknown
            Svc->>Dodo: Validate(licenseKey)
            alt Dodo valid (returns instance_id)
                Svc->>DB: UpdateLicenseProvider("dodo", instance_id)
            else
                Svc->>Polar: Validate(licenseKey)
                alt Polar valid
                    Svc->>DB: UpdateLicenseProvider("polar", NULL)
                end
            end
        end
        Svc->>DB: Update status/fields as needed
    end
    Svc-->>DB: done
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

enhancement, web, themes

Suggested reviewers

  • zze0s

Poem

🐰 I hopped through schemas, fields anew,

Dodo joined Polar in the queue,
Keys now find homes with provider names,
Services chatter with brand-new aims,
The rabbit twitches—deploy and woo!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main objective of the pull request: integrating Dodo Payments as a license provider alongside Polar.

✏️ 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/dodopayments

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

@blacksmith-sh

This comment has been minimized.

@s0up4200 s0up4200 changed the title draft: dodo feat: migrate to dodopayments Feb 4, 2026
@s0up4200
s0up4200 marked this pull request as ready for review February 4, 2026 22:33

@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

🤖 Fix all issues with AI agents
In @.gitignore:
- Line 62: The .gitignore contains a duplicate `.envrc` entry; remove the
redundant occurrence so `.envrc` appears only once (keep the first or the
intended single entry) and delete the duplicate lines (the repeated `.envrc` at
lines referenced in the review) ensuring no extra blank lines remain—update the
`.gitignore` so the only unique symbol `.envrc` is present once.

In `@internal/dodo/client.go`:
- Around line 213-255: Replace the unbounded io.ReadAll in parseError with a
bounded read (e.g., use io.LimitReader or io.ReadAll(io.LimitReader(resp.Body,
maxErrorBodyBytes))) and define a reasonable constant like maxErrorBodyBytes
(e.g., 64*1024). Use the resulting truncated buffer for the json.Unmarshal into
errorResponse and for building the message; if the buffer was truncated, append
a short marker like " (truncated)" to the message before calling wrapError or
returning APIError. Keep wrapError and the APIError logic unchanged, only ensure
parseError uses the size-limited read and the new maxErrorBodyBytes constant.

In `@internal/services/license/license_service.go`:
- Around line 52-81: The fallback logic fails to detect the "dodo client not
configured" case because activateWithDodo returns a plain error string; add a
typed package-level sentinel error (e.g., ErrDodoClientNotConfigured =
errors.New("dodo client not configured")), return that sentinel from
activateWithDodo when the Dodo client is nil, and update isDodoFallbackError to
include errors.Is(err, ErrDodoClientNotConfigured) (in addition to the existing
checks for dodo.ErrLicenseNotFound and dodo.ErrInvalidLicenseKey) so
activateWithPolar is attempted when Dodo is not configured.
🧹 Nitpick comments (4)
internal/web/swagger/openapi.yaml (1)

2445-2453: Consider documenting provider as an enum.

This makes the API contract clearer for clients and keeps it aligned with backend provider constants.

♻️ Suggested schema refinement
                     provider:
                       type: string
+                      enum:
+                        - polar
+                        - dodo
web/src/hooks/useLicense.ts (1)

78-88: Keep delete error messaging consistent with other license hooks.

Other license mutations use getLicenseErrorMessage, which likely preserves backend-specific messages; consider reusing it here too.

♻️ Suggested tweak
-    onError: (error: Error) => {
-      toast.error(error.message || "Failed to remove license")
-    },
+    onError: (error: Error) => {
+      toast.error(getLicenseErrorMessage(error))
+    },
internal/models/license.go (1)

50-51: Consider grouping provider constants separately.

The provider constants are placed under the "LicenseStatus constants" comment, but they're conceptually different. Consider either updating the comment to encompass both or creating a separate const block for providers.

♻️ Suggested organization
 // LicenseStatus constants
 const (
 	LicenseStatusActive  = "active"
 	LicenseStatusInvalid = "invalid"
+)
+
+// LicenseProvider constants
+const (
 	LicenseProviderDodo  = "dodo"
 	LicenseProviderPolar = "polar"
 )
web/src/components/themes/LicenseManager.tsx (1)

54-58: Consider guarding for future multi‑license support.

portalUrl/portalLabel are derived from licenses[0]. If multi‑license support is introduced later, consider deriving this from the license being rendered/acted on to avoid provider mismatches.

Comment thread .gitignore
Comment thread internal/dodo/client.go
Comment thread internal/services/license/license_service.go 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: 2

🤖 Fix all issues with AI agents
In `@internal/services/license/license_service.go`:
- Around line 582-584: The default branch in RefreshAllLicenses currently
returns ErrDodoClientNotConfigured immediately, which bypasses the fallback
logic used in ValidateLicenses; instead, change the default branch in
RefreshAllLicenses to treat the missing s.dodoClient like a transient/fallback
error so it flows into handleTransient()/isDodoFallbackError() the same way
ValidateLicenses does (i.e., do not return ErrDodoClientNotConfigured
directly—invoke the same fallback/error-path handling used elsewhere so Polar
fallback is attempted).
- Around line 217-221: The switch branch that currently returns
errors.New("activation limit exceeded") discards the sentinel
polar.ErrActivationLimitExceeded; change that branch to wrap and return the
original sentinel (e.g., use errors.Wrap(polar.ErrActivationLimitExceeded,
"activation limit exceeded") or fmt.Errorf("activation limit exceeded: %w",
polar.ErrActivationLimitExceeded)) so callers can still use errors.Is(...,
polar.ErrActivationLimitExceeded); update the switch in the license activation
logic where the code checks errors.Is(err, polar.ErrActivationLimitExceeded) to
return the wrapped sentinel instead of a new error.

Comment thread internal/services/license/license_service.go
Comment thread internal/services/license/license_service.go

@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 `@internal/services/license/license_service.go`:
- Around line 474-478: The loop in Refresh/validation over licenses should not
abort on single-provider failures; replace hard returns (e.g., returning
ErrDodoClientNotConfigured and other `return err` occurrences inside the
for-loop for Dodo/Polar) with a continue plus a logged error or accumulation of
the error so the loop proceeds to subsequent licenses. Specifically, in the
switch handling normalizeProvider(license.Provider) for
models.LicenseProviderDodo and models.LicenseProviderPolar, change checks like
`if s.dodoClient == nil { return ErrDodoClientNotConfigured }` and any `return
err` inside the loop to `log`/record the error and `continue` (mirror how
ErrActivationLimitExceeded branches do), or delegate to the existing
handleTransient pattern used by ValidateLicenses to mark transient failures
without aborting the whole refresh. Ensure you reference
s.dodoClient/s.polarClient, ErrDodoClientNotConfigured, and
handleTransient/ValidateLicenses behavior when making the changes.
- Around line 656-677: ValidateLicenses can dereference a nil s.dodoClient after
ensureDodoActivation returns early; add the same nil-check used in
RefreshAllLicenses before calling s.dodoClient.Validate: inside the switch case
handling models.LicenseProviderDodo (in ValidateLicenses), ensure s.dodoClient
!= nil (and return/handle as transient or mark invalid as appropriate) before
invoking s.dodoClient.Validate(...) so we avoid a nil pointer panic from
s.dodoClient; update error handling to mirror the existing RefreshAllLicenses
guard behavior.
🧹 Nitpick comments (1)
internal/services/license/license_service.go (1)

639-642: Commented-out code: remove or restore the recently-validated skip.

The time.Since(license.LastValidated) < time.Hour guard is active in RefreshAllLicenses (line 458) but commented out here. If the skip is intentionally disabled for ValidateLicenses, a brief comment explaining why would help; otherwise, clean it up.

Comment thread internal/services/license/license_service.go
Comment thread internal/services/license/license_service.go

@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

Caution

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

⚠️ Outside diff range comments (2)
internal/services/license/license_service.go (2)

20-22: ⚠️ Potential issue | 🟠 Major

Remove duplicate ErrLicenseNotFound sentinel — use models.ErrLicenseNotFound consistently.

This package declares its own ErrLicenseNotFound at line 21 while models.ErrLicenseNotFound already exists (internal/models/license.go line 13), both with identical error text. Since errors.New creates distinct sentinel objects, they cannot be matched by a single errors.Is() call. The handler in internal/api/handlers/licenses.go line 177 must check both sentinels separately, which is unnecessary duplication. Delete the declaration at line 21 and use models.ErrLicenseNotFound throughout this package.


27-43: ⚠️ Potential issue | 🟡 Minor

Remove unused db field from Service struct.

The db *database.DB field (line 28) is declared but never initialized in the constructor (lines 36-43) and is not used anywhere in the service. Remove it to avoid confusion.

🤖 Fix all issues with AI agents
In `@internal/services/license/license_service.go`:
- Around line 884-911: The Dodo branch in DeleteLicense enforces stricter
behavior than the Polar branch by returning ErrDodoClientNotConfigured when
s.dodoClient is nil, preventing local DB deletion; make the behaviors consistent
by changing the Dodo handling in DeleteLicense (the switch on normalizeProvider
and the block referencing s.dodoClient, DodoInstanceID, and
ErrDodoClientNotConfigured) so that if s.dodoClient is nil you log a warning and
skip remote deactivation (similar to the Polar path) instead of returning an
error—keep existing error handling for actual Deactivate() failures but do not
block local deletion when the client is unavailable.
🧹 Nitpick comments (5)
internal/services/license/license_service_dodo_regression_test.go (1)

221-282: Solid multi-license continuation test.

Verifies that a Dodo client error for one license doesn't block processing of a subsequent Polar license. The assertion that the Polar license remains active after the Dodo error confirms the recordRefreshErr + continue pattern works correctly.

One observation: the test doesn't assert the Dodo license's final status. If you want full coverage, consider also fetching and checking the Dodo license to confirm it wasn't inadvertently modified.

internal/services/license/license_service.go (4)

532-536: Asymmetric nil-client handling: Polar configuration errors are silently swallowed.

When dodoClient is nil (lines 483-488), the error is recorded via recordRefreshErr and eventually returned to the caller. When polarClient is nil (lines 533-536), the license is silently skipped with only a log warning — recordRefreshErr is not called. The same asymmetry exists in ValidateLicenses (line 740-744 vs 680-683).

If this is intentional (Polar is legacy, Dodo is primary), a brief comment explaining the difference would help future maintainers. Otherwise, consider recording the error consistently.


661-664: Remove or restore the commented-out time-check block.

This 1-hour skip is active in RefreshAllLicenses (line 465) but commented out here. If intentionally disabled for ValidateLicenses, add a brief comment explaining why. If this is leftover debug code, remove it to avoid confusion.


863-868: Return logic is subtle but correct — consider a clarifying comment.

The intent: when a license is definitively invalid (!allValid), suppress any transient error so the caller can cleanly distinguish "invalid" from "transient failure". When everything appears valid but a transient error occurred, surface it so the caller knows validation was incomplete.

A brief inline comment would help future readers understand the priority-based error suppression.


385-430: ensurePolarActivation does not set license.Status = Active unlike ensureDodoActivation.

ensureDodoActivation (line 371) explicitly sets license.Status = models.LicenseStatusActive, but ensurePolarActivation omits this. Currently safe because callers pre-filter for active licenses, but this inconsistency could bite if the method is reused in a broader context later.

Proposed fix
 	license.Provider = models.LicenseProviderPolar
 	license.DodoInstanceID = ""
 	license.PolarActivationID = activateResp.Id
 	license.PolarCustomerID = &activateResp.LicenseKey.CustomerID
 	license.PolarProductID = &activateResp.LicenseKey.BenefitID
 	license.ActivatedAt = time.Now()
 	license.ExpiresAt = activateResp.LicenseKey.ExpiresAt
+	license.Status = models.LicenseStatusActive

Comment thread internal/services/license/license_service.go

@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

Caution

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

⚠️ Outside diff range comments (1)
internal/api/handlers/licenses.go (1)

177-183: ⚠️ Potential issue | 🟠 Major

Add check for dodo.ErrLicenseNotFound in the error handling.

The handler checks errors.Is(err, models.ErrLicenseNotFound) which will match repository errors, but validateExistingDodoLicense wraps dodo client errors with fmt.Errorf, allowing dodo.ErrLicenseNotFound to propagate unwrapped in the error chain. Since dodo.ErrLicenseNotFound is a distinct sentinel from models.ErrLicenseNotFound, it will not match the check and will fall through to the generic 403 Forbidden response instead of 404 Not Found. Consider also checking errors.Is(err, dodo.ErrLicenseNotFound) or normalize the error in the service layer.

🧹 Nitpick comments (4)
internal/services/license/license_service_dodo_regression_test.go (1)

1-1: Copyright year inconsistency.

This file uses 2025 while licenses.go and license_service.go use 2025-2026. Consider updating for consistency.

internal/services/license/license_service.go (3)

528-532: Asymmetric error recording: Polar nil-client skips recordRefreshErr, Dodo does not.

When dodoClient is nil (lines 479-484), the error is captured via recordRefreshErr(ErrDodoClientNotConfigured) and surfaces to the caller. When polarClient is nil (lines 529-532), it silently skips without recording. This means RefreshAllLicenses returns nil if only Polar licenses couldn't be refreshed due to a missing client, but returns an error if only Dodo licenses couldn't.

If this asymmetry is intentional (e.g., Polar is being phased out), a brief comment would clarify intent.


446-451: recordRefreshErr only captures the first error — consider errors.Join for richer diagnostics.

If multiple licenses fail for different reasons, only the first error is preserved. With Go 1.20+ errors.Join, you could accumulate all errors while still letting callers use errors.Is against any of them. This is optional and low priority.


130-142: Multiple time.Now() calls per activation create slight timestamp drift between fields.

In both activateWithDodo and activateWithPolar, ActivatedAt, LastValidated, CreatedAt, and UpdatedAt each call time.Now() separately, which could produce slightly different timestamps. Consider capturing now := time.Now() once and reusing it.

Example for activateWithDodo
+	now := time.Now()
 	if existingLicense != nil {
 		existingLicense.ProductName = productName
 		existingLicense.Status = models.LicenseStatusActive
-		existingLicense.ActivatedAt = time.Now()
+		existingLicense.ActivatedAt = now
 		existingLicense.ExpiresAt = activateResp.ExpiresAt
-		existingLicense.LastValidated = time.Now()
+		existingLicense.LastValidated = now
 		// ...
-		existingLicense.UpdatedAt = time.Now()
+		existingLicense.UpdatedAt = now

Also applies to: 156-167, 205-217, 231-244

@s0up4200
s0up4200 merged commit 77e9abf into develop Feb 6, 2026
13 checks passed
@s0up4200
s0up4200 deleted the feat/dodopayments branch February 6, 2026 10:19
alexlebens pushed a commit to alexlebens/infrastructure that referenced this pull request Feb 22, 2026
This PR contains the following updates:

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

---

### Release Notes

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

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

[Compare Source](autobrr/qui@v1.13.1...v1.14.0)

#### Changelog

##### New Features

- [`6f8e6ed`](autobrr/qui@6f8e6ed): feat(api): add torrent field endpoint for select all copy ([#&#8203;1477](autobrr/qui#1477)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`2d9b4c7`](autobrr/qui@2d9b4c7): feat(automation): trigger external programs automatically via automation rules ([#&#8203;1284](autobrr/qui#1284)) ([@&#8203;0rkag](https://github.com/0rkag))
- [`32692a4`](autobrr/qui@32692a4): feat(automations): Add the ability to define the move automation with a templated path ([#&#8203;1376](autobrr/qui#1376)) ([@&#8203;ColinHebert](https://github.com/ColinHebert))
- [`61bbeb1`](autobrr/qui@61bbeb1): feat(automations): add Resume action to Automations ([#&#8203;1350](autobrr/qui#1350)) ([@&#8203;cy1der](https://github.com/cy1der))
- [`450b98f`](autobrr/qui@450b98f): feat(automations): grouping + release fields ([#&#8203;1467](autobrr/qui#1467)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`18d4a64`](autobrr/qui@18d4a64): feat(automations): match tracker conditions by display name ([#&#8203;1420](autobrr/qui#1420)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`7c67b82`](autobrr/qui@7c67b82): feat(automations): show activity run details ([#&#8203;1385](autobrr/qui#1385)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`177ef4d`](autobrr/qui@177ef4d): feat(crossseed): Multiple hard/reflink dirs ([#&#8203;1289](autobrr/qui#1289)) ([@&#8203;rybertm](https://github.com/rybertm))
- [`a72b673`](autobrr/qui@a72b673): feat(crossseed): gazelle-only OPS/RED ([#&#8203;1436](autobrr/qui#1436)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`6a29384`](autobrr/qui@6a29384): feat(crossseed): match bit depth ([#&#8203;1427](autobrr/qui#1427)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c7fd5aa`](autobrr/qui@c7fd5aa): feat(dirscan): add max searchee age filter ([#&#8203;1486](autobrr/qui#1486)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d595a55`](autobrr/qui@d595a55): feat(documentation): add AI doc actions and llms discoverability ([#&#8203;1451](autobrr/qui#1451)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`562ab3f`](autobrr/qui@562ab3f): feat(metrics): add tracker metrics ([#&#8203;1073](autobrr/qui#1073)) ([@&#8203;Winter](https://github.com/Winter))
- [`1b9aa9d`](autobrr/qui@1b9aa9d): feat(notifications): add shoutrrr and notifiarr ([#&#8203;1371](autobrr/qui#1371)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`6d1dac7`](autobrr/qui@6d1dac7): feat(pwa): add protocol and file handlers for magnet links and torrent files ([#&#8203;783](autobrr/qui#783)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`42fa501`](autobrr/qui@42fa501): feat(torrents): add unified cross-instance torrent table ([#&#8203;1481](autobrr/qui#1481)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`498eaca`](autobrr/qui@498eaca): feat(ui): show speeds in page title ([#&#8203;1292](autobrr/qui#1292)) ([@&#8203;NoLife141](https://github.com/NoLife141))
- [`94a506e`](autobrr/qui@94a506e): feat(unregistered): nem talalhato ([#&#8203;1483](autobrr/qui#1483)) ([@&#8203;KyleSanderson](https://github.com/KyleSanderson))
- [`8bf366c`](autobrr/qui@8bf366c): feat(web): add logs nav ([#&#8203;1458](autobrr/qui#1458)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`babc88d`](autobrr/qui@babc88d): feat(web): add responsive popover with mobile drawer support ([#&#8203;1398](autobrr/qui#1398)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`06d341b`](autobrr/qui@06d341b): feat(web): add torrent table selection quick wins ([#&#8203;1455](autobrr/qui#1455)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`56fbbec`](autobrr/qui@56fbbec): feat(web): hide selection column ([#&#8203;1460](autobrr/qui#1460)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`46814aa`](autobrr/qui@46814aa): feat(web): qBittorrent autorun preferences ([#&#8203;1430](autobrr/qui#1430)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`342643e`](autobrr/qui@342643e): feat(web): unify instance settings & qbit options dialog ([#&#8203;1257](autobrr/qui#1257)) ([@&#8203;0rkag](https://github.com/0rkag))
- [`e634d01`](autobrr/qui@e634d01): feat: add cross-seed blocklist ([#&#8203;1391](autobrr/qui#1391)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`13aaac8`](autobrr/qui@13aaac8): feat: add dry-run workflows ([#&#8203;1395](autobrr/qui#1395)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`f01101d`](autobrr/qui@f01101d): feat: add option to disable built-in authentication ([#&#8203;1464](autobrr/qui#1464)) ([@&#8203;libussa](https://github.com/libussa))
- [`6d1da50`](autobrr/qui@6d1da50): feat: download individual content files from context menu ([#&#8203;1465](autobrr/qui#1465)) ([@&#8203;libussa](https://github.com/libussa))
- [`77e9abf`](autobrr/qui@77e9abf): feat: migrate to dodopayments ([#&#8203;1407](autobrr/qui#1407)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`9f6c856`](autobrr/qui@9f6c856): feat: support basic auth for ARR and Torznab ([#&#8203;1442](autobrr/qui#1442)) ([@&#8203;s0up4200](https://github.com/s0up4200))

##### Bug Fixes

- [`8a06d4b`](autobrr/qui@8a06d4b): fix(api): correct add-torrent OpenAPI param names and add missing fields ([#&#8203;1426](autobrr/qui#1426)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`b9a687c`](autobrr/qui@b9a687c): fix(api): honor explicit basic auth clear from URL userinfo ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`948ca67`](autobrr/qui@948ca67): fix(api): tighten CORS/auth routing and base URL joins ([#&#8203;1325](autobrr/qui#1325)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`12bea13`](autobrr/qui@12bea13): fix(automations): improve applied action summaries ([#&#8203;1478](autobrr/qui#1478)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`8fe658b`](autobrr/qui@8fe658b): fix(automations): negate regex match for NotContains/NotEqual operators ([#&#8203;1441](autobrr/qui#1441)) ([@&#8203;andresatierf](https://github.com/andresatierf))
- [`8a808eb`](autobrr/qui@8a808eb): fix(automations): respect remove-only tag conditions ([#&#8203;1444](autobrr/qui#1444)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`a72715e`](autobrr/qui@a72715e): fix(backups): add failure cooldown and export throttling ([#&#8203;1214](autobrr/qui#1214)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`2e75c14`](autobrr/qui@2e75c14): fix(backups): skip exports missing metadata ([#&#8203;1362](autobrr/qui#1362)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5658421`](autobrr/qui@5658421): fix(config): update commented log settings in place ([#&#8203;1402](autobrr/qui#1402)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`62c50c0`](autobrr/qui@62c50c0): fix(crossseed): tighten TV title matching ([#&#8203;1445](autobrr/qui#1445)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`e7cc489`](autobrr/qui@e7cc489): fix(dirscan): prevent immediate requeue after cancel ([#&#8203;1446](autobrr/qui#1446)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`36cbfcf`](autobrr/qui@36cbfcf): fix(docs): avoid mdx jsx parse error ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d8d6f62`](autobrr/qui@d8d6f62): fix(filters): stabilize dense sidebar layout ([#&#8203;1384](autobrr/qui#1384)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`b959fc6`](autobrr/qui@b959fc6): fix(orphanscan): NFC-normalize paths to avoid false orphans ([#&#8203;1422](autobrr/qui#1422)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`598e994`](autobrr/qui@598e994): fix(reflink): retry EAGAIN clones ([#&#8203;1360](autobrr/qui#1360)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`aaa5ee0`](autobrr/qui@aaa5ee0): fix(reflinktree): retry transient FICLONE EINVAL and add diagnostics ([#&#8203;1487](autobrr/qui#1487)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`647af31`](autobrr/qui@647af31): fix(rss): enable rules list scrolling ([#&#8203;1359](autobrr/qui#1359)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c356a6f`](autobrr/qui@c356a6f): fix(sync): Optimize torrent sorting and reference management ([#&#8203;1474](autobrr/qui#1474)) ([@&#8203;KyleSanderson](https://github.com/KyleSanderson))
- [`cf4310e`](autobrr/qui@cf4310e): fix(ui): update placeholder text in ArrInstanceForm based on instance type ([#&#8203;1375](autobrr/qui#1375)) ([@&#8203;pashioya](https://github.com/pashioya))
- [`92b6748`](autobrr/qui@92b6748): fix(web): format IPv6 peer addresses and copy IP without port ([#&#8203;1417](autobrr/qui#1417)) ([@&#8203;sleepm](https://github.com/sleepm))
- [`25039bc`](autobrr/qui@25039bc): fix(web): handle SSO session expiry behind Cloudflare Access and other proxies ([#&#8203;1438](autobrr/qui#1438)) ([@&#8203;nitrobass24](https://github.com/nitrobass24))
- [`77fe310`](autobrr/qui@77fe310): fix(web): prevent category submenu re-render ([#&#8203;1357](autobrr/qui#1357)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`a42ab1e`](autobrr/qui@a42ab1e): fix(web): raise instance preferences max value from 999 to 99999 ([#&#8203;1311](autobrr/qui#1311)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`540168c`](autobrr/qui@540168c): fix(web): raise virtualization threshold ([#&#8203;1355](autobrr/qui#1355)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`8547dc6`](autobrr/qui@8547dc6): fix(web): remove column filters when column is hidden ([#&#8203;1418](autobrr/qui#1418)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`6b09b8d`](autobrr/qui@6b09b8d): fix(web): remove panel size bounds ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`db4cdc4`](autobrr/qui@db4cdc4): fix(web): show piece size in torrent details ([#&#8203;1365](autobrr/qui#1365)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`1f94a06`](autobrr/qui@1f94a06): fix(web): use absolute for scroll-to-top on desktop ([#&#8203;1419](autobrr/qui#1419)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`e31fe3a`](autobrr/qui@e31fe3a): fix: detect tracker health support after qBit upgrade ([#&#8203;909](autobrr/qui#909)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`52f01da`](autobrr/qui@52f01da): fix: disable update indicators when update checks are off ([#&#8203;1364](autobrr/qui#1364)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`f7e3fed`](autobrr/qui@f7e3fed): fix: normalize DD+ and DDP file keys ([#&#8203;1456](autobrr/qui#1456)) ([@&#8203;s0up4200](https://github.com/s0up4200))

##### Other Changes

- [`d914301`](autobrr/qui@d914301): chore(ci): fire Blacksmith (my wallet screamed) ([#&#8203;1408](autobrr/qui#1408)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`b43327d`](autobrr/qui@b43327d): chore(deps): bump the golang group with 2 updates ([#&#8203;1378](autobrr/qui#1378)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`57747bd`](autobrr/qui@57747bd): chore(deps): bump the npm group across 1 directory with 27 updates ([#&#8203;1379](autobrr/qui#1379)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`a43850d`](autobrr/qui@a43850d): chore(docs): add BIMI SVG logo ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`914bede`](autobrr/qui@914bede): chore(funding): add Patreon to FUNDING.yml ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`8b76f1e`](autobrr/qui@8b76f1e): docs(automations): clarify tag matching examples ([#&#8203;1457](autobrr/qui#1457)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`2994054`](autobrr/qui@2994054): docs(readme): restore concise README ([#&#8203;1452](autobrr/qui#1452)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`51237d4`](autobrr/qui@51237d4): docs: Add configuration reference ([#&#8203;1440](autobrr/qui#1440)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`741462c`](autobrr/qui@741462c): docs: add Windows installation guide ([#&#8203;1463](autobrr/qui#1463)) ([@&#8203;soggy-cr0uton](https://github.com/soggy-cr0uton))
- [`6a11430`](autobrr/qui@6a11430): docs: clarify autobrr filter + apply troubleshooting ([#&#8203;1459](autobrr/qui#1459)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5a2edc2`](autobrr/qui@5a2edc2): docs: update 2 documentation files ([#&#8203;1454](autobrr/qui#1454)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`139ada9`](autobrr/qui@139ada9): docs: update contributing.md ([#&#8203;1470](autobrr/qui#1470)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`3909aa1`](autobrr/qui@3909aa1): docs: update docs/features/automations.md ([#&#8203;1447](autobrr/qui#1447)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5dc57ca`](autobrr/qui@5dc57ca): docs: update intro.md ([#&#8203;1453](autobrr/qui#1453)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5d9e986`](autobrr/qui@5d9e986): perf(web): memoize useDateTimeFormatters ([#&#8203;1403](autobrr/qui#1403)) ([@&#8203;jabloink](https://github.com/jabloink))

**Full Changelog**: <autobrr/qui@v1.13.1...v1.14.0>

#### Docker images

- `docker pull ghcr.io/autobrr/qui:v1.14.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:eyJjcmVhdGVkSW5WZXIiOiI0My4yNS43IiwidXBkYXRlZEluVmVyIjoiNDMuMjUuNyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW1hZ2UiXX0=-->

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant