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

@@ -3,6 +3,7 @@ mod db;
mod download;
mod feed;
mod ipc;
mod logbuf;
mod retention;
mod torrent;
mod web;
@@ -119,13 +120,19 @@ impl Ctx {
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_env("IPX_LOG")
.unwrap_or_else(|_| "ipx=info".into()),
)
.with_writer(std::io::stderr)
.init();
// Everything goes to stderr as before, and is mirrored into a ring the UI can read.
{
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_env("IPX_LOG")
.unwrap_or_else(|_| "ipx=info".into()),
)
.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
.with(logbuf::RingLayer)
.init();
}
let config_path = cli.config.clone().unwrap_or_else(config::config_path);
let cfg = config::Config::load(&config_path)?;
@@ -207,6 +214,12 @@ async fn daemon(
anyhow::bail!("a daemon is already listening on {}", socket.display());
}
match ctx.db.requeue_interrupted() {
Ok(n) if n > 0 => tracing::info!(count = n, "requeued downloads interrupted by a restart"),
Ok(_) => {}
Err(e) => tracing::warn!(error = ?e, "could not requeue interrupted downloads"),
}
let (tx_cmd, mut rx_cmd) = mpsc::channel::<Cmd>(64);
let web = start_web(&ctx, &config_path, web_addr, &tx_cmd, &events).await?;