From 46e5e2a059c5521eb3d002aecb96f772db72df0d Mon Sep 17 00:00:00 2001
From: Sam Chau
Date: Thu, 22 Jan 2026 09:25:39 +1030
Subject: [PATCH] refactor: moved upgrade/filter info into a seperate page on
upgrades/info
---
docs/todo/scratchpad.md | 5 +-
src/lib/client/ui/button/Button.svelte | 41 ++--
src/lib/shared/filters.ts | 78 +++++---
src/routes/arr/[id]/upgrades/+page.svelte | 5 +-
.../upgrades/components/FilterSettings.svelte | 7 -
.../components/FiltersInfoModal.svelte | 165 ----------------
.../components/UpgradesInfoModal.svelte | 66 -------
.../arr/[id]/upgrades/info/+page.server.ts | 21 ++
.../arr/[id]/upgrades/info/+page.svelte | 179 ++++++++++++++++++
9 files changed, 282 insertions(+), 285 deletions(-)
delete mode 100644 src/routes/arr/[id]/upgrades/components/FiltersInfoModal.svelte
delete mode 100644 src/routes/arr/[id]/upgrades/components/UpgradesInfoModal.svelte
create mode 100644 src/routes/arr/[id]/upgrades/info/+page.server.ts
create mode 100644 src/routes/arr/[id]/upgrades/info/+page.svelte
diff --git a/docs/todo/scratchpad.md b/docs/todo/scratchpad.md
index 6ac4e88..0307052 100644
--- a/docs/todo/scratchpad.md
+++ b/docs/todo/scratchpad.md
@@ -14,10 +14,7 @@
- maybe move langauges to general tab or put info directly below it to fill
space
- adding a database requires double click??? im not running into this personally
-- default delay profiles upon adding an arr. different for radarr/sonarr
- - wait for seraphys to give default profiles
- - maybe need to get feedback and update over time
- - on by default, turn off in settings > general
+- pageinate the profilarr log pages
# Adaptive Backoff
diff --git a/src/lib/client/ui/button/Button.svelte b/src/lib/client/ui/button/Button.svelte
index 828571a..7b5ba9f 100644
--- a/src/lib/client/ui/button/Button.svelte
+++ b/src/lib/client/ui/button/Button.svelte
@@ -14,6 +14,8 @@
// Responsive: auto-switch to xs on smaller screens (< 1280px)
export let responsive: boolean = false;
export let fullWidth: boolean = false;
+ // Optional href - renders as anchor instead of button
+ export let href: string | undefined = undefined;
let isSmallScreen = false;
let mediaQuery: MediaQueryList | null = null;
@@ -64,15 +66,30 @@
$: classes = `${baseClasses} ${sizeClasses[effectiveSize]} ${variantClasses[variant]} ${widthClass}`;
-
- {#if icon && iconPosition === 'left'}
-
- {/if}
- {#if text}
- {text}
- {/if}
-
- {#if icon && iconPosition === 'right'}
-
- {/if}
-
+{#if href}
+
+ {#if icon && iconPosition === 'left'}
+
+ {/if}
+ {#if text}
+ {text}
+ {/if}
+
+ {#if icon && iconPosition === 'right'}
+
+ {/if}
+
+{:else}
+
+ {#if icon && iconPosition === 'left'}
+
+ {/if}
+ {#if text}
+ {text}
+ {/if}
+
+ {#if icon && iconPosition === 'right'}
+
+ {/if}
+
+{/if}
diff --git a/src/lib/shared/filters.ts b/src/lib/shared/filters.ts
index fef3011..54b84b3 100644
--- a/src/lib/shared/filters.ts
+++ b/src/lib/shared/filters.ts
@@ -8,6 +8,7 @@ import { uuid } from './uuid.ts';
export interface FilterOperator {
id: string;
label: string;
+ description?: string;
}
export type FilterValueType = string | number | boolean | null;
@@ -20,6 +21,7 @@ export interface FilterValue {
export interface FilterField {
id: string;
label: string;
+ description: string;
operators: FilterOperator[];
valueType: 'boolean' | 'select' | 'text' | 'number' | 'date';
values?: FilterValue[];
@@ -82,42 +84,42 @@ export const filterModes: { id: FilterMode; label: string; description: string }
* Common operator sets
*/
const booleanOperators: FilterOperator[] = [
- { id: 'is', label: 'is' },
- { id: 'is_not', label: 'is not' }
+ { id: 'is', label: 'is', description: 'Exact match' },
+ { id: 'is_not', label: 'is not', description: 'Does not match' }
];
const numberOperators: FilterOperator[] = [
- { id: 'eq', label: 'equals' },
- { id: 'neq', label: 'does not equal' },
- { id: 'gt', label: 'is greater than' },
- { id: 'gte', label: 'is greater than or equal' },
- { id: 'lt', label: 'is less than' },
- { id: 'lte', label: 'is less than or equal' }
+ { id: 'eq', label: 'equals', description: 'Exactly equals the value' },
+ { id: 'neq', label: 'does not equal', description: 'Does not equal the value' },
+ { id: 'gt', label: 'is greater than', description: 'Greater than the value' },
+ { id: 'gte', label: 'is greater than or equal', description: 'Greater than or equal to the value' },
+ { id: 'lt', label: 'is less than', description: 'Less than the value' },
+ { id: 'lte', label: 'is less than or equal', description: 'Less than or equal to the value' }
];
const textOperators: FilterOperator[] = [
- { id: 'contains', label: 'contains' },
- { id: 'not_contains', label: 'does not contain' },
- { id: 'starts_with', label: 'starts with' },
- { id: 'ends_with', label: 'ends with' },
- { id: 'eq', label: 'equals' },
- { id: 'neq', label: 'does not equal' }
+ { id: 'contains', label: 'contains', description: 'Contains the text (case-insensitive)' },
+ { id: 'not_contains', label: 'does not contain', description: 'Does not contain the text' },
+ { id: 'starts_with', label: 'starts with', description: 'Starts with the text' },
+ { id: 'ends_with', label: 'ends with', description: 'Ends with the text' },
+ { id: 'eq', label: 'equals', description: 'Exactly equals the text (case-insensitive)' },
+ { id: 'neq', label: 'does not equal', description: 'Does not equal the text' }
];
const dateOperators: FilterOperator[] = [
- { id: 'before', label: 'is before' },
- { id: 'after', label: 'is after' },
- { id: 'in_last', label: 'in the last' },
- { id: 'not_in_last', label: 'not in the last' }
+ { id: 'before', label: 'is before', description: 'The date is before the specified date' },
+ { id: 'after', label: 'is after', description: 'The date is after the specified date' },
+ { id: 'in_last', label: 'in the last', description: 'Within the last N days' },
+ { id: 'not_in_last', label: 'not in the last', description: 'Not within the last N days' }
];
const ordinalOperators: FilterOperator[] = [
- { id: 'eq', label: 'is exactly' },
- { id: 'neq', label: 'is not' },
- { id: 'gte', label: 'has reached' },
- { id: 'lte', label: "hasn't passed" },
- { id: 'gt', label: 'is past' },
- { id: 'lt', label: 'is before' }
+ { id: 'eq', label: 'is exactly', description: 'Exactly matches the status' },
+ { id: 'neq', label: 'is not', description: 'Does not match the status' },
+ { id: 'gte', label: 'has reached', description: 'Has reached this status or further in the progression' },
+ { id: 'lte', label: "hasn't passed", description: 'Has not passed this status in the progression' },
+ { id: 'gt', label: 'is past', description: 'Is past this status (further along)' },
+ { id: 'lt', label: 'is before', description: 'Is before this status (not yet reached)' }
];
/**
@@ -139,6 +141,7 @@ export const filterFields: FilterField[] = [
{
id: 'monitored',
label: 'Monitored',
+ description: 'Whether the item is being monitored for upgrades',
operators: booleanOperators,
valueType: 'boolean',
values: [
@@ -149,6 +152,7 @@ export const filterFields: FilterField[] = [
{
id: 'cutoff_met',
label: 'Cutoff Met',
+ description: "Whether the item's quality score meets the filter's cutoff percentage",
operators: booleanOperators,
valueType: 'boolean',
values: [
@@ -161,6 +165,7 @@ export const filterFields: FilterField[] = [
{
id: 'minimum_availability',
label: 'Minimum Availability',
+ description: 'The minimum availability status set in Radarr. Progresses: TBA → Announced → In Cinemas → Released',
operators: ordinalOperators,
valueType: 'select',
values: [
@@ -175,48 +180,56 @@ export const filterFields: FilterField[] = [
{
id: 'title',
label: 'Title',
+ description: 'The title of the movie',
operators: textOperators,
valueType: 'text'
},
{
id: 'quality_profile',
label: 'Quality Profile',
+ description: 'The assigned quality profile name',
operators: textOperators,
valueType: 'text'
},
{
id: 'collection',
label: 'Collection',
+ description: 'The collection the movie belongs to (e.g., "Marvel Cinematic Universe")',
operators: textOperators,
valueType: 'text'
},
{
id: 'studio',
label: 'Studio',
+ description: 'The production studio',
operators: textOperators,
valueType: 'text'
},
{
id: 'original_language',
label: 'Original Language',
+ description: 'The original language of the movie (e.g., "en", "ja")',
operators: textOperators,
valueType: 'text'
},
{
id: 'genres',
label: 'Genres',
+ description: 'Movie genres (Action, Comedy, Drama, etc.)',
operators: textOperators,
valueType: 'text'
},
{
id: 'keywords',
label: 'Keywords',
+ description: 'TMDb keywords associated with the movie',
operators: textOperators,
valueType: 'text'
},
{
id: 'release_group',
label: 'Release Group',
+ description: 'The release group of the current file',
operators: textOperators,
valueType: 'text'
},
@@ -225,48 +238,56 @@ export const filterFields: FilterField[] = [
{
id: 'year',
label: 'Year',
+ description: 'The release year',
operators: numberOperators,
valueType: 'number'
},
{
id: 'popularity',
label: 'Popularity',
+ description: 'TMDb popularity score',
operators: numberOperators,
valueType: 'number'
},
{
id: 'runtime',
- label: 'Runtime (minutes)',
+ label: 'Runtime',
+ description: 'Movie runtime in minutes',
operators: numberOperators,
valueType: 'number'
},
{
id: 'size_on_disk',
- label: 'Size on Disk (GB)',
+ label: 'Size on Disk',
+ description: 'Current file size in GB',
operators: numberOperators,
valueType: 'number'
},
{
id: 'tmdb_rating',
label: 'TMDb Rating',
+ description: 'TMDb rating (0-10)',
operators: numberOperators,
valueType: 'number'
},
{
id: 'imdb_rating',
label: 'IMDb Rating',
+ description: 'IMDb rating (0-10)',
operators: numberOperators,
valueType: 'number'
},
{
id: 'tomato_rating',
- label: 'Rotten Tomatoes Rating',
+ label: 'Rotten Tomatoes',
+ description: 'Rotten Tomatoes score (0-100)',
operators: numberOperators,
valueType: 'number'
},
{
id: 'trakt_rating',
label: 'Trakt Rating',
+ description: 'Trakt rating (0-100)',
operators: numberOperators,
valueType: 'number'
},
@@ -275,18 +296,21 @@ export const filterFields: FilterField[] = [
{
id: 'date_added',
label: 'Date Added',
+ description: 'When the movie was added to your library',
operators: dateOperators,
valueType: 'date'
},
{
id: 'digital_release',
label: 'Digital Release',
+ description: 'The digital release date from TMDb',
operators: dateOperators,
valueType: 'date'
},
{
id: 'physical_release',
label: 'Physical Release',
+ description: 'The physical release date from TMDb',
operators: dateOperators,
valueType: 'date'
}
diff --git a/src/routes/arr/[id]/upgrades/+page.svelte b/src/routes/arr/[id]/upgrades/+page.svelte
index 5ceb363..601d128 100644
--- a/src/routes/arr/[id]/upgrades/+page.svelte
+++ b/src/routes/arr/[id]/upgrades/+page.svelte
@@ -9,7 +9,6 @@
import CoreSettings from './components/CoreSettings.svelte';
import FilterSettings from './components/FilterSettings.svelte';
import RunHistory from './components/RunHistory.svelte';
- import UpgradesInfoModal from './components/UpgradesInfoModal.svelte';
import DirtyModal from '$ui/modal/DirtyModal.svelte';
import StickyCard from '$ui/card/StickyCard.svelte';
import Button from '$ui/button/Button.svelte';
@@ -37,7 +36,6 @@
// Dev mode check - use VITE_CHANNEL which is explicitly set in dev mode
const isDev = import.meta.env.VITE_CHANNEL === 'dev';
- let showInfoModal = false;
let saving = false;
let running = false;
let clearing = false;
@@ -86,7 +84,7 @@
-
(showInfoModal = true)} />
+
{#if !isNewConfig && data.config?.dryRun}
{/if}
-
diff --git a/src/routes/arr/[id]/upgrades/components/FilterSettings.svelte b/src/routes/arr/[id]/upgrades/components/FilterSettings.svelte
index e74289f..bd118ee 100644
--- a/src/routes/arr/[id]/upgrades/components/FilterSettings.svelte
+++ b/src/routes/arr/[id]/upgrades/components/FilterSettings.svelte
@@ -3,7 +3,6 @@
Plus,
Pencil,
Check,
- Info,
Power,
Copy,
ClipboardCopy,
@@ -16,7 +15,6 @@
import { createSearchStore } from '$lib/client/stores/search';
import FilterGroupComponent from './FilterGroup.svelte';
import NumberInput from '$ui/form/NumberInput.svelte';
- import FiltersInfoModal from './FiltersInfoModal.svelte';
import DropdownSelect from '$ui/dropdown/DropdownSelect.svelte';
import ActionsBar from '$ui/actions/ActionsBar.svelte';
import ActionButton from '$ui/actions/ActionButton.svelte';
@@ -37,8 +35,6 @@
onFiltersChange?.(filters);
}
- let showInfoModal = false;
-
// Filter the filters list based on search
$: filteredFilters = filterByName(filters, $debouncedQuery);
@@ -186,7 +182,6 @@
- (showInfoModal = true)} />
@@ -390,8 +385,6 @@
-
-
- import InfoModal from '$ui/modal/InfoModal.svelte';
- import { filterFields } from '$lib/shared/filters';
-
- export let open: boolean = false;
-
- // Group fields by type for better organization
- const booleanFields = filterFields.filter((f) => f.valueType === 'boolean');
- const selectFields = filterFields.filter((f) => f.valueType === 'select');
- const textFields = filterFields.filter((f) => f.valueType === 'text');
- const numberFields = filterFields.filter((f) => f.valueType === 'number');
- const dateFields = filterFields.filter((f) => f.valueType === 'date');
-
-
-
-
-
- Use these fields to build filter rules. Combine multiple rules with AND/OR logic to create
- complex filters.
-
-
- {#if booleanFields.length > 0}
-
-
Yes/No Fields
-
- {#each booleanFields as field}
-
- {field.label}:
-
- {#if field.id === 'monitored'}
- Whether the item is being monitored for upgrades
- {:else if field.id === 'cutoff_met'}
- Whether the item's score meets the filter's cutoff percentage
- {/if}
-
-
- {/each}
-
-
- {/if}
-
- {#if selectFields.length > 0}
-
-
Selection Fields
-
- {#each selectFields as field}
-
- {field.label}:
-
- {#if field.id === 'minimum_availability'}
- The minimum availability status ({field.values?.map((v) => v.label).join(', ')})
- {/if}
-
-
- {/each}
-
-
- {/if}
-
- {#if textFields.length > 0}
-
-
Text Fields
-
All text comparisons are case-insensitive.
-
- {#each textFields as field}
-
- {field.label}:
-
- {#if field.id === 'title'}
- The title of the movie
- {:else if field.id === 'quality_profile'}
- The assigned quality profile name
- {:else if field.id === 'collection'}
- The collection the movie belongs to (e.g., "Marvel Cinematic Universe")
- {:else if field.id === 'studio'}
- The production studio
- {:else if field.id === 'original_language'}
- The original language of the movie
- {:else if field.id === 'genres'}
- Movie genres (Action, Comedy, Drama, etc.)
- {:else if field.id === 'keywords'}
- TMDb keywords associated with the movie
- {:else if field.id === 'release_group'}
- The release group of the current file
- {/if}
-
-
- {/each}
-
-
- {/if}
-
- {#if numberFields.length > 0}
-
-
Number Fields
-
- {#each numberFields as field}
-
- {field.label}:
-
- {#if field.id === 'year'}
- The release year
- {:else if field.id === 'popularity'}
- TMDb popularity score
- {:else if field.id === 'runtime'}
- Movie runtime in minutes
- {:else if field.id === 'size_on_disk'}
- Current file size in GB
- {:else if field.id === 'tmdb_rating'}
- TMDb rating (0-10)
- {:else if field.id === 'imdb_rating'}
- IMDb rating (0-10)
- {:else if field.id === 'tomato_rating'}
- Rotten Tomatoes score (0-100)
- {:else if field.id === 'trakt_rating'}
- Trakt rating (0-100)
- {/if}
-
-
- {/each}
-
-
- {/if}
-
- {#if dateFields.length > 0}
-
-
Date Fields
-
- {#each dateFields as field}
-
- {field.label}:
-
- {#if field.id === 'date_added'}
- When the movie was added to your library
- {/if}
-
-
- {/each}
-
-
- {/if}
-
-
-
Filter Settings
-
-
- Cutoff %:
- Score threshold (0-100%) for the "Cutoff Met" filter
-
-
- Cooldown:
- Skip items that were searched within this many hours
-
-
- Method:
- How to select items from the filtered results
-
-
- Count:
- Number of items to search per run
-
-
-
-
-
diff --git a/src/routes/arr/[id]/upgrades/components/UpgradesInfoModal.svelte b/src/routes/arr/[id]/upgrades/components/UpgradesInfoModal.svelte
deleted file mode 100644
index b4f9208..0000000
--- a/src/routes/arr/[id]/upgrades/components/UpgradesInfoModal.svelte
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
-
-
-
Why is this needed?
-
-
- *arr apps only upgrade via RSS feeds, which are incremental and only show what's new. RSS
- doesn't search for the best available release — it just grabs whatever comes through.
-
-
- This module lets you define exactly what to upgrade, when, and how — all within Profilarr.
-
-
-
-
-
-
Core Settings
-
- Configure the basic behavior: set how often it runs and choose how to cycle through your
- filters (round robin or random shuffle).
-
-
-
-
-
Filters
-
- Define which items to consider for upgrades. Create multiple named filters with different
- criteria (e.g., "Marvel Movies", "2024 Releases", "Low Quality"). Each filter can have
- complex rules with AND/OR logic and nested groups.
-
-
-
-
-
Filter Mode
-
-
Round Robin: Cycle through filters in order, one per run.
-
- Random Shuffle: Randomly shuffle filters, use each once before repeating.
-
-
-
-
-
-
Selection
-
- Each filter has its own selection settings. Choose how to pick items for upgrade (e.g.,
- random) and how many items to process per run. This prevents overwhelming your indexers with
- too many searches at once.
-
-
-
-
-
How it runs
-
- On each scheduled run: pick a filter (based on mode) → apply filter rules to your library →
- select items (based on selection method) → trigger searches in your *arr app.
-
-
-
-
diff --git a/src/routes/arr/[id]/upgrades/info/+page.server.ts b/src/routes/arr/[id]/upgrades/info/+page.server.ts
new file mode 100644
index 0000000..8c8ae41
--- /dev/null
+++ b/src/routes/arr/[id]/upgrades/info/+page.server.ts
@@ -0,0 +1,21 @@
+import { error } from '@sveltejs/kit';
+import type { ServerLoad } from '@sveltejs/kit';
+import { arrInstancesQueries } from '$db/queries/arrInstances.ts';
+
+export const load: ServerLoad = ({ params }) => {
+ const id = parseInt(params.id || '', 10);
+
+ if (isNaN(id)) {
+ error(404, `Invalid instance ID: ${params.id}`);
+ }
+
+ const instance = arrInstancesQueries.getById(id);
+
+ if (!instance) {
+ error(404, `Instance not found: ${id}`);
+ }
+
+ return {
+ instance
+ };
+};
diff --git a/src/routes/arr/[id]/upgrades/info/+page.svelte b/src/routes/arr/[id]/upgrades/info/+page.svelte
new file mode 100644
index 0000000..01c65fa
--- /dev/null
+++ b/src/routes/arr/[id]/upgrades/info/+page.svelte
@@ -0,0 +1,179 @@
+
+
+
+ {data.instance.name} - Upgrades Info - Profilarr
+
+
+
+
+
How Upgrades Work
+
+
+
+
+
+
+
+
+
+
+ Radarr and Sonarr don't search for the best release. They monitor RSS feeds and grab the
+ first thing that qualifies as an upgrade. To get optimal releases, you need manual searches.
+ This module automates that: Filter
+ your library,
+ Select items to search,
+ then
+ Search for better releases.
+
+
+
+
+
+ Concepts
+ row.id}
+ emptyMessage="No concepts"
+ chevronPosition="right"
+ flushExpanded
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+