feat: Recently Completed downloads history, tab UI, and light theme refresh #7

Merged
Gandalf merged 19 commits from develop into main 2026-05-17 13:55:08 +01:00
9 changed files with 28 additions and 17 deletions
Showing only changes of commit 9751dbf98d - Show all commits

View File

@@ -2,6 +2,7 @@
!theme plain
title sofarr — Download Matching Activity Diagram
start
:Read cached data from MemoryCache;

View File

@@ -183,6 +183,14 @@ package "sofarr Internal Models" {
+ downloads : Download[]
}
class "SSE Event\n/stream (data: frame)" as sser {
+ user : string
+ isAdmin : boolean
+ downloads : Download[]
' Same shape as /user-downloads response.
' Pushed after every poll cycle completes.
}
class "Status Response\n/status" as statr {
+ server : ServerInfo
+ polling : PollingInfo
@@ -212,6 +220,7 @@ package "sofarr Internal Models" {
}
apir *-- dl
sser *-- dl
statr *-- si
statr *-- pi
}

View File

@@ -136,11 +136,14 @@ package "server/utils" {
- polling : boolean
- lastPollTimings : PollTimings|null
- intervalHandle : number|null
- subscribers : Set<Function>
--
+ startPoller() : void
+ stopPoller() : void
+ pollAllServices() : Promise<void>
+ getLastPollTimings() : PollTimings|null
+ onPollComplete(cb) : void
+ offPollComplete(cb) : void
--
- timed(label, fn) : TimedResult
}

View File

@@ -16,7 +16,7 @@ package "Browser" as browser {
package "Express Server" as server {
[index.js\nEntry Point] as entry
[app.js\ncreatApp() factory] as appfactory
[app.js\ncreateApp() factory] as appfactory
package "Middleware" {
[helmet\n(CSP nonce, HSTS)] as hm
@@ -111,8 +111,8 @@ 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
appjs --> auth : POST /login\nGET /me\nGET /csrf\nPOST /logout
appjs --> dashboard : GET /stream (SSE)\nGET /user-downloads\nGET /status
es --> html : serve static
@enduml

View File

@@ -54,14 +54,14 @@ alt Valid credentials
Never sent to the client.
31-day TTL, atomic JSON write.
end note
auth -> auth : Set emby_user cookie\n{ id, name, isAdmin }\nhttpOnly, sameSite=strict\nsecure (prod), signed (COOKIE_SECRET)\nrememberMe=true → Max-Age 30d\nrememberMe=false → session cookie
auth -> auth : Set emby_user cookie\n{ id, name, isAdmin }\nhttpOnly, sameSite=strict\nsecure (if TRUST_PROXY), signed (COOKIE_SECRET)\nrememberMe=true → Max-Age 30d\nrememberMe=false → session cookie
auth -> auth : Generate csrfToken\n(32-byte random hex)
auth -> auth : Set csrf_token cookie\nhttpOnly=false (JS-readable)\nsameSite=strict, secure (prod)
auth -> auth : Set csrf_token cookie\nhttpOnly=false (JS-readable)\nsameSite=strict, secure (if TRUST_PROXY)
auth --> browser : { success: true, user, csrfToken }
browser -> browser : store csrfToken in memory
browser -> browser : fadeOutLogin()
browser -> browser : showDashboard()
browser -> browser : startAutoRefresh()
browser -> browser : startSSE()
browser -> browser : dismissSplash()
else Invalid credentials
emby --> auth : 401 Error
@@ -82,7 +82,7 @@ browser -> browser : store new csrfToken in memory
== Logout ==
user -> browser : Click Logout
browser -> browser : stopAutoRefresh()
browser -> browser : stopSSE()
browser -> auth : POST /api/auth/logout\n(no CSRF required — auth routes\nexempt; sameSite:strict protects)
activate auth
auth -> auth : Parse emby_user cookie → user

View File

@@ -2,6 +2,7 @@
!theme plain
title sofarr — Dashboard SSE Stream Sequence
actor User as user
participant "Browser\n(app.js)" as browser
participant "Express\n/api/dashboard" as dashboard

View File

@@ -2,6 +2,7 @@
!theme plain
title sofarr — Background Polling Cycle
participant "index.js\n(startup)" as entry
participant "Poller" as poller
participant "Config" as config

View File

@@ -2,6 +2,7 @@
!theme plain
title sofarr — Poller State Diagram
[*] --> CheckConfig : startPoller()
state CheckConfig <<choice>>

View File

@@ -43,15 +43,12 @@ state Dashboard {
state "Status Panel Closed" as status_closed
[*] --> rendering
rendering --> rendering : SSE message received
→ renderDownloads()
rendering --> rendering : SSE message received\n-> renderDownloads()
rendering --> rendering : Theme change
status_closed --> status_open : Click "Status" btn
(admin only)
status_open --> status_closed : Click close (×)
status_open --> status_open : 5s timer
→ renderStatusPanel()
status_closed --> status_open : Click "Status" btn\n(admin only)
status_open --> status_closed : Click close (x)
status_open --> status_open : 5s timer -> renderStatusPanel()
[*] --> status_closed
@@ -66,8 +63,6 @@ state Dashboard {
}
}
Dashboard --> LoginForm : Logout
(stopSSE,
clear state)
Dashboard --> LoginForm : Logout\n(stopSSE, clear state)
@enduml