Add a log view, and Docker packaging

The Log button shows the running daemon live: feed scans, downloads,
torrents and every HTTP request. It reads a ring buffer filled by a
tracing layer rather than tailing a file, so it works under Docker where
logs go to stdout. The access-log middleware skips /api/logs, or the
panel's poll would log itself forever.

Detached torrents could leave a row stuck in 'downloading' across a
restart, where nothing would ever revisit it; those are requeued at
startup.

Dockerfile, entrypoint and compose: 114 MB runtime, config bound to
0.0.0.0 on first run since container loopback is unreachable, drops to
PUID:PGID for Unraid, and a healthcheck that goes through the control
socket so a wedged worker reads as unhealthy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
2026-09-10 12:06:54 +00:00
parent 19309d609f
commit 5f6e2a8dc1
12 changed files with 513 additions and 9 deletions

View File

@@ -56,6 +56,38 @@ and until now nothing set them.
---
## 2026-09-10 — Log view in the app, and Docker
**Log view.** A ring buffer (2000 lines) fed by a `tracing` layer, exposed at `/api/logs` with a
sequence cursor so the UI polls for "everything after N" without duplicates. Tailing a file was the
obvious approach and the wrong one: under Docker the logs go to stdout and there is no file. An
`access_log` middleware adds a line per HTTP request, so the daemon and the web side share one
stream — which is what Ray asked for. That middleware skips `/api/logs` itself, or the panel's
2-second poll would generate a line per poll forever, a log of nothing but its own requests.
UI: sidebar **Log** button, level and text filters, follow-tail toggle, copy button, colour-coded
levels.
**Restart safety.** Detached torrents introduced a `downloading` state, and a row left in it by a
restart would sit there forever — the pending queue skips it and nothing else revisits it.
`requeue_interrupted()` resets those at startup, leaving alone any row that already has a file.
**Docker.** Multi-stage build (deps cached separately from source), `debian:bookworm-slim` runtime,
114 MB. Entrypoint writes a starter config bound to `0.0.0.0` on first run, because a container's
loopback is unreachable from outside, and drops to `PUID:PGID` via gosu. Healthcheck is
`ipx status`, which goes through the control socket to the worker and so catches a *wedged* daemon,
not just a dead one.
Built and actually run, not just written: image builds, container starts, writes its config, serves
the UI (401 without a token, 200 with), `/api/logs` answers, and the process runs as
`uid=99(ipx) gid=100(users)` with files owned `99:100` and the token file at mode 600.
Worth noting for future testing here: this session's Docker socket belongs to the Unraid host, so
`-v /tmp/...:/config` binds a path on the *host*, not one visible from inside this container. The
mount looks empty from here while being perfectly correct — verify inside the container instead.
---
## 2026-09-10 — Torrents: they work, and one was freezing the whole daemon
Asked "is torrents working?". The honest answer had been "unverified since step 7" — so it got tested