Commit Graph

66 Commits

Author SHA1 Message Date
d11f11be69 Fix missing speed on SAB cards and remove incorrect missing pieces display
All checks were successful
Build and Push Docker Image / build (push) Successful in 16s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 29s
CI / Security audit (push) Successful in 59s
CI / Tests & coverage (push) Successful in 59s
2026-05-20 00:47:07 +01:00
712c98d817 Move card logo to bottom right with absolute positioning, fix duplication
All checks were successful
Build and Push Docker Image / build (push) Successful in 23s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 41s
CI / Security audit (push) Successful in 1m14s
CI / Tests & coverage (push) Successful in 1m18s
2026-05-20 00:37:01 +01:00
ff7ace9f4f Fix duplicate icon and user tag on page reload by adding class and duplicate check
All checks were successful
Build and Push Docker Image / build (push) Successful in 41s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 50s
CI / Security audit (push) Successful in 1m28s
CI / Tests & coverage (push) Successful in 1m44s
2026-05-20 00:29:44 +01:00
73500751a0 Increase download client logo size in cards to 64x64px (4x), keep filter picker at 20x20px
All checks were successful
Build and Push Docker Image / build (push) Successful in 48s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 48s
CI / Security audit (push) Successful in 1m10s
CI / Tests & coverage (push) Successful in 1m15s
2026-05-20 00:26:54 +01:00
82a9df134b Fix duplicate user tag and logo in download cards by removing old elements before updating
All checks were successful
Build and Push Docker Image / build (push) Successful in 32s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 30s
CI / Security audit (push) Successful in 50s
CI / Tests & coverage (push) Successful in 1m9s
2026-05-20 00:23:17 +01:00
67fa79796b Add download client logo to download card with right-side positioning
All checks were successful
Build and Push Docker Image / build (push) Successful in 20s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 36s
CI / Security audit (push) Successful in 1m4s
CI / Tests & coverage (push) Successful in 1m10s
2026-05-20 00:20:03 +01:00
f5883d4929 Add download client logos to filter UI with fallback handling
Some checks failed
Build and Push Docker Image / build (push) Successful in 30s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m4s
CI / Security audit (push) Has been cancelled
CI / Tests & coverage (push) Has been cancelled
2026-05-20 00:14:20 +01:00
80cf3eaa39 Fix filtering to use both client type and instanceId for unique identification
All checks were successful
Build and Push Docker Image / build (push) Successful in 59s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m2s
CI / Security audit (push) Successful in 1m29s
CI / Tests & coverage (push) Successful in 1m32s
2026-05-20 00:00:17 +01:00
1ab7e52167 Use index-based unique identifiers for download client selection to prevent cross-selection
All checks were successful
Build and Push Docker Image / build (push) Successful in 28s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 58s
CI / Security audit (push) Successful in 1m19s
CI / Tests & coverage (push) Successful in 1m32s
2026-05-19 23:56:05 +01:00
544c168b82 Fix duplicate checkbox ID issue causing cross-selection between clients
All checks were successful
Build and Push Docker Image / build (push) Successful in 26s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 48s
CI / Security audit (push) Successful in 1m21s
CI / Tests & coverage (push) Successful in 1m26s
2026-05-19 23:51:57 +01:00
747a14ebd3 Fix double-toggling issue in download client filter
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m15s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m0s
CI / Security audit (push) Successful in 1m26s
CI / Tests & coverage (push) Successful in 1m38s
2026-05-19 23:48:29 +01:00
be791ed044 Add multi-select download client filter with client type display
All checks were successful
Build and Push Docker Image / build (push) Successful in 24s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 45s
CI / Security audit (push) Successful in 1m23s
CI / Tests & coverage (push) Successful in 1m35s
2026-05-19 23:41:43 +01:00
720de6688b Add download client ordering and filtering to active downloads list
All checks were successful
Build and Push Docker Image / build (push) Successful in 22s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m5s
CI / Security audit (push) Successful in 1m26s
CI / Tests & coverage (push) Successful in 1m44s
2026-05-19 23:29:38 +01:00
743b169989 Fix webhooks panel: hide on app load to sync with status panel
All checks were successful
Build and Push Docker Image / build (push) Successful in 36s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 38s
CI / Security audit (push) Successful in 56s
CI / Tests & coverage (push) Successful in 1m9s
2026-05-19 23:05:20 +01:00
794cb7268e Fix status panel: remove innerHTML wipe that destroys status-content div
All checks were successful
Build and Push Docker Image / build (push) Successful in 38s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 57s
CI / Security audit (push) Successful in 1m14s
CI / Tests & coverage (push) Successful in 1m27s
2026-05-19 23:01:14 +01:00
96f24eb3b7 Fix status card regression: revert webhooks-section to sibling structure
All checks were successful
Build and Push Docker Image / build (push) Successful in 47s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 53s
CI / Security audit (push) Successful in 1m2s
CI / Tests & coverage (push) Successful in 1m14s
2026-05-19 22:57:21 +01:00
abcb9bfded debug: Add DOM structure verification to trace missing contentDiv
All checks were successful
Build and Push Docker Image / build (push) Successful in 40s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m1s
CI / Security audit (push) Successful in 1m24s
CI / Tests & coverage (push) Successful in 1m34s
2026-05-19 22:35:05 +01:00
e5920b207f debug: Add more detailed logging to renderStatusPanel
All checks were successful
Build and Push Docker Image / build (push) Successful in 26s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m8s
CI / Security audit (push) Successful in 1m21s
CI / Tests & coverage (push) Successful in 1m38s
2026-05-19 22:33:09 +01:00
d3483f3be7 debug(ui): Add visible styling and debug logging for status panel
All checks were successful
Build and Push Docker Image / build (push) Successful in 25s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 56s
CI / Security audit (push) Successful in 1m9s
CI / Tests & coverage (push) Successful in 1m26s
Added debug logging to trace status panel rendering:
- Log when refresh starts
- Log when data is received
- Log errors with details

