@startuml component !theme plain title sofarr — Component Diagram skinparam componentStyle rectangle skinparam packageStyle frame package "Browser" as browser { [index.html] as html [app.js] as appjs [style.css] as css html ..> appjs : loads html ..> css : loads } package "Express Server" as server { [index.js\nEntry Point] as entry [app.js\ncreatApp() factory] as appfactory package "Middleware" { [helmet\n(CSP nonce, HSTS)] as hm [express-rate-limit\n(API + login)] as rl [cookie-parser\n(signed cookies)] as cp [express.json\n(64kb limit)] as ej [express.static] as es [requireAuth.js] as requireauth [verifyCsrf.js\n(double-submit)] as verifycsrf } package "Routes" as routes { [auth.js\n/api/auth\n(pre-CSRF)] as auth [dashboard.js\n/api/dashboard\n(+SSE /stream)] as dashboard [emby.js\n/api/emby] as emby_route [sabnzbd.js\n/api/sabnzbd] as sab_route [sonarr.js\n/api/sonarr] as sonarr_route [radarr.js\n/api/radarr] as radarr_route } package "Utilities" as utils { [poller.js] as poller [cache.js\nMemoryCache] as cache [config.js] as config [qbittorrent.js\nQBittorrentClient] as qbt [tokenStore.js\n(tokens.json)] as tokenstore [sanitizeError.js] as sanitize [logger.js] as logger } entry --> appfactory : createApp() entry --> es : serve public/ entry --> poller : startPoller() appfactory --> hm appfactory --> rl appfactory --> cp appfactory --> ej appfactory --> auth : mount before verifyCsrf appfactory --> verifycsrf : applied to all /api below appfactory --> dashboard appfactory --> emby_route appfactory --> sab_route appfactory --> sonarr_route appfactory --> radarr_route emby_route --> requireauth sab_route --> requireauth sonarr_route --> requireauth radarr_route --> requireauth dashboard --> requireauth auth --> tokenstore : storeToken / getToken / clearToken dashboard --> cache : read poll:* keys dashboard --> poller : pollAllServices()\n(on-demand mode) dashboard --> config : getSonarrInstances()\ngetRadarrInstances() dashboard --> qbt : mapTorrentToDownload() poller --> cache : set poll:* keys poller --> config : get all instances poller --> qbt : getTorrents() poller --> logger qbt --> config : getQbittorrentInstances() qbt --> logger auth ..> sanitize dashboard ..> sanitize note "Browser uses EventSource\n(SSE) to /stream.\nServer pushes on each\npoll cycle — no client timer." as sseNote sseNote .. dashboard } cloud "External Services" as external { [Emby / Jellyfin] as emby [SABnzbd] as sab [Sonarr] as sonarr [Radarr] as radarr [qBittorrent] as qbit } auth --> emby : authenticate\nuser profile dashboard --> emby : GET /Users\n(user-summary + tag badge classification) emby_route --> emby sab_route --> sab sonarr_route --> sonarr radarr_route --> radarr poller --> sab : queue + history poller --> sonarr : tags + queue + history poller --> radarr : tags + queue + history qbt --> qbit : login + torrents/info appjs --> auth : POST /login\nGET /me appjs --> dashboard : GET /user-downloads\nGET /status es --> html : serve static @enduml