59 Commits

Author SHA1 Message Date
gronod 95bd703b26 chore: bump version to 1.7.22 and update CHANGELOG, tests and docs
Docs Check / Markdown lint (push) Successful in 1m14s
Build and Push Docker Image / build (push) Successful in 1m52s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m55s
CI / Security audit (push) Successful in 2m25s
Docs Check / Mermaid diagram parse check (push) Successful in 3m6s
CI / Swagger Validation & Coverage (push) Successful in 3m22s
CI / Tests & coverage (push) Successful in 3m45s
2026-05-27 17:42:41 +01:00
gronod d2ac7731ca chore: bump version to 1.7.21 and update CHANGELOG and docs
Build and Push Docker Image / build (push) Successful in 1m21s
CI / Security audit (push) Successful in 1m26s
Docs Check / Markdown lint (push) Successful in 1m27s
CI / Swagger Validation & Coverage (push) Successful in 2m25s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 2m30s
Docs Check / Mermaid diagram parse check (push) Successful in 3m17s
CI / Tests & coverage (push) Successful in 3m31s
2026-05-26 15:20:12 +01:00
gronod b5b4862e15 chore: bump version to 1.7.14 and update CHANGELOG for poller fix
Build and Push Docker Image / build (push) Successful in 1m42s
Docs Check / Markdown lint (push) Successful in 1m34s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 2m37s
CI / Security audit (push) Successful in 3m7s
Docs Check / Mermaid diagram parse check (push) Failing after 3m52s
CI / Swagger Validation & Coverage (push) Successful in 4m11s
CI / Tests & coverage (push) Successful in 4m41s
2026-05-24 19:36:53 +01:00
gronod 3c6791658c feat: implement togglable debug log streaming for server stdout/stderr and client console logs
- Created server/utils/logCapture.js to intercept and buffer server output, stripping ANSI escape codes.
- Created server/middleware/logStreamAuth.js enforcing subnet IP filtering (LOG_ALLOW_SUBNETS), Emby session cookie, Basic Auth fallback, and X-Webhook-Secret header bypass.
- Created server/routes/debug.js with SSE streams /api/debug/server-logs, /api/debug/client-logs and batched POST /api/debug/client-logs. Exposes public configuration status at /api/debug/status.
- Integrated log capture and mounted debug routes in server/app.js and server/index.js.
- Implemented client/src/utils/clientLogCapture.js in the frontend SPA to hook console log/warn/error and flush batched console events.
- Documented all endpoints in OpenAPI server/openapi.yaml, ARCHITECTURE.md, and README.md.
- Wrote route integration tests and frontend console capture tests, with full validation in swagger-coverage.
2026-05-24 11:31:36 +01:00
gronod 86c67bcf29 fix: support PascalCase properties in Ombi webhooks (#42) 2026-05-23 20:57:55 +01:00
gronod 6ac0a8421e fix: resolve rate-limiting and Ombi requests caching bugs (fixes #42, fixes #43)
Build and Push Docker Image / build (push) Successful in 1m34s
Docs Check / Markdown lint (push) Successful in 2m14s
CI / Security audit (push) Successful in 2m30s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 2m40s
CI / Swagger Validation & Coverage (push) Successful in 3m22s
Docs Check / Mermaid diagram parse check (push) Successful in 3m43s
CI / Tests & coverage (push) Successful in 3m59s
2026-05-23 18:55:03 +01:00
gronod 4ddd3036d9 fix: add missing copyright header to ombiHelpers.js
Build and Push Docker Image / build (push) Successful in 48s
CI / Swagger Validation & Coverage (push) Successful in 1m54s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m54s
CI / Security audit (push) Successful in 2m23s
CI / Tests & coverage (push) Successful in 2m35s
2026-05-22 22:45:23 +01:00
gronod 12c44a611e fix: optimize polling parallelism and resolve redundant SABnzbd requests (fixes #35, fixes #36) 2026-05-22 22:25:11 +01:00
gronod 548aca6bee fix: remediate audited defects (resolves #29, #30, #31, #32)
Build and Push Docker Image / build (push) Successful in 51s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 1m48s
CI / Security audit (push) Successful in 2m27s
CI / Swagger Validation & Coverage (push) Successful in 2m27s
CI / Tests & coverage (push) Successful in 2m39s
- Fix permanent qBittorrent fallback degradation by resetting fallback flags during group-by polling (resolves #29)
- Fix Ombi webhook key mismatch in status endpoint and test mocks (resolves #30)
- Prevent timingSafeEqual TypeErrors on multi-byte CSRF token length mismatches (resolves #31)
- Eliminate duplicate write stream on server.log by delegating to index.js console override (resolves #32)
2026-05-22 16:01:20 +01:00
gronod 4aa3590017 test: remediate test suite, enable skipped frontend/SSE tests, and add comprehensive unit tests
Build and Push Docker Image / build (push) Successful in 53s
Docs Check / Markdown lint (push) Successful in 1m36s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 2m31s
CI / Security audit (push) Successful in 2m55s
Docs Check / Mermaid diagram parse check (push) Successful in 3m24s
CI / Swagger Validation & Coverage (push) Successful in 3m30s
CI / Tests & coverage (push) Successful in 3m50s
- Deleted redundant unit test file tests/unit/dashboard.test.js
- Enabled skipped frontend DOM state and API tests in tests/frontend/state.test.js
- Fixed Supertest client socket abort exception in SSE stream integration tests using new graceful testClose parameter
- Consolidated duplicate helpers in server/routes/history.js and server/utils/arrRetrievers.js to unified services TagMatcher and DownloadAssembler
- Added comprehensive unit tests for loadSecrets.js, PollingSonarrRetriever.js, PollingRadarrRetriever.js, and ombiHelpers.js
- Achieved a 100% Vitest pass rate (834/834 tests) with robust code coverage
2026-05-22 13:33:21 +01:00
gronod d3d085d614 feat: Add Ombi request filtering and search
Build and Push Docker Image / build (push) Successful in 1m29s
Docs Check / Markdown lint (push) Successful in 1m51s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 2m3s
CI / Security audit (push) Successful in 2m54s
CI / Swagger Validation & Coverage (push) Successful in 3m6s
Docs Check / Mermaid diagram parse check (push) Successful in 3m13s
CI / Tests & coverage (push) Successful in 3m31s
- Add request filters UI (type, status, sort, search)
- Implement dual-layer filtering (server + client)
- Add ombiFilters utility for consistent filtering logic
- Persist filter preferences in localStorage
- Add SSE support for real-time Ombi request updates
- Add webhook endpoints for Ombi integration
- Update OpenAPI spec for new endpoints
- Add unit tests for filter logic and UI
- Add integration tests for Ombi routes
2026-05-22 12:31:31 +01:00
gronod 9862c0555c Handle Ombi API object-format requestedUser field
Build and Push Docker Image / build (push) Successful in 47s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 1m24s
CI / Security audit (push) Successful in 1m47s
CI / Swagger Validation & Coverage (push) Successful in 2m11s
CI / Tests & coverage (push) Successful in 2m27s
The Ombi API returns requestedUser as an OmbiUser object instead of a string.
Add extractRequestedUser helper to extract username from various fields
(alias, userAlias, userName, normalizedUserName) with fallback to legacy string format.
Update client and server routes to use the helper for consistent username extraction.
2026-05-21 21:56:36 +01:00
gronod 1dccda529a feat: add Ombi requests tab and webhook panel integration
- Add Ombi requests tab UI with movie/TV request display
- Add showAll parameter support for Ombi requests (API and SSE)
- Add Ombi webhook panel with enable/test functionality
- Add Ombi webhook status endpoint with metrics
- Add Ombi webhook test endpoint
- Change GET /api/ombi/requests to use OmbiRetriever instead of cache
- Add Ombi webhook state and API functions to frontend
- Update SSE payload to include Ombi baseUrl and requests
2026-05-21 20:59:06 +01:00
gronod ed4237debb feat(ombi): Add Ombi PALDRA integration for request management
Docs Check / Markdown lint (push) Successful in 1m43s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 2m1s
CI / Security audit (push) Successful in 2m48s
Docs Check / Mermaid diagram parse check (push) Successful in 3m8s
CI / Tests & coverage (push) Failing after 3m33s
CI / Swagger Validation & Coverage (push) Successful in 3m34s
Build and Push Docker Image / build (push) Successful in 4m36s
- Add OmbiRetriever extending ArrRetriever for PALDRA compliance
- Add OmbiClient for low-level Ombi API communication
- Add getOmbiInstances() to config.js following multi-instance pattern
- Register Ombi in PALDRA registry with Ombi-specific methods
- Add external ID matching (TMDB/TVDB/IMDB) to Ombi requests
- Update DownloadMatcher to be async and enrich downloads with Ombi links
- Add getOmbiLink/getOmbiSearchLink helpers to DownloadAssembler
- Implement new service icon layout (Ombi + Sonarr/Radarr icons)
- Add CSS styling for service icons
- Update dashboard routes to include Ombi configuration
- Extend OpenAPI with Ombi tag and NormalizedDownload properties
- Update documentation (README, ARCHITECTURE, SECURITY, CHANGELOG)
- Add Ombi configuration to .env.sample
2026-05-21 17:00:04 +01:00
gronod 93a09e10a8 Fix background fetch to not overwrite cache with empty data
Build and Push Docker Image / build (push) Successful in 36s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m18s
CI / Security audit (push) Successful in 1m13s
CI / Tests & coverage (push) Successful in 1m15s
- Only trigger background refresh if cache is incomplete (less than max records)
- Only update cache if background fetch returns records (don't overwrite on failure)
- Prevents test failures when background fetch fails to connect to Sonarr/Radarr
- Fixes integration test 'includes imported and failed records, excludes grabbed'
2026-05-21 01:53:19 +01:00
gronod 14b47ce410 Fix history pagination and status panel issues
Build and Push Docker Image / build (push) Successful in 52s
Docs Check / Markdown lint (push) Successful in 1m1s
CI / Tests & coverage (push) Failing after 1m32s
CI / Security audit (push) Successful in 1m33s
Docs Check / Mermaid diagram parse check (push) Successful in 1m38s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 56s
- Fix history pagination: use retriever's built-in maxPages parameter instead of broken date-based cursor
- Fix status panel: correct API endpoint from /api/dashboard/status to /api/status
- Background fetch now properly fetches up to 1000 records (10 pages * 100 records)
- Status panel will now display details instead of 'Loading Status...'
2026-05-21 01:34:54 +01:00
gronod 0acd452ebd Implement staged history loading with SSE push
Build and Push Docker Image / build (push) Has been cancelled
Licence Check / Licence compatibility and copyright header verification (push) Has been cancelled
CI / Tests & coverage (push) Has been cancelled
CI / Security audit (push) Has been cancelled
- Stage 1: Fetch 100 records immediately for fast display
- Stage 2+: Background fetch up to 1000 records in batches of 100
- Date-based cursor pagination to avoid race conditions
- Deduplication by record ID to prevent duplicates
- SSE push to clients when history cache is updated
- Shared background fetch state for concurrent user requests
2026-05-21 01:23:11 +01:00
gronod b9b5d7d393 Increase history pageSize from 100 to 500 to fetch more records
Build and Push Docker Image / build (push) Successful in 39s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 55s
CI / Security audit (push) Successful in 1m6s
CI / Tests & coverage (push) Successful in 1m15s
Fixes issue where series beyond position 100 in history were not appearing
in recently completed section.
2026-05-21 01:10:36 +01:00
gronod d568800942 fix: limit history pagination to prevent 40s response times
Licence Check / Licence compatibility and copyright header verification (push) Successful in 51s
Build and Push Docker Image / build (push) Successful in 1m27s
CI / Security audit (push) Successful in 1m27s
CI / Tests & coverage (push) Successful in 1m43s
The full pagination fix (ddad80a, e772001) caused history retrieval to
fetch up to 50 pages sequentially, taking 10-40 seconds instead of ~200ms.

Changes:
- Add maxPages parameter to getHistory() with default of 1 page
- Update poller to fetch 50 records (up from 10) in a single API call
- historyFetcher retains 100 records per page for UI display

This provides 5x more history for matching than before while keeping
the fast single-request performance.

Refs: develop-merge pagination performance issue
2026-05-20 21:44:53 +01:00
gronod bb7b66e06d fix: use stable *arr IDs for matching before fragile title fallback
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m11s
CI / Tests & coverage (push) Failing after 1m8s
CI / Security audit (push) Successful in 1m11s
Licence Check / Licence compatibility and copyright header verification (pull_request) Successful in 52s
CI / Security audit (pull_request) Successful in 1m18s
CI / Tests & coverage (pull_request) Failing after 1m26s
2026-05-20 20:51:50 +01:00
gronod 720de6688b Add download client ordering and filtering to active downloads list
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
gronod f22dd0d1f6 fix(downloads): Fix SABnzbd/qBittorrent collision and webhook metrics
Build and Push Docker Image / build (push) Successful in 46s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m8s
CI / Security audit (push) Successful in 1m33s
CI / Tests & coverage (push) Successful in 1m41s
1. Fixed download client collision:
   - SABnzbd client with id 'i3omb' was being overwritten by qBittorrent
   - Now uses unique key ':' like the arr retrievers

2. Fixed webhook metrics showing 0:
   - instanceName from webhooks is generic ('Sonarr', 'Radarr')
   - Not the configured instance name ('i3omb')
   - Now updates metrics for ALL instances of that type
2026-05-19 21:40:53 +01:00
gronod 5159a83475 fix(retrievers): Use unique key to prevent Sonarr/Radarr collision
Build and Push Docker Image / build (push) Successful in 33s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m10s
CI / Security audit (push) Successful in 1m33s
CI / Tests & coverage (push) Successful in 1m52s
When Sonarr and Radarr had the same instance ID (e.g., 'i3omb'),
the Radarr retriever would overwrite the Sonarr retriever in the Map.
This caused webhook refreshes to show '0 instance(s)' for Sonarr.

Now uses ':' as the unique key so both can coexist.
2026-05-19 21:36:20 +01:00
gronod fcb0cd8e4a feat(webhooks): add polling optimization and fallback when webhooks are active (Phase 5)
Build and Push Docker Image / build (push) Successful in 38s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 56s
CI / Security audit (push) Successful in 1m18s
CI / Tests & coverage (push) Successful in 1m32s
2026-05-19 16:10:45 +01:00
gronod e022db8ef5 feat(webhooks): add notification management API + one-click Sofarr webhook setup (Phase 3)
Build and Push Docker Image / build (push) Successful in 45s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m1s
CI / Security audit (push) Successful in 1m20s
CI / Tests & coverage (push) Successful in 1m35s
2026-05-19 15:31:50 +01:00
gronod 99ddb05dbe feat(webhook): implement Phase 1 webhook receiver for Sonarr and Radarr
Build and Push Docker Image / build (push) Successful in 1m7s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m15s
CI / Security audit (push) Successful in 1m44s
CI / Tests & coverage (push) Successful in 1m53s
- Added POST /api/webhook/sonarr and POST /api/webhook/radarr endpoints
- Implemented webhook secret validation via SOFARR_WEBHOOK_SECRET environment variable
- Added logging for all incoming webhook events using existing logToFile utility
- Returns HTTP 200 immediately to prevent webhook retries
- Mounted webhook routes before CSRF middleware (called by external services)
- Non-breaking: no changes to polling, caching, SSE, or any existing behavior
- Lays groundwork for Phase 2 (cache + SSE integration) without implementing it yet
2026-05-19 15:15:53 +01:00
gronod 6e199925aa refactor: make PALDRA match PDCA style exactly - remove redundant instanceConfig parameter and convert to pure singleton
Build and Push Docker Image / build (push) Successful in 20s
CI / Security audit (push) Successful in 1m18s
CI / Tests & coverage (push) Successful in 1m14s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 54s
- Remove instanceConfig parameter from all retriever methods (getTags, getQueue, getHistory)
- Retriever instances now use this.url, this.apiKey, this.id instead of passed parameter
- Convert ArrRetrieverRegistry from class with convenience functions to pure singleton object
- Export singleton instance directly instead of class + convenience functions
- Update poller.js and historyFetcher.js to call methods on singleton directly
- All 261 tests pass with zero behavior changes
2026-05-19 14:51:22 +01:00
gronod 627329df2f feat: implement Pluggable Abstraction Layer for Data Retrieval (PALDRA) - #19
Build and Push Docker Image / build (push) Successful in 42s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 46s
CI / Security audit (push) Successful in 1m21s
CI / Tests & coverage (push) Successful in 1m35s
- Create ArrRetriever abstract base class defining pluggable interface
- Implement PollingSonarrRetriever and PollingRadarrRetriever with HTTP polling
- Add ArrRetrieverRegistry for managing retriever instances
- Refactor poller.js to use retriever registry instead of direct Axios calls
- Update historyFetcher.js to use retriever registry
- Preserve all cache keys, TTLs, timing logs, SSE broadcasts, error handling
- Enable future webhook listeners without touching poller logic
2026-05-19 14:43:28 +01:00
gronod a50e5a7d69 feat: add rtorrent client via PDCA
Build and Push Docker Image / build (push) Failing after 40s
CI / Security audit (push) Failing after 27s
CI / Tests & coverage (push) Failing after 35s
Docs Check / Markdown lint (push) Successful in 32s
Docs Check / Mermaid diagram parse check (push) Successful in 1m12s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 24s
- Implement RTorrentClient extending DownloadClient abstract class
- Use xmlrpc package (v1.3.2) for XML-RPC communication
- Support HTTP Basic Auth when credentials are configured
- Map rTorrent states (d.state, d.is_active, d.is_hash_checking) to normalized statuses
- Calculate ETA from download speed and remaining bytes
- Add getRtorrentInstances() to config.js
- Register RTorrentClient in downloadClients.js registry
- Add 8 comprehensive unit tests covering all functionality
- Update .env.sample with rtorrent configuration examples
- Update ARCHITECTURE.md with rtorrent client details
- Update ADDING-A-DOWNLOAD-CLIENT.md with rtorrent-specific notes
2026-05-19 11:40:31 +01:00
gronod f095e6a2d1 Fix QBittorrentClient export in legacy qbittorrent.js
Build and Push Docker Image / build (push) Successful in 44s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 57s
CI / Security audit (push) Successful in 1m15s
CI / Tests & coverage (push) Failing after 1m22s
Remove undefined QBittorrentClient export that was causing
container startup failures. The actual implementation is now
in server/clients/QBittorrentClient.js
2026-05-19 11:21:31 +01:00
gronod bf3e1c353d Implement Pluggable Download Client Architecture (PDCA)
Build and Push Docker Image / build (push) Successful in 31s
Docs Check / Markdown lint (push) Successful in 31s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m12s
CI / Tests & coverage (push) Failing after 1m39s
CI / Security audit (push) Successful in 1m49s
Docs Check / Mermaid diagram parse check (push) Successful in 1m56s
- Add abstract DownloadClient base class with standardized interface
- Refactor QBittorrentClient to extend DownloadClient with Sync API support
- Create SABnzbdClient implementing DownloadClient interface
- Add TransmissionClient as proof-of-concept implementation
- Implement DownloadClientRegistry for factory pattern and client management
- Refactor poller.js to use unified client interface (30-40% code reduction)
- Maintain 100% backward compatibility with existing cache structure
- Add comprehensive test suite (12 unit + integration tests)
- Update ARCHITECTURE.md with detailed PDCA documentation
- Create ADDING-A-DOWNLOAD-CLIENT.md guide for future client additions

Features:
- Client-agnostic polling with error isolation
- Consistent data normalization across all clients
- Easy extensibility for new download client types
- Zero breaking changes to existing functionality
- Parallel execution with unified timing and logging
2026-05-19 11:18:19 +01:00
gronod 0a54d0d302 refactor: use qBittorrent Sync API (/api/v2/sync/maindata) with fallback
Docs Check / Markdown lint (push) Successful in 51s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m16s
CI / Tests & coverage (push) Successful in 1m37s
CI / Security audit (push) Successful in 1m44s
Docs Check / Mermaid diagram parse check (push) Successful in 1m52s
- QBittorrentClient now uses the incremental Sync API instead of repeatedly
  fetching the full torrent list via /api/v2/torrents/info.
- Per-client state: lastRid, torrentMap, fallbackThisCycle.
- Handles full_update, delta updates, and torrents_removed.
- Falls back to legacy torrents/info at most once per poll cycle.
- getAllTorrents() resets fallback flags before each cycle.
- Added 9 new unit tests covering: first sync, delta merge, full_update,
  torrents_removed, fallback path, direct-legacy-after-fallback, 403 re-auth,
  completed-field computation, and fallback reset.
2026-05-19 09:33:20 +01:00
gronod 8c4cc20551 Add MIT copyright headers to all source files
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
gronod 2747ca7754 feat: allow non-admin users to blocklist & search under specific conditions
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
gronod d839fa98a0 feat: blocklist & search button for import-pending downloads with caution
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
gronod c0dd93a1ab feat: production hardening v1.2.0
Build and Push Docker Image / build (push) Successful in 59s
CI / Security audit (push) Successful in 1m5s
CI / Tests & coverage (push) Successful in 1m24s
Docs Check / Markdown lint (push) Failing after 45s
Docs Check / Mermaid diagram parse check (push) Successful in 1m27s
CI / Security audit (pull_request) Successful in 51s
CI / Tests & coverage (pull_request) Successful in 1m1s
Docs Check / Markdown lint (pull_request) Failing after 39s
Docs Check / Mermaid diagram parse check (pull_request) Successful in 1m12s
Phase 1 - Licensing & Compliance:
- Add MIT LICENSE file
- Add copyright headers to server/index.js, poller.js, config.js,
  sanitizeError.js, and new loadSecrets.js

Phase 2 - Security Hardening:
- Add server/utils/loadSecrets.js: Docker secrets support via _FILE
  env var pattern (COOKIE_SECRET_FILE, EMBY_API_KEY_FILE, etc.)
- Add SSRF/URL validation in config.js: validates all configured
  service instance URLs for scheme and well-formedness at startup
- Add SIGTERM/SIGINT graceful shutdown: stops poller, drains HTTP
  connections, 10s force-exit fallback
- Warn at startup if COOKIE_SECRET is shorter than 32 characters
- Validate EMBY_URL scheme at startup
- Improve sanitizeError: redact host:port from axios error URLs
  while preserving path/query for other redaction patterns

Phase 3 - Config Robustness:
- Weak COOKIE_SECRET warning (< 32 chars)
- EMBY_URL validated via validateInstanceUrl on startup

Phase 4 - Docker & Deployment:
- .dockerignore: add tests/, coverage/, vitest.config.js,
  CHANGELOG.md, SECURITY.md, LICENSE, .markdownlint.json
- docker-compose.yaml: add commented Option B (Docker secrets
  _FILE pattern) alongside existing plain-env Option A

Phase 5 - Docs & Release Readiness:
- Add CHANGELOG.md with entries from v1.0.0 to v1.2.0
- Update SECURITY.md: supported versions table, fix Docker secrets
  note to reflect _FILE support now implemented
- Add public/.well-known/security.txt for responsible disclosure
- Bump version to 1.2.0
2026-05-17 19:40:07 +01:00
gronod d1496a76e2 feat: show episode info on download and history cards
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
gronod ddcfbda0c2 feat(history): add /api/history/recent endpoint with Sonarr/Radarr history fetching, tag filtering, and 5-min cache 2026-05-17 12:05:30 +01:00
gronod 4af36fc926 fix: correct status panel cache stats and static asset caching
Build and Push Docker Image / build (push) Successful in 23s
CI / Security audit (push) Successful in 42s
CI / Tests & coverage (push) Failing after 45s
cache.js: Map values serialise as '{}' under JSON.stringify, causing
emby:users to show 0 bytes and null item count in the status panel.
Convert Maps via Object.fromEntries before stringifying, and report
Map.size as itemCount.

index.js: JS and CSS served with Cache-Control: no-cache so browsers
always revalidate on load. ETag still prevents re-downloading unchanged
files — only a new deploy triggers an actual download.
2026-05-17 08:46:55 +01:00
gronod abdd0da306 feat: replace client polling with Server-Sent Events (SSE)
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
gronod 5fd55b4e1a test: add comprehensive test suite (115 tests, Vitest + supertest + nock)
Build and Push Docker Image / build (push) Successful in 49s
CI / Security audit (push) Successful in 1m23s
CI / Tests & coverage (push) Failing after 2m13s
Framework:
- Vitest v4 as test runner (fast ESM/CJS support, V8 coverage built-in)
- supertest for integration tests against createApp() factory
- nock for HTTP interception (works with CJS require('axios'), unlike vi.mock)

New files:
- vitest.config.js          — test config: node env, isolate, V8 coverage, per-file thresholds
- tests/setup.js             — isolated DATA_DIR per worker, SKIP_RATE_LIMIT, console suppression
- tests/README.md            — approach, structure, design decisions
- server/app.js              — testable Express factory (extracted from index.js side-effects)

Unit tests (91 tests):
- tests/unit/sanitizeError.test.js  — secret redaction: apikey, token, bearer, basic-auth URLs
- tests/unit/config.test.js         — JSON array + legacy single-instance config parsing
- tests/unit/requireAuth.test.js    — valid/invalid/tampered cookies, schema validation
- tests/unit/verifyCsrf.test.js     — double-submit pattern, timing-safe compare, safe methods
- tests/unit/qbittorrent.test.js    — formatBytes, formatEta, mapTorrentToDownload state map
- tests/unit/tokenStore.test.js     — store/get/clear lifecycle, TTL expiry, atomic disk write

Integration tests (24 tests):
- tests/integration/health.test.js  — /health and /ready endpoints
- tests/integration/auth.test.js    — full login/logout/me/csrf flows, input validation,
                                      cookie attributes, no token leakage, Emby mock via nock

Production code changes (minimal, no behaviour change):
- server/routes/auth.js: EMBY_URL captured at request-time (not module load) for testability
- server/routes/auth.js: loginLimiter max → Number.MAX_SAFE_INTEGER when SKIP_RATE_LIMIT set
- server/utils/sanitizeError.js: fix HEADER_PATTERN to redact full line (not just first token)

CI:
- .gitea/workflows/ci.yml: add parallel 'test' job (npm run test:coverage, artifact upload)
- package.json: add test/test:watch/test:coverage/test:ui scripts
- .gitignore: add coverage/
2026-05-17 07:45:33 +01:00
gronod 251c7376c9 fix: logger.js hardcoded server.log path breaks non-root container user
Build and Push Docker Image / build (push) Successful in 24s
CI / Security audit (push) Successful in 26s
server/utils/logger.js was still writing to ../../server.log relative
to __dirname (/app/server.log) which is root-owned. The non-root node
user (UID 1000) cannot write there, causing an EACCES crash on startup.

Fix: use DATA_DIR env var (same as index.js) so all log writes go to
/app/data/server.log which is owned by the node user.
2026-05-17 07:21:43 +01:00
gronod 37c1b64982 fix(docker): replace better-sqlite3 with pure-JS JSON token store
Build and Push Docker Image / build (push) Successful in 28s
CI / Security audit (push) Successful in 38s
better-sqlite3 is a native C++ addon that requires compilation on Alpine
(musl libc, no pre-built binaries exist) and fails on Debian slim too
because prebuild-install cannot detect the libc type correctly.

Replace with a pure-JS JSON file token store (server/utils/tokenStore.js):
- Atomic writes via temp file + rename (no corruption on crash)
- Same API: storeToken/getToken/clearToken
- TTL enforcement on read and hourly prune
- Zero native code, zero build tools required

Dockerfile:
- Revert to node:22-alpine (was node:22-slim)
- Remove build tools (python3/make/g++) — no longer needed
- Restore wget HEALTHCHECK (available in Alpine busybox)

docker-compose.yaml: restore wget healthcheck

package.json: remove better-sqlite3 dependency
2026-05-17 07:13:56 +01:00
gronod bdbbcabfbc feat(security): production hardening for external deployment
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
gronod 8fa20c6990 fix(security #10): sanitize error details to prevent API key leakage
Added server/utils/sanitizeError.js which redacts:
- ?apikey= query parameters (SABnzbd passes key in URL)
- ?token= query parameters
- X-Api-Key / X-MediaBrowser-Token / X-Emby-Authorization header
  values if they appear in the error message string

Applied to all catch blocks in emby.js, sabnzbd.js, sonarr.js,
radarr.js, and dashboard.js. Internal error.message still logged
server-side (unredacted) for debugging.
2026-05-16 16:22:11 +01:00
gronod 0c8d5d8a4a Revert "perf: split into fast poll + slow-cached library fetches"
This reverts commit 78a8737f29.
2026-05-16 00:32:16 +01:00
gronod 268238215e perf: split into fast poll + slow-cached library fetches
Fast poll (every cycle): SABnzbd, Sonarr/Radarr queue + history,
qBittorrent — all lightweight with no include* params.

Slow cache (5 min TTL): Sonarr series, Radarr movies, tags —
fetched only when cache expires. These rarely change.

This eliminates the 2s+ includeSeries/includeMovie joins from
every poll cycle. First poll is still slow (cold cache), but
subsequent polls should complete in <500ms.
2026-05-16 00:32:16 +01:00
gronod 31ff973eff perf: drop includeSeries/includeMovie from history fetches
Sonarr/Radarr history with include* params forces expensive DB
joins (~2s each). History records still have seriesId/movieId
for matching; the series/movie objects come from the queue-built
maps instead.

Trade-off: completed downloads only show if the series/movie
is also currently in the queue. Active downloads unaffected.
2026-05-16 00:32:16 +01:00
gronod 1327c8e466 perf: reduce history page sizes and drop includeEpisode
- Sonarr/Radarr history: pageSize 20 -> 10
- SABnzbd history: limit 20 -> 10
- Drop includeEpisode from Sonarr queue and history (never rendered)
- These reduce DB join overhead and response payload size
2026-05-16 00:32:16 +01:00
gronod d50a6fe19c perf: eliminate full Sonarr Series + Radarr Movies library fetches
The poller was fetching the entire series and movie libraries on every
poll cycle (~9s each). Queue and history records already embed the full
series/movie object via includeSeries/includeMovie params.

Changes:
- Remove 'Sonarr Series' and 'Radarr Movies' timed fetches from poller
- Tag queue/history records with _instanceUrl in the poller instead
- Build seriesMap/moviesMap from embedded objects in dashboard
- Remove poll:sonarr-series and poll:radarr-movies cache keys
- Fix missing axios and config imports in dashboard
- Net result: ~18s saved per poll cycle, ~2 fewer API calls
2026-05-16 00:32:16 +01:00