ARCHITECTURE.md
Rama main · réplica de lectura del repositorio original.
# Weir Architecture
This is the top-level map for agents and contributors. Deeper decisions live in [`docs/adr/`](docs/adr/).
## Product Shape
Weir is a self-hosted media operations app:
- **Processing** remuxes watched media into cleaner outputs. It is configured as any number
of **workflows** — each a row carrying its own paths, admission rules, schedule,
guardrails and media manager connections — rather than one fixed movie scope and one
fixed TV scope. The API and database still call a workflow a "library"; Settings calls it a
workflow because in Deluno, Radarr and Sonarr a library is where media ends up. A workflow's
`media_type` (Movies or TV; named `media_scope` until #460)
is a property of the workflow, not what the module partitions on. It still decides the
cleanup shape, which manager queue is asked when a workflow links none, and which workflow a
job belongs to when its payload names none. A hand-off from a manager lands in the workflow
whose watched folder holds the file. Adding a workflow is a POST. See
[ADR-0014](docs/adr/ADR-0014-processing-libraries-replace-fixed-scopes.md). The singleton
settings rows that workflows replaced were dropped in `0025`, and the scope-shaped
`path-settings` and `remux-rules-settings` routes that outlived them were removed in #460.
- **Media managers** are the products Weir accepts work from and reports back to.
A connection carries a *kind* (Radarr, Sonarr, Deluno, or anything posting Weir's
own payload) rather than each product having its own routes and columns; every inbound
event arrives at `POST /api/v1/intake/webhook/{source}`. See
[ADR-0013](docs/adr/ADR-0013-media-managers-are-kinds-not-products.md).
Outbound, a media scope resolves to **every** connection that looks after it and each
one is asked what it is importing and which files it still keeps. "Could not ask" is a
distinct answer from "nothing is importing", and only the latter clears a delete. See
[ADR-0015](docs/adr/ADR-0015-media-manager-port-outbound.md).
- **Library mode** cleans files already imported into a library, replacing each one in place with
its cleaned copy through the crash-safe swap described in the
[file lifecycle contract](docs/file-lifecycle-contract.md).
- The app's pages are **Processing** (the first screen: every file Weir is working on now),
**Activity**, **Library**, **Settings** and **System**. System holds updates, backups, security
and logs.
## Runtime Shape
- Server: C# / .NET 10 under `apps/server` (ASP.NET Core minimal APIs, SQLite through
`Microsoft.Data.Sqlite` with explicit SQL and numbered SQL migrations). See
[ADR-0017](docs/adr/ADR-0017-backend-on-dotnet.md).
- Frontend: React + Vite under `apps/web/src`, served by the server from `WEIR_WEB_DIST`.
- Packaging: a Docker image (linux/amd64 and linux/arm64) and a Windows Velopack installer whose
.NET tray app (`apps/tray`) starts and watches the server, and stops it by setting a named Windows event
the server listens for (`Local\Weir-Stop-<pid>`), killing it only if it does not exit in time. Both carry ffmpeg and mkvmerge
(MKVToolNix) — the Docker image installs them from Debian packages, the Windows package vendors
checksum-verified builds. See [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
- Runtime data: `WEIR_HOME`.
```mermaid
flowchart LR
UI["Frontend (React/Vite)"] --> API["Weir.Api (ASP.NET Core endpoints)"]
API --> Core["Core + Platform Services"]
Core --> Processing["Processing (the application)"]
Core --> Activity["Activity (event records)"]
Core --> Integrations["Media managers (Sonarr, Radarr, Deluno) and Deluno's metadata service"]
Core --> DB["SQLite (numbered SQL migrations)"]
Processing --> Jobs["Durable jobs (jobs) + workers"]
```
## Repository map
| Folder | What it is for |
| --- | --- |
| `apps/server` | The .NET 10 server: API, SQLite, durable jobs, the remux pass. Builds `Weir` / `Weir.exe`. |
| `apps/web` | The React and TypeScript web app the server serves, with its dev simulator and the committed OpenAPI contract. |
| `apps/tray` | The Windows tray app that starts and watches the server. |
| `docker` | The container entrypoint and the Docker deployment guide; the `Dockerfile` and `compose.yaml` are at the root. |
| `packaging` | Windows installer build (Velopack) and brand assets. |
| `docs` | Contracts, ADRs, release notes, runbooks and the agent documentation map; the index is [`docs/README.md`](docs/README.md). |
| `docs-site` | The public documentation website (Docusaurus). |
| `scripts` | Repository tooling, CI gates and release checks; see [`scripts/README.md`](scripts/README.md). |
| `.github` | CI and release workflows, issue and pull request templates. |
| `.githooks` | The pre-push hook that runs the local checks. |
## Server Map
Solution `apps/server/Weir.slnx`; details in [`apps/server/README.md`](apps/server/README.md).
- `src/Weir.Host`: the process — configuration, logging, database startup, Kestrel, Windows Service and systemd support. Builds `Weir` / `Weir.exe`.
- `src/Weir.Api`: endpoints and HTTP behaviour — auth and CSRF, security headers, request ids, the OpenAPI document, serving the web app.
- `src/Weir.Infrastructure`: SQLite (connections, stores, numbered migrations in `Migrations/`), the durable job queue and workers, the remux pass, the remux writers (ffmpeg/ffprobe and the mkvmerge writer added in #548), media manager clients, the filesystem and log files.
- `src/Weir.Core`: records and rules with no IO — the Processing rules engine, job rules, media manager rules, settings and security primitives.
- `tests/*`: xUnit tests per project. Two of the projects judge a running server from outside, as a process over HTTP and never in-process: `Weir.Contract.Tests` (the contract suite, one test class per area under `Activity/`, `Auth/`, ...; `Weir.Contract.FakeTools` is its fake ffprobe/ffmpeg) and `Weir.E2E.Tests` (Playwright for .NET, the browser smoke).
- `tools/Weir.LiveAudit`: the packaged live audit, a Playwright for .NET walk through every screen of an installed Weir (the Docker image or the Windows package); CI and the release workflow run it.
- `apps/tray/Weir.Tray`: the Windows tray app shipped in the installer.
## Frontend Map
- `src/app`: app-level router and providers.
- `src/layouts`: shell/navigation layout.
- `src/pages`: feature pages (Dashboard, Activity, Library, Setup, System), plus sign-in and setup.
- `src/lib`: API clients, query hooks, typed data helpers, and UI helpers.
- `src/components`: reusable UI and brand components.
- `src/styles`: design tokens and shell styling.
- `src/test`: frontend test setup.
## Boundary Rules
- Processing code should keep destructive or irreversible behavior behind explicit services and tests.
- Backend APIs should expose typed schemas at boundaries instead of inferred shapes.
- Frontend pages should use typed API/query helpers from `src/lib` rather than ad hoc fetch calls.
- Cross-cutting runtime concerns belong in shared services (`Weir.Core` rules, `Weir.Infrastructure` platform services), not inside Processing implementation details.
- `Weir.Core` stays free of IO; filesystem, database, process and HTTP work lives in `Weir.Infrastructure` behind seams tests can replace.
- File lifecycle changes must preserve the safety contract in [`docs/file-lifecycle-contract.md`](docs/file-lifecycle-contract.md).
## Live Data
Every screen follows Weir from one server-sent event stream, `GET /api/v1/activity/stream`, shared by the whole web app. A new number arrives by a push, never by a timer or a Refresh button.
- **Publish:** a component that changes data calls `DataChangePublisher.Publish(DataTopics.<Topic>)` in `Weir.Infrastructure.Activity` once the change has committed (`uow.OnCommitted`); `PublishOnCommit(uow, topic)` does that and says each topic once however many rows the unit of work wrote. Each open stream sends a `data.changed` frame, `{ "topic": "<snake_case_name>" }`.
- **Consume:** `LIVE_TOPIC_QUERIES` in `apps/web/src/lib/live/live-topics.ts` says which queries each topic reads again. A screen adds its query keys to its topic and drops its `refetchInterval`.
- **Not from the database:** data that changes outside a commit has a publisher of its own. `TrayHandOffWatcher` watches the files the Windows tray writes in the data folder (`update-state.json`, `update-settings.json`, `lan-access`) and publishes `update` and `network_access` when one is written, by the tray or by the server. `TrayStatusWriter` goes the other way: it listens to the `pause`, `connections` and `folder_checks` topics and keeps `tray-status.json` true for the tray ([`docs/release.md`](docs/release.md)). `FolderReachability` is the component that knows which workflow folders Weir cannot reach: it looks every 15 seconds and at once when the workflows change, browser open or not, asks about each folder on a thread of its own for at most 10 seconds (a hung share counts as unreachable and is not asked again until it answers), reports a folder only after two misses in a row, and publishes `folder_checks` when its answer changes. A forgotten or cancelled file publishes `library_scan`, which is what refreshes the file lists. `UpdateOutlook` publishes `update` when a release check finds something new, and `MetricsChangeTask` publishes `metrics` at most every five seconds while a stream is open. After "Restart and apply" the page needs no check of its own: the stream reconnects and the new `boot_id` reads every query again.
- **Frames:** what changes too often to read again each time, or arrives whole, travels in its own frame instead of a topic: `processing.progress`, `system.stats` (a reading a second, with the machine's facts and the drives), `system.overview` (the facts System shows about Weir, read once for every open stream and sent when one changes), `system.tasks`, `system.checks` (when the server last checked the workflows' folders and whether Weir is ready, at every look, so Health says when the server looked rather than when the page read) and `system.log` (warnings, errors and Weir's own information).
- **Watching tasks:** what has no change to hook, such as a worker that stops or a folder that goes missing, is looked at by the server on a timer, only while a browser holds the stream, and published when the answer differs from last time (`readiness-changes`, `folder-checks`). A long-running job says so as it goes, at most once a second (`ThrottledDataChange`), and once more when its job row is final.
- **Connection:** `server.hello` opens every stream with `{ "boot_id" }`, new on each server start. While the connection is lost the shell shows "Live updates paused", and when it is back every query is read again; a different `boot_id` after a reconnect also reloads the page if the server now serves a newer build. A stream that sends nothing for 40 seconds while the tab is showing is dropped and opened again, and one the server refuses sends the page to sign-in when the session has ended.
## Job Lifecycle
```mermaid
flowchart LR
Enqueue["Enqueue request"] --> Jobs["jobs + workers"]
Jobs --> Result["Job result (completed/failed/pending retry)"]
Result --> Activity["Activity + logs"]
Result --> Metrics["Runtime metrics / Prometheus"]
```
## Architecture Decision Records
Current ADR index: [`docs/adr/README.md`](docs/adr/README.md).
Add an ADR when a decision changes runtime storage, data safety, security boundaries, release mechanics, or packaging behavior.