@startuml class-server !theme plain title sofarr — Server Class / Module Diagram package "server/index.js" as entry { class "EntryPoint" as ep <> { - LOG_LEVELS : Object - currentLevel : number - logFile : WriteStream + shouldLog(level) : boolean -- Configures Express app, mounts routes, starts poller } } package "server/routes" { class "auth.js" as auth <> { + POST /login + GET /me + POST /logout -- Authenticates via Emby API Sets/reads httpOnly cookie } class "dashboard.js" as dashboard <> { - activeClients : Map - CLIENT_STALE_MS : 30000 -- + GET /user-downloads + GET /user-summary + GET /status -- - getCoverArt(item) : string|null - extractAllTags(tags, tagMap) : string[] - extractUserTag(tags, tagMap, username) : string|null - buildTagBadges(allTags, embyUserMap) : TagBadge[] - getEmbyUsers() : Promise - sanitizeTagLabel(input) : string - tagMatchesUser(tag, username) : boolean - getImportIssues(record) : string[]|null - getSonarrLink(series) : string|null - getRadarrLink(movie) : string|null - getActiveClients() : ClientInfo[] } class "emby.js" as emby_r <> { + GET /sessions + GET /users/:id + GET /users + GET /session/:sessionId/user } class "sabnzbd.js" as sab_r <> { + GET /queue + GET /history } class "sonarr.js" as sonarr_r <> { + GET /queue + GET /history + GET /series/:id + GET /series } class "radarr.js" as radarr_r <> { + GET /queue + GET /history + GET /movies/:id + GET /movies } } package "server/middleware" { class "requireAuth.js" as requireauth <> { + requireAuth(req, res, next) : void -- Reads emby_user cookie Attaches parsed user to req.user Returns 401 if absent/invalid } } package "server/utils" { class "MemoryCache" as cache { - store : Map + get(key) : any|null + set(key, value, ttlMs) : void + invalidate(key) : void + clear() : void + getStats() : CacheStats } class "CacheEntry" as ce <> { + value : any + expiresAt : number } class "CacheStats" as cs <> { + entryCount : number + totalSizeBytes : number + entries : CacheEntryStats[] } class "Poller" as poller <> { - POLL_INTERVAL : number - POLLING_ENABLED : boolean - polling : boolean - lastPollTimings : PollTimings|null - intervalHandle : number|null -- + startPoller() : void + stopPoller() : void + pollAllServices() : Promise + getLastPollTimings() : PollTimings|null -- - timed(label, fn) : TimedResult } class "PollTimings" as pt <> { + totalMs : number + timestamp : string (ISO) + tasks : { label, ms }[] } class "Config" as config <> { + getSABnzbdInstances() : Instance[] + getSonarrInstances() : Instance[] + getRadarrInstances() : Instance[] + getQbittorrentInstances() : Instance[] -- - parseInstances(envVar, ...) : Instance[] } class "Instance" as inst <> { + id : string + name : string + url : string + apiKey : string + username? : string + password? : string } class "QBittorrentClient" as qbt { - id : string - name : string - url : string - username : string - password : string - authCookie : string|null -- + login() : Promise + makeRequest(endpoint, config) : Promise + getTorrents() : Promise } class "qbittorrent.js" as qbt_mod <> { - persistedClients : QBittorrentClient[]|null -- + getTorrents() : Promise + getClients() : QBittorrentClient[] + mapTorrentToDownload(torrent) : Download + formatBytes(bytes) : string + formatSpeed(bps) : string + formatEta(seconds) : string } class "Logger" as logger <> { - logFile : WriteStream + logToFile(message) : void } class "TagBadge" as tb <> { + label : string + matchedUser : string | null } class "ClientInfo" as ci <> { + user : string + refreshRateMs : number + lastSeen : number (timestamp) } } ' Relationships ep --> auth ep --> dashboard ep --> emby_r ep --> sab_r ep --> sonarr_r ep --> radarr_r dashboard --> requireauth : uses emby_r --> requireauth : uses sab_r --> requireauth : uses sonarr_r --> requireauth : uses radarr_r --> requireauth : uses ep --> poller : startPoller() dashboard --> cache : read/write dashboard --> poller : pollAllServices() dashboard --> qbt_mod : mapTorrentToDownload() dashboard --> config poller --> cache : set poll:* keys poller --> config : get instances poller --> qbt_mod : getTorrents() qbt_mod --> config : getQbittorrentInstances() qbt_mod *-- qbt : creates qbt --> logger cache *-- ce : stores cache ..> cs : returns from getStats() poller ..> pt : stores/returns dashboard *-- ci : stores in activeClients config ..> inst : returns @enduml