Also added visible dashed border and background to #status-content
to make it obvious when the div is present but empty.
2026-05-19 22:30:54 +01:00
57908e2b9e fix(ui): Add status-content container to preserve webhooks panel
All checks were successful
Build and Push Docker Image / build (push) Successful in 37s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 50s
CI / Security audit (push) Successful in 1m1s
CI / Tests & coverage (push) Successful in 1m19s
The webhooks panel was being destroyed when renderStatusPanel set
panel.innerHTML. Added a dedicated #status-content div for status
data, keeping webhooks section intact when status refreshes.
2026-05-19 22:27:11 +01:00
e2757768c7 fix(ui): Integrate webhooks panel into status panel
All checks were successful
Build and Push Docker Image / build (push) Successful in 27s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 59s
CI / Security audit (push) Successful in 1m21s
CI / Tests & coverage (push) Successful in 1m35s
The webhooks panel was appearing separately from the status panel.
Now it's properly nested inside the status-panel div:

- Moved webhooks-section inside status-panel in HTML
- Updated CSS so nested webhooks looks like a subsection (no double borders)
- Simplified JS toggle logic - webhooks shows/hides automatically with status panel
- Admin users see webhooks inside status panel, collapsed by default
2026-05-19 22:24:15 +01:00
2e85fae57a fix(webhooks): Load collapsed by default, add webhook metrics to status panel
All checks were successful
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m8s
CI / Security audit (push) Successful in 1m28s
CI / Tests & coverage (push) Successful in 1m53s
Build and Push Docker Image / build (push) Successful in 35s
- Fixed webhooks section to load collapsed (content hidden, toggle arrow reset)
- Added webhook metrics card to status panel for admin users:
  - Shows Sonarr/Radarr enabled/disabled status
  - Shows events received and polls skipped counts
- Updated /api/dashboard/status endpoint to include webhook metrics
- Metrics are aggregated from all Sonarr/Radarr instances
2026-05-19 21:24:28 +01:00
aeacadbe68 refactor(webhooks): Integrate webhooks panel into status card
All checks were successful
Build and Push Docker Image / build (push) Successful in 41s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 59s
CI / Security audit (push) Successful in 1m20s
CI / Tests & coverage (push) Successful in 1m33s
- Moved webhooks-section to be inline with status-panel in HTML
- Updated toggleStatusPanel() to show/hide webhooks section for admin users
- Updated closeStatusPanel() to also hide webhooks section
- Removed webhooks visibility from showDashboard() - now tied to status panel
- Updated CSS to make webhooks section styling consistent with status panel:
  - Same border, border-radius, margin, box-shadow
  - Updated webhook-stats to use status-card styling (background, border)
