docs: comprehensive architecture documentation with PlantUML diagrams
- docs/ARCHITECTURE.md: full system overview, technology stack, directory structure, component architecture, data flow, auth, polling/caching, download matching pipeline, API reference, frontend architecture, configuration, deployment guide - docs/diagrams/component.puml: system component diagram - docs/diagrams/seq-auth.puml: authentication sequence diagram - docs/diagrams/seq-dashboard.puml: dashboard request sequence diagram - docs/diagrams/seq-polling.puml: background polling cycle sequence - docs/diagrams/class-server.puml: server-side class/module diagram - docs/diagrams/class-data.puml: data model / entity diagram - docs/diagrams/state-ui.puml: frontend UI state diagram - docs/diagrams/state-poller.puml: poller state diagram - docs/diagrams/activity-matching.puml: download matching activity diagram
This commit is contained in:
609
docs/ARCHITECTURE.md
Normal file
609
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,609 @@
|
||||
# sofarr — Architecture Documentation
|
||||
|
||||
Comprehensive technical documentation covering the full architecture of the **sofarr** application: a personal media download dashboard that aggregates downloads from SABnzbd, Sonarr, Radarr, and qBittorrent, filtered by Emby user identity.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [System Overview](#1-system-overview)
|
||||
2. [Technology Stack](#2-technology-stack)
|
||||
3. [Directory Structure](#3-directory-structure)
|
||||
4. [Component Architecture](#4-component-architecture)
|
||||
5. [Data Flow](#5-data-flow)
|
||||
6. [Authentication & Authorisation](#6-authentication--authorisation)
|
||||
7. [Background Polling & Caching](#7-background-polling--caching)
|
||||
8. [Download Matching Pipeline](#8-download-matching-pipeline)
|
||||
9. [API Reference](#9-api-reference)
|
||||
10. [Frontend Architecture](#10-frontend-architecture)
|
||||
11. [Configuration](#11-configuration)
|
||||
12. [Deployment](#12-deployment)
|
||||
13. [UML Diagrams (PlantUML)](#13-uml-diagrams-plantuml)
|
||||
|
||||
---
|
||||
|
||||
## 1. System Overview
|
||||
|
||||
sofarr is a **single-page web application** with a Node.js/Express backend. It provides a personalised view of media downloads by:
|
||||
|
||||
1. **Authenticating** users against an Emby/Jellyfin media server.
|
||||
2. **Aggregating** download data from multiple *arr service instances and download clients.
|
||||
3. **Filtering** downloads by user — each user only sees media tagged with their username in Sonarr/Radarr.
|
||||
4. **Presenting** a real-time dashboard with progress, speeds, cover art, and status.
|
||||
|
||||
Admin users can view all users' downloads, see server status, cache statistics, and poll timings.
|
||||
|
||||
### High-Level Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Browser (SPA) │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
|
||||
│ │ Login │ │Dashboard │ │ Status Panel │ │
|
||||
│ │ Form │ │ Cards │ │ (Admin only) │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └───────┬───────────┘ │
|
||||
│ │ │ │ │
|
||||
└───────┼──────────────┼────────────────┼──────────────┘
|
||||
│ POST /login │ GET /user- │ GET /status
|
||||
│ │ downloads │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Express Server (:3001) │
|
||||
│ ┌────────┐ ┌──────────┐ ┌────────┐ ┌────────────┐ │
|
||||
│ │ Auth │ │Dashboard │ │ Emby │ │ Static │ │
|
||||
│ │ Routes │ │ Routes │ │ Routes │ │ Files │ │
|
||||
│ └────┬───┘ └────┬─────┘ └────┬───┘ └────────────┘ │
|
||||
│ │ │ │ │
|
||||
│ ┌────┴──────────┴────────────┴──────────────────┐ │
|
||||
│ │ Utilities Layer │ │
|
||||
│ │ ┌────────┐ ┌────────┐ ┌──────┐ ┌──────────┐ │ │
|
||||
│ │ │ Poller │ │ Cache │ │Config│ │qBittorrent│ │ │
|
||||
│ │ └───┬────┘ └────────┘ └──────┘ └──────────┘ │ │
|
||||
│ └──────┼────────────────────────────────────────┘ │
|
||||
└─────────┼────────────────────────────────────────────┘
|
||||
│ HTTP/API calls
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ External Services │
|
||||
│ ┌──────────┐ ┌────────┐ ┌────────┐ ┌────────────┐ │
|
||||
│ │ SABnzbd │ │ Sonarr │ │ Radarr │ │qBittorrent │ │
|
||||
│ │ (Usenet) │ │ (TV) │ │(Movie) │ │ (Torrent) │ │
|
||||
│ └──────────┘ └────────┘ └────────┘ └────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ Emby / Jellyfin │ │
|
||||
│ │ (Authentication + User DB) │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Technology Stack
|
||||
|
||||
| Layer | Technology | Purpose |
|
||||
|-------|-----------|---------|
|
||||
| **Runtime** | Node.js 18+ | Server runtime |
|
||||
| **Framework** | Express 4.x | HTTP server, routing, middleware |
|
||||
| **HTTP Client** | axios 1.x | External API communication |
|
||||
| **Frontend** | Vanilla JS + CSS | Single-page app, no build step |
|
||||
| **Auth** | Emby API + httpOnly cookies | Session management |
|
||||
| **Caching** | In-memory Map with TTL | Reduce external API load |
|
||||
| **Scheduling** | `setInterval` | Background polling |
|
||||
| **Containerisation** | Docker (Alpine) | Production deployment |
|
||||
| **Logging** | Custom logger + `console.*` | File + stdout logging with levels |
|
||||
|
||||
---
|
||||
|
||||
## 3. Directory Structure
|
||||
|
||||
```
|
||||
sofarr/
|
||||
├── server/ # Backend application
|
||||
│ ├── index.js # Entry point: Express setup, middleware, startup
|
||||
│ ├── routes/
|
||||
│ │ ├── auth.js # POST /login, GET /me, POST /logout
|
||||
│ │ ├── dashboard.js # GET /user-downloads, /user-summary, /status
|
||||
│ │ ├── 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
|
||||
│ └── utils/
|
||||
│ ├── cache.js # MemoryCache class (Map + TTL + stats)
|
||||
│ ├── config.js # Multi-instance service configuration parser
|
||||
│ ├── logger.js # File logger (server.log)
|
||||
│ ├── poller.js # Background polling engine + timing
|
||||
│ └── qbittorrent.js # qBittorrent client with auth + torrent mapping
|
||||
├── public/ # Static frontend (served by Express)
|
||||
│ ├── index.html # HTML shell: splash, login, dashboard
|
||||
│ ├── app.js # All frontend logic (auth, rendering, status)
|
||||
│ ├── style.css # Themes, layout, responsive design
|
||||
│ └── images/ # Logo / splash screen assets
|
||||
├── Dockerfile # Production container image
|
||||
├── docker-compose.yaml # Example compose deployment
|
||||
├── package.json # Dependencies and scripts
|
||||
├── .env.sample # Annotated environment variable template
|
||||
└── README.md # User-facing documentation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Component Architecture
|
||||
|
||||
### 4.1 Server Entry Point (`server/index.js`)
|
||||
|
||||
Responsibilities:
|
||||
- Load environment variables via `dotenv`
|
||||
- Configure structured logging with level filtering (`LOG_LEVEL`)
|
||||
- Redirect `console.*` to both stdout and `server.log`
|
||||
- Mount Express middleware (CORS, cookie-parser, JSON, static files)
|
||||
- Mount route modules under `/api/*`
|
||||
- Start the background poller
|
||||
|
||||
### 4.2 Route Modules
|
||||
|
||||
| Module | Mount Point | Auth Required | Purpose |
|
||||
|--------|------------|---------------|---------|
|
||||
| `auth.js` | `/api/auth` | No | Login, session check, logout |
|
||||
| `dashboard.js` | `/api/dashboard` | Yes (cookie) | Aggregated download data, status |
|
||||
| `emby.js` | `/api/emby` | No | Proxy to Emby API |
|
||||
| `sabnzbd.js` | `/api/sabnzbd` | No | Proxy to SABnzbd API |
|
||||
| `sonarr.js` | `/api/sonarr` | No | Proxy to Sonarr API |
|
||||
| `radarr.js` | `/api/radarr` | No | Proxy to Radarr API |
|
||||
|
||||
> **Note:** The proxy routes (`emby`, `sabnzbd`, `sonarr`, `radarr`) use legacy single-instance env vars and are largely unused by the dashboard — they exist for direct API access. The dashboard reads from the poller cache.
|
||||
|
||||
### 4.3 Utility Modules
|
||||
|
||||
**`config.js`** — Parses service instances from environment variables. Supports both JSON array format (`SONARR_INSTANCES=[{...}]`) and legacy single-instance format (`SONARR_URL` + `SONARR_API_KEY`). Each instance gets an `id` derived from `name` or index.
|
||||
|
||||
**`cache.js`** — Singleton `MemoryCache` backed by a `Map`. Each entry has an expiration timestamp. Provides `get`, `set`, `invalidate`, `clear`, and `getStats` (returns per-key size, item count, TTL remaining).
|
||||
|
||||
**`poller.js`** — Background polling engine. Fetches data from all configured service instances in parallel with per-task timing. Stores results in the cache with a configurable TTL. Can be disabled entirely (`POLL_INTERVAL=0`), in which case data is fetched on-demand per dashboard request.
|
||||
|
||||
**`qbittorrent.js`** — `QBittorrentClient` class with cookie-based authentication, automatic re-auth on 403, and persistent client instances. Includes torrent-to-download mapping (`mapTorrentToDownload`) and formatting utilities.
|
||||
|
||||
**`logger.js`** — Simple file appender writing timestamped messages to `server.log`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Flow
|
||||
|
||||
### 5.1 Polling Cycle
|
||||
|
||||
Every `POLL_INTERVAL` ms (default 5000), the poller fetches from all services in parallel:
|
||||
|
||||
| Task | API Call | Params |
|
||||
|------|----------|--------|
|
||||
| SABnzbd Queue | `GET /api?mode=queue` | `output=json` |
|
||||
| SABnzbd History | `GET /api?mode=history` | `limit=10` |
|
||||
| Sonarr Tags | `GET /api/v3/tag` | — |
|
||||
| Sonarr Queue | `GET /api/v3/queue` | `includeSeries=true` |
|
||||
| Sonarr History | `GET /api/v3/history` | `pageSize=10` |
|
||||
| Radarr Queue | `GET /api/v3/queue` | `includeMovie=true` |
|
||||
| Radarr History | `GET /api/v3/history` | `pageSize=10` |
|
||||
| Radarr Tags | `GET /api/v3/tag` | — |
|
||||
| qBittorrent | `GET /api/v2/torrents/info` | — |
|
||||
|
||||
Results are stored in the cache under `poll:*` keys with a TTL of `POLL_INTERVAL × 3`.
|
||||
|
||||
### 5.2 Dashboard Request
|
||||
|
||||
When a user requests `/api/dashboard/user-downloads`:
|
||||
|
||||
1. Read all `poll:*` keys from cache
|
||||
2. Build `seriesMap` and `moviesMap` from embedded objects in queue records
|
||||
3. Build `sonarrTagMap` and `radarrTagMap` from tag data
|
||||
4. For each SABnzbd queue slot → try to match against Sonarr/Radarr queue records by title
|
||||
5. For each SABnzbd history slot → try to match against Sonarr/Radarr history records
|
||||
6. For each qBittorrent torrent → try to match against Sonarr/Radarr queue, then history
|
||||
7. For each match, resolve the series/movie, extract the user tag, check if it belongs to the requesting user
|
||||
8. Return only the user's downloads (or all, if admin with `showAll=true`)
|
||||
|
||||
---
|
||||
|
||||
## 6. Authentication & Authorisation
|
||||
|
||||
### Flow
|
||||
|
||||
1. User submits credentials via the login form
|
||||
2. Backend calls Emby `POST /Users/authenticatebyname`
|
||||
3. On success, fetches full user profile to determine admin status
|
||||
4. Sets an `httpOnly` cookie (`emby_user`) containing: `{ id, name, isAdmin, token }`
|
||||
5. Cookie expires after 24 hours
|
||||
6. All subsequent dashboard requests read this cookie for identity
|
||||
|
||||
### Authorisation Matrix
|
||||
|
||||
| Feature | Regular User | Admin |
|
||||
|---------|:----------:|:-----:|
|
||||
| View own downloads | ✓ | ✓ |
|
||||
| View all users' downloads | ✗ | ✓ (`showAll`) |
|
||||
| See download/target paths | ✗ | ✓ |
|
||||
| See Sonarr/Radarr links | ✗ | ✓ |
|
||||
| View status panel | ✗ | ✓ |
|
||||
|
||||
### Tag Matching
|
||||
|
||||
Users are matched to downloads via tags in Sonarr/Radarr:
|
||||
|
||||
1. **Exact match**: tag label (lowercased) === username (lowercased)
|
||||
2. **Sanitised match**: handles Ombi's tag mangling — `sanitizeTagLabel()` converts to lowercase, replaces non-alphanumeric with hyphens, collapses, trims
|
||||
|
||||
---
|
||||
|
||||
## 7. Background Polling & Caching
|
||||
|
||||
### Polling Modes
|
||||
|
||||
| Mode | `POLL_INTERVAL` | Behaviour |
|
||||
|------|----------------|-----------|
|
||||
| **Background** | `> 0` (e.g. `5000`) | Periodic fetch every N ms |
|
||||
| **On-demand** | `0` / `off` / `false` | Fetch triggered by first user request when cache is empty |
|
||||
|
||||
### Cache Keys
|
||||
|
||||
| Key | Content | Source |
|
||||
|-----|---------|--------|
|
||||
| `poll:sab-queue` | `{ slots, status, speed, kbpersec }` | SABnzbd queue |
|
||||
| `poll:sab-history` | `{ slots }` | SABnzbd history |
|
||||
| `poll:sonarr-tags` | `[{ instance, data: [{id, label}] }]` | Sonarr tag API |
|
||||
| `poll:sonarr-queue` | `{ records }` — includes embedded `series` objects | Sonarr queue (includeSeries) |
|
||||
| `poll:sonarr-history` | `{ records }` — lightweight, no embedded objects | Sonarr history |
|
||||
| `poll:radarr-queue` | `{ records }` — includes embedded `movie` objects | Radarr queue (includeMovie) |
|
||||
| `poll:radarr-history` | `{ records }` — lightweight, no embedded objects | Radarr history |
|
||||
| `poll:radarr-tags` | `[{id, label}]` | Radarr tag API |
|
||||
| `poll:qbittorrent` | `[torrent, ...]` | qBittorrent all torrents |
|
||||
|
||||
### TTL Strategy
|
||||
|
||||
- **Polling enabled**: `POLL_INTERVAL × 3` — ensures data survives between polls even if one poll is slow
|
||||
- **Polling disabled**: `30000` ms — stale after 30s, next request triggers a fresh fetch
|
||||
|
||||
### Active Client Tracking
|
||||
|
||||
Each dashboard request reports the client's refresh rate. The server tracks active clients in a `Map<username, { user, refreshRateMs, lastSeen }>`, pruning entries older than 30 seconds. This data powers the admin status panel's "effective refresh mode" display.
|
||||
|
||||
---
|
||||
|
||||
## 8. Download Matching Pipeline
|
||||
|
||||
The core logic in `dashboard.js` matches raw download client data to *arr service metadata to determine which user each download belongs to.
|
||||
|
||||
### Matching Strategy
|
||||
|
||||
For each download item (SABnzbd slot or qBittorrent torrent):
|
||||
|
||||
```
|
||||
1. Try Sonarr QUEUE match (by title substring)
|
||||
→ resolve series via seriesMap (embedded in queue record)
|
||||
→ extract user tag → check tag matches requesting user
|
||||
|
||||
2. Try Radarr QUEUE match (by title substring)
|
||||
→ resolve movie via moviesMap (embedded in queue record)
|
||||
→ extract user tag → check tag matches requesting user
|
||||
|
||||
3. Try Sonarr HISTORY match (by title substring)
|
||||
→ resolve series via seriesMap (from queue) using seriesId
|
||||
→ extract user tag → check tag matches requesting user
|
||||
|
||||
4. Try Radarr HISTORY match (by title substring)
|
||||
→ resolve movie via moviesMap (from queue) using movieId
|
||||
→ extract user tag → check tag matches requesting user
|
||||
```
|
||||
|
||||
### Title Matching
|
||||
|
||||
Matches are **bidirectional substring matches** (case-insensitive):
|
||||
```javascript
|
||||
rTitle.includes(downloadTitle) || downloadTitle.includes(rTitle)
|
||||
```
|
||||
|
||||
### Download Object Structure
|
||||
|
||||
Each matched download produces an object with:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `type` | `'series'` / `'movie'` / `'torrent'` | Media type |
|
||||
| `title` | string | Raw download title |
|
||||
| `coverArt` | string / null | Poster URL from *arr |
|
||||
| `status` | string | Download status |
|
||||
| `progress` | string | Percentage complete |
|
||||
| `size` / `mb` / `mbmissing` | string / number | Size info |
|
||||
| `speed` | string | Current download speed |
|
||||
| `eta` | string | Estimated time remaining |
|
||||
| `seriesName` / `movieName` | string | Friendly media title |
|
||||
| `episodeInfo` / `movieInfo` | object | Full *arr queue/history record |
|
||||
| `userTag` | string | Matched user tag |
|
||||
| `importIssues` | string[] / null | Import warning/error messages |
|
||||
| `downloadPath` | string / null | (Admin) Download client path |
|
||||
| `targetPath` | string / null | (Admin) *arr target path |
|
||||
| `arrLink` | string / null | (Admin) Link to *arr web UI |
|
||||
|
||||
---
|
||||
|
||||
## 9. API Reference
|
||||
|
||||
### `POST /api/auth/login`
|
||||
|
||||
Authenticate a user via Emby.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{ "username": "string", "password": "string" }
|
||||
```
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"user": { "id": "string", "name": "string", "isAdmin": true }
|
||||
}
|
||||
```
|
||||
|
||||
**Response (401):**
|
||||
```json
|
||||
{ "success": false, "error": "Invalid username or password" }
|
||||
```
|
||||
|
||||
**Side Effect:** Sets `emby_user` httpOnly cookie (24h TTL).
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/auth/me`
|
||||
|
||||
Check current session.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"authenticated": true,
|
||||
"user": { "id": "string", "name": "string", "isAdmin": false }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/auth/logout`
|
||||
|
||||
Clear session cookie.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/dashboard/user-downloads`
|
||||
|
||||
Fetch downloads for the authenticated user.
|
||||
|
||||
**Query Parameters:**
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `showAll` | `"true"` | (Admin) Show all users' downloads |
|
||||
| `refreshRate` | number (ms) | Client's current refresh rate for tracking |
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"user": "string",
|
||||
"isAdmin": true,
|
||||
"downloads": [ /* download objects */ ]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/dashboard/status`
|
||||
|
||||
Admin-only server status.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"server": {
|
||||
"uptimeSeconds": 3600,
|
||||
"nodeVersion": "v18.19.0",
|
||||
"memoryUsageMB": 45.2,
|
||||
"heapUsedMB": 28.1,
|
||||
"heapTotalMB": 35.0
|
||||
},
|
||||
"polling": {
|
||||
"enabled": true,
|
||||
"intervalMs": 5000,
|
||||
"lastPoll": {
|
||||
"totalMs": 1234,
|
||||
"timestamp": "2026-05-16T00:00:00.000Z",
|
||||
"tasks": [
|
||||
{ "label": "SABnzbd Queue", "ms": 120 },
|
||||
{ "label": "Sonarr Queue", "ms": 890 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"entryCount": 9,
|
||||
"totalSizeBytes": 51200,
|
||||
"entries": [
|
||||
{ "key": "poll:sab-queue", "sizeBytes": 1024, "itemCount": 3, "ttlRemainingMs": 12000, "expired": false }
|
||||
]
|
||||
},
|
||||
"clients": [
|
||||
{ "user": "Alice", "refreshRateMs": 5000, "lastSeen": 1715817600000 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/dashboard/user-summary`
|
||||
|
||||
Admin-only per-user download counts (fetches live from APIs, not cached).
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
[
|
||||
{ "username": "Alice", "seriesCount": 12, "movieCount": 5 }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Frontend Architecture
|
||||
|
||||
The frontend is a **vanilla JavaScript SPA** with no build step. All logic resides in `app.js` (754 lines), styled by `style.css`, and structured by `index.html`.
|
||||
|
||||
### UI States
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Splash Screen│────▶│ Login Form │────▶│ Dashboard │
|
||||
│ (on load) │ │ (if no │ │ (after auth) │
|
||||
│ │ │ session) │ │ │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
│
|
||||
┌─────┴─────┐
|
||||
│ Status │
|
||||
│ Panel │
|
||||
│ (admin) │
|
||||
└───────────┘
|
||||
```
|
||||
|
||||
### Key Frontend Functions
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `checkAuthentication()` | On load: check session → show dashboard or login |
|
||||
| `handleLogin()` | Authenticate, fade login → splash → dashboard |
|
||||
| `fetchUserDownloads()` | GET `/user-downloads`, update state, re-render |
|
||||
| `renderDownloads()` | Diff-based card rendering (create/update/remove) |
|
||||
| `createDownloadCard()` | Build DOM for a single download card |
|
||||
| `updateDownloadCard()` | Update existing card in-place (progress, speed, etc.) |
|
||||
| `toggleStatusPanel()` | Show/hide admin status panel |
|
||||
| `renderStatusPanel()` | Build status HTML (server, polling, cache, clients) |
|
||||
| `startAutoRefresh()` | Start periodic `fetchUserDownloads` |
|
||||
| `initThemeSwitcher()` | Light / Dark / Mono theme support |
|
||||
|
||||
### Themes
|
||||
|
||||
Three CSS themes via `data-theme` attribute on `<html>`:
|
||||
- **Light** — Purple gradient header, white cards
|
||||
- **Dark** — Dark surfaces, muted accents
|
||||
- **Mono** — Monochrome, minimal colour
|
||||
|
||||
Theme selection persists in `localStorage`.
|
||||
|
||||
### Auto-Refresh
|
||||
|
||||
The dashboard polls the server at the user-selected interval (1s, 5s, 10s, or Off). The status panel auto-refreshes in sync. Client refresh rate is sent to the server on each request for active-client tracking.
|
||||
|
||||
---
|
||||
|
||||
## 11. Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|:--------:|---------|-------------|
|
||||
| `PORT` | No | `3001` | Server listen port |
|
||||
| `EMBY_URL` | Yes | — | Emby/Jellyfin server URL |
|
||||
| `EMBY_API_KEY` | Yes | — | Emby API key |
|
||||
| `SONARR_INSTANCES` | Yes* | — | JSON array of Sonarr instances |
|
||||
| `SONARR_URL` | Yes* | — | Legacy single Sonarr URL |
|
||||
| `SONARR_API_KEY` | Yes* | — | Legacy single Sonarr API key |
|
||||
| `RADARR_INSTANCES` | Yes* | — | JSON array of Radarr instances |
|
||||
| `RADARR_URL` | Yes* | — | Legacy single Radarr URL |
|
||||
| `RADARR_API_KEY` | Yes* | — | Legacy single Radarr API key |
|
||||
| `SABNZBD_INSTANCES` | Yes* | — | JSON array of SABnzbd instances |
|
||||
| `SABNZBD_URL` | Yes* | — | Legacy single SABnzbd URL |
|
||||
| `SABNZBD_API_KEY` | Yes* | — | Legacy single SABnzbd API key |
|
||||
| `QBITTORRENT_INSTANCES` | No | — | JSON array of qBittorrent instances |
|
||||
| `POLL_INTERVAL` | No | `5000` | Poll interval in ms, or `off`/`0` to disable |
|
||||
| `LOG_LEVEL` | No | `info` | `debug`, `info`, `warn`, `error`, `silent` |
|
||||
|
||||
\* Either `*_INSTANCES` (JSON array) or legacy `*_URL` + `*_API_KEY` format is required.
|
||||
|
||||
### Instance JSON Format
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "main",
|
||||
"url": "https://sonarr.example.com",
|
||||
"apiKey": "your-api-key"
|
||||
},
|
||||
{
|
||||
"name": "4k",
|
||||
"url": "https://sonarr4k.example.com",
|
||||
"apiKey": "your-4k-api-key"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
qBittorrent instances use `username` and `password` instead of `apiKey`.
|
||||
|
||||
---
|
||||
|
||||
## 12. Deployment
|
||||
|
||||
### Docker
|
||||
|
||||
```dockerfile
|
||||
FROM node:18-alpine
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY server/ ./server/
|
||||
COPY public/ ./public/
|
||||
EXPOSE 3001
|
||||
ENV NODE_ENV=production
|
||||
CMD ["node", "server/index.js"]
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
sofarr:
|
||||
image: docker.i3omb.com/sofarr:latest
|
||||
container_name: sofarr
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3001:3001"
|
||||
environment:
|
||||
- EMBY_URL=https://emby.example.com
|
||||
- EMBY_API_KEY=your-emby-api-key
|
||||
- SONARR_INSTANCES=[{"name":"main","url":"...","apiKey":"..."}]
|
||||
- RADARR_INSTANCES=[{"name":"main","url":"...","apiKey":"..."}]
|
||||
- SABNZBD_INSTANCES=[{"name":"main","url":"...","apiKey":"..."}]
|
||||
- QBITTORRENT_INSTANCES=[{"name":"main","url":"...","username":"...","password":"..."}]
|
||||
- POLL_INTERVAL=5000
|
||||
- LOG_LEVEL=info
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. UML Diagrams (PlantUML)
|
||||
|
||||
All diagrams below are in PlantUML syntax. Render with any PlantUML tool or VS Code extension.
|
||||
|
||||
### 13.1 Component Diagram
|
||||
|
||||
See [`diagrams/component.puml`](diagrams/component.puml)
|
||||
|
||||
### 13.2 Sequence Diagrams
|
||||
|
||||
- **Authentication**: [`diagrams/seq-auth.puml`](diagrams/seq-auth.puml)
|
||||
- **Dashboard Request**: [`diagrams/seq-dashboard.puml`](diagrams/seq-dashboard.puml)
|
||||
- **Polling Cycle**: [`diagrams/seq-polling.puml`](diagrams/seq-polling.puml)
|
||||
|
||||
### 13.3 Class / Entity Diagrams
|
||||
|
||||
- **Server Classes**: [`diagrams/class-server.puml`](diagrams/class-server.puml)
|
||||
- **Data Model**: [`diagrams/class-data.puml`](diagrams/class-data.puml)
|
||||
|
||||
### 13.4 State Diagrams
|
||||
|
||||
- **Frontend UI States**: [`diagrams/state-ui.puml`](diagrams/state-ui.puml)
|
||||
- **Poller States**: [`diagrams/state-poller.puml`](diagrams/state-poller.puml)
|
||||
|
||||
### 13.5 Activity Diagram
|
||||
|
||||
- **Download Matching**: [`diagrams/activity-matching.puml`](diagrams/activity-matching.puml)
|
||||
Reference in New Issue
Block a user