diff --git a/.gitignore b/.gitignore index 3401af4..d145b14 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ data/ *.db-wal *.db-shm .agents/ -.windsurf/ \ No newline at end of file +.windsurf/ +scratch/ \ No newline at end of file diff --git a/scratch/blocklist_issue.txt b/scratch/blocklist_issue.txt deleted file mode 100644 index f70763c..0000000 --- a/scratch/blocklist_issue.txt +++ /dev/null @@ -1,21 +0,0 @@ -Problem: -The "Blocklist & Search" button on download cards fails with a "400 Bad Request (Missing required fields)" when clicked on any television release in Sonarr that represents a full season package or a multi-episode release. - -Root Cause: -1. In `server/services/DownloadMatcher.js`, when a download is matched with a Sonarr queue record, `arrContentId` is populated with `sonarrMatch.episodeId || null`. -2. However, for multi-episode packs or full season grabs in Sonarr v3, the `episodeId` field is missing from the queue record payload (since the release is associated with multiple episodes). Instead, Sonarr provides an `episodeIds` array. As a result, `arrContentId` is normalized to `null`. -3. When the user clicks the "Blocklist & Search" button in the UI, the frontend calls the `POST /api/dashboard/blocklist-search` endpoint. The request body includes `arrContentId: null`. -4. The backend route validator in `server/routes/dashboard.js` strictly requires all fields including `arrContentId` to be truthy: - ```javascript - if (!arrQueueId || !arrType || !arrInstanceUrl || !arrContentId || !arrContentType) { - return res.status(400).json({ error: 'Missing required fields' }); - } - ``` - Because `arrContentId` is `null`, this check fails and returns `400 Missing required fields`, completely blocking the blocklist operation (even though queue removal itself does not require an episode ID). -5. Furthermore, the search trigger logic in `dashboard.js` only handles single episode searches via `{ name: 'EpisodeSearch', episodeIds: [arrContentId] }` and has no logic to handle `episodeIds` arrays or fallback searches (such as `SeriesSearch` or `SeasonSearch`). - -Proposed Fix: -1. **Relax Backend Validation**: Allow `arrContentId` to be optional or null for `sonarr` queue records to ensure the deletion and blocklist steps can still execute. -2. **Robust Search Triggers**: - - If `episodeId` is missing but `episodeIds` array is available on the matched record, pass the array of IDs to the frontend/backend. - - Modify the `dashboard.js` re-search block to support `EpisodeSearch` with multiple IDs, or fall back to triggering a `SeriesSearch` command using the `seriesId` if no specific episode IDs are resolved. diff --git a/scratch/client_log_feature.txt b/scratch/client_log_feature.txt deleted file mode 100644 index b624312..0000000 --- a/scratch/client_log_feature.txt +++ /dev/null @@ -1,16 +0,0 @@ -Title: -FEATURE: Client-side console log capturing and streaming API endpoint with dual-authentication - -Problem / Requirement: -To aid in frontend troubleshooting, developers need a way to capture and gather client-side console logs (`console.log`, `console.warn`, `console.error`) and make them accessible over a real-time log stream endpoint. This helps debug frontend issues (such as SSE failures, CSP violations, and state synchronization issues) in environments without direct access to browser devtools. - -Success Criteria: -1. Client-Side Interceptor: Intercept standard browser console methods at SPA startup and place captured logs into an in-memory queue. -2. Batched Log Transmission (Selected Option A): Periodic HTTP POST batch queries to `POST /api/debug/client-logs` (every 2 seconds or when the queue hits 20 items) to minimize browser thread and network overhead. -3. Server storage and SSE log streaming: - - Save incoming logs into a separate rolling 1000-line buffer `clientLogBuffer`. - - Expose `GET /api/debug/client-logs/stream` to stream client-side logs in real-time via SSE. -4. Security & Configuration: - - Enableable only when the environment variable `ENABLE_LOG_STREAM=true` is set. - - Enforce exact same dual-auth rules (Emby session cookie, Basic Auth fallback, and X-Webhook-Secret header bypass) on both client logs endpoints. -5. API Documentation: Documented in `server/openapi.yaml`. diff --git a/scratch/client_logging_options.txt b/scratch/client_logging_options.txt deleted file mode 100644 index 5390318..0000000 --- a/scratch/client_logging_options.txt +++ /dev/null @@ -1,12 +0,0 @@ -Amended the plan to add client-side console log capturing and streaming options: - -### Proposed Client Logging Design: -- **Client-Side Capture (Frontend Interception)**: Hook into standard browser console methods (`console.log`, `console.warn`, `console.error`) at client-side startup. -- **Client-to-Server Transmission**: - - **Option A (Recommended)**: Store captured logs in a local memory queue, and periodically perform a batched `POST /api/debug/client-logs` (every 2 seconds or when the queue hits 20 items) to minimize network overhead. - - **Option B (WebSocket Channel)**: Stream logs instantly via persistent WebSockets, which adds structural and connection management complexity. -- **Server Storage & SSE Streaming**: - - Store incoming client logs in a separate rolling 1000-line buffer `clientLogBuffer`. - - Expose `GET /api/debug/client-logs/stream` (under the exact same dual-auth/webhook-secret constraints) to stream client-side logs in real-time via SSE to debugging tools. - -The `implementation_plan.md` artifact has been successfully updated with these options. diff --git a/scratch/fetch-remote-logs.js b/scratch/fetch-remote-logs.js deleted file mode 100644 index 119aad7..0000000 --- a/scratch/fetch-remote-logs.js +++ /dev/null @@ -1,32 +0,0 @@ -const axios = require('axios'); -const fs = require('fs'); - -const secret = '63f7eaf2b4e3ca7da48c003d5986afc8ba31119404d49e45102c61fc3a27329a'; -const serverLogsUrl = 'https://sofarr.i3omb.com/api/debug/server-logs?testClose=true'; -const clientLogsUrl = 'https://sofarr.i3omb.com/api/debug/client-logs?testClose=true'; - -async function fetchLogs(url, filename) { - console.log(`Fetching logs from ${url}...`); - try { - const response = await axios.get(url, { - headers: { - 'x-webhook-secret': secret - } - }); - fs.writeFileSync(filename, response.data); - console.log(`Logs saved to ${filename} (${response.data.length} bytes).`); - } catch (err) { - console.error(`Failed to fetch from ${url}:`, err.message); - if (err.response) { - console.error(`Status: ${err.response.status}`); - console.error(`Body:`, JSON.stringify(err.response.data)); - } - } -} - -async function run() { - await fetchLogs(serverLogsUrl, 'scratch/remote_server.log'); - await fetchLogs(clientLogsUrl, 'scratch/remote_client.log'); -} - -run(); diff --git a/scratch/issue48_reopen_comment.txt b/scratch/issue48_reopen_comment.txt deleted file mode 100644 index 1a57bb4..0000000 --- a/scratch/issue48_reopen_comment.txt +++ /dev/null @@ -1,26 +0,0 @@ -## Regression: Fix in v1.7.16 was insufficient — issue persists in production - -### Updated Root Cause Analysis - -Post-release investigation of live server debug logs on `sofarr.i3omb.com` confirms the blocklist feature is **still failing** after v1.7.16. The server logs still show: - -``` -[Blocklist] Download not found: { arrQueueId: 439913856, arrType: 'radarr' } -``` - -The v1.7.16 fix cast both sides of the comparison to `String`, which was the correct approach — but it was applied to the **wrong data source**. - -The permission check at line 693 of `dashboard.js` calls: - -```js -const allDownloads = await downloadClientRegistry.getAllDownloads(); -const download = allDownloads.find(d => d.arrQueueId != null && String(d.arrQueueId) === String(arrQueueId) && d.arrType === arrType); -``` - -`downloadClientRegistry.getAllDownloads()` fetches **raw download client data** directly from qBittorrent, SABnzbd, etc. — these are unmatched objects with no Sonarr/Radarr queue metadata. The `arrQueueId` field is only populated during `DownloadMatcher.js` processing (which runs during the SSE/dashboard build from the *arr cache). Because qBittorrent's `normalizeDownload()` never sets `arrQueueId`, the lookup **always returns `undefined`** for any qBittorrent torrent, regardless of type casting. - -### Correct Fix - -The permission check should validate against the **Sonarr/Radarr queue cache records** directly (where `id` is the queue record ID), rather than against raw download client data. The fix will replace the `downloadClientRegistry.getAllDownloads()` lookup with a direct cache lookup of `poll:sonarr-queue` / `poll:radarr-queue` records, matching by `String(record.id) === String(arrQueueId)`. - -This will be released in v1.7.17. diff --git a/scratch/issue_blocklist.txt b/scratch/issue_blocklist.txt deleted file mode 100644 index 32473ee..0000000 --- a/scratch/issue_blocklist.txt +++ /dev/null @@ -1,46 +0,0 @@ -## Summary - -The "Blocklist and search" feature is broken for all users. Clicking the blocklist button on a download (e.g. the film "Project Hail Mary", `arrQueueId: 905000340`, `arrType: radarr`) consistently returns a `403 Download not found or permission denied` error. - -## Root Cause - -The server-side lookup in `server/routes/dashboard.js` uses strict equality (`===`) to find the matching download: - -```js -const download = allDownloads.find(d => d.arrQueueId === arrQueueId && d.arrType === arrType); -``` - -- `d.arrQueueId` is populated from the Radarr/Sonarr queue API response as a **number** (e.g. `905000340`). -- `arrQueueId` from `req.body` originates from the client SPA via a DOM `dataset` attribute, which is always a **string** (e.g. `"905000340"`). -- Due to the type mismatch, `905000340 === "905000340"` evaluates to `false`, so the lookup always fails and returns `403`. - -## Evidence - -Server log (live environment, `2026-05-24`): - -``` -[Blocklist] Download not found: { arrQueueId: 905000340, arrType: 'radarr' } -``` - -Client log confirms user clicked blocklist at `21:01:19`, `21:01:32`, and `21:02:35`. - -## Steps to Reproduce - -1. Open the dashboard on a Radarr or Sonarr download with a pending queue entry. -2. Click the "Blocklist and search" button. -3. The action silently fails; the download is not removed and no re-search is triggered. -4. Server logs show `[Blocklist] Download not found`. - -## Proposed Fix - -Cast both sides of the comparison to `String` before comparing: - -```js -const download = allDownloads.find(d => d.arrQueueId != null && String(d.arrQueueId) === String(arrQueueId) && d.arrType === arrType); -``` - -This fix will be released in version `1.7.16`. - -## Severity - -**High** — The blocklist-and-search feature is completely non-functional for all users. There is no workaround within the UI. diff --git a/scratch/log_stream_feature.txt b/scratch/log_stream_feature.txt deleted file mode 100644 index 9883df4..0000000 --- a/scratch/log_stream_feature.txt +++ /dev/null @@ -1,19 +0,0 @@ -Title: -FEATURE: Log streaming debug endpoint with dual-authentication and togglable runtime configuration - -Problem / Requirement: -Administrators and developers need a lightweight, real-time method to stream application stdout/stderr logs (which correspond exactly to Docker container logs in standard setups) directly through the API. This enables easier live debugging without requiring full Docker daemon or terminal access. - -Success Criteria: -1. **Lightweight Log Streaming**: Streams process standard output/error (representing Docker logs) via Server-Sent Events (SSE). Keep a rolling buffer of the last 1000 lines in memory. -2. **Dual-Authentication**: - - Accepts existing session cookie (`emby_user`) with administrative credentials. - - Accepts standard HTTP Basic Authentication (`Authorization: Basic `) using Emby administrator username/password credentials. -3. **Runtime Configuration Toggle**: Enableable using a runtime environment variable `ENABLE_LOG_STREAM=true` (defaulting to `false`/disabled). When disabled, returns a `403 Forbidden` response. -4. **API Spec Documentation**: Documented in `server/openapi.yaml` under the `/api/debug/logs` endpoint, including the query format and response schemas. - -Proposed Implementation: -1. **Log Interceptor**: Implement a global stdout/stderr hook in `server/index.js` or in a new `server/utils/logCapture.js` to collect a rolling buffer of 1000 log lines and expose a Node `EventEmitter` to push new logs to active subscribers. -2. **Authentication Middleware**: Create `server/middleware/logStreamAuth.js` which verifies active sessions or fallback Basic Auth headers by calling Emby's `/Users/authenticatebyname` and `/Users/{id}` endpoints to verify the user is a valid administrator. -3. **Route Definition**: Define `server/routes/debug.js` to register `GET /api/debug/logs` backing the SSE stream, enforce the `ENABLE_LOG_STREAM === 'true'` check, and execute `logStreamAuth` checks. -4. **OpenAPI Spec Integration**: Define `/api/debug/logs` schemas, parameters, security schemes, and basic auth descriptions inside `server/openapi.yaml`. diff --git a/scratch/ombi_bug_desc.txt b/scratch/ombi_bug_desc.txt deleted file mode 100644 index 0cef067..0000000 --- a/scratch/ombi_bug_desc.txt +++ /dev/null @@ -1,21 +0,0 @@ -### Bug Description -Ombi webhooks are currently failing to authenticate. In `server/routes/webhook.js`, all `/api/webhook/*` endpoints (sonarr, radarr, and ombi) require the custom `X-Sofarr-Webhook-Secret` HTTP header to be present and match the configured `SOFARR_WEBHOOK_SECRET`. - -However, Ombi's built-in Webhook notification agent does not support adding custom HTTP headers to its outgoing webhook notification requests. This makes it impossible for Ombi to successfully authenticate using the current header-only validation mechanism. - -### Root Cause -In `server/routes/webhook.js`, `validateWebhookSecret(req)` only inspects `req.get('X-Sofarr-Webhook-Secret')`: -```javascript -function validateWebhookSecret(req) { - const expectedSecret = getWebhookSecret(); - const providedSecret = req.get('X-Sofarr-Webhook-Secret'); - ... -} -``` -Since Ombi sends standard JSON payloads to a configured URL without custom headers, it cannot supply this header, resulting in a `401 Unauthorized` response. - -### Proposed Remediation -1. **Fallback Authentication Method**: Update `validateWebhookSecret(req)` in `server/routes/webhook.js` to look for the secret in either the `X-Sofarr-Webhook-Secret` header OR as a `secret` query parameter (`req.query.secret`). -2. **Registration Update**: Update the `/webhook/enable` route in `server/routes/ombi.js` to automatically append `?secret=${webhookSecret}` to the registered `webhookUrl` sent to Ombi. -3. **OpenAPI Spec & JSDoc Updates**: Document the query-parameter fallback authentication option in `server/openapi.yaml` and the `@openapi` JSDoc comments in `server/routes/webhook.js`. -4. **Integration Testing**: Add new integration tests in `tests/integration/webhook.test.js` to assert that authentication via query parameters succeeds, and that invalid query parameters are rejected. diff --git a/scratch/plan_amendment.txt b/scratch/plan_amendment.txt deleted file mode 100644 index 527d2ad..0000000 --- a/scratch/plan_amendment.txt +++ /dev/null @@ -1,6 +0,0 @@ -Amended the plan to include a high-priority bypass using the `X-Webhook-Secret` request header: - -1. **Webhook Secret Bypass**: If the request contains the `X-Webhook-Secret` header, we verify if it matches the configured `SOFARR_WEBHOOK_SECRET` environment variable. -2. **Access Granted**: If matching, the request is immediately authorized, completely bypassing session and Emby Basic Auth checks. This is ideal for curl scripts, server-to-server monitoring, or external debugging logs captures. - -I have updated the `implementation_plan.md` artifact to reflect this amendment. diff --git a/scratch/plan_comment.txt b/scratch/plan_comment.txt deleted file mode 100644 index 9849bbd..0000000 --- a/scratch/plan_comment.txt +++ /dev/null @@ -1,15 +0,0 @@ -I have investigated the blocklist & search failure reported in this issue and created a technical remediation plan: - -### Root Cause -For television grabs representing a full-season pack or multi-episode package in Sonarr, the `episodeId` property is absent (instead, it has an `episodeIds` array). This maps to a `null` value for `arrContentId` on the client download card. The `/api/dashboard/blocklist-search` route strictly requires all fields including `arrContentId` to be truthy, returning `400 Bad Request: Missing required fields` and completely blocking the queue blocklist/removal action. - -### Remediation Plan -1. **Enrich Backend Match Data**: Expose `arrContentIds` (`sonarrMatch.episodeIds`) and `arrSeriesId` (`sonarrMatch.seriesId`) from `DownloadMatcher.js` to the normalized download card object. -2. **Relax API Route Validation**: Remove `arrContentId` from the mandatory request parameters check in `server/routes/dashboard.js`. -3. **Enhance Search Commands**: - - If a single `arrContentId` is provided, trigger `EpisodeSearch` for that single ID. - - If an `arrContentIds` array is provided, trigger `EpisodeSearch` with that list of IDs. - - If no specific episode IDs can be resolved but `arrSeriesId` is provided, fall back to triggering a series-wide `SeriesSearch`. -4. **Update Frontend & Documentation**: Update the client payload, update the OpenAPI spec, and add integration tests covering single/multi/fallback searches. - -Upon approval, I will execute this plan, merge to `main`, close this ticket referencing the resolving commit, and cut a new point release (v1.7.11). diff --git a/scratch/server_log_feature.txt b/scratch/server_log_feature.txt deleted file mode 100644 index bcebf1f..0000000 --- a/scratch/server_log_feature.txt +++ /dev/null @@ -1,14 +0,0 @@ -Title: -FEATURE: Togglable server-side (Docker) log streaming debug endpoint with dual-authentication - -Problem / Requirement: -Administrators and developers need a lightweight, real-time method to stream application stdout/stderr logs (which correspond exactly to Docker container logs in standard setups) directly through the API. This enables easier live debugging without requiring full Docker daemon or terminal access. - -Success Criteria: -1. Lightweight Log Streaming: Streams process standard output/error (representing Docker logs) via Server-Sent Events (SSE). Keep a rolling buffer of the last 1000 lines in memory. -2. Dual-Authentication with Webhook Secret Bypass: - - Accepts existing session cookie (emby_user) with administrative credentials. - - Accepts standard HTTP Basic Authentication (Authorization: Basic ) using Emby administrator username/password credentials. - - Accepts X-Webhook-Secret header matching the SOFARR_WEBHOOK_SECRET environment variable for programmatic bypass. -3. Runtime Configuration Toggle: Enableable using a runtime environment variable ENABLE_LOG_STREAM=true (defaulting to false/disabled). When disabled, returns a 403 Forbidden response. -4. API Spec Documentation: Documented in server/openapi.yaml under the /api/debug/logs endpoint, including the query format and response schemas. diff --git a/scratch/tag_msg.txt b/scratch/tag_msg.txt deleted file mode 100644 index 61c477a..0000000 --- a/scratch/tag_msg.txt +++ /dev/null @@ -1,9 +0,0 @@ -Release v1.7.16 - -Remediate the blocklist-search queue ID type mismatch. The "Blocklist and -search" action was returning 403 for all users because the arrQueueId -comparison used strict equality between a string (from the SPA DOM dataset) -and a number (from the Radarr/Sonarr API). Both values are now cast to -String before comparison. - -See CHANGELOG.md for full details. diff --git a/scratch/webhook_secret.txt b/scratch/webhook_secret.txt deleted file mode 100644 index 86fba6c..0000000 --- a/scratch/webhook_secret.txt +++ /dev/null @@ -1 +0,0 @@ -63f7eaf2b4e3ca7da48c003d5986afc8ba31119404d49e45102c61fc3a27329a