merge branch 'develop' into 'main' - Update Release v1.7.19 to ignore scratch directory
Build and Push Docker Image / build (push) Successful in 1m8s
Create Release / release (push) Successful in 36s
CI / Tests & coverage (push) Successful in 1m55s
CI / Security audit (push) Successful in 2m12s
CI / Swagger Validation & Coverage (push) Successful in 1m50s

This commit is contained in:
2026-05-25 08:33:16 +01:00
14 changed files with 2 additions and 239 deletions
+2 -1
View File
@@ -11,4 +11,5 @@ data/
*.db-wal
*.db-shm
.agents/
.windsurf/
.windsurf/
scratch/
-21
View File
@@ -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.
-16
View File
@@ -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`.
-12
View File
@@ -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.
-32
View File
@@ -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();
-26
View File
@@ -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.
-46
View File
@@ -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.
-19
View File
@@ -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 <base64>`) 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`.
-21
View File
@@ -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.
-6
View File
@@ -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.
-15
View File
@@ -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).
-14
View File
@@ -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 <base64>) 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.
-9
View File
@@ -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.
-1
View File
@@ -1 +0,0 @@
63f7eaf2b4e3ca7da48c003d5986afc8ba31119404d49e45102c61fc3a27329a