Skip to content

v1.3.0 — Collections, Rules Engine v2, API Key Auth#17

Merged
helliott20 merged 29 commits intomainfrom
feature/v1.3-collections-and-rules-v2
Apr 6, 2026
Merged

v1.3.0 — Collections, Rules Engine v2, API Key Auth#17
helliott20 merged 29 commits intomainfrom
feature/v1.3-collections-and-rules-v2

Conversation

@helliott20
Copy link
Copy Markdown
Owner

v1.3.0

Collections

  • Sync movie collections from Radarr (TMDB-sourced)
  • Protect entire collections to prevent cleanup
  • Queue collections for bulk deletion with grace periods
  • Protection cascades to individual items in library view
  • Collections page with grid, search, sync button
  • Collection detail page with items list and protection toggle

Rules Engine v2

  • Nested AND/OR/NOT condition groups (depth-2 cap)
  • 28 condition fields across 6 categories (basics, quality, ratings, watching, collections, metadata)
  • Three builder modes: Templates, Easy Setup (sentence builder), Custom Builder
  • Full-screen rule builder overlay
  • Live preview with poster art, result counts, and reclaimable storage
  • Searchable field picker dropdown with categories and icons
  • Searchable user picker combobox
  • Priority system (0-100, higher wins)
  • v1 rules auto-upgrade to v2 format

API Key Authentication

  • Auto-generated 64-char hex key on first startup
  • Required for all external API requests (X-Api-Key header)
  • Web UI exempt via Sec-Fetch-Site same-origin detection
  • PRUNERR_API_KEY env var override for Docker
  • HMAC-SHA256 constant-time comparison
  • Settings page: view, copy, regenerate

Data Sync

  • Radarr/Sonarr tag name resolution (was numeric IDs only)
  • Plex users sync (scheduled daily + post-scan + manual button)
  • Expanded metadata: genres, studio, ratings, codecs, HDR, runtime, language, etc.

Performance

  • React.memo on MediaCard, RecommendationCard, QueueItemRow
  • Removed staggered animations across all pages
  • Lazy image loading with async decoding
  • Reduced dashboard polling (30s to 60-120s)
  • Batch collection protection queries (eliminated N+1)
  • Cached collection membership in rule evaluation context

UX

  • Polished search input with framer-motion transitions
  • Rich loading skeletons matching actual card layout
  • Smooth AnimatePresence transitions between loading/content/error states
  • Activity timeline with human-readable action labels
  • Storage displays as TB when over 1000 GB
  • Protected badge in library cards links to specific collection
  • Dashboard collections count in Quick Stats

Security

  • Depth guard on condition tree validation (prevents stack overflow)
  • API key stripped from settings export
  • API key blocked from settings import
  • Collection protection enforced in mark-deletion and bulk endpoints

Docs

  • Full wiki (9 pages): Installation, Configuration, Rules, Collections, Deletion, API, Mobile, Troubleshooting
  • README slimmed with wiki links
  • nzb360 mobile app setup guide

Test plan

  • Build passes (client + server)
  • API key auth tested (no key → 401, correct → 200, wrong → 401)
  • Code reviewed (4 rounds, all HIGH/CRITICAL resolved)
  • Wiki accuracy verified against codebase

helliott-redactbox and others added 29 commits April 5, 2026 21:56
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
@helliott20 helliott20 merged commit c3ef7f4 into main Apr 6, 2026
@helliott20 helliott20 deleted the feature/v1.3-collections-and-rules-v2 branch April 6, 2026 17:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants