merge: develop into main (1.0.x doc + UI fixes)
Some checks failed
CI / Tests & coverage (push) Has been cancelled
CI / Security audit (push) Has been cancelled

This commit is contained in:
2026-05-17 10:06:44 +01:00
4 changed files with 33 additions and 18 deletions

View File

@@ -263,7 +263,7 @@ sofarr polls all configured services in the background and caches the results. D
### For qBittorrent Downloads
- **Seeds** - Number of seeders
- **Peers** - Number of peers
- **Availability** - Percentage available in swarm
- **Availability** - Percentage available in swarm (shown in red when below 100%)
## API Endpoints

View File

@@ -203,7 +203,7 @@ sofarr/
| Module | Mount Point | Auth Required | CSRF Required | Purpose |
|--------|------------|:-------------:|:-------------:|--------|
| `auth.js` | `/api/auth` | No | No | Login, session check, CSRF token, logout |
| `dashboard.js` | `/api/dashboard` | Yes (`requireAuth`) | Yes | Aggregated download data, status, cover-art proxy |
| `dashboard.js` | `/api/dashboard` | Yes (`requireAuth`) | Yes (except `/stream` — GET) | SSE stream, aggregated download data, status, cover-art proxy |
| `emby.js` | `/api/emby` | Yes (`requireAuth`) | Yes | Proxy to Emby API |
| `sabnzbd.js` | `/api/sabnzbd` | Yes (`requireAuth`) | Yes | Proxy to SABnzbd API |
| `sonarr.js` | `/api/sonarr` | Yes (`requireAuth`) | Yes | Proxy to Sonarr API |
@@ -293,7 +293,7 @@ For each connected user the server:
6. Sets an `httpOnly` `emby_user` cookie containing only `{ id, name, isAdmin }`:
- **`rememberMe: true`** → persistent cookie, `Max-Age` 30 days
- **`rememberMe: false`** → session cookie (no `Max-Age`; expires when browser closes)
- `secure` flag enabled when `NODE_ENV=production`
- `secure` flag enabled when `TRUST_PROXY` is set (i.e. a TLS-terminating reverse proxy is in front)
- Signed with HMAC when `COOKIE_SECRET` is set
7. Issues a `csrf_token` cookie (`httpOnly: false` so JS can read it) containing a 32-byte random hex token
8. Returns `{ success: true, user, csrfToken }` — the SPA stores `csrfToken` in memory and sends it as `X-CSRF-Token` on all subsequent state-changing requests
@@ -657,7 +657,7 @@ The status panel refreshes on a fixed 5-second timer and shows each SSE client w
| Variable | Required | Default | Description |
|----------|:--------:|---------|-------------|
| `PORT` | No | `3001` | Server listen port |
| `NODE_ENV` | No | — | Set to `production` to enable `secure` cookies and HTTPS upgrades |
| `NODE_ENV` | No | — | Set to `production` for production logging and startup validation. Does **not** control cookie `secure` flag or CSP `upgrade-insecure-requests` (both gated on `TRUST_PROXY`). |
| `DATA_DIR` | No | `./data` | Directory for `tokens.json` and `server.log`. Must be writable by the server process. In Docker: `/app/data` (named volume). |
| `COOKIE_SECRET` | No | — | If set, signs all session cookies with HMAC-SHA256. Strongly recommended in production. |
| `TRUST_PROXY` | No | — | Express `trust proxy` setting. Set to `1` (or a hop count) when behind a reverse proxy (nginx, Traefik, etc.) so `req.ip` and `req.secure` are correct. |
@@ -724,7 +724,7 @@ The production image uses a two-stage build on `node:22-alpine`:
2. **`runtime` stage** — copies `node_modules`, `server/`, `public/`, and `package.json`. Runs as the built-in non-root `node` user (UID 1000). `/app/data` is owned by `node` for writable token store and logs.
Key environment variables set in the image:
- `NODE_ENV=production` — enables secure cookies and HTTPS upgrade CSP directive
- `NODE_ENV=production` — enables production startup validation and logging
- `DATA_DIR=/app/data` — token store and log file location
### Docker Compose
@@ -762,7 +762,7 @@ volumes:
- **Set `COOKIE_SECRET`** — enables HMAC-signed cookies, preventing client-side forgery.
- **Set `TRUST_PROXY=1`** when behind a reverse proxy — ensures `req.secure` is `true` so the `secure` cookie flag is enforced and HTTPS-upgrade CSP fires.
- **Mount a named volume** for `DATA_DIR` — token store and log file survive container recreates.
- **Use HTTPS** — the CSP includes `upgrade-insecure-requests` in production and the HSTS header is set with a 1-year `maxAge`.
- **Use HTTPS** — set `TRUST_PROXY=1` to enable the CSP `upgrade-insecure-requests` directive, the `secure` cookie flag, and HSTS (1-year `maxAge`).
- **Rate limiting** — the login endpoint is limited to 10 failed attempts per IP per 15 minutes; all API endpoints share a 300 req/15 min window. Set `TRUST_PROXY` correctly so the client IP is not the proxy's IP.
### CI / CD

View File

@@ -359,9 +359,10 @@ function updateDownloadCard(card, download) {
peersEl.textContent = download.peers;
}
const availabilityEl = card.querySelector('.detail-item[data-label="Availability"] .detail-value');
if (availabilityEl && download.availability !== undefined) {
availabilityEl.textContent = `${download.availability}%`;
const availabilityItem = card.querySelector('.detail-item[data-label="Availability"]');
if (availabilityItem && download.availability !== undefined) {
availabilityItem.querySelector('.detail-value').textContent = `${download.availability}%`;
availabilityItem.classList.toggle('availability-warning', parseFloat(download.availability) < 100);
}
}
}
@@ -558,6 +559,7 @@ function createDownloadCard(download) {
if (download.availability !== undefined) {
const availability = createDetailItem('Availability', `${download.availability}%`);
if (parseFloat(download.availability) < 100) availability.classList.add('availability-warning');
details.appendChild(availability);
}
}

View File

@@ -462,17 +462,26 @@ body {
.download-details {
display: flex;
flex-wrap: wrap;
gap: 4px 14px;
gap: 4px 6px;
padding-top: 6px;
border-top: 1px solid var(--border);
align-items: center;
}
.detail-item {
display: flex;
display: inline-flex;
align-items: baseline;
gap: 4px;
gap: 3px;
font-size: 0.78rem;
background: var(--bg-secondary, rgba(0,0,0,0.04));
border-radius: 4px;
padding: 2px 6px;
white-space: nowrap;
}
.detail-item.availability-warning .detail-value {
color: var(--danger, #e53e3e);
font-weight: 700;
}
.detail-label {
@@ -491,10 +500,17 @@ body {
/* ===== Progress Bar (Compact) ===== */
.progress-item {
flex-basis: 100%;
display: flex;
align-items: center;
background: none;
padding: 0;
white-space: normal;
border-radius: 0;
}
.progress-container {
display: flex;
flex: 1;
align-items: center;
gap: 8px;
}
@@ -510,13 +526,15 @@ body {
}
.progress-segment {
position: absolute;
top: 0;
left: 0;
height: 100%;
transition: width 0.3s ease;
}
.progress-segment.downloaded {
background: linear-gradient(90deg, var(--progress-fill-start) 0%, var(--progress-fill-end) 100%);
float: left;
}
.progress-text {
@@ -1021,11 +1039,6 @@ body {
width: 40px;
}
.download-details {
flex-direction: column;
gap: 2px;
}
.progress-container {
flex-wrap: wrap;
}