Skip to content

fix: OR rule sections incorrectly evaluated as AND due to operator coercion#2971

Merged
enoch85 merged 5 commits into
Maintainerr:developmentfrom
stormshaker:upstream/fix-or-section-operator
May 24, 2026
Merged

fix: OR rule sections incorrectly evaluated as AND due to operator coercion#2971
enoch85 merged 5 commits into
Maintainerr:developmentfrom
stormshaker:upstream/fix-or-section-operator

Conversation

@stormshaker

@stormshaker stormshaker commented May 24, 2026

Copy link
Copy Markdown
Contributor

Problem

Each rule section combines with the previous via the operator on the section's first rule. When that operator is left unset it is stored as null, and the comparator misread null as AND:

// rule.comparator.service.ts — choosing the section combine
sectionActionAnd = +parsedRule.operator === 0;

RuleOperators.AND === 0, and in JS +null === 0 is true, so an unset section operator was treated as AND. Items that matched only one section of such a rule were then dropped by the AND intersection.

Explicitly choosing AND or OR always worked (+'1' === 0 is false); only the unset default was affected. Two related gaps made that state reachable and sticky:

  • the rule editor allowed saving a section without choosing an operator, and
  • YAML export dropped a numeric AND (0) operator via a truthy check (rule.operator ? ...), losing it on round-trip.

Fix

Null-guard the coercion so an unset operator falls through to OR, consistent with how null is already treated within a section:

sectionActionAnd =
  parsedRule.operator != null &&
  +parsedRule.operator === RuleOperators.AND;

Operators are persisted as strings (e.g. "0"/"1"), so the + coercion is kept — a strict parsedRule.operator === RuleOperators.AND would never match the stored "0".

Also included

  • Rule editor — require an explicit operator on every non-first rule (the docs state it is required from the second rule/section onward).
  • Migration — backfill existing rules with an unset operator to the value they already evaluate as today, so no existing rule changes behaviour:
    // section boundary defaulted to AND, within-section defaulted to OR
    parsed.operator = isFirstRuleOfSection ? '0' : '1';
  • YAML export — use a null check instead of a truthy check so a numeric AND (0) operator is preserved:
    ...(rule.operator != null ? { operator: RuleOperators[+rule.operator] } : {}),

@stormshaker stormshaker force-pushed the upstream/fix-or-section-operator branch from a01ca5a to 4613c10 Compare May 24, 2026 05:22
@stormshaker stormshaker marked this pull request as ready for review May 24, 2026 05:25
@stormshaker stormshaker requested a review from enoch85 as a code owner May 24, 2026 05:25
@enoch85

enoch85 commented May 24, 2026

Copy link
Copy Markdown
Collaborator

Hey thanks for the PR.

Can you describe the reasoning behind this? Please also describe how you prompted and if you ever corrrected the AI.

Would also like to know if you run this yourself in your prod?

@SmolSoftBoi SmolSoftBoi added the bug Something isn't working label May 24, 2026
@enoch85

enoch85 commented May 24, 2026

Copy link
Copy Markdown
Collaborator

@stormshaker Looking at this now. The bug is real. Though this PR is incomplete.

I will fix it for you. Will commit soon. Once I've committed, please test this. Even if I tested it you are the one with the issue, so we need to confirm this for real instead of assuming anything - this is a behavioral change, and quite critical.

An unset section operator (null) was coerced to AND by `+null === 0`, so
sections meant to OR together were evaluated as AND. The coercion is now
null-guarded, resolving an unset operator to OR — consistent with how a
null operator is already treated within a section.

Also require an explicit operator on non-first rules in the rule editor,
backfill existing unset operators to OR, and stop dropping a numeric AND
section operator on YAML export.

Co-Authored-By: James Nobes <github@stormshaker.com>
@enoch85 enoch85 force-pushed the upstream/fix-or-section-operator branch from a520758 to a1afe2a Compare May 24, 2026 12:26
@enoch85

enoch85 commented May 24, 2026

Copy link
Copy Markdown
Collaborator

@stormshaker This is ready for testing on your end now. Please checkout the branch and run against a working server.

I asked AI to do some tests, and also write down some test sceneraios. What's left to test needs a real server, and manual testing. Here they are:

The latest commit (b5aacc5d, the regression fixes) adds YAML-decode backfill of omitted non-first operators and a server-side operator guard.

🔧 = rows added or changed in a follow-up verification pass (the last-commit re-tests and the mock-Jellyfin round-trip/evaluation). Unmarked rows are from the original write-up.

Tests performed

