14 KiB
Drift Detection
Source: src/lib/server/jobs/handlers/arrDrift.ts,
src/lib/server/jobs/handlers/arrSync.ts,
src/lib/server/db/queries/arrDriftSettings.ts,
src/lib/server/db/queries/arrDriftStatus.ts,
src/lib/server/drift/customFormats.ts,
src/lib/server/drift/qualityProfiles.ts,
src/lib/server/drift/delayProfiles.ts,
src/lib/server/drift/mediaManagement.ts,
src/lib/server/drift/display.ts,
src/routes/arr/[id]/drift/+page.svelte,
src/routes/arr/[id]/drift/+page.server.ts,
src/routes/arr/[id]/drift/components/DriftFieldDiffTable.svelte,
src/routes/arr/[id]/sync/+page.server.ts,
src/routes/arr/[id]/sync/components/QualityProfiles.svelte,
src/routes/arr/[id]/sync/components/DelayProfiles.svelte
Drift detection checks whether an Arr instance still matches the configuration Profilarr would sync now. It is observational: it does not write to Arr, repair config, delete stale items, or replace cleanup.
Drift detection currently covers custom formats, quality profiles, the default delay profile, and media management media settings, naming, and quality definitions.
Job
The scheduled job type is arr.drift with payload { instanceId }.
Scheduling is per Arr instance and uses arr_drift_settings:
| Field | Purpose |
|---|---|
enabled |
Master switch for drift detection |
cron |
Cron expression for scheduled checks |
next_run_at |
Next scheduled run stored as UTC text |
The job queue uses dedupe key arr.drift:{instanceId} so each instance has at
most one scheduled drift job.
Current handler behavior:
- invalid instance ids fail
- missing instances fail
- missing or disabled settings cancel the job
- unsupported Arr types are skipped
- enabled jobs compare custom formats, quality profiles, delay profiles, media management media settings, media management naming, and media management quality definitions, store the latest result, and return success
- drift-detected and failed runs notify subscribed services when the current drift or error hash has not already been notified
- scheduled jobs calculate and store the next run before returning
Latest Status
Drift stores only the latest result per Arr instance in arr_drift_status.
Job run history remains the operational history.
| Field | Purpose |
|---|---|
status |
never_checked, clean, drift_detected, failed |
last_checked_at |
Last completed check time |
counts_json |
Count summary by drift section |
diff_json |
Structured latest drift result |
diff_hash |
Stable hash of the structured drift result |
last_notified_hash |
Last drift hash sent as a notification |
last_notified_at |
Last drift notification time |
last_error |
Latest failure detail |
error_hash |
Stable hash of the latest failure detail |
last_notified_error_hash |
Last failure hash sent as a notification |
Notifications
Drift emits notification events through the shared notification manager:
| Event | Trigger | Severity |
|---|---|---|
arr.drift.detected |
Latest drift hash has not been sent | warning |
arr.drift.failed |
Latest failure hash has not been sent | error |
Detected notifications emit one section block per drift category (Custom
Format, Quality Profile, Delay Profile, Media Management) listing up to 15
displayable drift entities total. If more entities exist, an additional More
block summarises the remainder as +N more. Discord renders each section as a
code-block field. Webhook receives the full payload. Summary-tier services such
as Ntfy and Telegram show only the title because section blocks are omitted.
Failed notifications use the first error line as the message and include the
full error text in an Error section.
Custom Formats
Custom format drift compares Profilarr-managed custom formats referenced by synced quality profile selections.
Comparison rules:
- build expected custom formats with the same transformer used by sync
- fetch actual custom formats from Arr
- match by custom format name
- ignore Arr ids
- compare
includeCustomFormatWhenRenaming - compare normalized specifications and fields
- ignore unmanaged extra Arr custom formats
Quality Profiles
Quality profile drift compares selected Profilarr-managed profiles by name. Expected profiles are built through the same quality profile transformer used by sync, with actual Arr custom format ids used to resolve scoring rows.
Comparison rules:
- report selected quality profiles missing from Arr
- ignore Arr profile ids
- compare
upgradeAllowed,cutoff,minFormatScore,cutoffFormatScore, andminUpgradeFormatScore - compare Radarr language; ignore language for Sonarr
- compare normalized quality item order, grouping, and allowed flags
- compare custom format scores for custom formats managed by that profile, including explicit zero scores
- report an expected managed custom format as missing from scoring if the custom format is missing from Arr
- ignore unmanaged custom format scoring rows with score 0
- report unmanaged custom format scoring rows with nonzero scores because they affect profile behavior
Delay Profiles
Delay profile drift compares the selected Profilarr delay profile against Arr's
default delay profile (id=1). This mirrors sync, which always overwrites the
default Arr profile instead of creating or matching by name.
Comparison rules:
- build expected delay profile with the same transformer used by sync
- no selected delay profile is clean
- report the selected delay profile as missing if Arr does not return
id=1 - ignore non-default extra Arr delay profiles
- compare id, derived protocol, delays, bypass fields, minimum custom format score, order, and tags
Media Management
Media management drift compares configured media settings, naming, and quality definition selections against Arr's media management, naming, and quality definition configs.
Comparison rules:
- build expected media settings, naming, and quality definitions with the same transformers used by sync
- no selected media settings, naming, or quality definitions config is clean
- missing selected PCD cache or config fails the drift check
- compare
downloadPropersAndRepacksandenableMediaInfo - compare Radarr naming fields: rename, illegal-character replacement, colon replacement, movie format, and movie folder format
- compare Sonarr naming fields: rename, illegal-character replacement, colon replacement, custom colon replacement, multi-episode style, episode formats, series folder format, and season folder format
- normalize Sonarr Arr enum integers back to semantic strings before comparing
- compare quality definition
minSize,maxSize, andpreferredSize - normalize PCD quality definition
0size values to Arrnullfor unlimited maximum and preferred sizes - ignore unmapped PCD quality definitions, mapped definitions missing in Arr, extra Arr quality definitions, and unmanaged Arr fields
Display Formatter
src/lib/server/drift/display.ts maps the raw diff_json stored in
arr_drift_status into a typed list of DriftDisplayEntity objects consumed
by the page. One entity is one drifted managed item (e.g. one custom format).
Each entity carries:
sectionandsectionLabel(e.g.custom_formats/Custom Format)stateandstateLabel(missing/modified/extra)tonefor badge color signalingsummary(one-line description, e.g.3 changes detected)changes[]: per-fieldDriftDisplayChangerows withlabel, optionaldetail, andexpected/actualDriftDisplayValues. Values carrytext, optionalmono, and optionaltone.
For custom formats the formatter resolves Arr enum ids back to friendly names (sources, resolutions, indexer flags, languages, release types, quality modifiers), formats sizes in human-readable bytes, and turns specification paths into labeled changes (negate / required / per-field changes / missing condition / extra condition).
For quality profiles the formatter turns settings, language, qualities, and custom format score paths into labeled changes. Quality profile score rows show expected and actual score values, missing custom formats, missing score rows, and unmanaged nonzero score rows.
For delay profiles the formatter turns the derived protocol, delays, bypass flags, minimum score, order, and tags into friendly field rows.
For media management the formatter turns media settings, naming, and quality definition changes into friendly field rows.
Display types live in src/lib/shared/drift.ts.
Arr Drift Page
Route: /arr/[id]/drift. Source: src/routes/arr/[id]/drift/+page.svelte
and +page.server.ts.
Layout:
- Sticky header with
Run Now(queues a manualarr.driftjob) andSave(persists the schedule and enabled state). - Settings bar (borderless, full-width, with a bottom rule):
Detectiontoggle,ScheduleCronInput, and timing pills aligned right (Paused/Ready/Next .../Last ...). - Entities section: an
ExpandableTablewithName/Entity/Statecolumns, mirroring the dev changes-page diff idiom. Each drifted entity is one row; expanding shows aDriftFieldDiffTablewithField/Profilarr/<Arr type> - <Arr name>columns rendering the entity'schanges[].
State rendering inside the entities section:
| Latest status | Rendered as |
|---|---|
| Detection disabled | Empty ExpandableTable chrome with a disabled message in the empty row |
never_checked |
Neutral message box |
clean |
Success message box |
failed |
Error message box with last_error |
drift_detected, displayable items |
Populated ExpandableTable |
drift_detected, no displayable items |
Amber message box (formatter produced nothing for the stored diff) |
The page is read-only for drift results. It never writes to Arr, repairs configuration, or triggers sync.
Sync Page Progress
Route: /arr/[id]/sync. Source: src/routes/arr/[id]/sync/+page.server.ts,
src/routes/arr/[id]/sync/components/QualityProfiles.svelte,
src/routes/arr/[id]/sync/components/DelayProfiles.svelte,
src/routes/arr/[id]/sync/components/MediaManagement.svelte.
Per-section drift progress is rendered as ProgressIndicator chips in each
sync section header (right-aligned on tablet+, stacked below the title on
mobile). The Quality Profiles header carries two chips: one for QPs themselves
and one for the custom formats referenced by those QPs. The Delay Profiles
header carries one chip. The Media Management header carries up to three chips
— one each for Naming, Quality Definitions, and Media Settings — corresponding
to the three sub-configs the user selects in that section.
Each chip shows current / total where:
total: managed items Profilarr would sync. Selected QP count for the QP chip, expected CF count frombuildExpectedCustomFormatsfor the CF chip,0or1for the delay profile chip and each of the three Media Management chips (Naming, Quality Definitions, Media Settings).current:total - drifted. For the QP chip,driftedcounts only QPs the drift comparison flagged directly. QPs that are only "transitively" affected (their scoring rows reference a CF that has been deleted from Arr) are not subtracted from the QP chip; the user-visible impact is surfaced via the CF chip's tooltip instead.
Chips render with colorMode="completion": green check when met, yellow
bar otherwise. Anything below 100% is yellow regardless of how close to
completion, because for drift "almost in sync" is still actionable.
Each chip has a tooltip listing up to three affected entity names with a
+N more suffix when the list overflows. Examples:
"HD Movies, 4K Movies drifted.""HD Movies affected by drifted custom formats.""Streaming Tier drifted, used in HD Movies, TV.""Standard Delay drifted."
Chips are hidden entirely when any of these conditions hold:
FEATURES.driftis off- per-instance
arr_drift_settings.enabledis false - drift status is
never_checkedorfailed(or no row exists) - the section's denominator is zero (e.g. no selected QPs)
Failures (status === 'failed') surface only on the dedicated drift page.
Post-sync drift refresh
The arr sync handler (src/lib/server/jobs/handlers/arrSync.ts) enqueues an
arr.drift job after a successful sync that touched Arr (any section ran).
This keeps the sync page chips fresh after the user fixes drift by syncing,
instead of waiting for the next scheduled drift run. The chain is gated on
FEATURES.drift and per-instance arr_drift_settings.enabled.
The sync page subscribes to job-finished events via
jobStatus.onJobFinished and calls invalidateAll() whenever an arr.drift
job completes. The raw hook is used (not the store's state machine) because
the state machine drops finished events for jobs it is not actively tracking,
including a drift job chained right after a sync's completion holdoff window.
SSE is opened on demand when the user triggers a sync and auto-closes after
the post-completion idle window, so this does not hold a persistent connection.