A terminal reader, ipx; the server is ipxd (#437)

ipx-tui (crates/tui) is iPX's reader in ratatui: your feeds, a feed's
items, an item's text as plain text, read and flagged as on the page,
through the API with an API token. It installs as `ipx`, so the daemon's
binary is now `ipxd`: the Dockerfile, release script, tests and every
doc and message that names its commands follow.

Naming the binary ipxd alone renamed its crate, and with it every log
target (ipx::scan became ipxd::scan): the default filters are ipx=info,
so nearly every line would have been dropped and the dashboard's queries
on the targets gone blank. The daemon's code is now a library still
called ipx, under a five-line ipxd main.rs, and the targets are as they
were.

ratatui is taken without its default features: 32 new packages in place
of 78.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqkrVDqdiQEMWAkNGQ2tYA
This commit is contained in:
2026-11-09 11:52:10 +00:00
parent f844cf349a
commit 711b0395f1
40 changed files with 1128 additions and 238 deletions

View File

@@ -5,7 +5,7 @@ Three services, each a container of its own with its own version and changelog (
| Service | What it is | Source | Image |
|---|---|---|---|
| ipx-web | Caddy: serves the page, a static site built from `web/`, and passes `/api` and `/media` to ipx on the same address. It asks ipx before it serves the app or the admin page (`/api/page-check`) | `web/`, `web/Caddyfile` | `Dockerfile.web` |
| ipx | The daemon and CLI, one binary. `ipx daemon` runs a scheduler, a Unix-socket control server and the HTTP API in one process; everything else is a CLI that either does the work itself or hands it to a running daemon | `crates/ipx` | `Dockerfile` |
| ipx | The daemon and CLI, one binary, `ipxd`. `ipxd daemon` runs a scheduler, a Unix-socket control server and the HTTP API in one process; everything else is a CLI that either does the work itself or hands it to a running daemon | `crates/ipx` | `Dockerfile` |
| ipx-directory | The directory of public podcasts that ipx servers share. ipx asks it on its people's behalf; the page never talks to it ([directory.md](directory.md)) | `crates/directory` | `Dockerfile.directory` |
The Rust source is a Cargo workspace (#202); `crates/feed` (`ipx-feed`) fetches and parses feeds
@@ -27,7 +27,8 @@ id, and Caddy's request lines theirs. ipx also serves Prometheus metrics (`[web]
| File | Responsibility | What it replaced in the Python |
|---|---|---|
| `crates/ipx/src/main.rs` | The module list and `main`: reads the command line and hands it on | `iPXAgent.py` |
| `crates/ipx/src/lib.rs` | The module list and `main`: reads the command line and hands it on | `iPXAgent.py` |
| `crates/ipx/src/main.rs` | `ipxd`, which calls `main`: a library under it keeps the modules' log targets `ipx::...` | — |
| `crates/ipx/src/cli.rs` | The commands, and what each does when no daemon takes it | `iPXAgent.py` |
| `crates/ipx/src/ctx.rs` | `Ctx`, what every command shares: configuration, database, clients | — |
| `crates/ipx/src/daemon.rs` | The daemon: worker loop, web server, shutdown, `status` | `iPXAgent.py` |
@@ -68,6 +69,7 @@ id, and Caddy's request lines theirs. ipx also serves Prometheus metrics (`[web]
| `crates/directory/src/store.rs` | The directory's database: feeds, snapshots, blocked | — |
| `crates/directory/src/config.rs` | The directory's config file: its tokens and their access | — |
| `crates/directory/src/otel.rs` | Traces for both Rust services: the exporter, `traceparent` in and out | — |
| `crates/tui/src/main.rs` | `ipx` (package `ipx-tui`), the reader in a terminal: ratatui over the API, with an API token | — |
| `web/index.html` | The app's markup | — |
| `web/admin.html` | The admin page's markup: server settings, accounts, the log. Sent to admins only | — |
| `web/app.css` | The stylesheet both pages share | — |
@@ -156,7 +158,7 @@ printf '{"cmd":"fetch","force":true}\n' | socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/
**Commands** — `fetch` (optional `feed`, `force`), `reap` (optional `dry_run`), `download`
(`enclosure`), `status`, `reload` (read the catalogue and settings again from the database, which
`ipx add`, `rm` and `import` send after changing them in a process of their own; answered with
`ipxd add`, `rm` and `import` send after changing them in a process of their own; answered with
`status`).
**Events** — `feed_start`, `feed_skip`, `feed_done`, `feed_error`, `progress`, `download_done`,

View File

@@ -10,15 +10,15 @@ processes must never download the same thing. `--local` forces the work to happe
| Command | What it does |
|---|---|
| `ipx list` | Subscriptions and their state |
| `ipx status` | Counts: feeds, pending, downloaded |
| `ipx fetch [FEED] [--force]` | Scan everything, or one feed. `--force` ignores the TTL |
| `ipx add <url> [--folder X] [--keywords a,b] [--list] [--category C]` | Subscribe; the id comes from the feed title. `--list` subscribes no one but keeps it in the catalogue, and so in what this server shares with the directory, when its last subscriber leaves; `--category` is the category the directory files it under when the feed names none. Run for a feed already in the catalogue, these list it or set its category |
| `ipx rm <feed>` | Unsubscribe; downloads and history are kept |
| `ipx import <file.opml>` / `ipx export <file.opml>` | Move subscriptions in or out. Import subscribes the first admin, as the shared web token does; in the web UI it subscribes whoever is signed in |
| `ipx reap [--dry-run]` | Run retention now |
| `ipx user <add\|list\|passwd\|rm>` | Accounts for the web UI |
| `ipx daemon [--web ADDR]` | Scheduler, control socket and the HTTP API (the page is ipx-web's) |
| `ipxd list` | Subscriptions and their state |
| `ipxd status` | Counts: feeds, pending, downloaded |
| `ipxd fetch [FEED] [--force]` | Scan everything, or one feed. `--force` ignores the TTL |
| `ipxd add <url> [--folder X] [--keywords a,b] [--list] [--category C]` | Subscribe; the id comes from the feed title. `--list` subscribes no one but keeps it in the catalogue, and so in what this server shares with the directory, when its last subscriber leaves; `--category` is the category the directory files it under when the feed names none. Run for a feed already in the catalogue, these list it or set its category |
| `ipxd rm <feed>` | Unsubscribe; downloads and history are kept |
| `ipxd import <file.opml>` / `ipxd export <file.opml>` | Move subscriptions in or out. Import subscribes the first admin, as the shared web token does; in the web UI it subscribes whoever is signed in |
| `ipxd reap [--dry-run]` | Run retention now |
| `ipxd user <add\|list\|passwd\|rm>` | Accounts for the web UI |
| `ipxd daemon [--web ADDR]` | Scheduler, control socket and the HTTP API (the page is ipx-web's) |
`add`, `rm` and `import` change the catalogue in a process of their own. With a daemon running they
then tell it to read the catalogue again; without that it kept its own copy and wrote it back at
@@ -29,11 +29,11 @@ its next change, undoing them. If it does not answer they say so: restart it.
Passwords are read from **stdin**, so they miss the shell history and any `ps` listing.
```sh
echo -n 'a good password' | ipx user add ray # local account
ipx user add ray@example.com --no-password # signs in through the proxy only
echo -n 'a good password' | ipx user passwd admin # change a password; signs the account out everywhere
ipx user list # who exists, and how each signs in
ipx user rm sam # account, subscriptions and read state
echo -n 'a good password' | ipxd user add ray # local account
ipxd user add ray@example.com --no-password # signs in through the proxy only
echo -n 'a good password' | ipxd user passwd admin # change a password; signs the account out everywhere
ipxd user list # who exists, and how each signs in
ipxd user rm sam # account, subscriptions and read state
```
The first account created is an admin; later ones are ordinary users. A database with no accounts
@@ -43,14 +43,14 @@ config.toml (or `IPX_ADMIN_PASSWORD`), announced in the log — change it.
To avoid even the command line, read it interactively:
```sh
read -s PW && echo -n "$PW" | ipx user passwd admin
read -s PW && echo -n "$PW" | ipxd user passwd admin
```
## Scanning
```sh
ipx fetch # everything due
ipx fetch atp --force # one feed, ignoring its TTL and schedule
ipxd fetch # everything due
ipxd fetch atp --force # one feed, ignoring its TTL and schedule
```
A scan: conditional GET (`If-None-Match` / `If-Modified-Since`), parse, record new entries, apply
@@ -60,8 +60,8 @@ answers `304` and costs one request.
## Retention
```sh
ipx reap --dry-run # what would go, oldest first
ipx reap # actually delete
ipxd reap --dry-run # what would go, oldest first
ipxd reap # actually delete
```
Files are deleted to get back under `max_total_gb`, oldest first, and items past `max_age_days`
@@ -72,8 +72,8 @@ stops the next scan fetching it again.
## The daemon
```sh
ipx daemon # scheduler + socket + HTTP API
ipx daemon --web 0.0.0.0:8099 # override the configured bind for one run
ipxd daemon # scheduler + socket + HTTP API
ipxd daemon --web 0.0.0.0:8099 # override the configured bind for one run
```
One daemon per socket; a second refuses to start rather than fight over the database. It shuts down

View File

@@ -4,7 +4,7 @@ Two places. **config.toml** holds what ipx needs before it reaches its database,
who gets in: where things are (`download_dir`, `socket`, `organize`), `[torrent]` and `[web]`.
**The database** holds the catalogue of feeds (`[feeds.<id>]` below) and the server settings the
admin page edits (`schedule`, `max_total_gb`, `max_age_days`, `max_new_per_check`,
`media_types`, `art_cache_mb`). Change those in the web UI, or with `ipx add`, `ipx rm` and `ipx import`; they
`media_types`, `art_cache_mb`). Change those in the web UI, or with `ipxd add`, `ipxd rm` and `ipxd import`; they
take effect without a restart.
The first time ipx meets a database that holds no catalogue, it takes the feeds and those
@@ -164,8 +164,8 @@ The app registers each phone through the page as the person signed in there
## `[feeds.<id>]`
Kept in the database once ipx has moved them in: a feed's settings are changed in the web UI, and
feeds come and go with `ipx add`, `ipx rm` and `ipx import`. The table key is the feed id: stable,
human-readable, and used in paths and the API. `ipx add` derives it from the feed title.
feeds come and go with `ipxd add`, `ipxd rm` and `ipxd import`. The table key is the feed id: stable,
human-readable, and used in paths and the API. `ipxd add` derives it from the feed title.
```toml
[feeds.atp]
@@ -192,7 +192,7 @@ up in Admin, Directory: its address, this server's token and whether to share. T
settings, kept in the database (`directory_url`, `directory_token`, `directory_share`), not here.
With sharing on, this server sends it every feed in the catalogue that is not private, with how
many people here subscribe, and the `category` above for a feed that names none. To offer one with
nobody subscribed, `ipx add --list --category News <url>`: it subscribes no one, and a listed feed
nobody subscribed, `ipxd add --list --category News <url>`: it subscribes no one, and a listed feed
stays in the catalogue, and so in what is shared, when its last subscriber leaves, where another
is dropped. A feed nobody
subscribes to is checked once a day. One that is dead (failing for 30 days) or quiet (nothing

View File

@@ -63,7 +63,7 @@ Cloudflare and Authentik were already right. Three things on the ipx side were n
see the next section.
3. **The account had the wrong name.** It was made by hand as `rays`, but the header carries
`rays@sdf1.net`. With `auto_create_users` on, the first visit would have made a second, empty
account. `ipx user rename rays rays@sdf1.net` fixed that without losing anything.
account. `ipxd user rename rays rays@sdf1.net` fixed that without losing anything.
### The address to trust, and why it is 192.168.64.1
@@ -105,7 +105,7 @@ ordinary user with no feeds. An account made before the proxy can be given the n
send:
```sh
docker exec iPX ipx user rename <old name> <email address>
docker exec iPX ipxd user rename <old name> <email address>
```
### Signing out
@@ -225,19 +225,19 @@ Check it: the busybox request under [Check it](#check-it), which sends the email
token from the Docker bridge, now gets `sign in`, and the site still signs you in through Authentik.
**Turning it off:** clear `trusted_header` and restart. Proxy-made accounts stay, but nobody can sign
in with them until they are given a password (`ipx user passwd <name>`).
in with them until they are given a password (`ipxd user passwd <name>`).
---
## Everyday administration
```sh
ipx user list # who exists, how each signs in, and when
echo -n 'secret123' | ipx user add sam # local account, password on stdin
ipx user add sam@example.com --no-password # proxy-only account, made ahead of time
ipx user rename sam sam@example.com # give an account the name the proxy sends
echo -n 'newsecret' | ipx user passwd sam # change a password
ipx user rm sam # remove the account
ipxd user list # who exists, how each signs in, and when
echo -n 'secret123' | ipxd user add sam # local account, password on stdin
ipxd user add sam@example.com --no-password # proxy-only account, made ahead of time
ipxd user rename sam sam@example.com # give an account the name the proxy sends
echo -n 'newsecret' | ipxd user passwd sam # change a password
ipxd user rm sam # remove the account
```
In the container, put `docker exec iPX` in front, and `docker exec -i iPX` for the ones
@@ -276,4 +276,4 @@ rule is on Cloudflare, not in this repository; if the hostname is ever served wi
whole of ipx is on the internet behind bearer tokens alone.
See also [users.md](users.md) for what several people share, [configuration.md](configuration.md)
for every `[web]` key, and [cli.md](cli.md) for the `ipx user` commands.
for every `[web]` key, and [cli.md](cli.md) for the `ipxd user` commands.

View File

@@ -56,13 +56,13 @@ A database with no accounts creates **admin** on the next daemon start, with the
readable by ipx's user alone. The log says where the password is, never what it is. Change it:
```sh
echo -n 'a good password' | ipx user passwd admin
echo -n 'a good password' | ipxd user passwd admin
```
## Adding someone
```sh
echo -n 'their password' | ipx user add sam
echo -n 'their password' | ipxd user add sam
```
They sign in at `/login` and start with **no feeds**: subscriptions are per person. Adding a feed
@@ -107,5 +107,5 @@ and who else is an admin, and read the log, which names everyone's feeds and sig
gets the Settings and Log buttons hidden and a `403` if they ask anyway.
```sh
ipx user list # the admin column says who
ipxd user list # the admin column says who
```