v1.3.0 — Collections, Rules Engine v2, API Key Auth#17
Merged
helliott20 merged 29 commits intomainfrom Apr 6, 2026
Merged
Conversation
Adds schema, repository, service, and routes to sync Plex users (owner + shared friends/home users) into the local DB so the rules engine can target per-user conditions. - Migration v14: plex_users table with username index - Repository: findAll, findById, findByUsername, upsert, replace (transactional full-sync that prunes stale rows) - Service: fetches owner from plex.tv/users/account.xml and friends from plex.tv/api/users in parallel; falls back to PMS /accounts if plex.tv is unreachable - Routes: GET /api/users and POST /api/users/sync with camelCase response envelope
Add a priority column to the rules table so higher-priority rules win when an item matches multiple rules. Foundation for advanced rules work. - Migration v15 adds priority INTEGER DEFAULT 0 + priority index - Repo sorts rules by priority DESC, id ASC (tie-break on older id) - Zod schemas accept priority (0-100) on create/update - Rule types and client types carry priority through - Default 0 preserves existing rule behavior
Add database schema, repository, service methods, and API routes for syncing Radarr collections (TMDB movie franchises) into Prunerr. - Migration v12: collections + collection_items tables with indexes - Repository: findAll, findById, findByTmdbId, upsert, setMembership, findByMediaItem, setProtection, findProtectedContainingItem - RadarrService.getCollections() calls /api/v3/collection - RadarrService.syncCollections() upserts collections and replaces membership transactionally, matching movies by tmdb_id - Routes: GET /collections, GET /collections/:id, GET /collections/:id/items, POST /collections/sync, PATCH /collections/:id/protection - Chain collection sync after library scan in scanner.ts (non-fatal)
Adds 16 new metadata columns to media_items so rules can filter on genres, tags, studio, codecs, HDR, bitrate, runtime, ratings, content rating, series status, season/episode counts, and original language. - Migration v13 adds the nullable columns idempotently - Plex XML parser now extracts Genre/Label/Collection tags, Media/Part/ Stream info (HDR derived from colorTrc + Dolby Vision flags), studio, contentRating, and originalLanguage - Scanner merges Plex metadata with Radarr/Sonarr fallbacks (ratings from Radarr, series_status/season/episode counts from Sonarr) - Repository upserts and reads serialize genres/tags as JSON arrays
…llections feat: Radarr collections sync backend (unit 1/4)
…es-v2' into feature/v1.3-unit-02-metadata-enrichment # Conflicts: # server/src/db/schema.ts
…enrichment feat: expand media_items metadata columns (unit 2/4)
…es-v2' into feature/v1.3-unit-03-plex-users # Conflicts: # server/src/db/schema.ts # server/src/routes/index.ts
feat: Plex users import backend (unit 3/4)
…es-v2' into feature/v1.3-unit-04-rule-priority # Conflicts: # server/src/db/schema.ts
feat: add rule priority ordering (unit 4/4)
…(wave 2a) - Versioned RuleConditions schema (v1 + v2) with backward-compatible auto-upgrade - Recursive tree walker evaluating nested AND/OR/NOT groups - New operators: in, not_in, between, regex_match, is_null, is_not_null, matches_any, matches_all, contains_any, contains_all, not_contains - New field evaluators: genres, tags, studio, codecs, hdr, bitrate, runtime, season/episode count, series_status, imdb/tmdb/rt ratings, content_rating, original_language - JOIN-based evaluators: collection_membership (in_any_protected, in_collection_id, not_in_any_protected) and watched_by_user (watched_since, not_watched_since, ever_watched, never_watched) with prefetched watch cache map - Regex safety via safe-regex at rule save time (400 on catastrophic patterns) - /rules/preview endpoint returns totalMatches, wouldQueue, wouldSkipProtected, storageFreedGB, samples (top 10 by file size) - Vitest harness + 37 tests covering migration idempotency, tree walker, all new operators, mocked JOIN evaluators, v1→v2 regression
Addresses code review blockers on PR #13: - **C1/C2**: POST /api/rules/:id/run was still calling the legacy flat evaluator and stripping the v2 {version, root} envelope, causing any v2 rule or new-operator rule (between, regex_match, contains_any, etc.) to silently evaluate incorrectly — a data-loss risk. Now routes through evaluateRuleConditions() with a proper EvaluationContext (collectionsRepo, watchLookup, now) matching the /preview endpoint. Per-item eval failures are logged and skipped instead of crashing the whole run. - **H1**: clarified NOT group evaluator as !children.every(...) with a code comment. Added test coverage for multi-child NOT (semantic: NOT(AND(children))) and empty-group NOT behavior.
…ne-v2 feat: rules engine v2 — nested groups, new operators, new conditions (wave 2a)
Rewrites the rule builder to match the v2 engine. The builder now edits a
nested condition tree (AND / OR / NOT groups, depth-2 cap) instead of a flat
sentence, and exposes the full v2 field/operator surface via a single
FieldCatalog source of truth.
Client
- New FieldCatalog module: ~28 fields across basics / quality / ratings /
watching / collections / metadata, each declaring its allowed operators,
value widget type, unit, and default value.
- New ConditionEditor component: recursive group/leaf editor with ALL/ANY/
NONE toggles, per-field value widgets (number + unit, text, enum select,
list chip input, date, user picker, collection picker).
- New LivePreview panel: debounced (400ms) POST /rules/preview with v2 body;
renders totalMatches / wouldQueue / wouldSkipProtected / storageFreedGB
plus top-10 sample matches.
- Rewritten SmartRuleBuilder: template gallery + custom tree builder with
metadata row (name, priority 0-100, applies-to). Loads existing v1 rules
as a flat AND group; saves new rules as { version: 2, root } trees.
- Rules list now sorts by priority DESC (then id ASC) and shows per-rule
P{n} and v1/v2 badges.
- New treeOps module with immutable tree helpers (appendChild, removeNode,
updateNode, setGroupLogic, buildDefaultLeaf, depthOf).
- Added collectionsApi.list and usersApi.list for picker widgets.
Server
- CreateRuleSchema / UpdateRuleSchema now accept either the legacy flat
conditions array OR a v2 `{ version: 2, root }` payload. The repo already
stringifies whatever shape it receives; the engine auto-upgrades on read.
- validateRegexSafety middleware also walks v2 trees nested inside the
`conditions` field.
- RuleCondition.operator widened to string (engine validates by name).
Tests
- vitest + @testing-library/react + jsdom added to the client.
- 29 unit tests covering tree ops (append, remove, update, logic, depth,
getNode) and FieldCatalog (uniqueness, operator labels, collection/user/
list field shapes, operatorNeedsValue).
Addresses HIGH issues from PR #15 code review: - **HIGH-3**: ConditionEditor used array index as React key, which broke reconciliation on remove/reorder and caused stale input state. Added client-only `_uiId` field to ConditionLeaf/ConditionGroupNode, assigned at node creation (buildDefaultLeaf / emptyRoot / emptyGroup), filled in on rule load via `ensureUiIds`, stripped before serialising to the server via `stripUiIds`. Editor keys off `_uiId` with a fallback. - **HIGH-1**: LivePreview had a stale-response race where an older in-flight request could overwrite a newer result. Replaced useMutation with a ref-based generation counter that discards stale responses. - **HIGH-2**: Added defensive depthOf guard in handleSave — UI prevents nesting beyond MAX_DEPTH, but validate the saved tree to catch paste/import/template bugs. - Also: preview now strips `_uiId` before POSTing so the server never sees UI-only fields. Test coverage: 6 new treeOps tests for ensureUiIds/stripUiIds idempotency and roundtrip, plus emptyGroup. 35/35 passing.
feat: rules UI v2 — nested groups + field catalog + live preview (wave 2b)
…n cascade - Collections list page with grid, search, sync from Radarr - Collection detail page with protection toggle and queue-for-deletion - POST /api/collections/:id/queue endpoint for bulk queuing collection items - Collection protection cascades to library items at display and evaluation time - Activity timeline shows collection protection events with friendly labels - Rules builder: full-screen overlay, restored Easy Setup sentence builder tab - Rich categorized field dropdown with group icons and descriptions - LivePreview: wider side panel, full height, poster art in sample items - Dashboard: collections count in Quick Stats row - Protected badge in library cards links to specific collection
- S1: batch query for collection protection in library list (N+1 → 1 query) - S2: queue endpoint now checks cross-collection protection - S3: mark-deletion + bulk mark-deletion check collection protection - S4: protection toggle + activity log wrapped in DB transaction - C1: guard against NaN collection ID from invalid URLs - C2: pass queue options through mutate() to avoid stale closures - C3: replace non-null assertions with safe error handling in API client
- Resolve Radarr/Sonarr tag IDs to names during scan (fallback after Plex labels) - Add syncPlexUsers scheduled task (daily 3:45 AM) and post-scan hook - Register syncPlexUsers in scheduler task registry and default config - Add usersApi.sync() client method - Add "Sync Users" button in rule builder user picker when list is empty
- React.memo on MediaCard with custom comparator (skip re-renders for unchanged cards) - Remove staggered fade-in animations (eliminate layout thrashing on 24+ cards) - Add decoding="async" and loading="lazy" to poster images - Remove poster hover scale (redundant with card-level hover) - Searchable field picker dropdown with auto-focus - Searchable user picker combobox (type to filter or enter custom username) - Format storage sizes: values over 1000 GB display as TB - Reduce dashboard polling: recentActivity 30s→60s, healthStatus 30s→120s - Install @tanstack/react-virtual for future grid virtualization
- Remove animate-fade-up + staggered delays from Dashboard, Sidebar, Recommendations, DiskStatsModal (eliminates layout thrashing) - Add decoding="async" and loading="lazy" to all poster images - Wrap RecommendationCard and QueueItemRow with React.memo
- Add depth guard to validateConditionTree (prevents stack overflow) - Batch collection protection check in queue endpoint (N+1 → 1 query) - Batch collection protection check in bulk mark-deletion (N+1 → 1 query) - Cache collection membership lookups in EvaluationContext during rule eval - Update CollectionsRepoLike interface and test mocks for findAll
…reference - Complete feature documentation covering rules engine v2, collections, protection cascade, deletion management, and all integrations - nzb360 mobile app setup instructions - Full REST API endpoint reference - Updated installation guides for Docker, Compose, and Unraid - Tech stack and development setup details
- Auto-generate 64-char hex API key on first startup - All external API requests require X-Api-Key header - Web UI exempt via same-origin detection (Sec-Fetch-Site, Origin, Referer) - PRUNERR_API_KEY env var override for Docker deployments - Constant-time key comparison to prevent timing attacks - Settings page: view key, copy to clipboard, regenerate with confirmation - No enable/disable toggle — auth is always on for external consumers
- Use HMAC-SHA256 for constant-time key comparison (no length leak) - Narrow same-origin check to Sec-Fetch-Site only (drop Origin/Referer) - Strip api_key from settings export (prevents accidental leak) - Block api_key from settings import (prevents overwrite attack) - Add security documentation comments about trusted network assumption
- Animated icon swap between search and loading spinner (AnimatePresence) - Search icon highlights accent on focus - Result count badge appears after search completes - Clear button with scale animation - Bottom accent line on focus - Loading state covers debounce + fetch + initial load - Install framer-motion for future animation use
- Grid skeleton cards mimic real card shape (poster + title + badge placeholders) - Table skeleton rows show poster thumbnail + text + size placeholders - AnimatePresence transitions between loading/content/error/empty states - Content fades to 50% opacity during background re-fetches (page changes) - Empty state slides in with subtle y-offset animation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v1.3.0
Collections
Rules Engine v2
API Key Authentication
Data Sync
Performance
UX
Security
Docs
Test plan