merge: develop into main (fix Mermaid diagram rendering)
Some checks failed
CI / Security audit (push) Has been cancelled
CI / Tests & coverage (push) Has been cancelled

This commit is contained in:
2026-05-17 18:26:32 +01:00

View File

@@ -941,8 +941,8 @@ graph TB
sonarr_r --> sonarr
radarr_r --> radarr
appjs -->|POST /login\nGET /me\nGET /csrf\nPOST /logout| auth
appjs -->|GET /stream SSE\nGET /user-downloads\nGET /status| dashboard
appjs -->|POST /login, GET /me, GET /csrf, POST /logout| auth
appjs -->|GET /stream SSE, GET /user-downloads, GET /status| dashboard
es -->|serve static| html
```
@@ -976,16 +976,16 @@ sequenceDiagram
Note over Browser,Emby: Login
User->>Browser: Enter credentials (+ rememberMe)
Browser->>Auth: POST /api/auth/login
Note right of Auth: Rate limit: max 10 failed\nattempts per IP / 15 min
Auth->>Emby: POST /Users/authenticatebyname\nDeviceId = sha256(username)[0:16]
Note right of Auth: Rate limit: max 10 failed attempts per IP / 15 min
Auth->>Emby: POST /Users/authenticatebyname (DeviceId = sha256(username)[0:16])
alt Valid credentials
Emby-->>Auth: { User.Id, AccessToken }
Auth->>Emby: GET /Users/{id}
Emby-->>Auth: { Name, Policy.IsAdministrator }
Auth->>Tokens: storeToken(userId, AccessToken)
Note right of Tokens: Server-side only\n31-day TTL, atomic write
Auth->>Auth: Set emby_user cookie\nhttpOnly, sameSite=strict\nsecure (if TRUST_PROXY)\nrememberMe → Max-Age 30d
Auth->>Auth: Set csrf_token cookie\nhttpOnly=false, sameSite=strict
Note right of Tokens: Server-side only, 31-day TTL, atomic write
Auth->>Auth: Set emby_user cookie (httpOnly, sameSite=strict, secure if TRUST_PROXY)
Auth->>Auth: Set csrf_token cookie (httpOnly=false, sameSite=strict)
Auth-->>Browser: { success: true, user, csrfToken }
Browser->>Browser: showDashboard() + startSSE()
else Invalid credentials
@@ -1023,7 +1023,7 @@ sequenceDiagram
User->>Browser: Login success / valid session
Browser->>Dashboard: GET /api/dashboard/stream (EventSource)
Dashboard->>Dashboard: requireAuth: extract user/isAdmin
Dashboard->>Dashboard: Set Content-Type: text/event-stream\nRegister in activeClients
Dashboard->>Dashboard: Set Content-Type: text/event-stream, register in activeClients
opt Polling disabled AND cache empty
Dashboard->>Poller: pollAllServices()
@@ -1033,7 +1033,7 @@ sequenceDiagram
end
Dashboard->>Cache: get all poll:* keys
Dashboard->>Dashboard: Build maps, match downloads\nextractUserTag / buildTagBadges
Dashboard->>Dashboard: Build maps, match downloads, extractUserTag / buildTagBadges
Dashboard-->>Browser: data: { user, isAdmin, downloads }
Browser->>Browser: hideLoading() + renderDownloads()
@@ -1050,7 +1050,7 @@ sequenceDiagram
User->>Browser: Close tab / logout
Browser->>Dashboard: TCP close (req close event)
Dashboard->>Dashboard: offPollComplete(cb)\nclearInterval(heartbeat)\ndelete activeClients[key]
Dashboard->>Dashboard: offPollComplete(cb), clearInterval(heartbeat), delete activeClients[key]
```
### 13.4 Background Polling Cycle
@@ -1094,8 +1094,8 @@ sequenceDiagram
Poller->>QBT: getTorrents()
QBT-->>Poller: [{ name, progress, ... }]
Poller->>Poller: Record per-task timings\nlastPollTimings = { totalMs, timestamp, tasks }
Poller->>Cache: set poll:* keys (TTL = POLL_INTERVAL × 3)
Poller->>Poller: Record per-task timings: lastPollTimings = { totalMs, timestamp, tasks }
Poller->>Cache: set poll:* keys (TTL = POLL_INTERVAL x 3)
Poller->>Poller: Notify SSE subscribers (forEach cb())
Poller->>Poller: polling = false
```
@@ -1330,11 +1330,11 @@ stateDiagram-v2
Submitting --> [*] : Auth success
}
LoginForm --> Dashboard : Auth success\n(fade transition)
LoginForm --> Dashboard : Auth success (fade transition)
state Dashboard {
[*] --> Rendering
Rendering --> Rendering : SSE message renderDownloads()
Rendering --> Rendering : SSE message triggers renderDownloads()
Rendering --> Rendering : Theme change
state SSEConnection {
@@ -1368,13 +1368,13 @@ stateDiagram-v2
state Disabled {
[*] --> OnDemand
OnDemand : No background timer.\nData fetched when dashboard\nrequest finds empty cache.
OnDemand : No background timer. Data fetched when dashboard request finds empty cache.
}
Disabled --> Polling : dashboard triggers pollAllServices()
Polling --> Disabled : Poll complete (on-demand)
Idle --> Polling : setInterval fires\nor immediate first poll
Idle --> Polling : setInterval fires or immediate first poll
state Polling {
[*] --> Locked
@@ -1382,7 +1382,7 @@ stateDiagram-v2
Locked --> Fetching
Fetching --> Storing : All promises resolved
Fetching --> HandleError : Per-service error (caught)
Storing --> Notifying : Cache updated\nTTL = POLL_INTERVAL × 3
Storing --> Notifying : Cache updated, TTL = POLL_INTERVAL x 3
Notifying : Notify SSE subscribers
Notifying --> Done
Done : polling = false
@@ -1401,7 +1401,7 @@ stateDiagram-v2
[*] --> Skip
Skip : polling === true, skip cycle
}
Idle --> ConcurrentSkip : Interval fires while\nprevious still running
Idle --> ConcurrentSkip : Interval fires while previous still running
ConcurrentSkip --> Idle : Log skip
```