- Webhooks metrics now display inline with status panel for admin users
2026-05-19 21:20:34 +01:00
3ef35a8c43 fix(webhooks): Send full notification object to test endpoint
All checks were successful
Build and Push Docker Image / build (push) Successful in 48s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m9s
CI / Security audit (push) Successful in 1m23s
CI / Tests & coverage (push) Successful in 1m42s
The /notifications/test endpoint requires the full notification object,
not just the ID. Changed testSonarrWebhook() and testRadarrWebhook() to
send the complete notification object (sonarrSofarr/radarrSofarr).

Fixes: 400 validation error when testing webhooks
2026-05-19 21:16:31 +01:00
2d04402284 fix(webhooks): Show webhooks panel only to admin users
All checks were successful
Build and Push Docker Image / build (push) Successful in 39s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 57s
CI / Security audit (push) Successful in 1m4s
CI / Tests & coverage (push) Successful in 1m24s
2026-05-19 20:36:33 +01:00
0310f10e5d fix(webhooks): Restore original vanilla JS app and add webhooks panel properly
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m14s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m8s
CI / Security audit (push) Successful in 1m22s
CI / Tests & coverage (push) Successful in 1m43s
The React build replaced the full-featured vanilla JS app with a
simpler UI, causing the dashboard to disappear and lose theming.

This commit:
- Restores original vanilla JS app with auth, themes, tabs, history
- Adds Webhooks Configuration panel for admin users
- Adds webhook status, enable/test buttons, triggers, and stats
- Uses proper CSS variables for theme support

Fixes the dashboard disappearing issue and restores all original functionality.
2026-05-19 20:33:23 +01:00
a7363fcb3a v1.5.2: Build and deploy React client with Webhooks Configuration panel
Some checks failed
Build and Push Docker Image / build (push) Successful in 48s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 1m6s
CI / Security audit (push) Successful in 1m29s
CI / Tests & coverage (push) Successful in 1m46s
2026-05-19 20:27:11 +01:00
917939a9fc fix(ui): wire status panel close button via addEventListener
Some checks failed
CI / Security audit (push) Failing after 29s
Docs Check / Markdown lint (push) Successful in 47s
Build and Push Docker Image / build (push) Successful in 1m3s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m14s
CI / Tests & coverage (push) Successful in 1m39s
Docs Check / Mermaid diagram parse check (push) Successful in 1m54s
Inline onclick attribute was silently blocked by the server CSP nonce
policy. Replace with addEventListener after innerHTML is set.

