add a transmission sidecar and ui

officer-transmission is a new pm2 peer that owns the transmission rpc
connection and exposes a curated /_officer/* contract instead of proxying
raw rpc. it absorbs the three quirks callers otherwise have to know about:
the 409 x-transmission-session-id handshake, failures returned as
{"result": "..."} inside http 200, and basic auth where an empty username
must send no header at all.

/transmission is the ui, on the workspace/panel framework: a filter nav and
three sections (torrents, stats, settings). the torrent list is virtualised
with 30 available columns, multi-select, and a right-click menu; the detail
pane covers general, files as a real tree, peers and trackers. filters and
the open torrent live in the url, so a filtered view is a link.

phase 1 goal was parity with _references/transmission-web. follow-up work is
recorded in TODO.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 17:09:19 +00:00
co-authored by Claude Opus 5
parent c39460ce2d
commit 387664964c
47 changed files with 5273 additions and 0 deletions
+30
View File
@@ -72,6 +72,36 @@ Deferred work. Context: Officer is collapsing from multi-tenant / open-source-re
as one of the tabs (main view).
- Open question: tabs vs. some other view switcher; which view is default.
## Transmission
Phase 1 (parity with `_references/transmission-web`) shipped 2026-07-30: the `officer-transmission`
sidecar plus `/transmission` (torrents / stats / settings). Everything below is **phase 2** — none of
it exists in the reference app, so none of it was in scope for parity.
### Probably needed regardless
- [ ] **Resizable detail pane.** `TorrentsView` pins it at `DETAIL_HEIGHT = '45%'`, which is a guess.
Either a drag handle or a persisted height in `useLocalStorageState`, alongside the column set.
- [ ] **Revisit `DEFAULT_VISIBLE_COLUMNS`.** Eleven of thirty, chosen before ever seeing the table
rendered against the real 43-torrent library. Likely wrong in both directions.
- [ ] **Files tab on huge torrents.** `detail/FilesTab.tsx` builds the whole tree and renders every
node — no virtualisation. Fine at 14 files; unknown at a few thousand. If it stalls, the fix is
`useVirtualizer` over a flattened visible-node list, the same shape as `TorrentTable`.
### Ideas, unprioritised
- [ ] **Container→host path mapping.** The daemon reports its own namespace (`/downloads/complete`),
which is not a path on this host. A mapping table (`/downloads``~/hdds/08_TB_01/Torrents`,
`/downloadsTV``14_TB_02/Torrents`) would make locations clickable through to `/files` and
make "Set location" offer real destinations. Deliberately dropped from phase 1: the reference
app has no such feature, so it would have been config nothing read.
- [ ] **Push instead of poll.** Transmission RPC has no push channel, so `useTransmissionData` polls
every 5s. The sidecar could poll once and fan out over a WebSocket, which is both cheaper and
what every other live surface in the platform already does.
- [ ] **Cross-surface links.** Soulseek and `download-media` both land files in the same library;
Transmission is the third door to it. Worth a think about whether they should know about each
other at all.
## Known bugs
- [ ] **`bootstrap.ts` runs `npm install -g` for Pi on every boot.** `findPiPackageDir` checks stale