docs: update architecture docs and diagrams for recent changes
All checks were successful
Build and Push Docker Image / build (push) Successful in 24s

ARCHITECTURE.md:
- Directory structure: add middleware/requireAuth.js and favicon assets
- §4.1: remove CORS from middleware list
- §4.2: all proxy routes now auth-required via requireAuth; add
  middleware description
- §6: cookie payload corrected (no token); document secure+sameSite
- §7: add emby:users cache key (60s TTL)
- §8: Download Object table: userTag → allTags/matchedUserTag/tagBadges
- §9 POST /login: document cookie security attributes
- §10: add Tag Badge Rendering section; remove hardcoded line count

Diagrams:
- class-server.puml: add requireAuth middleware module; update
  dashboard.js methods (extractAllTags, extractUserTag w/ username,
  buildTagBadges, getEmbyUsers); add TagBadge value class; add auth
  relationships for all proxy routes
- class-data.puml: Download Object userTag → allTags/matchedUserTag/
  tagBadges; add TagBadge class; remove token from Session Cookie
- seq-auth.puml: cookie payload no longer contains token; add
  secure/sameSite note
- component.puml: remove CORS component; add requireAuth; consolidate
  Emby connection to show tag badge + user-summary usage
- activity-matching.puml: update to extractAllTags/extractUserTag
  (with username); showAll uses hasAnyTag; tagBadges built from
  embyUserMap; add Emby user fetch step; update legend
- seq-dashboard.puml: add emby:users cache lookup / Emby fetch for
  showAll; update matching groups to show tag classification; add
  tag badge rendering note on renderDownloads()
This commit is contained in:
2026-05-16 15:41:23 +01:00
parent 54647ab7cf
commit 6675e5dcfe
7 changed files with 135 additions and 35 deletions

View File

