Add release blocklist feature

Introduce a per-request release blocklist to auto-block permanently failing releases and provide admin management. Changes include:

- Database: add BlockedRelease model (blocked_releases) to Prisma schema with unique (requestId, releaseKey) and indexes; documented in backend database docs.
- Service & utils: new blocklist.service, release-key and filter helpers for normalization and matching; processors updated to emit auto-blocks (monitor-download, organize-files, search processors, RSS).
- HTTP API: add admin endpoints GET/DELETE /api/admin/blocklist, DELETE /api/admin/blocklist/[id], and GET /api/admin/blocklist/by-request/[requestId].
- Admin UI: new /admin/blocklist page and numerous React components (toolbar, filters, table, rows, pagination, skeleton, chips, date picker) with URL-driven state hook and per-row unblock UX.
- Tests: add unit/integration tests for service, routes, utils, and updated processor tests.

The blocklist is idempotent (upsert), filters search results before ranking (interactive search shows badges only), and admin-only APIs require auth. This commit wires docs, API, DB, frontend and tests for the new feature.
This commit is contained in:
kikootwo
2026-05-18 12:15:51 -04:00
parent fb0445d95f
commit b1492fc32e
41 changed files with 4098 additions and 12 deletions
+20
View File
@@ -111,12 +111,32 @@ PostgreSQL database storing users, audiobooks, requests, downloads, configuratio
- Indexes: `job_id`, `created_at`
- **Purpose:** Store detailed event logs for job operations (shown in admin logs UI)
### Blocked_Releases
- `id` (UUID PK), `request_id` (FK → Requests, CASCADE on hard delete)
- `release_name` (text) - original release title as the indexer returned it
- `release_key` (text) - normalized lookup key: `trim().toLowerCase()` of release_name
- `release_hash` (nullable) - `torrentHash` (qBit) OR `nzbId` (SAB/NZBGet); mutually exclusive in source
- `indexer_name` (nullable), `indexer_id` (int, nullable)
- `source` ('organize_fail'|'download_fail'|'manual'; 'manual' reserved for v2)
- `reason` (text) - short, e.g. "No audiobook files found", "Download failed (par2)"
- `reason_detail` (text, nullable) - raw client error string (SAB failMessage, NZBGet Par/Unpack code)
- `download_history_id` (nullable) - traceability to the DownloadHistory row that drove the block
- `job_id` (nullable) - origin job; also drives JobEvent emission via RMABLogger.forJob
- `created_at` (timestamp)
- Unique: `(request_id, release_key)` - idempotency for concurrent auto-block writes
- Indexes: `request_id`, `release_key`, `release_hash`, `created_at DESC`
- **Purpose:** Per-request blocklist. Search processors filter their candidate set against this table so future searches skip releases that have already failed for the same request.
- **Soft/hard delete:** Soft-delete (sets `requests.deleted_at`) does NOT cascade - blocklist entries survive. Hard-delete cascades and wipes entries.
- **Match rules:** Case-insensitive exact match on `release_key` OR exact match on `release_hash`.
- **Service:** Single writer is `src/lib/services/blocklist.service.ts` (`addAutoBlock` is idempotent via upsert; never throws).
## Relationships
- User → Requests (1:many)
- Audiobook → Requests (1:many)
- Request → Download History (1:many)
- Request → Jobs (1:many, nullable)
- Request → Blocked Releases (1:many, CASCADE on hard delete)
- Job → Job Events (1:many, CASCADE delete)
## Setup Strategy