Test What was performed Result Passed
Comparator — unset operator Unit test: two sections, section-1 operator null, item matches only one section. Item is kept (OR union); section reported as OR. Yes
Comparator — explicit AND (string) Unit test: section operator stored as the string "0" (AND), item matches only one section. Item is excluded (AND intersection); proves the + coercion is retained so "0" is not misread as OR. Yes
Comparator — overlapping OR dedup Unit test: one item satisfies two overlapping sections. Item appears exactly once (no duplicate). Yes
Migration — backfill values In-memory SQLite: null operators across two groups/sections. First rule of a group stays null; section boundary -> "0" (AND); within-section -> "1" (OR); explicit operators untouched. Yes
Migration — field preservation In-memory SQLite: rewrite a null-operator rule. Only operator changes; other ruleJson fields unchanged. Yes
Migration — at real app boot Seeded a rule with a null section operator, started the app, inspected the DB. Migration ran at startup, normalised the null operator, recorded in the migrations table; no schema drift. Yes
YAML export (service) Unit test: encode a rule whose AND operator is the numeric 0. operator: AND is emitted (not dropped); survives encode -> decode round-trip. Yes
🔧 YAML decode — omitted non-first operator (b5aacc5d) Live API POST /api/rules/yaml/decode on the running app: legacy YAML with the non-first operators deleted (two sections, three rules). All three rules decode (none truncated); operators backfilled to null, OR, AND by the same positional heuristic as the migration. Yes
🔧 Server guard — null non-first operator (b5aacc5d) Live API PUT /api/rules: a non-first rule with operator: null, then the same payload with an explicit OR. Null is rejected with "Operator is required for every rule after the first" before any DB lookup; the explicit-OR payload clears the guard (fails later only because the throwaway id does not exist). Yes
UI guard (component) Unit test: non-first rule with first value + action but blank operator. Reported incomplete (not committed); commits once an operator is chosen. Yes
End-to-end (HTTP) rules-test-matrix.e2e.ts via /api/rules/test, media server mocked as live: null section operator, item matches only section 0. Returns true (OR union); the AND counterpart returns false (intersection). Yes
Manual 1 — within-section operator Browser: added a second rule in the same section, filled first value + action, left the within-section "Operator" blank. Rule stayed incomplete; selecting the operator completed it. Guard applies to all non-first rules. Yes
Manual 3 — reorder follows position Browser: dragged rule 2 above rule 1. New first rule lost its operator field; new second rule gained a required (blank) operator and re-flagged incomplete. Yes
Manual 4 — YAML export in UI Browser: built a two-rule section with AND, opened Export. YAML contained operator: AND. Yes
🔧 Manual 5 — YAML import, omitted operators (b5aacc5d) Browser: edited a seeded group, Import -> Upload YAML of a legacy two-section/three-rule document with the non-first operators removed. All 2 sections / 3 rules render (none dropped); operator dropdowns pre-filled OR (within-section) and AND (section boundary); no "incomplete rules" warning. This is the HIGH regression — confirmed fixed. Yes
🔧 Manual 2 — edit/save round-trip Browser, with a local mock Jellyfin stub answering the adapter's calls (connection, /Users, /Library/MediaFolders, item metadata): imported the omitted-operator YAML, clicked Save, reopened the rule. Saved and persisted; reopened with 2 sections / 3 rules, operators pre-selected OR + AND, no "incomplete" warning. DB rows store operator as null, "1", "0". Earlier "blocked by environment" note is now resolved via the stub. Yes
🔧 Live OR evaluation — legacy null operator With the mock stub: planted a group whose section-1 operator is null (the state the new save guard rejects) directly in the DB, then POST /api/rules/test for three movies whose mock rating drives the rule. Operator resolves to OR; rating 9 -> kept (both), rating 6 -> kept (matches only section 0), rating 2 -> dropped (neither). Confirms the fix over the real comparator+getter via HTTP, not just the mocked unit harness. Yes
🔧 Live AND evaluation — explicit AND With the mock stub: section-0 rating>5 AND section-1 rating>8, via /api/rules/test. rating 9 -> kept (both), rating 6 -> dropped (matches only section 0), rating 2 -> dropped. The exact mirror of the OR case — proves explicit AND still intersects and is not misread as OR. Yes
🔧 3-section chain, unset operator mid-chain With the mock stub: s0(>8) [AND] s1(>5) [operator omitted -> OR] s2(>5), via /api/rules/test. rating 6: (false AND true) OR true -> kept; the unset mid-chain operator resolves to OR and rescues the item the bug would have dropped. rating 2 -> dropped. Yes
🔧 Numeric-operator round-trip Encode rules with operators stored as the numbers 0/1, inspect the YAML, decode it back (live encode/decode API). YAML emits operator: AND and operator: OR (numeric 0 not dropped — the export fix); decode returns [null, 1, 0] unchanged. Yes
🔧 Migration — 600-row on-disk simulation Real migration class run against a real on-disk SQLite file seeded with 600 adversarial legacy rows across 80 groups (344 null, 48 operator-key-omitted, 104 string, 104 numeric; non-contiguous sections; rows inserted interleaved so id order != group order). Checked every row against an independent oracle, then re-ran and called down(). All 600 backfilled to their current effective behaviour (first-of-group stays null, section boundary -> AND, within-section -> OR); explicit string/number operators untouched; all other ruleJson fields preserved; no rows lost; second run is a no-op; down() changes nothing. Yes
🔧 Migration — partial/interrupted state Real migration class run against an on-disk DB where half the rows were already normalized (simulating a backfill that died mid-loop), then re-run. Re-run reaches the exact same state as a clean run; already-normalized rows are left untouched; no rows lost or double-applied. Yes
🔧 Full executor run (collection mutation) With the mock stub: set group to rating>5, triggered POST /api/rules/{id}/execute (the real Run Rules executor, not single-item test), polled to completion, inspected collection_media. Executor pulled the library, evaluated each item via the real comparator+getter, and reconciled the collection: rating 9 & 6 -> includedByRule=1, rating 2 -> includedByRule=0. Against the mock, not a real server; *arr side-effects not exercised. Yes
🔧 Post-migration editor load Browser: reopened a saved rule whose section operators are the backfilled '0'/'1' (the migration's output). Editor shows the section boundary as AND and within-section as OR, pre-selected, with no "incomplete rules" warning — a backfilled rule loads cleanly and re-saves without a false incomplete flag. Yes
🔧 Media-server switch — cross-server rule migration POST /api/settings/media-server/switch Jellyfin↔Emby round-trip (migrateRules=true), plus the Plex preview. Jellyfin→Emby migrated all 662 rules (390 media-server props app 6→7), 0 skipped; Emby→Jellyfin round-trips losslessly (back to 390 on app 6); *arr/Seerr/Tautulli rules left untouched; Jellyfin→Plex preview drops 16 Jellyfin-only props (favoritedBy) with reasons. Operators are carried through unchanged, so the fix's data survives a server switch. Yes
🔧 Reorder persistence (rules + sections) Browser: reordered rules within a section and whole sections; plus a chaos pass — 3 sections × 3 rules (9 distinct values) shuffled with 7 interleaved rule/section moves. Saved, reopened, inspected the DB. Order, section grouping and operators persist exactly (editor == DB == reload); no rules lost; sections re-index contiguously. The null operator is always forced onto the top rule on save; dragging the existing top rule down correctly flags it incomplete until an operator is chosen, so there is never a non-top null. Yes
🔧 Full unit suites @maintainerr/server and @maintainerr/ui test runs (re-run this session). Server 1363/1363, UI 213/213. (1363, up from 1358 — the last commit added specs.) Yes
🔧 Static checks check-types, lint, format:check (re-run this session). All clean. Yes

What is left to test

Critical gaps, mostly things that require a reachable media server or a real
upgraded database — neither of which exists in this dev instance.

Area What to test Why it matters / risk Priority
🔧 Real rule execution on a real server Repeat the OR/AND/3-section and executor checks against a live Jellyfin/Plex/Emby with real library data (incl. real *arr side-effects). OR + AND + 3-section + the collection-mutating executor are all now verified against a local mock stub, but never against real library data or real *arr actions. High
🔧 Migration on your real upgrade DB Run the upgrade against your actual production DB. A 600-row adversarial on-disk simulation (mixed string/number/omitted operators, non-contiguous sections, interleaved ids) passed against an independent oracle, so the logic is covered; only your literal production data remains unconfirmed. Medium
*arr side-effects in Run Rules A real Radarr/Sonarr so the executor's delete/unmonitor/exclusion actions actually fire. The executor's collection add/remove was verified against the mock, but the *arr actions were only accepted by the stub, not really performed. Medium
🔧 Live OR/AND evaluation on real Plex/Emby Run the actual OR/AND rule evaluation against a live Plex and Emby (not only Jellyfin). Cross-server rule migration (incl. operators) is now verified via the switch test above; the comparator is server-agnostic. What remains is confirming each server's getter resolves values correctly — needs a real Plex/Emby. Low

@enoch85

enoch85 commented May 24, 2026

Copy link
Copy Markdown
Collaborator

The last push was regression fixes.

@enoch85

enoch85 commented May 24, 2026

Copy link
Copy Markdown
Collaborator

Did a few more tests. Updated the comment above. 👍

@enoch85

enoch85 commented May 24, 2026

Copy link
Copy Markdown
Collaborator

@stormshaker Testing rules now. :)

image

@enoch85

enoch85 commented May 24, 2026

Copy link
Copy Markdown
Collaborator

@stormshaker I've been on this for 10 hours straight now. I've tested what I can in my dev environment (codespace) but not yet on "a real" server.

Merging this to development for easier testing. Please test this thoroughly yourself. Write to me in this PR. Thanks! 🙏

@enoch85 enoch85 merged commit 518b999 into Maintainerr:development May 24, 2026
11 checks passed
@stormshaker

Copy link
Copy Markdown
Contributor Author

WIll do... you've been busy! I'm in Australia, so we're offset on time zones. But I'm off to work now, will take a look tonight. Thanks!

enoch85 added a commit that referenced this pull request May 25, 2026
…t notes

How to exercise both import paths (YAML encode/decode vs community-rule
/migrate), why they don't share code, and quick checks for the #2971/#2976
behaviours and v2.0.0+ community-rule import.
enoch85 added a commit that referenced this pull request May 25, 2026
migrateImportedRuleDtos remapped apps and reasserted section-boundary operators
but left within-section unset operators as null. A pre-explicit-operator
community rule (e.g. one authored before #2971) could therefore carry a null
operator on a non-first rule, which the new "operator is required for every rule
after the first" save validation rejects on import. Backfill those to OR after
the boundary pass — the same default the comparator and the
NormalizeRuleSectionOperators migration apply.
@stormshaker

Copy link
Copy Markdown
Contributor Author

I'm still testing this, but @enoch85, I thought I'd give you an update. Migration looks good, but I think we need to target OR for the existing rule migration to replicate how they worked before the bugfix, rather than AND. The first run after the migration (using AND) resulted in a 14-item collection -> 0. Changing the section operator to OR resulted in the same 14 items as before migration. That was on a Movies rule, I'll continue with TV Series.

enoch85 added a commit that referenced this pull request May 25, 2026
…ort operator backfill) (#2986)

* fix(rules): resolve TEXT_LIST custom value on YAML import

The encoder serialises a custom value's type as the RuleType humanName, so
TEXT_LIST becomes "text list" (with a space). The decoder's switch only matched
'TEXT_LIST', so the spaced key fell through, left ruleType undefined, and threw
on the return's .toString() — failing the entire YAML import. Normalise spaces
to underscores before matching.

* style: run yarn format on rules constants spec

Agent-Logs-Url: https://github.com/Maintainerr/Maintainerr/sessions/38e8bea3-daf7-4de0-aeac-a61c2d3e5fa2

Co-authored-by: enoch85 <4511254+enoch85@users.noreply.github.com>

* fix(rules): backfill unset within-section operator on community import

migrateImportedRuleDtos remapped apps and reasserted section-boundary operators
but left within-section unset operators as null. A pre-explicit-operator
community rule (e.g. one authored before #2971) could therefore carry a null
operator on a non-first rule, which the new "operator is required for every rule
after the first" save validation rejects on import. Backfill those to OR after
the boundary pass — the same default the comparator and the
NormalizeRuleSectionOperators migration apply.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@enoch85

enoch85 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the update. Please test on latest development build. Pushed some more fixes.

@enoch85

enoch85 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Here's something for your AI after some investigation;

image image

@enoch85

enoch85 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

@stormshaker Please also ask your AI to read the latest release review documents. They explain how to test.

Note; you always need a clean untouched DB on 3.12.1 before you upgrade to the development container. Else you won't get true/real results.

@stormshaker

Copy link
Copy Markdown
Contributor Author

Thanks for investigating the 14→0 case. Here's the raw pre-migration ruleJson as requested — extracted from a CA Backup taken 2026-05-23 04:48 UTC, before any upgrade.

Rule 11 (Movies Leaving Soon) — raw rows:

id section operator (raw) description
439 0 null viewCount EQUALS 0 — first rule of group
440 0 0 (numeric) addDate BEFORE 60d
441 1 null viewCount BIGGER 0 — section boundary
442 1 0 (numeric) lastViewedAt BEFORE 180d
443 1 0 (numeric) imdb SMALLER 7.5
444 1 0 (numeric) rating_user SMALLER 8

Rule 12 (TV Leaving Soon) — raw rows:

id section operator (raw) description
453 0 null sw_amountOfViews EQUALS 0 — first rule of group
454 0 "0" (string) addDate BEFORE 45d
455 0 "0" (string) Sonarr.monitored EQUALS false
456 1 null sw_amountOfViews BIGGER 0 — section boundary
457 1 "0" (string) sw_lastWatched BEFORE 3y
458 1 "0" (string) imdb SMALLER 6.8
459 1 "0" (string) rating_user SMALLER 8
460 1 "0" (string) Sonarr.monitored EQUALS false

This confirms your analysis exactly: rows 441 and 456 have operator: null at the section boundary. Under 3.12.1's +null === 0 → AND evaluation, both rules were ANDing mutually exclusive sections (viewCount=0 AND viewCount>0), so they were producing 0 new matches on 3.12.1. The 14 collection items were residual from an earlier rule configuration.

Also worth noting: Rule 12 mixes storage formats — some operators are stored as string "0", others as numeric 0. The + coercion handles both identically.

I'm now running a clean test using the May 23 backup DB (pre-migration) against the latest development image. Will report back.

@enoch85

enoch85 commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Great!

@enoch85

enoch85 commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Suggested release note:

Rule section operators: an unset section operator now defaults to OR (previously AND). Existing rules are automatically migrated to preserve their current behavior — your collections won't change semantics. The first rule run after upgrading will reconcile collections (removing items that no longer match). This migration is not reversible.

@stormshaker

Copy link
Copy Markdown
Contributor Author

Clean DB test results

Tested against a pristine pre-migration DB (CA Backup from 2026-05-23 04:48 UTC, before any upgrade), running the latest development image.

Migration verification

Migration NormalizeRuleSectionOperators1779622081794 ran at startup. Confirmed in the migrations table. The two section-boundary rows backfilled correctly:

Rule Row id Pre-migration operator Post-migration operator
11 (Movies Leaving Soon) 441 null "0" (AND)
12 (TV Leaving Soon) 456 null "0" (AND)

All other fields in ruleJson unchanged. Migration is correct and lossless.

Rule 11 execution (Movies Leaving Soon)

  • Active items after run: 0
  • Cumulative mediaCount: 0
  • Duration: ~66 minutes (full library scan)

Result: 0 matches, as expected. With section-boundary operator now explicitly AND, the mutually exclusive sections (section 0: viewCount = 0, section 1: viewCount > 0) can never both be satisfied simultaneously. This is consistent with 3.12.1 behaviour — the rule was already producing 0 new matches before the upgrade. The 14 items we reported earlier were stale residuals from a prior rule configuration, not actively matched items.

Conclusion

The migration correctly preserves 3.12.1's effective behaviour. The 14→0 observation from our earlier testing was not caused by the migration — it simply made explicit what was already true: these rules had broken section logic that was always AND under 3.12.1.

To get the rules working as intended (OR between sections), users need to manually set the section operator to OR in the rule editor — which the new UI now makes possible.


Test environment: Plex + Radarr + Sonarr on real production library data. DB restored from CA Backup taken the morning of 2026-05-23, before any upstream-dev container was run.

@stormshaker

Copy link
Copy Markdown
Contributor Author

Thanks for writing that up — the structure is clear and the migration explanation is accurate. One small suggestion before it ships:

The final sentence "The first rule run after upgrading will reconcile collections (removing items that no longer match)" may cause unnecessary alarm — since the migration preserves 3.12.1 semantics exactly, the first run should produce identical results to before the upgrade. Our clean-DB test confirmed this: 0 new items were added or removed on the first run post-migration. It might be worth removing that sentence, or it could be read as implying the migration changes behaviour when it doesn't.

Also two things that might be worth adding, since they're user-facing:

  • The rule editor now requires an explicit operator on every non-first condition — users editing existing rules will notice new required fields
  • Users who had multi-section rules that weren't matching as expected (silently ANDed) can now open the rule editor and correct the section operator to OR

Here's a suggested rewrite incorporating those points, feel free to adjust the tone:

Rule section operators

Fixed a bug where an unset section operator was incorrectly treated as AND. Sections now correctly default to OR when no operator is set, consistent with their intended purpose.

Existing rules are automatically migrated to preserve their current behaviour — section-boundary operators are made explicit rather than inferred, so your rules will evaluate identically after upgrading.

If you have multi-section rules that weren't matching as expected, this is likely why. You can now open the rule editor and set the section operator explicitly to OR — the field is now visible between sections where it was previously hidden.

New rules now require an explicit operator on every non-first condition. The rule editor enforces this before saving.

This migration is not reversible.

Happy to leave it entirely with you if you'd prefer to word it differently — just flagging the contradiction before it goes out.

@enoch85

enoch85 commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Thanks! So testing proves everything is OK?

Did you run the PR-tagged container to see the exact logs?

@enoch85

enoch85 commented May 26, 2026

Copy link
Copy Markdown
Collaborator

@stormshaker Also, what is your opinion on this? Does it work as you would expect?

@stormshaker

Copy link
Copy Markdown
Contributor Author

I would like to test it some more. Without starting from a blank database... I'd like to take an existing database with null values in the rules, with collections that have mediaCount > 0, and upgrade the container just as a user would. Then, I'd like to see the rules run, and my mediaCount remain the same. No impact of the upgrade - have it all seamless. The only change from a user's perspective should be that the section operator dropdowns now show the operator, rather than being empty.

So in my opinion, I'd like more testing. For a few more days, it's worth it for more assurance.

@enoch85

enoch85 commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Good call! I'll be waiting for your results. The logging PR I made will only log now for testing sake. Then I'll remove it or keep the regression tests.

But, you can use it for testing to get logged results. That's easier for you. 👍🏼

@enoch85

enoch85 commented May 26, 2026

Copy link
Copy Markdown
Collaborator

I also tested this once more, based on clean DB and the community rules. Here are the result:

Method: clean 3.12.1 DB (verified: migration not in migrations table) → import all community rules with their original null operators → run every rule → upgrade to development (migration backfills operators at boot) → run every rule again → diff outputs.

Test slice Count Before (3.12.1) After (dev + migration) Same?
Community rule groups imported 167 (890 rules)
Comparisons (groups × 3 mock items) 501 501 outputs 501 outputs ✅ 0 diffs
Multi-section groups (operator applies) 118 identical identical
AND/OR-sensitive groups (sections disagree) 47 identical identical
Per-section results 501 identical identical
Comparisons that matched (kept = true) 43 43
Non-first rules still null after migration n/a 0

Conclusion: every output is identical before vs after — including the 47 rules where AND-vs-OR is outcome-determining. The migration makes section operators explicit (null"0"/"1"); it does not change how any rule evaluates.

@stormshaker

Copy link
Copy Markdown
Contributor Author

OK, that's a solid test. I'm still setting mine up, but I have only a few rules in my prod - nothing like the community rules. I'll let you know when I'm done but those results above are very promising :)

@stormshaker

Copy link
Copy Markdown
Contributor Author

Hi @enoch85, I've completed a full pass pre to post upgrade and verified that the counts look good. One small discrepancy when I captured the pre-upgrade 3.12.1 Rule 11 number. Overall, I'd call that a pass on my large library.


Test environment: Claude Code (AI assistant) ran the API calls and monitoring; results reviewed and verified by me throughout.

Setup

Tested Rules 11 (Movies Leaving Soon) and 12 (TV Leaving Soon) — both multi-section rules, each with a section 0 and a section 1. The null section-boundary operators were left as-is (not manually patched) to let the migration do its work naturally.

3.12.1 baseline (pre-upgrade)

Rule DB collection_media Plex collection childCount
11 — Movies Leaving Soon 439 448
12 — TV Leaving Soon 157

The 9-item gap between Rule 11's DB count and Plex collection count is write-lag: the Plex collection sync had finished at 448 but the DB batch write was still in progress when we captured the snapshot.

Dev image (post-upgrade)

Rule DB collection_media Plex collection childCount
11 — Movies Leaving Soon 448 448
12 — TV Leaving Soon 157

Rule 11 DB and Plex collection are now in sync at 448. Rule 12 unchanged at 157. ✅

NormalizeRuleSectionOperators migration

On first startup the migration ran and converted the null operators at section boundaries to explicit 0 for both rules. This is the correct backward-compat behaviour: existing rules keep their current AND semantics; new rules created after the upgrade will have null at section boundaries and be correctly OR'd by the fixed comparator.

Unrelated finding during testing

Rule 12 logs show repeated PlexGetterService - Action failed … Cannot read properties of undefined (reading 'viewedAt') errors for TV shows. This is not related to this PR — it's caused by missing Plex watch-history data on my server (the Plex database was recently restored from a snapshot). Flagging it in case it's useful context, but it didn't affect the collection counts.

Verdict: Pass on a large library (~2,800 movies, ~1,000 TV shows). Migration runs cleanly, counts are consistent pre/post upgrade.

@enoch85

enoch85 commented May 27, 2026

Copy link
Copy Markdown
Collaborator

Perfect!

This goes into main. 🎉

maintainerr-automation Bot added a commit that referenced this pull request May 27, 2026
* feat(ui): migrate to Tailwind CSS v4 (CSS-first) + dev DB seed

- CSS-first theme: @theme + @plugin in globals.css; remove tailwind.config.js
- Use @tailwindcss/vite; drop postcss, autoprefixer, @tailwindcss/aspect-ratio
- Fix v4 regressions: @tailwindcss/forms .form-input collision (strategy: base),
  dead bg-opacity-* -> slash syntax, checkbox focus ring + single .checkbox class,
  translucent backgrounds, unlayered prose colors
- Standardize Plex checkboxes, Manual badge, and input-action button states;
  delay the overlay-schedule loading hint
- Add dev/seed-db.mjs to seed collections/rules/storage for local testing

* feat(ui): adopt Tailwind v4.x features

- color-scheme: dark so native controls render dark
- container query for the media-card grid (sizes to container, not viewport)
- field-sizing-content on the rule description and overlay text textareas
- text-shadow on poster-overlaid title/year/summary
- drop the v3->v4 border-color compat shim (one implicit border made explicit)

Closes #2970

* refactor(ui): single source for form field styling

- Move the field base style into the global input/select/textarea rule so it
  is the single source of truth; Forms/Input and Forms/Select keep deltas only
  (adornments, join, error, select chevron). One edit now restyles every field.
- SearchBar uses the shared field style (drops the pill override; fixes the
  clipped icon and the blue focus ring).
- Include input[type=search] in the shared rule; maintainerr focus app-wide.
- Keep the v4 field-sizing auto-grow on the rule description + overlay text,
  with min-h so the textareas no longer collapse when empty.
- Uniform rule-form row spacing; blank the log filter's default option (was '-').

* Document Codex MCP config and media integration coverage (#2974)

* perf: cache hygiene for external API and metadata responses (#2972)

- ExternalApiService: skip caching Buffer/null/non-object responses, so binary
  blobs and empty bodies can't poison the shared NodeCaches.
- cache: exempt tmdb/tvdb (immutable external metadata, 6h TTL) from the per-run
  flushAll(), so rule runs don't needlessly re-fetch them.
- tmdb metadata provider: ignore non-object records.

* fix(release): emit changelog version header and correct breaking-change classification

- Pass NEXT_RELEASE_VERSION/LAST_RELEASE_GITTAG/NEXT_RELEASE_GITHEAD into
  the notes generator via generateNotesCmd. semantic-release does not export
  these to plugin commands, so buildHeader() always saw an empty version and
  emitted no '# [x.y.z](compare) (date)' header for v3.11.2..v3.12.1.
- Tell the model to net out the commit range: an identifier introduced and
  renamed/removed within one release never shipped, so it is not breaking.
- Backfill the missing v3.11.2/v3.12.0/v3.12.1 headers and drop the false
  'Breaking Changes' (an internal settings-service rename) from v3.12.1.

* fix: OR rule sections incorrectly evaluated as AND due to operator coercion (#2971)

* fix: treat unset rule section operator as OR instead of AND

An unset section operator (null) was coerced to AND by `+null === 0`, so
sections meant to OR together were evaluated as AND. The coercion is now
null-guarded, resolving an unset operator to OR — consistent with how a
null operator is already treated within a section.

Also require an explicit operator on non-first rules in the rule editor,
backfill existing unset operators to OR, and stop dropping a numeric AND
section operator on YAML export.

Co-Authored-By: James Nobes <github@stormshaker.com>

* fix(rules): normalize legacy section operators

---------

Co-authored-by: enoch85 <mailto@danielhansson.nu>
Co-authored-by: Kristian Matthews-Kennington <kristian@matthews-kennington.com>

* fix(rules): robust community/YAML rule import across media servers (#2976)

Imports could silently break when a rule's properties don't all map to
the configured media server:

- Migrate firstVal and lastVal independently by each field's own app, so a
  media-server property compared against a Seerr/Tautulli value is no longer
  stranded on the source server (Plex/Emby) and rendered blank. Covers
  Plex/Jellyfin/Emby.
- Surface rules dropped on import (no equivalent on the target server) via a
  toast on both the community and YAML paths; keep the warn log.
- YAML export no longer writes `App.undefined` for an unknown property, and
  import skips an unresolved rule (reported as a skipped count) instead of
  rejecting the whole document.
- validateRule returns a clean status instead of throwing on a missing
  application/property; rule-group create no longer fails when notifications
  is omitted.

* chore(dev): add offline mock Jellyfin server

Dev-only HTTP stub answering the Jellyfin endpoints the server's adapter
calls (connection, users, libraries, item metadata, item images). Pairs
with dev/seed-db.mjs so the editor library list, rule-group save,
collection grids and rule evaluation work without a real Jellyfin.
Invented data only; not referenced by application code.

* feat(rules): add Plex "Amount of episodes marked as watched" rule (#2975)

Adds a show/season rule property (sw_markedWatchedEpisodes) backed by
Plex's viewedLeafCount (watched state) instead of play history.

Unlike "Amount of watched episodes" (sw_viewedEpisodes), which counts
episodes that have a play-history entry, this counts episodes Plex
considers watched -- including those manually marked as watched, since
Plex sets viewedLeafCount for manual marks but writes no play history.

* Refactor shared media getter rule helpers (#2922)


Co-authored-by: enoch85 <mailto@danielhansson.nu>

* feat(rules): add Streamystats watchlist rule properties for Jellyfin (#2977)

Adds a Jellyfin-only Streamystats rule Application (the Jellyfin analog of
Tautulli) with two watchlist-backed properties: "Is in a watchlist" and
"[list] In watchlist of (username)". Upstream Streamystats now accepts
Jellyfin API-key auth on its watchlist endpoints, so Maintainerr can reach
them via the MediaBrowser token scheme.

Only public Streamystats watchlists are visible to the API key. The
membership snapshot is cached in the shared Streamystats NodeCache (flushed
between rule-group runs). The Application is removed from the rule constants
unless Streamystats is configured, and the UI hides it on non-Jellyfin
servers. Username resolution failures surface as a transient skip rather
than an empty list.

* fix: Test Media search styling (Tailwind v4) + unary-rule comparison crash (#2978)

* docs: unify agent instructions into a single index, add handoff notes (#2980)

- AGENTS.md is the single documentation index; the Claude, Copilot, Cursor,
  and Codex entrypoints route to it and name the standing rules directly so
  they can't be missed.
- Split standing rules (read every session) from task-specific docs (read on
  demand); scope release-review's Copilot applyTo to release artifacts so it
  no longer loads on every interaction.
- Add project-notes.instructions.md (non-obvious project knowledge and
  conventions for handoff) and README_AGENTS.md (the wiring map).
- Move dev mocks + DB seed to tools/dev/ (fix seed-db repo-root resolution);
  add the seeded-DB + Playwright step to the release-review checklist.

* docs: strengthen migration rule and fix release-review SQL guidance

- implementation rule 8: demand TypeORM + typeorm_instructions.txt workflow,
  require migrations be tested end-to-end before considered working
- release-review: fix broken typeorm_instructions.txt path (root, not
  .github/instructions/); clarify that generated schema migrations may contain
  raw DDL while hand-written data migrations must use QueryBuilder

* docs: add YAML import/export + community-rule testing guide to project notes

How to exercise both import paths (YAML encode/decode vs community-rule
/migrate), why they don't share code, and quick checks for the #2971/#2976
behaviours and v2.0.0+ community-rule import.

* build(deps): bump the nestjs group with 4 updates (#2981)

Bumps the nestjs group with 4 updates: [@nestjs/common](https://github.com/nestjs/nest/tree/HEAD/packages/common), [@nestjs/core](https://github.com/nestjs/nest/tree/HEAD/packages/core), [@nestjs/platform-express](https://github.com/nestjs/nest/tree/HEAD/packages/platform-express) and [@nestjs/testing](https://github.com/nestjs/nest/tree/HEAD/packages/testing).


Updates `@nestjs/common` from 11.1.23 to 11.1.24
- [Release notes](https://github.com/nestjs/nest/releases)
- [Commits](https://github.com/nestjs/nest/commits/v11.1.24/packages/common)

Updates `@nestjs/core` from 11.1.23 to 11.1.24
- [Release notes](https://github.com/nestjs/nest/releases)
- [Commits](https://github.com/nestjs/nest/commits/v11.1.24/packages/core)

Updates `@nestjs/platform-express` from 11.1.23 to 11.1.24
- [Release notes](https://github.com/nestjs/nest/releases)
- [Commits](https://github.com/nestjs/nest/commits/v11.1.24/packages/platform-express)

Updates `@nestjs/testing` from 11.1.23 to 11.1.24
- [Release notes](https://github.com/nestjs/nest/releases)
- [Commits](https://github.com/nestjs/nest/commits/v11.1.24/packages/testing)

---
updated-dependencies:
- dependency-name: "@nestjs/common"
  dependency-version: 11.1.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nestjs
- dependency-name: "@nestjs/core"
  dependency-version: 11.1.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nestjs
- dependency-name: "@nestjs/platform-express"
  dependency-version: 11.1.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nestjs
- dependency-name: "@nestjs/testing"
  dependency-version: 11.1.24
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: nestjs
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump date-fns from 4.2.1 to 4.3.0 (#2982)

Bumps [date-fns](https://github.com/date-fns/date-fns) from 4.2.1 to 4.3.0.
- [Release notes](https://github.com/date-fns/date-fns/releases)
- [Commits](date-fns/date-fns@v4.2.1...v4.3.0)

---
updated-dependencies:
- dependency-name: date-fns
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump typescript-eslint from 8.59.4 to 8.60.0 (#2984)

Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.59.4 to 8.60.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.60.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.60.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump @swc/core from 1.15.33 to 1.15.40 (#2985)

Bumps [@swc/core](https://github.com/swc-project/swc/tree/HEAD/packages/core) from 1.15.33 to 1.15.40.
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/commits/v1.15.40/packages/core)

---
updated-dependencies:
- dependency-name: "@swc/core"
  dependency-version: 1.15.40
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump knip from 6.14.1 to 6.14.2 (#2987)

Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) from 6.14.1 to 6.14.2.
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.14.2/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-version: 6.14.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(rules): harden rule import (TEXT_LIST YAML decode + community-import operator backfill) (#2986)

* fix(rules): resolve TEXT_LIST custom value on YAML import

The encoder serialises a custom value's type as the RuleType humanName, so
TEXT_LIST becomes "text list" (with a space). The decoder's switch only matched
'TEXT_LIST', so the spaced key fell through, left ruleType undefined, and threw
on the return's .toString() — failing the entire YAML import. Normalise spaces
to underscores before matching.

* style: run yarn format on rules constants spec

Agent-Logs-Url: https://github.com/Maintainerr/Maintainerr/sessions/38e8bea3-daf7-4de0-aeac-a61c2d3e5fa2

Co-authored-by: enoch85 <4511254+enoch85@users.noreply.github.com>

* fix(rules): backfill unset within-section operator on community import

migrateImportedRuleDtos remapped apps and reasserted section-boundary operators
but left within-section unset operators as null. A pre-explicit-operator
community rule (e.g. one authored before #2971) could therefore carry a null
operator on a non-first rule, which the new "operator is required for every rule
after the first" save validation rejects on import. Backfill those to OR after
the boundary pass — the same default the comparator and the
NormalizeRuleSectionOperators migration apply.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>

* build(deps): bump typeorm from 0.3.29 to 1.0.0 (#2983)

* build(deps): bump typeorm from 0.3.29 to 1.0.0

Bumps [typeorm](https://github.com/typeorm/typeorm) from 0.3.29 to 1.0.0.
- [Release notes](https://github.com/typeorm/typeorm/releases)
- [Changelog](https://github.com/typeorm/typeorm/blob/master/CHANGELOG.md)
- [Commits](typeorm/typeorm@0.3.29...1.0.0)

---
updated-dependencies:
- dependency-name: typeorm
  dependency-version: 1.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(server): use object-form find relations for typeorm 1.0.0

TypeORM 1.0.0 removed string-based relations from find methods (#12215).
Convert the six affected findOne/find calls to the object form, which is
valid in both 0.3.x and 1.0.0.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: enoch85 <mailto@danielhansson.nu>

* build(deps-dev): bump @tanstack/eslint-plugin-query (#2992)

Bumps [@tanstack/eslint-plugin-query](https://github.com/TanStack/query/tree/HEAD/packages/eslint-plugin-query) from 5.100.11 to 5.100.14.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/eslint-plugin-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/eslint-plugin-query@5.100.14/packages/eslint-plugin-query)

---
updated-dependencies:
- dependency-name: "@tanstack/eslint-plugin-query"
  dependency-version: 5.100.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump react-hook-form from 7.76.0 to 7.76.1 (#2993)

Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.76.0 to 7.76.1.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](react-hook-form/react-hook-form@v7.76.0...v7.76.1)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.76.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump nodemailer from 8.0.8 to 8.0.9 (#2994)

Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 8.0.8 to 8.0.9.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](nodemailer/nodemailer@v8.0.8...v8.0.9)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 8.0.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump @tanstack/react-query from 5.100.11 to 5.100.14 (#2995)

Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.100.11 to 5.100.14.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.100.14/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.100.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(notifications): omit empty Discord embed thumbnail

Discord rejects webhook payloads with 400 Invalid Form Body when an
embed contains an empty thumbnail object. Imageless notifications (e.g.
media-removed / rule-handling events) always sent thumbnail.url=undefined,
which serialized to an empty thumbnail and silently failed. Only attach
the thumbnail when an image is present.

* build(deps-dev): bump turbo from 2.9.14 to 2.9.15 (#2997)

Bumps [turbo](https://github.com/vercel/turborepo) from 2.9.14 to 2.9.15.
- [Release notes](https://github.com/vercel/turborepo/releases)
- [Changelog](https://github.com/vercel/turborepo/blob/main/RELEASE.md)
- [Commits](vercel/turborepo@v2.9.14...v2.9.15)

---
updated-dependencies:
- dependency-name: turbo
  dependency-version: 2.9.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump @typescript-eslint/parser from 8.59.4 to 8.60.0 (#2998)

Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.59.4 to 8.60.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.60.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.60.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump rolldown from 1.0.2 to 1.0.3 (#2999)

Bumps [rolldown](https://github.com/rolldown/rolldown/tree/HEAD/packages/rolldown) from 1.0.2 to 1.0.3.
- [Release notes](https://github.com/rolldown/rolldown/releases)
- [Changelog](https://github.com/rolldown/rolldown/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rolldown/rolldown/commits/v1.0.3/packages/rolldown)

---
updated-dependencies:
- dependency-name: rolldown
  dependency-version: 1.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump @typescript-eslint/eslint-plugin (#3000)

Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 8.59.4 to 8.60.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.60.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.60.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(collections): create collections empty, then batch-add (avoid HTTP 414) (#3001)

Creating a new collection from a large rule seeded every item id into the
create request's query string (BULK_COLLECTION_CREATE, #2911), overflowing
the URL and failing with "Failed to create collection" / HTTP 414 URI Too
Long. Item ids can only travel in the query string on Plex/Jellyfin/Emby
(no request body), so the seed is unbounded.

Revert to the pre-#2911 / 2.x behaviour: create the collection empty and
populate it via the existing batched add path (addBatchToCollection), for
all media servers. Removes the now-unused BULK_COLLECTION_CREATE capability
and the initialItemIds plumbing.

* fix(rules): scope exclusions to their rule group under TypeORM 1.0 (#2991)

* fix(rules): scope exclusions to their rule group under TypeORM 1.0

Adopt TypeORM 1.0 where-clause null semantics (default throw; IsNull() to
match NULL) instead of 0.3.x's silent-ignore footgun, fixing latent exclusion
bugs surfaced by the typeorm 1.0.0 bump (#2983):

- getExclusions: a group-scoped exclusion no longer leaks into other rule
  groups (and no longer returns duplicates); global exclusions still apply
  everywhere.
- setExclusion: global exclusions subsume scoped ones — an item is global or
  scoped, never both.
- removeExclusion: removing a global exclusion no longer writes a spurious
  collection-log entry; logs media + ownership.
- getCollection: a null/blank id returns undefined instead of an arbitrary
  first collection.

Bumps engines.node to TypeORM 1.0's floor and adds getter-property and
exclusion-scoping regression coverage.

* feat(ui): warn before a global exclusion replaces rule-group exclusions

When adding a global exclusion for an item that already has rule-group
exclusions, show a confirmation listing each "<item> — <rule group>" (the
group links to its collection, reusing the backdrop's maintainerr-status data
so labels/links match and stay fresh) before proceeding, since going global
removes those scoped exclusions. Only triggers on the Add action, never on
Remove.

* test(ui): cover the global-exclusion warning in AddModal

* fix(ui): clarify Add/Remove action labels in the media modal

* fix(ui): don't block global exclusion when warning prefetch fails

* fix(plex): don't drop auth when plex.tv is unreachable (#2996)

* fix(plex): don't drop auth when plex.tv is unreachable

A plex.tv timeout was reported as an invalid token, so the Plex settings
page declared stored credentials invalid and demanded re-authentication
even when the saved token was fine. Distinguish a genuine rejection
(401/403) from a connectivity failure: on unreachable, keep the account
authenticated, warn, and auto-retry until plex.tv responds.

* fix(ui): clear stale Plex auth warning

* fix(ui): navigate client-side from global-exclusion warning links

The rule-group links in the AddModal global-exclusion warning used a raw
<a href>, which forced a full page reload and ignored the router basename
(404 under BASE_PATH subpath installs). Use useNavigate instead, and clear
the maintainerr-status cache + invalidate collection queries first so the
destination still fetches fresh — what the full reload did implicitly.

* test(server): add 1.x upgrade-path and all-rules comparator regression coverage

- upgrade-from-1x.spec.ts: reconstructs the v1.7.1 schema, seeds 1.x-era
  data (rules with null operators), applies every post-1.7.1 migration and
  asserts the operator backfill + late-migration schema survive.
- rules-test-matrix.e2e.ts: generated matrix covering every RuleType x
  RulePossibility (value/missing/present/absent), for cross-version diffing.

* docs: correct Yarn linker note in project-notes (node-modules, not PnP)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: maintainerr-automation[bot] <261505141+maintainerr-automation[bot]@users.noreply.github.com>
Co-authored-by: enoch85 <mailto@danielhansson.nu>
Co-authored-by: Kristian Matthews-Kennington <kristian@matthews-kennington.com>
Co-authored-by: James Nobes <github@stormshaker.com>
Co-authored-by: CampbellMG <40409896+CampbellMG@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@maintainerr-automation

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.13.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants