chore: bump to v1.3.0; update CHANGELOG, README, ARCHITECTURE docs
Some checks failed
Docs Check / Mermaid diagram parse check (push) Failing after 44s
Docs Check / Markdown lint (push) Successful in 1m7s
Build and Push Docker Image / build (push) Successful in 1m15s
Licence Check / Dependency licence compatibility (push) Successful in 1m37s
CI / Security audit (push) Successful in 2m2s
CI / Tests & coverage (push) Successful in 2m27s
Some checks failed
Docs Check / Mermaid diagram parse check (push) Failing after 44s
Docs Check / Markdown lint (push) Successful in 1m7s
Build and Push Docker Image / build (push) Successful in 1m15s
Licence Check / Dependency licence compatibility (push) Successful in 1m37s
CI / Security audit (push) Successful in 2m2s
CI / Tests & coverage (push) Successful in 2m27s
This commit is contained in:
21
CHANGELOG.md
21
CHANGELOG.md
@@ -6,6 +6,27 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||
|
||||
---
|
||||
|
||||
## [1.3.0] - 2026-05-17
|
||||
|
||||
### Added
|
||||
|
||||
- **History tab** — new "Recently Completed" tab showing imported and failed downloads from Sonarr/Radarr history for the last N days (configurable via the days input, persisted in `localStorage`). Auto-refreshes every 5 minutes.
|
||||
- **History deduplication** — when a failed download has subsequently been imported successfully, only the successful record is shown. If the most recent record for an item is a failure but the episode/movie is already on disk (upgrade attempt), the record is flagged as `availableForUpgrade`.
|
||||
- **"Upgrade available" badge** — failed history cards where the content is already on disk display an amber badge to indicate this is a failed upgrade rather than a missing item.
|
||||
- **"Hide upgrade failures" toggle** — checkbox in the history tab to filter out failed records that are already available on disk. State persists in `localStorage`. Tooltip explains the behaviour and matches the episode/multi-episode tooltip style.
|
||||
- **Blocklist & Search button** — admin-only button on download cards with an "Import Pending" caution. Removes the download from the client with `blocklist=true` (preventing re-grab of the same release) then immediately triggers an `EpisodeSearch`/`MoviesSearch` command in Sonarr/Radarr. Shows a confirmation dialog, loading/success/error states. Kicks a background poll on success.
|
||||
- **`POST /api/dashboard/blocklist-search`** — new admin-only endpoint backing the above button. Accepts `arrQueueId`, `arrType`, `arrInstanceUrl`, `arrInstanceKey`, `arrContentId`, `arrContentType`.
|
||||
- **Title link home navigation** — the sofarr logo/title in the header now navigates to the default view (Active Downloads tab, close status panel, reset "Show all users" toggle) without a page reload.
|
||||
- **Version footer link** — the version string in the dashboard footer links to the source repository.
|
||||
|
||||
### Changed
|
||||
|
||||
- History records are now deduplicated server-side before being sent to the client — only the most relevant record per content item per instance is returned.
|
||||
- Import-issue badge tooltip now uses the themed `var(--surface)` / `var(--text-primary)` / `var(--border)` CSS variables, matching the episode and toggle tooltip style.
|
||||
- Poller now stores `_instanceKey` on Sonarr/Radarr queue records in the cache, enabling the backend to look up API credentials for blocklist operations without an additional configuration lookup.
|
||||
|
||||
---
|
||||
|
||||
## [1.2.2] - 2026-05-17
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
sofarr connects to your media stack and shows you a personalized view of:
|
||||
- **Active Downloads** - See what's currently downloading from Usenet (SABnzbd) and BitTorrent (qBittorrent)
|
||||
- **Progress Tracking** - Real-time progress bars with speed, ETA, and completion estimates
|
||||
- **Recently Completed** - History tab showing imported and failed downloads from Sonarr/Radarr with deduplication and upgrade-awareness
|
||||
- **User Matching** - Downloads are matched to you based on tags in Sonarr/Radarr
|
||||
- **Multi-Instance Support** - Connect to multiple instances of each service
|
||||
|
||||
@@ -279,6 +280,10 @@ sofarr polls all configured services in the background and caches the results. D
|
||||
- `GET /api/dashboard/user-summary` — Per-user download counts (admin)
|
||||
- `GET /api/dashboard/status` — Server / polling / cache status (admin)
|
||||
- `GET /api/dashboard/cover-art` — Proxied cover art image
|
||||
- `POST /api/dashboard/blocklist-search` — Blocklist a release and trigger a new search (admin)
|
||||
|
||||
### History
|
||||
- `GET /api/history/recent` — Recently completed downloads from Sonarr/Radarr history
|
||||
|
||||
### Service APIs (proxy to your services)
|
||||
- `GET /api/sabnzbd/*` — SABnzbd API proxy
|
||||
@@ -323,7 +328,7 @@ npm run test:coverage # with V8 coverage report (outputs to coverage/)
|
||||
npm run test:ui # interactive Vitest UI
|
||||
```
|
||||
|
||||
115 tests across 8 test files covering the security-critical paths: auth middleware, CSRF protection, secret sanitization, config parsing, token store, and qBittorrent utilities. See [`tests/README.md`](tests/README.md) for design decisions and coverage targets.
|
||||
145 tests across 10 test files covering the security-critical paths: auth middleware, CSRF protection, secret sanitization, config parsing, token store, qBittorrent utilities, and history deduplication/classification. See [`tests/README.md`](tests/README.md) for design decisions and coverage targets.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -314,6 +314,7 @@ For each connected user the server:
|
||||
| See download/target paths | ✗ | ✓ |
|
||||
| See Sonarr/Radarr links | ✗ | ✓ |
|
||||
| View status panel | ✗ | ✓ |
|
||||
| Blocklist & search (import-pending) | ✗ | ✓ |
|
||||
|
||||
### Tag Matching
|
||||
|
||||
@@ -413,9 +414,16 @@ Each matched download produces an object with:
|
||||
| `matchedUserTag` | string / null | Tag label matching the requesting user, or `null` |
|
||||
| `tagBadges` | `{label, matchedUser}[]` / undefined | (Admin `showAll` only) Each tag classified against full Emby user list |
|
||||
| `importIssues` | string[] / null | Import warning/error messages |
|
||||
| `availableForUpgrade` | boolean / undefined | (History) `true` when outcome is `failed` but the content is already on disk (failed upgrade attempt) |
|
||||
| `downloadPath` | string / null | (Admin) Download client path |
|
||||
| `targetPath` | string / null | (Admin) *arr target path |
|
||||
| `arrLink` | string / null | (Admin) Link to *arr web UI |
|
||||
| `arrQueueId` | number / null | (Admin, import-pending only) Sonarr/Radarr queue record id |
|
||||
| `arrType` | `'sonarr'`/`'radarr'` / null | (Admin, import-pending only) Which *arr service owns this queue entry |
|
||||
| `arrInstanceUrl` | string / null | (Admin, import-pending only) Base URL of the *arr instance |
|
||||
| `arrInstanceKey` | string / null | (Admin, import-pending only) API key for the *arr instance |
|
||||
| `arrContentId` | number / null | (Admin, import-pending only) `episodeId` (Sonarr) or `movieId` (Radarr) for triggering a new search |
|
||||
| `arrContentType` | `'episode'`/`'movie'` / null | (Admin, import-pending only) Content type for the search command |
|
||||
|
||||
---
|
||||
|
||||
@@ -594,6 +602,48 @@ Admin-only per-user download counts (fetches live from APIs, not cached).
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/dashboard/blocklist-search`
|
||||
|
||||
Admin-only. Removes a Sonarr/Radarr queue item with `blocklist=true` (preventing the same release being grabbed again), then immediately triggers an `EpisodeSearch` or `MoviesSearch` command.
|
||||
|
||||
Requires CSRF token (`X-CSRF-Token` header).
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"arrQueueId": 1234,
|
||||
"arrType": "sonarr",
|
||||
"arrInstanceUrl": "https://sonarr.example.com",
|
||||
"arrInstanceKey": "your-api-key",
|
||||
"arrContentId": 5678,
|
||||
"arrContentType": "episode"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|:--------:|-------------|
|
||||
| `arrQueueId` | Yes | Sonarr/Radarr queue record `id` |
|
||||
| `arrType` | Yes | `"sonarr"` or `"radarr"` |
|
||||
| `arrInstanceUrl` | Yes | Base URL of the *arr instance |
|
||||
| `arrInstanceKey` | Yes | API key for the *arr instance |
|
||||
| `arrContentId` | Yes | `episodeId` (Sonarr) or `movieId` (Radarr) |
|
||||
| `arrContentType` | Yes | `"episode"` or `"movie"` |
|
||||
|
||||
**Response (200):** `{ "ok": true }`
|
||||
|
||||
**Response (400):** Missing or invalid fields.
|
||||
|
||||
**Response (403):** Non-admin user.
|
||||
|
||||
**Response (502):** Upstream *arr call failed.
|
||||
|
||||
**Side Effects:**
|
||||
- Calls `DELETE /api/v3/queue/{id}?removeFromClient=true&blocklist=true` on the *arr instance
|
||||
- Calls `POST /api/v3/command` with `EpisodeSearch`/`MoviesSearch` on the *arr instance
|
||||
- Triggers a background `pollAllServices()` so the next SSE push reflects the removed item
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/history/recent`
|
||||
|
||||
Returns recently completed (imported or failed) downloads from Sonarr/Radarr history for the authenticated user, filtered to the last `days` days.
|
||||
@@ -675,14 +725,19 @@ stateDiagram-v2
|
||||
|----------|---------|
|
||||
| `checkAuthentication()` | On load: check session → show dashboard or login |
|
||||
| `handleLogin()` | Authenticate, fade login → splash → dashboard |
|
||||
| `goHome()` | Navigate to default view: switch to Active Downloads tab, close status panel, reset showAll |
|
||||
| `startSSE()` | Open `EventSource` to `/stream`; handles incoming data + first-message loading hide |
|
||||
| `stopSSE()` | Close `EventSource` and cancel reconnect timer |
|
||||
| `renderDownloads()` | Diff-based card rendering (create/update/remove) |
|
||||
| `createDownloadCard()` | Build DOM for a single download card; renders tag badges |
|
||||
| `createDownloadCard()` | Build DOM for a single download card; renders tag badges, import-issue badge, blocklist button |
|
||||
| `updateDownloadCard()` | Update existing card in-place (progress, speed, etc.) |
|
||||
| `handleBlocklistSearch()` | Confirm dialog → POST `/blocklist-search` → update button state |
|
||||
| `toggleStatusPanel()` | Show/hide admin status panel |
|
||||
| `renderStatusPanel()` | Build status HTML (server, polling, SSE clients, cache) |
|
||||
| `initThemeSwitcher()` | Light / Dark / Mono theme support |
|
||||
| `loadHistory()` | Fetch `/api/history/recent`, store raw items, call `renderHistory()` |
|
||||
| `renderHistory()` | Filter items by `ignoreAvailable` flag, render history cards |
|
||||
| `createHistoryCard()` | Build DOM for a single history card with outcome/upgrade badges |
|
||||
|
||||
### Themes
|
||||
|
||||
@@ -1133,10 +1188,12 @@ classDiagram
|
||||
+GET /user-summary
|
||||
+GET /status
|
||||
+GET /cover-art
|
||||
+POST /blocklist-search
|
||||
buildDownloadPayload()
|
||||
extractUserTag()
|
||||
buildTagBadges()
|
||||
getEmbyUsers()
|
||||
getImportIssues()
|
||||
}
|
||||
class RequireAuth["requireAuth.js (Middleware)"] {
|
||||
+requireAuth(req, res, next)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sofarr",
|
||||
"version": "1.2.2",
|
||||
"version": "1.3.0",
|
||||
"description": "A personal media download dashboard that shows your downloads 'so far' while you relax on the sofa waiting for your *arr services to finish",
|
||||
"main": "server/index.js",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user