Phase 2: web front end

axum served from inside the daemon so it reads SQLite and the event bus
directly: browse feeds, read show notes, play with seeking, download and
delete files, mark read/flag, and edit feed settings.

Config is now hot-reloadable (Ctx.cfg behind RwLock<Arc<Config>>), so UI
edits apply without a daemon restart. Access is a shared token minted from
/dev/urandom, carried in a cookie because an <audio> element cannot send
headers. Show notes are untrusted feed HTML and are sanitized with ammonia
server-side.

read/flagged finally have a writer, which retention has needed since it
started ordering by them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPyeapneuXrCdojsaiXGbe
This commit is contained in:
2026-09-10 00:55:16 +00:00
parent ed47e456d4
commit 74ec6e9281
9 changed files with 1391 additions and 52 deletions

View File

@@ -5,18 +5,20 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Default, Deserialize, Serialize)]
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct Config {
#[serde(default)]
pub general: General,
#[serde(default)]
pub torrent: Torrent,
#[serde(default)]
pub web: Web,
/// Keyed by feed id: the TOML table name, which replaces the old genHash(feedURL).
#[serde(default)]
pub feeds: BTreeMap<String, Feed>,
}
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct General {
pub download_dir: PathBuf,
@@ -39,7 +41,7 @@ pub enum Organize {
Date,
}
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct Torrent {
pub enabled: bool,
@@ -51,6 +53,32 @@ pub struct Torrent {
pub stall_mins: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct Web {
pub enabled: bool,
/// Use 0.0.0.0 to reach it from the LAN. Anything but loopback needs the token.
pub bind: String,
/// Shared secret. Generated and written back on first run when left empty.
pub token: String,
}
impl Default for Web {
fn default() -> Self {
Self {
enabled: false,
bind: "127.0.0.1:8080".into(),
token: String::new(),
}
}
}
impl Web {
pub fn binds_publicly(&self) -> bool {
!self.bind.starts_with("127.") && !self.bind.starts_with("localhost")
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Feed {
pub url: String,