Major feature release bringing technical-debt remediation, service extraction, frontend migration, and staged history loading.
### Added
- Staged history loading with SSE push — history data from Sonarr/Radarr is now fetched in stages; history-update SSE events push incremental results to the dashboard
- Frontend unit tests — added Vitest + jsdom test suite covering client/src/ modules
- Comprehensive tests for staged history loading — backend tests verify background-fetch behaviour, cache TTL handling, and SSE emission
- Integration test coverage — new integration and unit tests for dashboard, emby, sonarr, radarr, and sabnzbd routes
### Changed
- Technical-debt remediation — service extraction — extracted matching and assembly logic from monolithic dashboard.js (~1,360 lines → ~284 lines) into dedicated services: DownloadMatcher.js, DownloadAssembler.js, DownloadBuilder.js, TagMatcher.js, WebhookStatus.js
- Frontend architecture — migrated from monolithic public/app.js to vanilla ES modules under client/src/, bundled by Vite
- History pagination — replaced custom date-based cursor pagination with retriever's built-in pagination (pageSize=100, up to 10 pages / 1,000 records total)
- Status endpoint path — admin status route moved from /api/status/status to /api/status
- Background-fetch safety — poller no longer overwrites cache with empty data when background fetch fails
- SABnzbd progress calculation — progress computed from slot.mb and slot.mbleft / mbmissing
- Speed formatting consistency — updateDownloadCard() now calls formatSpeed() for uniform units
- Status-panel error handling — panel now surfaces error messages instead of blank box
### Fixed
- CSRF token reference errors — fixed ReferenceError bugs in api.js, auth.js, and blocklist handler
- Logout button — fixed undefined variable references
- Missing progress bar for SABnzbd — SABnzbd downloads now render correct progress bar
- Status route 404 — corrected Express router mount
- Status button DOM ID — fixed element-ID mismatch
- Tab selection — fixed to use data-tab attributes
- CSP violations and ignoreAvailable reference error
- Docker client-build stage — removed client/ from .dockerignore
- Unmatched torrent exclusion — torrents not matching Sonarr/Radarr record now correctly omitted
- Blocklist button CSRF — fixed ReferenceError
### Breaking Changes
- Unmatched torrents are no longer displayed — torrents that do not match a Sonarr/Radarr queue or history record are excluded
- Frontend build process changed — monolithic public/app.js source replaced by Vite build from client/src/
- Status API endpoint path changed — admin status endpoint moved from /api/status/status to /api/status
- Use state.csrfToken instead of undefined csrfToken in handleLogout
- Use state.currentUser instead of undefined currentUser in handleLogout
- Use state.csrfToken instead of undefined csrfToken in enableSonarrWebhook
- Fixes ReferenceError bugs in logout and webhook functions
- SABnzbd API uses different field names for remaining bytes
- Test data uses mbmissing, but real API may use mbleft
- Support both field names to handle all cases
- Fixes DownloadBuilder test failures
- Calculate progress from slot.mb and slot.mbleft instead of slot.percentage
- Apply fix to both series and movie downloads in matchSabSlots
- Add progress: 100 for history items in matchSabHistory (series and movie)
- SABnzbd slot data doesn't have percentage field, so progress was undefined
- 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'
- Use formatSpeed() when updating existing download card speed
- Ensures consistent unit display (B/s, KB/s, MB/s, GB/s) across all updates
- Previously used raw speed value causing inconsistent display
- Fix undefined variable references in handleLogoutClick
- Use state.statusRefreshHandle instead of statusRefreshHandle
- Use state.currentUser instead of currentUser
- 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...'
- 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
Downloads with no match in Sonarr or Radarr were being displayed
when they should not be. Removed the code in matchTorrents that
was adding unmatched torrents to results.
- Add .hidden utility class to style.css for CSP compliance
- Replace all inline style='display: none' with class='hidden' in HTML
- Update all UI modules to use classList.add/remove instead of style.display
- Fix ignoreAvailable reference error in history.js (use state.ignoreAvailable)
- Rebuild client bundle with vite
- Create tests/frontend/utils/format.test.js with 24 tests for formatting utilities
- Create tests/frontend/ui/downloads.test.js with 10 tests for DOM rendering functions
- Update vitest.config.js to support jsdom environment for frontend tests
- All 34 tests pass and cover edge cases (null, zero, large numbers, DOM structure)
Deleted obsolete public/app.js (replaced by Vite build)
All backend services extracted and dashboard.js slimmed to 284 lines
Frontend fully migrated to vanilla ES modules
All duplications eliminated
Comprehensive tests added
Vite build wired into Dockerfile
Extracted TagMatcher, DownloadAssembler, DownloadBuilder, and WebhookStatus services
Slimmed dashboard.js from 1360 → 284 lines (pure HTTP layer)
Created server/routes/status.js and mounted at /api/status
Migrated frontend to vanilla ES modules under client/src/
Eliminated all tag-badge and client-logo duplication
Wired Vite build into Dockerfile and removed obsolete public/app.js
Added comprehensive DownloadBuilder regression tests
- Consolidated duplicate client logo creation logic into single helper function
- Removed second duplicate logo block from createDownloadCard
- Updated updateDownloadCard to use the same helper
- Client logo now appears exactly once per download card
- Extract /status route to server/routes/status.js
- Create server/services/WebhookStatus.js with checkWebhookConfigured and aggregateMetrics
- Slim dashboard.js to pure HTTP orchestration (559→283 lines, 49.4% reduction)
- Remove /user-summary and /webhook-metrics routes from dashboard.js
- Mount status router at /api/status in server/index.js and server/app.js
- Update tests to use new /api/status/status endpoint
- Fix test expectation for speed field (number vs string)
All 571 tests passing.
- Created server/services/DownloadBuilder.js with buildUserDownloads function
- Added private helpers: buildSeriesMapFromRecords, buildMoviesMapFromRecords, matchSabSlots, matchSabHistory, matchTorrents, getSlotStatusAndSpeed
- Updated server/routes/dashboard.js to use buildUserDownloads in /user-downloads and SSE /stream
- Removed ~500 lines of duplicated download-assembly logic
- All unit tests passing (DownloadBuilder: 14, DownloadAssembler: 73, TagMatcher: 26)
- Extract six pure tag-related functions from dashboard.js into new server/services/TagMatcher.js
- Functions: sanitizeTagLabel, tagMatchesUser, extractAllTags, extractUserTag, getEmbyUsers, buildTagBadges
- Update dashboard.js to import TagMatcher and replace all inline function calls
- Add comprehensive unit tests in tests/unit/services/TagMatcher.test.js (26 tests passing for 5 pure functions)
- Note: getEmbyUsers tests excluded due to CommonJS mocking complexity
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