chore: bump version to 1.5.0a
2026-05-19 18:51:50 +01:00
8c4cc20551 Add MIT copyright headers to all source files
All checks were successful
Build and Push Docker Image / build (push) Successful in 48s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m21s
CI / Security audit (push) Successful in 1m47s
CI / Tests & coverage (push) Successful in 2m1s
2026-05-19 09:07:42 +01:00
2747ca7754 feat: allow non-admin users to blocklist & search under specific conditions
All checks were successful
Build and Push Docker Image / build (push) Successful in 37s
CI / Security audit (push) Successful in 1m30s
CI / Tests & coverage (push) Successful in 1m47s
- Added addedOn timestamp to qBittorrent torrent mapping
- Added canBlocklist helper function: true for admins, true for non-admins when (importIssues OR (torrent >1h old AND availability<100%))
- Added canBlocklist field to all download objects in /user-downloads and SSE /stream routes (8 blocks total)
- Frontend button now shows when (isAdmin OR download.canBlocklist) && download.arrQueueId
2026-05-17 23:57:06 +01:00
0341540751 feat: show blocklist & search button on all admin downloads (not just import-pending)
All checks were successful
Build and Push Docker Image / build (push) Successful in 35s
CI / Security audit (push) Successful in 1m23s
CI / Tests & coverage (push) Successful in 1m43s
- Remove importIssues condition from arr action fields threading in /user-downloads route (all 4 blocks: SAB+Sonarr, SAB+Radarr, qBit+Sonarr, qBit+Radarr)
- Remove importIssues condition from arr action fields threading in SSE /stream route (all 4 blocks)
- Move blocklist button rendering outside importIssues condition in frontend — now shows for all admin downloads with arrQueueId
2026-05-17 23:43:37 +01:00
d839fa98a0 feat: blocklist & search button for import-pending downloads with caution
All checks were successful
Build and Push Docker Image / build (push) Successful in 29s
CI / Security audit (push) Successful in 1m24s
CI / Tests & coverage (push) Successful in 1m42s
- Poller now stores _instanceKey alongside _instanceUrl on Sonarr/Radarr queue records
- dashboard route threads arrQueueId/arrType/arrInstanceUrl/arrInstanceKey/arrContentId/arrContentType as admin-only fields on downloads with importIssues
- POST /api/dashboard/blocklist-search: admin-only, removes queue item with blocklist=true then triggers EpisodeSearch/MoviesSearch
- Button renders in download card header (admin + importIssues + arrQueueId only)
- Confirm dialog, loading/success/error states on the button
- Kicks a background poll on success so SSE reflects removed item promptly
2026-05-17 23:15:33 +01:00
a92ab85bc0 fix: title link wired via JS goHome() — switches to downloads, closes status, resets showAll
All checks were successful
Build and Push Docker Image / build (push) Successful in 40s
CI / Security audit (push) Successful in 1m33s
CI / Tests & coverage (push) Successful in 1m56s
2026-05-17 23:08:27 +01:00
19b9c97e64 feat: add 'Hide upgrade failures' checkbox to history controls
All checks were successful
Build and Push Docker Image / build (push) Successful in 36s
CI / Security audit (push) Successful in 1m27s
CI / Tests & coverage (push) Successful in 1m43s
2026-05-17 22:52:55 +01:00
55a5577f2a feat: render availableForUpgrade badge on failed history items where episode/movie is already on disk
All checks were successful
Build and Push Docker Image / build (push) Successful in 38s
CI / Security audit (push) Successful in 1m41s
CI / Tests & coverage (push) Successful in 1m49s
2026-05-17 21:53:58 +01:00
6a8ca90fd3 feat: add version footer to dashboard UI (v1.2.1)
- /health endpoint now includes version field
- Footer displays 'sofarr vX.Y.Z' fetched on page load
- Subtle .app-version styling (smaller, dimmed)
- Bump version to 1.2.1, update CHANGELOG
2026-05-17 20:34:59 +01:00
d1496a76e2 feat: show episode info on download and history cards
All checks were successful
Build and Push Docker Image / build (push) Successful in 37s
CI / Security audit (push) Successful in 59s
CI / Tests & coverage (push) Successful in 54s
- Add includeEpisode:true to Sonarr queue and history API requests
  in both the poller and historyFetcher
- Add extractEpisode() / gatherEpisodes() helpers in dashboard.js
  and history.js to build a sorted, deduplicated episodes array
  covering all records matching a download title (handles multi-
  episode packs and series packs)
- Replace episodeInfo: sonarrMatch with episodes: gatherEpisodes()
  across all 8 assignment sites in dashboard.js
- Add episodes field to /api/history/recent response items
- Frontend: formatEpisodeInfo() renders S01E05 for single episodes
  or 'Multiple episodes' with hover tooltip listing all for packs
- CSS: .episode-info and .multi-episode tooltip styles
- ARCHITECTURE.md: update polling table and download/history schemas
2026-05-17 17:03:23 +01:00
e8ffd7f7dd feat(ui): split downloads and history into tabs 2026-05-17 13:09:01 +01:00
557137421d fix(history): reload history when showAll toggle changes 2026-05-17 13:02:15 +01:00
eb321312dc feat(history): add Recently Completed section to frontend dashboard 2026-05-17 12:05:39 +01:00
1f293ae70b ui: compact pill layout for detail items; red availability warning
All checks were successful
Build and Push Docker Image / build (push) Successful in 38s
CI / Security audit (push) Successful in 56s
CI / Tests & coverage (push) Successful in 1m1s
- Detail items (Size, Progress, Speed, ETA, Seeds, Peers, Availability,
  Completed) now render as inline pill badges with background + border-
  radius that wrap naturally on any screen width
- Remove mobile @media override that forced flex-direction:column,
  which was causing one-per-line centred layout on small screens
- Availability < 100%: value text shown in red (--danger) bold, both
  on card creation and on live SSE update via classList.toggle
- Also ensures updateDownloadCard keeps availability-warning in sync
2026-05-17 09:51:04 +01:00
7ff29b669c fix(ui): status panel empty on login / requires double-click to open
Some checks failed
Build and Push Docker Image / build (push) Successful in 26s
CI / Security audit (push) Successful in 40s
CI / Tests & coverage (push) Failing after 36s
showDashboard now explicitly resets the status panel to display:none and
clears its innerHTML on every call. This prevents a stale display value
from a previous session making toggleStatusPanel think it is already open
(causing it to hide on the first click instead of showing).