@@ -22,6 +22,12 @@ end note
:Build **sonarrTagMap** (tagId → label)
Build **radarrTagMap** (tagId → label);
if (showAll?) then (yes)
:Fetch full Emby user list
Build **embyUserMap** (lowerName → displayName)
[cached 60s];
endif
:Initialise **userDownloads** = [];
partition "Process SABnzbd Queue Slots" {
@@ -32,13 +38,20 @@ partition "Process SABnzbd Queue Slots" {
if (Title matches Sonarr **queue** record?) then (yes)
:series = seriesMap.get(match.seriesId)\n|| match.series;
if (series exists?) then (yes)
:userTag = extractUserTag(series.tags, sonarrTagMap);
if (showAll OR tagMatchesUser?) then (yes)
:allTags = extractAllTags(series.tags, sonarrTagMap)
matchedUserTag = extractUserTag(series.tags, sonarrTagMap, username);
if (showAll AND hasAnyTag?) then (yes)
:Build download object (type=series)
Add coverArt, status, progress, speed, eta
Add allTags, matchedUserTag
Add tagBadges = buildTagBadges(allTags, embyUserMap)
Add importIssues if any
Add admin fields (paths, arrLink);
:Push to **userDownloads**;
elseif (NOT showAll AND matchedUserTag?) then (yes)
:Build download object (type=series)
Add matchedUserTag;
:Push to **userDownloads**;
endif
endif
endif
@@ -46,13 +59,19 @@ partition "Process SABnzbd Queue Slots" {
if (Title matches Radarr **queue** record?) then (yes)
:movie = moviesMap.get(match.movieId)\n|| match.movie;
if (movie exists?) then (yes)
:userTag = extractUserTag(movie.tags, radarrTagMap);
if (showAll OR tagMatchesUser?) then (yes)
:allTags = extractAllTags(movie.tags, radarrTagMap)
matchedUserTag = extractUserTag(movie.tags, radarrTagMap, username);
if (showAll AND hasAnyTag?) then (yes)
:Build download object (type=movie)
Add coverArt, status, progress, speed, eta
Add allTags, matchedUserTag, tagBadges
Add importIssues if any
Add admin fields (paths, arrLink);
:Push to **userDownloads**;
elseif (NOT showAll AND matchedUserTag?) then (yes)
:Build download object (type=movie)
Add matchedUserTag;
:Push to **userDownloads**;
endif
endif
endif
@@ -67,16 +86,20 @@ partition "Process SABnzbd History Slots" {
if (Title matches Sonarr **history** record?) then (yes)
:series = seriesMap.get(match.seriesId)\n|| match.series;
if (series found?) then (yes)
:Check user tag, build download\n(type=series, with completedAt);
:Push to **userDownloads** if tag matches;
:extractAllTags + extractUserTag(username)
Build download (type=series, completedAt)
Add allTags, matchedUserTag, tagBadges if showAll;
:Push to **userDownloads** if showAll+anyTag or matchedUserTag;
endif
endif
if (Title matches Radarr **history** record?) then (yes)
:movie = moviesMap.get(match.movieId)\n|| match.movie;
if (movie found?) then (yes)
:Check user tag, build download\n(type=movie, with completedAt);
:Push to **userDownloads** if tag matches;
:extractAllTags + extractUserTag(username)
Build download (type=movie, completedAt)
Add allTags, matchedUserTag, tagBadges if showAll;
:Push to **userDownloads** if showAll+anyTag or matchedUserTag;
endif
endif
endwhile (no)
@@ -119,10 +142,15 @@ legend right
(bidirectional substring, case-insensitive):
""rTitle.includes(dlTitle) || dlTitle.includes(rTitle)""
**Tag Matching Logic**:
**Tag Matching Logic** (tagMatchesUser):
1. Exact: tag.toLowerCase() === username
2. Sanitised: sanitizeTagLabel(tag) === sanitizeTagLabel(username)
(handles Ombi-mangled email-style usernames)
**extractAllTags**: returns all resolved tag labels
**extractUserTag**: returns the ONE label matching current user
**buildTagBadges**: classifies each tag against full Emby user
list → { label, matchedUser: displayName | null }
end legend
@enduml

View File

@@ -153,7 +153,9 @@ package "sofarr Internal Models" {
+ movieName : string | null
+ episodeInfo : object | null
+ movieInfo : object | null
+ userTag : string
+ allTags : string[]
+ matchedUserTag : string | null
+ tagBadges : TagBadge[] | undefined
+ importIssues : string[] | null
+ downloadPath : string | null
+ targetPath : string | null
@@ -170,6 +172,11 @@ package "sofarr Internal Models" {
+ completedAt : string
}
class "TagBadge" as tagbadge <<value>> {
+ label : string
+ matchedUser : string | null
}
class "API Response\n/user-downloads" as apir {
+ user : string
+ isAdmin : boolean
@@ -201,7 +208,7 @@ package "sofarr Internal Models" {
+ id : string
+ name : string
+ isAdmin : boolean
+ token : string
' Note: Emby AccessToken intentionally excluded
}
apir *-- dl
@@ -215,7 +222,9 @@ sabh ..> dl : matched &\ntransformed
qbt ..> dl : mapTorrentToDownload()
ss ..> dl : coverArt, seriesName,\npath, tags
rm ..> dl : coverArt, movieName,\npath, tags
tag ..> dl : userTag resolution
tag ..> dl : allTags / matchedUserTag
eu ..> cookie : login creates
eu ..> tagbadge : buildTagBadges()
dl *-- tagbadge : tagBadges[]
@enduml

View File

@@ -33,7 +33,10 @@ package "server/routes" {
+ GET /status
--
- getCoverArt(item) : string|null
- extractUserTag(tags, tagMap) : string|null
- extractAllTags(tags, tagMap) : string[]
- extractUserTag(tags, tagMap, username) : string|null
- buildTagBadges(allTags, embyUserMap) : TagBadge[]
- getEmbyUsers() : Promise<Map>
- sanitizeTagLabel(input) : string
- tagMatchesUser(tag, username) : boolean
- getImportIssues(record) : string[]|null
@@ -69,6 +72,16 @@ package "server/routes" {
}
}
package "server/middleware" {
class "requireAuth.js" as requireauth <<middleware>> {
+ 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<string, CacheEntry>
@@ -158,6 +171,11 @@ package "server/utils" {
+ logToFile(message) : void
}
class "TagBadge" as tb <<value>> {
+ label : string
+ matchedUser : string | null
}
class "ClientInfo" as ci <<value>> {
+ user : string
+ refreshRateMs : number
@@ -172,6 +190,12 @@ 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

View File

@@ -16,10 +16,10 @@ package "Browser" as browser {
package "Express Server" as server {
package "Middleware" {
[CORS] as cors
[cookie-parser] as cp
[express.json] as ej
[express.static] as es
[requireAuth.js] as requireauth
}
package "Routes" as routes {
@@ -41,7 +41,6 @@ package "Express Server" as server {
[index.js\nEntry Point] as entry
entry --> cors
entry --> cp
entry --> ej
entry --> es
@@ -51,6 +50,12 @@ package "Express Server" as server {
entry --> sab_route
entry --> sonarr_route
entry --> radarr_route
emby_route --> requireauth
sab_route --> requireauth
sonarr_route --> requireauth
radarr_route --> requireauth
dashboard --> requireauth
entry --> poller : startPoller()
dashboard --> cache : read poll:* keys
@@ -76,7 +81,7 @@ cloud "External Services" as external {
}
auth --> emby : authenticate\nuser profile
dashboard ..> emby : /user-summary\n(live fetch)
dashboard --> emby : GET /Users\n(user-summary + tag badge classification)
emby_route --> emby
sab_route --> sab
sonarr_route --> sonarr

View File

@@ -37,7 +37,7 @@ alt Valid credentials
auth -> emby : GET /Users/{userId}
emby --> auth : { Name, Policy: { IsAdministrator } }
deactivate emby
auth -> auth : Set httpOnly cookie\nemby_user = { id, name, isAdmin, token }\n(24h TTL)
auth -> auth : Set httpOnly cookie\nemby_user = { id, name, isAdmin }\n(24h TTL, secure in prod, sameSite=strict)\nNote: AccessToken NOT stored
auth --> browser : { success: true, user: { name, isAdmin } }
browser -> browser : fadeOutLogin()
browser -> browser : showSplash()

View File

@@ -50,19 +50,28 @@ dashboard -> dashboard : Build seriesMap from\nSonarr queue records
dashboard -> dashboard : Build moviesMap from\nRadarr queue records
dashboard -> dashboard : Build tag maps\n(id → label)
alt showAll=true
dashboard -> cache : get('emby:users')
alt cache miss
dashboard -> ext : GET /Users (Emby)
ext --> dashboard : [{ Name, ... }]
dashboard -> cache : set('emby:users', map, 60s)
end
dashboard -> dashboard : Build embyUserMap\n(lowerName → displayName)
end
group SABnzbd Queue Matching
loop each queue slot
dashboard -> dashboard : Match title vs Sonarr queue
dashboard -> dashboard : Match title vs Radarr queue
dashboard -> dashboard : Resolve series/movie\n→ extract user tag\n→ filter by username
dashboard -> dashboard : extractAllTags() + extractUserTag(username)\nInclude if: showAll+anyTag OR matchedUserTag\nAttach allTags, matchedUserTag\nIf showAll: tagBadges = buildTagBadges(embyUserMap)
end
end
group SABnzbd History Matching
loop each history slot
dashboard -> dashboard : Match title vs Sonarr history
dashboard -> dashboard : Match title vs Radarr history
dashboard -> dashboard : Resolve via seriesMap/moviesMap\n→ extract user tag\n→ filter
dashboard -> dashboard : Match title vs Sonarr/Radarr history
dashboard -> dashboard : Same tag extraction + inclusion logic
end
end
@@ -72,14 +81,20 @@ group qBittorrent Matching
dashboard -> dashboard : 2. Match vs Radarr queue
dashboard -> dashboard : 3. Match vs Sonarr history
dashboard -> dashboard : 4. Match vs Radarr history
dashboard -> dashboard : mapTorrentToDownload()\n→ enrich with series/movie info
dashboard -> dashboard : mapTorrentToDownload()\n→ enrich: allTags, matchedUserTag, tagBadges
end
end
dashboard --> browser : { user, isAdmin,\ndownloads: [...] }
deactivate dashboard
browser -> browser : renderDownloads()\n(diff-based update)
browser -> browser : renderDownloads() (diff-based)
note right
createDownloadCard() renders tag badges:
- Normal: accent badge for matchedUserTag
- showAll: amber badges (unmatched tags)
accent badges (matched → show Emby displayName)
end note
deactivate browser
@enduml