docs(ARCHITECTURE): update to reflect develop-refactor2 changes
- Add Download Matching & Assembly Services section (3.3) covering DownloadBuilder, DownloadMatcher, DownloadAssembler, TagMatcher, and WebhookStatus - Update High-Level Architecture diagram with /api/status route - Update Data Flow pipeline to show service-based matching, stable downloadId priority, deduplication, unmatched-torrent exclusion - Update Key Subsystems: Vite+ES module frontend, client/src tree, CSP compliance, PDCA client fix callouts - Update Directory Structure with services/, client/src/, tests/frontend/ - Update Technology Stack with jsdom and Vite build notes - Update webhook replay protection and PALDRA pagination details
This commit is contained in:
+166
-52
@@ -37,6 +37,7 @@ Three pluggable layers form the architectural core:
|
||||
|-------|------|----------|
|
||||
| Download client abstraction | **PDCA** — Pluggable Download Client Architecture | `server/clients/` + `server/utils/downloadClients.js` |
|
||||
| *arr data retrieval | **PALDRA** — Pluggable *Arr Library Data Retrieval Architecture | `server/utils/arrRetrievers.js` |
|
||||
| Download matching & assembly | **Download Services** | `server/services/` |
|
||||
| Real-time push | **Webhook Receiver** | `server/routes/webhook.js` |
|
||||
|
||||
---
|
||||
@@ -50,6 +51,7 @@ flowchart TB
|
||||
dash["Dashboard Cards"]
|
||||
status["Status Panel\n(Admin only)"]
|
||||
history["History Tab"]
|
||||
webhooks["Webhook Config"]
|
||||
end
|
||||
|
||||
subgraph Server["Express Server (:3001)"]
|
||||
@@ -57,6 +59,7 @@ flowchart TB
|
||||
mw["Middleware\nHelmet · rate-limit · cookie-parser · verifyCsrf"]
|
||||
auth_r["Auth Routes\n/api/auth"]
|
||||
dash_r["Dashboard Routes\n/api/dashboard (SSE /stream)"]
|
||||
stat_r["Status Routes\n/api/status"]
|
||||
wh_r["Webhook Routes\n/api/webhook/sonarr|radarr"]
|
||||
hist_r["History Routes\n/api/history"]
|
||||
proxy_r["Proxy Routes\n/api/sonarr · /api/radarr\n/api/sabnzbd · /api/emby"]
|
||||
@@ -82,7 +85,7 @@ flowchart TB
|
||||
|
||||
login -->|"POST /api/auth/login"| auth_r
|
||||
dash -->|"GET /api/dashboard/stream (SSE)"| dash_r
|
||||
status -->|"GET /api/dashboard/status"| dash_r
|
||||
status -->|"GET /api/status/status"| stat_r
|
||||
history -->|"GET /api/history/recent"| hist_r
|
||||
|
||||
auth_r --> tokenstore
|
||||
@@ -90,6 +93,7 @@ flowchart TB
|
||||
|
||||
dash_r --> cache
|
||||
dash_r --> poller
|
||||
stat_r --> cache
|
||||
wh_r --> cache
|
||||
wh_r --> paldra
|
||||
hist_r --> cache
|
||||
@@ -121,10 +125,11 @@ Express Server (:3001)
|
||||
├── /api/auth → login, logout, me, csrf
|
||||
├── /api/webhook → [rate-limit] → [secret validation] → [payload validation]
|
||||
│ → [replay check] → updateWebhookMetrics → processWebhookEvent
|
||||
├── /api/dashboard → requireAuth → read cache → match downloads → SSE/JSON
|
||||
├── /api/history → requireAuth → historyFetcher (5 min cache) → filter + dedup
|
||||
├── /api/sonarr|radarr → requireAuth → verifyCsrf → proxy to *arr API
|
||||
└── /api/sabnzbd|emby → requireAuth → verifyCsrf → proxy
|
||||
├── /api/dashboard → requireAuth → read cache → DownloadBuilder → SSE/JSON
|
||||
├── /api/status → requireAuth → admin cache/polling/webhook status
|
||||
├── /api/history → requireAuth → historyFetcher (5 min cache) → filter + dedup
|
||||
├── /api/sonarr|radarr → requireAuth → verifyCsrf → proxy to *arr API
|
||||
└── /api/sabnzbd|emby → requireAuth → verifyCsrf → proxy
|
||||
|
||||
Background:
|
||||
Poller (setInterval POLL_INTERVAL ms)
|
||||
@@ -239,7 +244,7 @@ Each `QBittorrentClient` instance maintains:
|
||||
|
||||
Per-cycle flow:
|
||||
1. Attempt `GET /api/v2/sync/maindata?rid={lastRid}`.
|
||||
2. If `full_update` is `true`, rebuild `torrentMap` from scratch.
|
||||
2. If `full_update` is `true`, rebuild `torrentMap` from scratch (resets incremental state to prevent corruption).
|
||||
3. Otherwise merge delta fields into existing entries; remove `torrents_removed` hashes.
|
||||
4. On Sync API failure, fall back **once per cycle** to `GET /api/v2/torrents/info`.
|
||||
5. If the fallback also fails, return an empty array for this cycle and log the error.
|
||||
@@ -262,6 +267,10 @@ The rest of the application (poller, dashboard) receives data in the same format
|
||||
|
||||
The registry is used by both the background poller and the webhook processor, guaranteeing consistent data shapes across both update paths.
|
||||
|
||||
#### Error handling
|
||||
|
||||
Retriever methods now throw on HTTP failure rather than swallowing errors silently. This ensures the poller logs upstream problems and skips the affected instance cleanly instead of caching stale empty data.
|
||||
|
||||
#### Registry API
|
||||
|
||||
```javascript
|
||||
@@ -285,13 +294,55 @@ Each result element is `{ instance: instanceId, data: <arr API response> }`, all
|
||||
| Task | Endpoint | Key Parameters |
|
||||
|------|----------|----------------|
|
||||
| Sonarr tags | `GET /api/v3/tag` | — |
|
||||
| Sonarr queue | `GET /api/v3/queue` | `includeSeries=true`, `includeEpisode=true` |
|
||||
| Sonarr history | `GET /api/v3/history` | `pageSize=10`, `includeEpisode=true` |
|
||||
| Sonarr queue | `GET /api/v3/queue` | `includeSeries=true`, `includeEpisode=true`, `pageSize=1000` (paginated up to 50 pages) |
|
||||
| Sonarr history | `GET /api/v3/history` | `pageSize=50` (poller), `maxPages=1` (default), `includeEpisode=true` |
|
||||
| Radarr tags | `GET /api/v3/tag` | — |
|
||||
| Radarr queue | `GET /api/v3/queue` | `includeMovie=true` |
|
||||
| Radarr history | `GET /api/v3/history` | `pageSize=10` |
|
||||
| Radarr queue | `GET /api/v3/queue` | `includeMovie=true`, `pageSize=1000` (paginated up to 50 pages) |
|
||||
| Radarr history | `GET /api/v3/history` | `pageSize=50` (poller), `maxPages=1` (default) |
|
||||
|
||||
All fetches across all instances run in parallel via `Promise.allSettled`, so a single failing instance does not block others.
|
||||
All fetches across all instances run in parallel via `Promise.allSettled`, so a single failing instance does not block others. Queue fetches automatically paginate through all available records; history fetches default to a single page to avoid multi-second response times, while the UI history fetcher uses 100 records per page.
|
||||
|
||||
### 3.3 Download Matching & Assembly Services
|
||||
|
||||
#### Overview
|
||||
|
||||
The `server/services/` directory contains pure, testable services that transform raw cache data into user-facing download objects. These were extracted from `dashboard.js` during the technical-debt remediation, reducing the route file from ~1,360 lines to ~284 lines.
|
||||
|
||||
#### Service hierarchy
|
||||
|
||||
```
|
||||
DownloadBuilder.js Orchestrator: reads cache snapshot, calls matchers, deduplicates results
|
||||
├── DownloadMatcher.js Matches SABnzbd slots and qBittorrent torrents to Sonarr/Radarr records
|
||||
│ ├── matchSabSlots() Queue slots: matches by downloadId first, then bidirectional title substring
|
||||
│ ├── matchSabHistory() History slots: title matching against Sonarr/Radarr history
|
||||
│ └── matchTorrents() Torrents: queue → history fallback; unmatched torrents are excluded
|
||||
├── DownloadAssembler.js Pure helpers for building download objects
|
||||
│ ├── getCoverArt() Poster/fanart resolution
|
||||
│ ├── getImportIssues() Warning/error message extraction
|
||||
│ ├── getSonarrLink() / getRadarrLink()
|
||||
│ ├── canBlocklist() Admin vs non-admin blocklist eligibility
|
||||
│ ├── extractEpisode() Season/episode/title from queue/history record
|
||||
│ └── gatherEpisodes() Collect all episodes sharing the same download title
|
||||
└── TagMatcher.js Tag extraction, sanitisation, and user matching
|
||||
├── extractAllTags() / extractUserTag()
|
||||
├── tagMatchesUser() Exact or sanitised match (handles Ombi-mangled tags)
|
||||
├── getEmbyUsers() Cached Emby user Map (60 s TTL)
|
||||
└── buildTagBadges() Classify tags for admin showAll view
|
||||
```
|
||||
|
||||
#### Matching priority
|
||||
|
||||
For each download item, the matcher attempts matches in priority order:
|
||||
|
||||
1. **Stable ID match** — `downloadId` on SABnzbd slots is compared against Sonarr/Radarr queue/history `downloadId` fields (most reliable).
|
||||
2. **Title substring match** — bidirectional, case-insensitive substring check between the download name and the *arr `title` / `sourceTitle`.
|
||||
3. **Normalised title match** — dots replaced with spaces to handle release-name vs display-title mismatches.
|
||||
|
||||
Unmatched torrents are **not** included in the response (fixed in develop-refactor2).
|
||||
|
||||
#### Deduplication
|
||||
|
||||
`DownloadBuilder.buildUserDownloads()` deduplicates by `${type}:${title}` so the same download does not appear twice when it is present in both queue and history.
|
||||
|
||||
---
|
||||
|
||||
@@ -341,6 +392,10 @@ Sonarr/Radarr
|
||||
└── pollAllServices() → pollSubscribers.forEach(cb) → SSE push
|
||||
```
|
||||
|
||||
#### Replay protection improvements
|
||||
|
||||
The replay cache uses an atomic `Set`-based deduplication key (`{eventType}:{instanceName}:{date}`) with a 5-minute TTL. `instanceName` precision was tightened so that events from different *arr instances are never incorrectly flagged as duplicates.
|
||||
|
||||
The 200 response is sent **before** the background cache refresh completes, ensuring Sonarr/Radarr never have to wait for sofarr's downstream API calls.
|
||||
|
||||
#### Event Classification
|
||||
@@ -481,25 +536,36 @@ For each connected user the server:
|
||||
|
||||
1. Reads all `poll:*` keys from `MemoryCache`.
|
||||
2. Builds `seriesMap`, `moviesMap`, `sonarrTagMap`, and `radarrTagMap` from embedded objects in queue records.
|
||||
3. For each SABnzbd/qBittorrent download item, attempts matches in priority order: Sonarr queue → Radarr queue → Sonarr history → Radarr history.
|
||||
4. Title matching is a **bidirectional, case-insensitive substring match**: `rTitle.includes(dlTitle) || dlTitle.includes(rTitle)`.
|
||||
5. For each match, resolves the series/movie, extracts user tags, checks ownership.
|
||||
6. Returns only the requesting user's downloads (or all, if admin with `showAll=true`).
|
||||
3. Delegates to `DownloadBuilder.buildUserDownloads(cacheSnapshot, options)`, which orchestrates:
|
||||
- `DownloadMatcher.matchSabSlots()` — matches active SABnzbd queue slots
|
||||
- `DownloadMatcher.matchSabHistory()` — matches recent SABnzbd history slots
|
||||
- `DownloadMatcher.matchTorrents()` — matches qBittorrent torrents
|
||||
4. Each matcher attempts matches in priority order:
|
||||
- **Stable ID match** — `downloadId` compared against *arr `downloadId` (most reliable).
|
||||
- **Bidirectional title substring match** — case-insensitive `rTitle.includes(dlTitle) || dlTitle.includes(rTitle)`.
|
||||
- **Normalised title match** — dots replaced with spaces for release-name vs display-title mismatches.
|
||||
5. Unmatched torrents are excluded; matched results are deduplicated by `${type}:${title}`.
|
||||
6. For each match, `DownloadAssembler` resolves cover art, episodes, import issues, blocklist eligibility, and admin fields.
|
||||
7. `TagMatcher` extracts user tags and checks ownership.
|
||||
8. Returns only the requesting user's downloads (or all, if admin with `showAll=true`).
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["Download item"]) --> SQ{"Sonarr QUEUE\nmatch (title)"}
|
||||
Start(["Download item"]) --> DLID{"Stable ID match\ndownloadId?"}
|
||||
DLID -->|yes| IDResolve["Resolve series/movie\nfrom queue/history record"]
|
||||
DLID -->|no| SQ{"Sonarr QUEUE\ntitle match?"}
|
||||
SQ -->|yes| SQR["Resolve series · extract user tag"]
|
||||
SQ -->|no| RQ{"Radarr QUEUE\nmatch (title)"}
|
||||
SQ -->|no| RQ{"Radarr QUEUE\ntitle match?"}
|
||||
RQ -->|yes| RQR["Resolve movie · extract user tag"]
|
||||
RQ -->|no| SH{"Sonarr HISTORY\nmatch (title)"}
|
||||
RQ -->|no| SH{"Sonarr HISTORY\ntitle match?"}
|
||||
SH -->|yes| SHR["Resolve series via seriesId"]
|
||||
SH -->|no| RH{"Radarr HISTORY\nmatch (title)"}
|
||||
SH -->|no| RH{"Radarr HISTORY\ntitle match?"}
|
||||
RH -->|yes| RHR["Resolve movie via movieId"]
|
||||
RH -->|no| Skip(["Skip — unmatched"])
|
||||
|
||||
SQR & RQR & SHR & RHR --> Tagged{"Tag matches\nrequesting user?"}
|
||||
Tagged -->|yes| Include(["Include in response"])
|
||||
IDResolve & SQR & RQR & SHR & RHR --> Tagged{"Tag matches\nrequesting user?"}
|
||||
Tagged -->|yes| Dedup["Deduplicate by type:title"]
|
||||
Dedup --> Include(["Include in response"])
|
||||
Tagged -->|no| Skip
|
||||
```
|
||||
|
||||
@@ -642,8 +708,8 @@ See [Section 3.1](#31-pluggable-download-client-architecture-pdca) for full deta
|
||||
|
||||
```
|
||||
DownloadClient (abstract — server/clients/DownloadClient.js)
|
||||
├── SABnzbdClient.js — Usenet; REST; API key auth
|
||||
├── QBittorrentClient.js — Torrent; Sync API + fallback; cookie auth
|
||||
├── SABnzbdClient.js — Usenet; REST; API key auth; fixed global-speed assignment
|
||||
├── QBittorrentClient.js — Torrent; Sync API + fallback; cookie auth; full-sync corruption fix
|
||||
├── TransmissionClient.js — Torrent; JSON-RPC; session-ID management
|
||||
└── RTorrentClient.js — Torrent; XML-RPC; HTTP Basic Auth
|
||||
```
|
||||
@@ -660,12 +726,34 @@ DownloadClient (abstract — server/clients/DownloadClient.js)
|
||||
|
||||
### 7.3 Dashboard & Frontend
|
||||
|
||||
The frontend is a **vanilla JavaScript SPA** with no build step. All logic resides in `public/app.js`, styled by `public/style.css`, and structured by `public/index.html`. Three CSS themes are available via the `data-theme` attribute on `<html>` and persist in `localStorage`:
|
||||
The frontend is a **vanilla JavaScript SPA** built from ES modules in `client/src/` and bundled by **Vite** into `public/app.js`. Three CSS themes are available via the `data-theme` attribute on `<html>` and persist in `localStorage`:
|
||||
|
||||
- **Light** — Purple gradient header, white cards
|
||||
- **Dark** — Dark surfaces, muted accents
|
||||
- **Mono** — Monochrome, minimal colour
|
||||
|
||||
#### Module structure
|
||||
|
||||
```
|
||||
client/src/
|
||||
├── main.js Bootstrap: DOMContentLoaded → init theme, auth check
|
||||
├── state.js Global reactive state object
|
||||
├── api.js All HTTP fetch wrappers (+ CSRF handling)
|
||||
├── sse.js EventSource lifecycle, reconnect, heartbeat
|
||||
├── ui/
|
||||
│ ├── auth.js Login/logout form handlers
|
||||
│ ├── downloads.js Card rendering, create/update helpers, client-logo helpers
|
||||
│ ├── filters.js Download-client multi-select filter
|
||||
│ ├── history.js History tab: fetch, render, ignoreAvailable toggle
|
||||
│ ├── statusPanel.js Admin status panel (server, polling, cache, webhooks)
|
||||
│ ├── tabs.js Tab navigation (data-tab attributes)
|
||||
│ ├── theme.js Light/Dark/Mono theme switcher
|
||||
│ └── webhooks.js One-click Sonarr/Radarr webhook configuration
|
||||
└── utils/
|
||||
├── format.js Size, speed, duration, percentage formatters
|
||||
└── storage.js localStorage wrappers with JSON parsing
|
||||
```
|
||||
|
||||
#### UI state machine
|
||||
|
||||
```mermaid
|
||||
@@ -701,28 +789,24 @@ stateDiagram-v2
|
||||
}
|
||||
```
|
||||
|
||||
#### Key frontend functions
|
||||
#### Key frontend modules
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `checkAuthentication()` | On load: check session → show dashboard or login |
|
||||
| `handleLogin()` | Authenticate, fade login → splash → dashboard |
|
||||
| `startSSE()` | Open `EventSource` to `/stream`; handle incoming data |
|
||||
| `stopSSE()` | Close `EventSource` and cancel reconnect timer |
|
||||
| `renderDownloads()` | Diff-based card rendering (create/update/remove); filters by selected download clients; sorts by client order |
|
||||
| `createDownloadCard()` | Build DOM for a single card; renders tag badges, import-issue badge, blocklist button |
|
||||
| `updateDownloadCard()` | Update existing card in-place (progress, speed, etc.) |
|
||||
| `handleBlocklistSearch()` | Confirm dialog → POST `/blocklist-search` → update button state |
|
||||
| `toggleStatusPanel()` | Show/hide admin status panel |
|
||||
| `renderStatusPanel()` | Build status HTML (server, polling, SSE clients, cache) |
|
||||
| `initThemeSwitcher()` | Light / Dark / Mono theme support |
|
||||
| `loadHistory()` | Fetch `/api/history/recent`, store raw items, call `renderHistory()` |
|
||||
| `renderHistory()` | Filter items by `ignoreAvailable` flag, render history cards |
|
||||
| Module / Function | Purpose |
|
||||
|-------------------|---------|
|
||||
| `auth.js` | `checkAuthentication()`, `handleLogin()`, `handleLogout()` |
|
||||
| `sse.js` | `startSSE()`, `stopSSE()` — EventSource lifecycle and auto-reconnect |
|
||||
| `downloads.js` | `renderDownloads()`, `createDownloadCard()`, `updateDownloadCard()` — diff-based DOM; client-logo and tag-badge helpers deduplicated |
|
||||
| `filters.js` | `initDownloadClientFilter()` — multi-select dropdown, Select/Deselect All, localStorage persistence |
|
||||
| `history.js` | `loadHistory()`, `renderHistory()` — filter by `ignoreAvailable`, render cards |
|
||||
| `statusPanel.js` | `toggleStatusPanel()`, `renderStatusPanel()` — admin server/polling/cache/webhook status |
|
||||
| `theme.js` | `initThemeSwitcher()` — Light / Dark / Mono theme support |
|
||||
| `webhooks.js` | One-click Sonarr/Radarr webhook configuration via proxy API |
|
||||
| `format.js` | Size, speed, duration, percentage formatters (24 unit tests) |
|
||||
| `storage.js` | localStorage wrappers with JSON parsing and error handling |
|
||||
|
||||
#### Tag badge rendering
|
||||
#### CSP compliance
|
||||
|
||||
- **Regular user view** — a single accent-coloured badge showing the tag label that matched the current user's username (via `matchedUserTag`).
|
||||
- **Admin `showAll` view** — all tags on the download are rendered using `tagBadges[]`: tags with no matching Emby user → amber badge (leftmost); tags matched to a known Emby user → accent badge showing the Emby display name (rightmost).
|
||||
All UI modules use CSS class toggling (`.hidden`) instead of inline `style.display` to comply with the strict Content-Security-Policy enforced by Helmet.
|
||||
|
||||
#### Download Client Filter
|
||||
|
||||
@@ -732,14 +816,14 @@ The Active Downloads tab includes a multi-select dropdown filter that allows use
|
||||
- Use "Select All" / "Deselect All" buttons for bulk operations
|
||||
- Persist selection across sessions via localStorage
|
||||
|
||||
Downloads are sorted by client order (matching the configuration order) and filtered by the selected client IDs.
|
||||
|
||||
Related functions:
|
||||
Related functions in `filters.js`:
|
||||
- `initDownloadClientFilter()` — Sets up dropdown toggle, click-outside handler, Select/Deselect All buttons
|
||||
- `updateDownloadClientFilter()` — Populates checkbox list with client name + type badges
|
||||
- `toggleClientSelection()` — Updates selection array and localStorage
|
||||
- `updateSelectedCountDisplay()` — Updates button text to show "All clients" / "1 selected" / "N selected"
|
||||
|
||||
Downloads are sorted by client order (matching the configuration order) and filtered by the selected client IDs.
|
||||
|
||||
---
|
||||
|
||||
## 8. Directory Structure
|
||||
@@ -759,7 +843,8 @@ sofarr/
|
||||
│ │ └── PollingRadarrRetriever.js PALDRA — Radarr retriever
|
||||
│ ├── routes/
|
||||
│ │ ├── auth.js POST /login, GET /me, GET /csrf, POST /logout
|
||||
│ │ ├── dashboard.js SSE /stream, /user-downloads, /user-summary, /status, /cover-art, /blocklist-search
|
||||
│ │ ├── dashboard.js SSE /stream, /user-downloads, /blocklist-search
|
||||
│ │ ├── status.js GET /api/status/status — admin server/polling/webhook status
|
||||
│ │ ├── history.js GET /api/history/recent
|
||||
│ │ ├── webhook.js POST /api/webhook/sonarr|radarr
|
||||
│ │ ├── sonarr.js Sonarr API proxy + webhook management
|
||||
@@ -769,6 +854,12 @@ sofarr/
|
||||
│ ├── middleware/
|
||||
│ │ ├── requireAuth.js httpOnly cookie auth enforcement
|
||||
│ │ └── verifyCsrf.js Double-submit CSRF check (timing-safe)
|
||||
│ ├── services/ Download matching & assembly services
|
||||
│ │ ├── DownloadBuilder.js Orchestrator: cache snapshot → matched downloads
|
||||
│ │ ├── DownloadMatcher.js Match SABnzbd/qBittorrent to *arr records
|
||||
│ │ ├── DownloadAssembler.js Pure helpers: cover art, links, episodes, blocklist
|
||||
│ │ ├── TagMatcher.js Tag extraction, sanitisation, user matching
|
||||
│ │ └── WebhookStatus.js Webhook configuration check + metrics aggregation
|
||||
│ └── utils/
|
||||
│ ├── arrRetrievers.js PALDRA registry — Sonarr/Radarr fetch registry
|
||||
│ ├── cache.js MemoryCache + webhook metrics helpers
|
||||
@@ -780,15 +871,37 @@ sofarr/
|
||||
│ ├── qbittorrent.js Legacy compatibility shim → QBittorrentClient
|
||||
│ ├── sanitizeError.js Secret redaction from errors/logs
|
||||
│ └── tokenStore.js Emby token store (JSON file, atomic writes, 31-day TTL)
|
||||
├── public/ Static SPA (served by Express)
|
||||
├── client/ Frontend source (vanilla ES modules)
|
||||
│ ├── src/
|
||||
│ │ ├── main.js Bootstrap entry point
|
||||
│ │ ├── state.js Global reactive state
|
||||
│ │ ├── api.js HTTP fetch wrappers
|
||||
│ │ ├── sse.js EventSource management
|
||||
│ │ ├── ui/
|
||||
│ │ │ ├── auth.js
|
||||
│ │ │ ├── downloads.js
|
||||
│ │ │ ├── filters.js
|
||||
│ │ │ ├── history.js
|
||||
│ │ │ ├── statusPanel.js
|
||||
│ │ │ ├── tabs.js
|
||||
│ │ │ ├── theme.js
|
||||
│ │ │ └── webhooks.js
|
||||
│ │ └── utils/
|
||||
│ │ ├── format.js
|
||||
│ │ └── storage.js
|
||||
│ ├── index.html Development HTML shell
|
||||
│ ├── package.json Frontend dev dependencies (vite)
|
||||
│ └── vite.config.js Build config → ../public/app.js
|
||||
├── public/ Static SPA assets (served by Express)
|
||||
│ ├── index.html HTML shell: splash, login, dashboard
|
||||
│ ├── app.js All frontend logic
|
||||
│ ├── app.js Bundled frontend (Vite build output)
|
||||
│ ├── style.css Themes, layout, responsive design
|
||||
│ ├── favicon.ico / *.png Favicons
|
||||
│ └── images/ Logo / splash screen assets
|
||||
├── tests/
|
||||
│ ├── README.md Testing approach and coverage targets
|
||||
│ ├── setup.js Global setup: isolated DATA_DIR, rate-limit bypass
|
||||
│ ├── frontend/ Vitest + jsdom unit tests for client/src
|
||||
│ ├── unit/ Pure unit tests (no HTTP)
|
||||
│ └── integration/ Supertest + nock integration tests
|
||||
├── .gitea/workflows/
|
||||
@@ -797,7 +910,7 @@ sofarr/
|
||||
│ ├── create-release.yml Release tagging workflow
|
||||
│ ├── docs-check.yml Markdown lint + Mermaid validation
|
||||
│ └── licence-check.yml Production dependency licence check
|
||||
├── Dockerfile Multi-stage production image (node:22-alpine)
|
||||
├── Dockerfile Multi-stage production image (node:22-alpine) — includes Vite client build stage
|
||||
├── docker-compose.yaml Example compose deployment
|
||||
├── vitest.config.js Test runner configuration with per-file coverage thresholds
|
||||
├── package.json Dependencies and scripts
|
||||
@@ -929,7 +1042,7 @@ Each instance receives an `id` derived from `name` (or index if unnamed), used a
|
||||
| Framework | Express 4.x | HTTP server, routing, middleware |
|
||||
| HTTP client | axios 1.x | External API communication |
|
||||
| XML-RPC client | xmlrpc 1.3.2 | rTorrent communication |
|
||||
| Frontend | Vanilla JS + CSS | SPA, no build step required |
|
||||
| Frontend | Vanilla JS + CSS | SPA; Vite bundles ES modules from `client/src/` into `public/app.js` |
|
||||
| Containerisation | Docker multi-stage (node:22-alpine) | Non-root `node` user; minimal image |
|
||||
| Logging | Custom logger + `console.*` redirection | File + stdout with configurable levels |
|
||||
|
||||
@@ -957,12 +1070,13 @@ Each instance receives an `id` derived from `name` (or index if unnamed), used a
|
||||
| `vitest` | 4.x | Test runner with V8 coverage; per-file coverage thresholds in `vitest.config.js` |
|
||||
| `supertest` | 7.x | HTTP integration testing against the Express app factory |
|
||||
| `nock` | 14.x | HTTP interception at Node layer (compatible with CJS `require('axios')`) |
|
||||
| `jsdom` | 24.x | Browser-like DOM environment for frontend unit tests |
|
||||
|
||||
### CI/CD
|
||||
|
||||
| Workflow file | Trigger | Purpose |
|
||||
|---------------|---------|---------|
|
||||
| `ci.yml` | Every push / PR | `npm audit --audit-level=high` + full test suite with V8 coverage |
|
||||
| `ci.yml` | Every push / PR | `npm audit --audit-level=high` + full test suite (unit, integration, frontend) with V8 coverage |
|
||||
| `build-image.yml` | Push to `release/**` or `develop` | Build and push Docker image to registry |
|
||||
| `create-release.yml` | Tag push (`v*`) | Generate release notes and create a Gitea release |
|
||||
| `docs-check.yml` | Push / PR touching `**.md` | Markdown lint + Mermaid diagram parse validation |
|
||||
|
||||
Reference in New Issue
Block a user