Also cancel the status refresh timer on logout.
2026-05-17 09:02:00 +01:00
0dbf0e0899 fix: set timing bar widths via JS DOM assignment after innerHTML
Some checks failed
Build and Push Docker Image / build (push) Successful in 21s
CI / Security audit (push) Successful in 40s
CI / Tests & coverage (push) Failing after 51s
All previous attempts (inline style=, CSS custom property via style=)
were ineffective. Setting element.style.width directly in JS after
panel.innerHTML is assigned is the only approach that cannot be
interfered with by CSP or attribute sanitisation.

Width is stored as data-w attribute in the HTML string and applied
by querySelectorAll('.timing-bar[data-w]') post-render.
2026-05-17 08:59:21 +01:00
67a8610843 fix: use CSS custom property for timing bar width to bypass CSP blocking
Some checks failed
Build and Push Docker Image / build (push) Successful in 23s
CI / Security audit (push) Successful in 39s
CI / Tests & coverage (push) Failing after 35s
Inline style= attributes containing property:value pairs are blocked by
strict style-src-attr CSP. CSS custom properties (--foo:value) set via
style= are treated as data not styles and are not subject to this
restriction. The width is now resolved in the stylesheet via
var(--bar-w, 100%) so CSP cannot interfere.
2026-05-17 08:55:06 +01:00
cafa608e8c fix: allow inline style= attributes via CSP style-src-attr
Some checks failed
Build and Push Docker Image / build (push) Successful in 23s
CI / Security audit (push) Successful in 45s
CI / Tests & coverage (push) Failing after 46s
Timing bars in the status panel and any other dynamically-injected
style= attributes were being silently blocked by the Content Security
Policy. style-src only governs <style> blocks and linked stylesheets;
inline element attributes need style-src-attr separately.

Adding style-src-attr 'unsafe-inline' is the minimal fix — it only
affects attribute-level inline styles, not script execution.

Also removes the temporary debug console.log added in the previous commit.
2026-05-17 08:53:07 +01:00
35d50fad0a debug: log task timing data in status panel to diagnose full bars
Some checks failed
Build and Push Docker Image / build (push) Successful in 21s
CI / Security audit (push) Successful in 41s
CI / Tests & coverage (push) Failing after 42s
2026-05-17 08:50:13 +01:00
0ea9b769a3 fix(ui): normalise status panel timing bars against slowest task not totalMs
Some checks failed
Build and Push Docker Image / build (push) Successful in 21s
CI / Security audit (push) Successful in 42s
CI / Tests & coverage (push) Failing after 39s
Tasks run in parallel so any individual task time can exceed the wall-clock
total, causing all bars to render at 100%. Normalise against the maximum
individual task time so bars correctly show relative response times.
2026-05-17 08:38:57 +01:00
abdd0da306 feat: replace client polling with Server-Sent Events (SSE)
Some checks failed
Build and Push Docker Image / build (push) Successful in 23s
CI / Security audit (push) Successful in 38s
CI / Tests & coverage (push) Failing after 38s
Server:
- poller.js: add pollSubscribers Set with onPollComplete/offPollComplete;
  notify all SSE callbacks immediately after every successful poll
- dashboard.js: add GET /api/dashboard/stream endpoint (text/event-stream)
  - requireAuth enforced via cookie (no CSRF needed — GET is a safe method)
  - X-Accel-Buffering: no for nginx proxy compatibility
  - 25s heartbeat comments to survive proxy idle timeouts
  - initial payload sent immediately on connect
  - cleanup on req.close: deregister callback, stop heartbeat, remove client
  - active client tracking updated: type='sse', connectedAt, no refreshRateMs

Frontend:
- app.js: replace setInterval/fetchUserDownloads with EventSource
  - startSSE() opens /api/dashboard/stream; stopSSE() closes it
  - first incoming message hides loading spinner
  - showAll toggle re-opens stream with ?showAll=true param
  - logout calls stopSSE() before POST /api/auth/logout
  - status panel: fixed 5s refresh, shows SSE clients + connect duration
  - statusRefreshHandle now always 5s, not tied to old refresh-rate selector
- index.html: remove now-unused refresh-rate <select> element

Docs:
- ARCHITECTURE.md §4.3: update poller description
- ARCHITECTURE.md §5: rename to SSE Stream (§5.2) + Download Matching (§5.3)
- ARCHITECTURE.md §7: update active client tracking description
- ARCHITECTURE.md §9: add /stream endpoint, update /status clients schema
- ARCHITECTURE.md §10: update key functions table; replace Auto-Refresh
  section with Live Push via SSE
