🐛 Fixed
- Downloads marked complete at 99.995% of bytes received. Every normalizer was running progress through
parseFloat((x).toFixed(2))for display, and the unified item builder then used that rounded value (progress >= 100) as the completion oracle. JavaScript's.toFixed(2)rounds half-away-from-zero, so e.g. a 100GB aMule file with 5MB still missing (99.9952%) got flipped tocomplete=true— prematurepausedUPstate in the qBit-compat layer, premature shared-vs-active routing in delete/pause handlers. Completion is now sourced per-client from authoritative signals: aMule from bytes-equality onEC_TAG_PARTFILE_SIZE_DONE(verified parts only), rTorrent fromd.complete=, qBittorrent from rawprogress >= 1.0, Deluge from rawprogress >= 100, Transmission frompercentDone >= 1.0. Displayprogressstays at 2-decimal precision; only the completion flag changed. - Transmission
downloadedcounter could exceed total size on poisoned torrents — the field was being shipped fromdownloadedEver, which Transmission's docs explicitly warn "can grow very large" since it includes redundant re-fetches of corrupt data. NowhaveValid + haveUnchecked(bytes actually on disk). - Sonarr/Radarr never called
torrents/deleteafter import even after the v3.8.1pausedUPstate fix. Tracing Radarr'sQBittorrent.cs:240:CanBeRemovedrequires three conjuncts —RemoveCompletedDownloads,state ∈ {pausedUP, stoppedUP}, ANDHasReachedSeedLimit. We were emittingratio_limit: -2(= "use global config"), which only resolves to true if the user hasMaxRatioEnabledin qBit's preferences. The compat layer now emitsratio_limit: 0, realratio/uploaded/upspeedvalues fromEC_TAG_KNOWNFILE_XFERRED_ALL, so all three conjuncts are satisfied at completion and*arrcleanup fires (#42). /api/v2/torrents/inforeturned[]intermittently when no WebSocket clients were connected —autoRefreshManagerskipsgetBatchData()entirely when no WS clients are connected (the WS-or-history-due gate), so the cache aged out past the qBit-compat handler's 10s freshness window and the handler shipped empty arrays. The handler now uses a newDataFetchService.getOrFetchBatchData()that fetches on cache miss;getBatchData()itself is coalesced — concurrent callers (autoRefresh loop + Sonarr/Radarr/Prowlarr polls) share one in-flight fetch instead of triggering parallel client queries (#42).