docs: update architecture docs and diagrams for recent changes
All checks were successful
Build and Push Docker Image / build (push) Successful in 24s
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user