- class-server.puml: add /stream to dashboard routes; update ClientInfo
- component.puml: annotate dashboard with SSE note; update label
2026-05-17 08:35:22 +01:00
cc1e8af761 fix: proxy cover art through server to satisfy CSP img-src 'self'
All checks were successful
Build and Push Docker Image / build (push) Successful in 19s
CI / Security audit (push) Successful in 28s
The new CSP blocks direct browser requests to external image origins
(themoviedb.org, thetvdb.com, etc.) used for poster art.

- dashboard.js: add GET /api/dashboard/cover-art?url=... proxy endpoint
  (auth-required, http/https only, image content-type validated, 5MB cap,
  24h Cache-Control, streams response directly to client)
- app.js: route coverArt src through /api/dashboard/cover-art proxy
- server/utils/logger.js: fix hardcoded /app/server.log path (use DATA_DIR)
2026-05-17 07:24:15 +01:00
bdbbcabfbc feat(security): production hardening for external deployment
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m2s
CI / Security audit (push) Successful in 3m29s
Container (Dockerfile):
- Multi-stage build (deps + runtime) for minimal attack surface
- Upgrade base image from node:18-alpine to node:22-alpine
- Run as non-root 'node' user (UID 1000); source files owned by root
- /app/data directory owned by node for SQLite + logs
- Docker HEALTHCHECK: wget /health every 30s

docker-compose.yaml:
- Port bound to 127.0.0.1 only (expose via reverse proxy)
- read_only: true filesystem; /tmp tmpfs for Node.js
- no-new-privileges:true, cap_drop: ALL
- Named volume sofarr-data for persistent data
- TRUST_PROXY, COOKIE_SECRET, NODE_ENV added

Helmet v7 + CSP nonce:
- Upgrade helmet@4 → helmet@7, express-rate-limit@6 → @7
- CSP with per-request nonce injected into index.html script/link tags
  (replaces blanket unsafe-inline; nonce changes every request)
- HSTS: max-age=1yr, includeSubDomains, preload
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy: camera/mic/geolocation/payment/usb all off
- index.html served dynamically with nonce injection; static assets
  served normally via express.static({index:false})

Trust proxy:
- TRUST_PROXY env var configures app.set('trust proxy') so rate
  limiting and secure cookies work correctly behind Nginx/Caddy

Session & auth:
- Token store migrated from in-memory Map to SQLite via better-sqlite3
  (server/utils/tokenStore.js): survives restarts, WAL mode, 31-day TTL
- CSRF double-submit cookie pattern (server/middleware/verifyCsrf.js):
  POST/PUT/PATCH/DELETE on /api/* require X-CSRF-Token header matching
  the csrf_token cookie; timing-safe comparison
- CSRF token issued on login + GET /api/auth/csrf; cleared on logout
- Login input validation: username/password length + type checked before
  hitting Emby
- skipSuccessfulRequests:true on login rate limiter (only count failures)
- express.json({ limit: '64kb' }) to reject oversized payloads

Rate limiting:
- General API limiter: 300 req/15min per IP on all /api/* routes
- Login limiter unchanged (10 failures/15min) but now only counts fails

Logging:
- Log file moved from /app/server.log to DATA_DIR/server.log (writable
  by non-root node user in container)
- Size-based rotation: rotate at 10 MB, keep 3 files (server.log.1-3)
- DATA_DIR defaults to ./data locally, /app/data in container

Error handling:
- Global Express error handler: catches unhandled errors, logs message,
  returns generic 500 (no stack traces to clients)

Health/readiness:
- GET /health: returns {status:'ok', uptime:N} — used by HEALTHCHECK
- GET /ready: returns 503 if EMBY_URL not configured

Error sanitization (sanitizeError.js):
- Added patterns for password= params, bearer tokens, Basic auth in URLs

Supply chain:
- Remove unused cors dependency
- add better-sqlite3@^9
- CI: upgrade to Node 22, raise audit level to --audit-level=high
- .gitignore: add data/, *.db, *.db-wal, *.db-shm

Docs:
- SECURITY.md: threat model, hardening checklist, proxy examples,
  header table, rate limit table, Docker secrets guidance
- .env.example + .env.sample: TRUST_PROXY, DATA_DIR documented
2026-05-17 06:47:25 +01:00