From fa3c625fb86970bca24996b1d35ca5b7d0b49fb0 Mon Sep 17 00:00:00 2001 From: Gronod Date: Sun, 17 May 2026 12:05:53 +0100 Subject: [PATCH] docs: update ARCHITECTURE.md and README for history feature (v2) --- docs/ARCHITECTURE.md | 62 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6226984..50d013f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -132,13 +132,15 @@ sofarr/ │ │ ├── emby.js # Proxy routes to Emby API │ │ ├── sabnzbd.js # Proxy routes to SABnzbd API │ │ ├── sonarr.js # Proxy routes to Sonarr API -│ │ └── radarr.js # Proxy routes to Radarr API +│ │ ├── radarr.js # Proxy routes to Radarr API +│ │ └── history.js # GET /api/history/recent — recently completed downloads │ ├── middleware/ │ │ ├── requireAuth.js # httpOnly cookie auth enforcement │ │ └── verifyCsrf.js # CSRF double-submit cookie validation │ └── utils/ │ ├── cache.js # MemoryCache class (Map + TTL + stats) │ ├── config.js # Multi-instance service configuration parser +│ ├── historyFetcher.js # Fetch + cache Sonarr/Radarr history; event classification │ ├── logger.js # File logger (DATA_DIR/server.log) │ ├── poller.js # Background polling engine + timing │ ├── qbittorrent.js # qBittorrent client with auth + torrent mapping @@ -209,6 +211,7 @@ sofarr/ | `sabnzbd.js` | `/api/sabnzbd` | Yes (`requireAuth`) | Yes | Proxy to SABnzbd API | | `sonarr.js` | `/api/sonarr` | Yes (`requireAuth`) | Yes | Proxy to Sonarr API | | `radarr.js` | `/api/radarr` | Yes (`requireAuth`) | Yes | Proxy to Radarr API | +| `history.js` | `/api/history` | Yes (`requireAuth`) | No (GET only) | Recently completed downloads from Sonarr/Radarr history | **`requireAuth`** (`server/middleware/requireAuth.js`) reads the `emby_user` cookie (signed if `COOKIE_SECRET` is set) and attaches the parsed `{ id, name, isAdmin }` user to `req.user`. Returns `401` if the cookie is absent, tampered, or schema-invalid. @@ -230,6 +233,8 @@ sofarr/ **`sanitizeError.js`** — Redacts secrets from error message strings before they are logged or returned in API responses. Patterns: URL query-param secrets (`apikey=`, `token=`, etc.), HTTP auth headers (`Authorization:`, `X-Emby-Authorization:`, etc.), Bearer tokens, and basic-auth credentials in URLs. +**`historyFetcher.js`** — Fetches history records from all Sonarr/Radarr instances for a configurable date window (`since`). Results are cached under `history:sonarr` / `history:radarr` for 5 minutes. Exports `classifySonarrEvent` / `classifyRadarrEvent` (returns `'imported'` | `'failed'` | `'other'`) and `invalidateHistoryCache`. + **`logger.js`** — Simple file appender writing timestamped messages to `DATA_DIR/server.log`. --- @@ -342,6 +347,8 @@ Users are matched to downloads via tags in Sonarr/Radarr: | `poll:radarr-tags` | `[{id, label}]` | Radarr tag API | | `poll:qbittorrent` | `[torrent, ...]` | qBittorrent all torrents | | `emby:users` | `Map` | Full Emby user list (60s TTL) | +| `history:sonarr` | `[record, ...]` — flat array with `_instanceUrl` / `_instanceName` | Sonarr history (5 min TTL, fetched on-demand by `/api/history/recent`) | +| `history:radarr` | `[record, ...]` — flat array with `_instanceUrl` / `_instanceName` | Radarr history (5 min TTL, fetched on-demand by `/api/history/recent`) | ### TTL Strategy @@ -587,6 +594,50 @@ Admin-only per-user download counts (fetches live from APIs, not cached). --- +### `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. + +**Query Parameters:** +| Param | Type | Default | Description | +|-------|------|---------|-------------| +| `days` | integer | `RECENT_COMPLETED_DAYS` env (default `7`) | How many days back to search. Capped at 90. | +| `showAll` | `"true"` | — | (Admin) Return records for all tagged users, not just the current user | + +**Response (200):** +```json +{ + "user": "Alice", + "isAdmin": false, + "days": 7, + "history": [ + { + "type": "series", + "outcome": "imported", + "title": "Show.S01E01.720p", + "seriesName": "My Show", + "coverArt": "https://…/poster.jpg", + "completedAt": "2026-05-15T18:00:00.000Z", + "quality": "720p", + "instanceName": "Main Sonarr", + "arrLink": "https://sonarr.example.com/series/my-show", + "allTags": ["alice"], + "matchedUserTag": "alice", + "arrRecordId": 1234, + "failureMessage": null + } + ] +} +``` + +- `outcome` is `"imported"` or `"failed"`. Records with other event types (e.g. `grabbed`) are filtered out. +- `failureMessage` is only included when the authenticated user is an admin and `outcome` is `"failed"`. +- `arrRecordId` is only included for admin users. +- Results are sorted newest first. +- History data is cached server-side for 5 minutes (`history:sonarr` / `history:radarr` cache keys). + +--- + ## 10. Frontend Architecture The frontend is a **vanilla JavaScript SPA** with no build step. All logic resides in `app.js`, styled by `style.css`, and structured by `index.html`. @@ -701,6 +752,7 @@ The status panel refreshes on a fixed 5-second timer and shows each SSE client w | Variable | Required | Default | Description | |----------|:--------:|---------|-------------| | `POLL_INTERVAL` | No | `5000` | Poll interval in ms. Set to `0`, `off`, or `false` to disable background polling (on-demand mode). | +| `RECENT_COMPLETED_DAYS` | No | `7` | Default lookback window (days) for `GET /api/history/recent`. Overridable per-request via `?days=`. Max 90. | | `LOG_LEVEL` | No | `info` | `debug`, `info`, `warn`, `error`, `silent` | ### Instance JSON Format @@ -835,6 +887,7 @@ graph TB sab_r[sabnzbd.js\n/api/sabnzbd] sonarr_r[sonarr.js\n/api/sonarr] radarr_r[radarr.js\n/api/radarr] + history_r[history.js\n/api/history] end subgraph Utilities @@ -845,6 +898,7 @@ graph TB tokenstore[tokenStore.js\ntokens.json] sanitize[sanitizeError.js] logger[logger.js] + historyfetcher[historyFetcher.js] end entry --> appfactory @@ -854,11 +908,13 @@ graph TB appfactory --> hm & rl & cp & ej appfactory -->|pre-CSRF| auth appfactory --> verifycsrf - appfactory --> dashboard & emby_r & sab_r & sonarr_r & radarr_r + appfactory --> dashboard & emby_r & sab_r & sonarr_r & radarr_r & history_r - dashboard & emby_r & sab_r & sonarr_r & radarr_r --> requireauth + dashboard & emby_r & sab_r & sonarr_r & radarr_r & history_r --> requireauth auth --> tokenstore dashboard --> cache & poller & config & qbt + history_r --> cache & config & historyfetcher + historyfetcher --> cache & config poller --> cache & config & qbt & logger qbt --> config & logger auth & dashboard -.-> sanitize