@startuml seq-polling !theme plain title sofarr — Background Polling Cycle participant "index.js\n(startup)" as entry participant "Poller" as poller participant "Config" as config participant "SABnzbd\n(per instance)" as sab participant "Sonarr\n(per instance)" as sonarr participant "Radarr\n(per instance)" as radarr participant "qBittorrent\nClient" as qbt participant "MemoryCache" as cache == Startup == entry -> poller : startPoller() activate poller alt POLL_INTERVAL > 0 poller -> poller : pollAllServices() (immediate) poller -> poller : setInterval(pollAllServices,\nPOLL_INTERVAL) else POLL_INTERVAL = 0 poller --> entry : "Polling disabled, on-demand mode" end == Poll Cycle == poller -> poller : Check: polling flag?\n(skip if concurrent) poller -> poller : polling = true poller -> poller : start = Date.now() poller -> config : getSABnzbdInstances() config --> poller : [{ id, url, apiKey }] poller -> config : getSonarrInstances() config --> poller : [{ id, url, apiKey }] poller -> config : getRadarrInstances() config --> poller : [{ id, url, apiKey }] note over poller : All fetches run in\nparallel via Promise.all,\neach wrapped in timed() par SABnzbd Queue poller -> sab : GET /api?mode=queue sab --> poller : { queue: { slots, status, speed } } and SABnzbd History poller -> sab : GET /api?mode=history&limit=10 sab --> poller : { history: { slots } } and Sonarr Tags poller -> sonarr : GET /api/v3/tag sonarr --> poller : [{ id, label }] and Sonarr Queue poller -> sonarr : GET /api/v3/queue\n?includeSeries=true sonarr --> poller : { records: [{ seriesId, series, ... }] } and Sonarr History poller -> sonarr : GET /api/v3/history\n?pageSize=10 sonarr --> poller : { records: [{ seriesId, ... }] } and Radarr Queue poller -> radarr : GET /api/v3/queue\n?includeMovie=true radarr --> poller : { records: [{ movieId, movie, ... }] } and Radarr History poller -> radarr : GET /api/v3/history\n?pageSize=10 radarr --> poller : { records: [{ movieId, ... }] } and Radarr Tags poller -> radarr : GET /api/v3/tag radarr --> poller : [{ id, label }] and qBittorrent poller -> qbt : getTorrents() qbt --> poller : [{ name, progress, ... }] end poller -> poller : Record per-task timings\nlastPollTimings = { totalMs,\ntimestamp, tasks: [{label, ms}] } poller -> poller : cacheTTL = POLL_INTERVAL × 3 poller -> cache : set('poll:sab-queue', ..., cacheTTL) poller -> cache : set('poll:sab-history', ..., cacheTTL) poller -> cache : set('poll:sonarr-tags', ..., cacheTTL) note over poller : Tag queue records with\n_instanceUrl on embedded\nseries/movie objects poller -> cache : set('poll:sonarr-queue', ..., cacheTTL) poller -> cache : set('poll:sonarr-history', ..., cacheTTL) poller -> cache : set('poll:radarr-queue', ..., cacheTTL) poller -> cache : set('poll:radarr-history', ..., cacheTTL) poller -> cache : set('poll:radarr-tags', ..., cacheTTL) poller -> cache : set('poll:qbittorrent', ..., cacheTTL) poller -> poller : Notify SSE subscribers\npollSubscribers.forEach(cb => cb()) note over poller : Each registered SSE client\ncallback rebuilds its payload\nand writes a data: frame poller -> poller : polling = false\nlog elapsed time deactivate poller @enduml