The directory is at directory.ipxreader.app (#214)
The zone is in Cloudflare and the rays-unraid tunnel routes the name to ipx-directory, as it does ipx-dir.sdf1.net, which keeps answering until every server using it has moved: a redirect to another host would drop the Authorization header. Production ipx shares with the new name. CLAUDE.md, docs/directory.md, the prod-check skill and the dashboard name it; the admin page's placeholder and the settings' hint use directory.example.net. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ Production is three containers in the Arcane project `ipx` on Tower
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `ipx-web` | Caddy: serves the page, passes `/api` and `/media` to ipx (#203). Port 8099, the LAN and the tunnel | `{container="ipx-web"}`, JSON: a line a request (`http.log.access`), its own errors | service `ipx-web`: a span a request, passing `traceparent` on to ipx (#213) | none |
|
| `ipx-web` | Caddy: serves the page, passes `/api` and `/media` to ipx (#203). Port 8099, the LAN and the tunnel | `{container="ipx-web"}`, JSON: a line a request (`http.log.access`), its own errors | service `ipx-web`: a span a request, passing `traceparent` on to ipx (#213) | none |
|
||||||
| `iPX` | the daemon: API, scans, downloads, DeadAir | `{container="iPX"}`, JSON (`IPX_LOG_FORMAT=json`) | service `ipx`; passes `traceparent` to the directory | `job="ipx"`, its `/metrics` |
|
| `iPX` | the daemon: API, scans, downloads, DeadAir | `{container="iPX"}`, JSON (`IPX_LOG_FORMAT=json`) | service `ipx`; passes `traceparent` to the directory | `job="ipx"`, its `/metrics` |
|
||||||
| `ipx-directory` | the directory of public podcasts, `ipx-dir.sdf1.net` (#204) | `{container="ipx-directory"}`, JSON (`DIRECTORY_LOG_FORMAT=json`) | service `ipx-directory`: a span a request, joining ipx's trace when ipx called; a trace a refresher round (#213) | none |
|
| `ipx-directory` | the directory of public podcasts, `directory.ipxreader.app` (#204, #214) | `{container="ipx-directory"}`, JSON (`DIRECTORY_LOG_FORMAT=json`) | service `ipx-directory`: a span a request, joining ipx's trace when ipx called; a trace a refresher round (#213) | none |
|
||||||
|
|
||||||
Under them, Tower itself: `job="node"` (the node exporter: load, CPU, memory, disks). The Grafana
|
Under them, Tower itself: `job="node"` (the node exporter: load, CPU, memory, disks). The Grafana
|
||||||
dashboard `iPX` draws all of it in that order (`grafana/dashboard.py`). A trace without the
|
dashboard `iPX` draws all of it in that order (`grafana/dashboard.py`). A trace without the
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ service of the `content` project, beside audiobookshelf, immich and Plex (#208).
|
|||||||
| Database | Postgres 18, database `ipodderx`, login `ipodderx`, on the `postgres` container of the Arcane project `databases` (`192.168.1.130:5433`). The URL is in `ipx.env` beside the compose file (`/mnt/fast/arcane/projects/ipx/ipx.env`, mode 600), passed to the container as `IPX_DATABASE_URL`. A relative `env_file`: Arcane runs compose in its own container, where `/mnt/fast/appdata` does not exist | |
|
| Database | Postgres 18, database `ipodderx`, login `ipodderx`, on the `postgres` container of the Arcane project `databases` (`192.168.1.130:5433`). The URL is in `ipx.env` beside the compose file (`/mnt/fast/arcane/projects/ipx/ipx.env`, mode 600), passed to the container as `IPX_DATABASE_URL`. A relative `env_file`: Arcane runs compose in its own container, where `/mnt/fast/appdata` does not exist | |
|
||||||
| Old database | `/mnt/user/ipx/state.db`, SQLite, used until the move to Postgres on 2026-09-18 and kept for rollback | `/data/state.db` |
|
| Old database | `/mnt/user/ipx/state.db`, SQLite, used until the move to Postgres on 2026-09-18 and kept for rollback | `/data/state.db` |
|
||||||
| Downloads | `/mnt/user/ipx/downloads` | `/downloads` |
|
| Downloads | `/mnt/user/ipx/downloads` | `/downloads` |
|
||||||
| Directory | `ipx-directory` in the same project, `192.168.1.130:8100`, public at `ipx-dir.sdf1.net` through the tunnel with no Access in front: tokens are its gate. Config `/mnt/fast/appdata/ipx-directory/directory.toml` (token hashes; Tower's own token in `tower.token` beside it, and entered in ipx's Admin, Directory, which keeps it in the database's `settings`), database `ipx_directory` on the same Postgres, URL in `directory.env` beside the compose file. See [docs/directory.md](docs/directory.md) | `/config/directory.toml` |
|
| Directory | `ipx-directory` in the same project, `192.168.1.130:8100`, public at `directory.ipxreader.app` through the tunnel (`ipx-dir.sdf1.net` also answers until #214 retires it) with no Access in front: tokens are its gate. Config `/mnt/fast/appdata/ipx-directory/directory.toml` (token hashes; Tower's own token in `tower.token` beside it, and entered in ipx's Admin, Directory, which keeps it in the database's `settings`), database `ipx_directory` on the same Postgres, URL in `directory.env` beside the compose file. See [docs/directory.md](docs/directory.md) | `/config/directory.toml` |
|
||||||
| Web UI | `ipx-web` (Caddy) in the same project, at `192.168.1.130:8099`, also `ipodderx.sdf1.net` via a Cloudflare tunnel. It serves the page and passes `/api` and `/media` to ipx, whose own port is published nowhere; its address on `ipx_default` is pinned to `192.168.64.10` (#203) | ipx `0.0.0.0:8099`, Caddy `:8080` |
|
| Web UI | `ipx-web` (Caddy) in the same project, at `192.168.1.130:8099`, also `ipodderx.sdf1.net` via a Cloudflare tunnel. It serves the page and passes `/api` and `/media` to ipx, whose own port is published nowhere; its address on `ipx_default` is pinned to `192.168.64.10` (#203) | ipx `0.0.0.0:8099`, Caddy `:8080` |
|
||||||
| Sign-in via the tunnel | Cloudflare Access app `ipodderx`, with Authentik as its identity provider; see [docs/sso.md](docs/sso.md) | Caddy passes `Cf-Access-*` only from `192.168.64.1`, the `ipx_default` gateway cloudflared arrives from (`TUNNEL_PEER`; the subnet is pinned in the compose file); ipx trusts them only from Caddy, `192.168.64.10`, and only with a valid `Cf-Access-Jwt-Assertion` (`access_team`, `access_aud`) |
|
| Sign-in via the tunnel | Cloudflare Access app `ipodderx`, with Authentik as its identity provider; see [docs/sso.md](docs/sso.md) | Caddy passes `Cf-Access-*` only from `192.168.64.1`, the `ipx_default` gateway cloudflared arrives from (`TUNNEL_PEER`; the subnet is pinned in the compose file); ipx trusts them only from Caddy, `192.168.64.10`, and only with a valid `Cf-Access-Jwt-Assertion` (`access_team`, `access_aud`) |
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ pub struct General {
|
|||||||
/// How much artwork iPX keeps on disk, in MB, so the page loads it from iPX rather than from
|
/// How much artwork iPX keeps on disk, in MB, so the page loads it from iPX rather than from
|
||||||
/// every publisher; the least recently shown goes first. 0 keeps none.
|
/// every publisher; the least recently shown goes first. 0 keeps none.
|
||||||
pub art_cache_mb: u64,
|
pub art_cache_mb: u64,
|
||||||
/// The directory service the Directory shows (#205), as `https://ipx-dir.sdf1.net`. Empty
|
/// The directory service the Directory shows (#205), as `https://directory.ipxreader.app`. Empty
|
||||||
/// for none: then the Directory says so, and feeds are added by their address.
|
/// for none: then the Directory says so, and feeds are added by their address.
|
||||||
pub directory_url: String,
|
pub directory_url: String,
|
||||||
/// The token the directory gave this server. Never sent to the page: it says only whether
|
/// The token the directory gave this server. Never sent to the page: it says only whether
|
||||||
|
|||||||
@@ -2177,7 +2177,7 @@ async fn patch_settings(
|
|||||||
if let Some(v) = body.directory_url {
|
if let Some(v) = body.directory_url {
|
||||||
let v = v.trim().trim_end_matches('/').to_owned();
|
let v = v.trim().trim_end_matches('/').to_owned();
|
||||||
if !v.is_empty() && !url::Url::parse(&v).is_ok_and(|u| matches!(u.scheme(), "http" | "https")) {
|
if !v.is_empty() && !url::Url::parse(&v).is_ok_and(|u| matches!(u.scheme(), "http" | "https")) {
|
||||||
return Err(ApiError::bad_request(format!("{v:?} is not a web address; try https://ipx-dir.example.net")));
|
return Err(ApiError::bad_request(format!("{v:?} is not a web address; try https://directory.example.net")));
|
||||||
}
|
}
|
||||||
cfg.general.directory_url = v;
|
cfg.general.directory_url = v;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ fetches each feed's title, artwork, category, description and latest twenty item
|
|||||||
and any server holding a token can browse it. It never downloads a file, and it never stores
|
and any server holding a token can browse it. It never downloads a file, and it never stores
|
||||||
one's address: every ipx server still fetches its own feeds.
|
one's address: every ipx server still fetches its own feeds.
|
||||||
|
|
||||||
This server's is at `https://ipx-dir.sdf1.net`, the `ipx-directory` service of the Arcane project
|
This server's is at `https://directory.ipxreader.app` (moved from `ipx-dir.sdf1.net`, which answers as well until every server using it has moved, #214), the `ipx-directory` service of the Arcane project
|
||||||
`ipx`, with its database `ipx_directory` on the `databases` project's Postgres.
|
`ipx`, with its database `ipx_directory` on the `databases` project's Postgres.
|
||||||
|
|
||||||
## In ipx
|
## In ipx
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ ts("Artwork", [prom('max(ipx_art_cache_bytes{job="ipx"}) / max(ipx_art_cache_lim
|
|||||||
"limit: many at every sweep, with a falling hit rate, means the limit is too small.")["fieldConfig"]["overrides"] = bar_overrides("images evicted")
|
"limit: many at every sweep, with a falling hit rate, means the limit is too small.")["fieldConfig"]["overrides"] = bar_overrides("images evicted")
|
||||||
y += 8
|
y += 8
|
||||||
|
|
||||||
# ---- ipx-directory: public at ipx-dir.sdf1.net, shared by ipx servers (#204)
|
# ---- ipx-directory: public at directory.ipxreader.app, shared by ipx servers (#204)
|
||||||
row("ipx-directory")
|
row("ipx-directory")
|
||||||
stat("Snapshots", loki(f'sum(count_over_time({DIR} | message="snapshot" [$__range])) or vector(0)', instant=True), 0,
|
stat("Snapshots", loki(f'sum(count_over_time({DIR} | message="snapshot" [$__range])) or vector(0)', instant=True), 0,
|
||||||
desc="ipx sends one when what it shares changes, and every half hour (#211).")
|
desc="ipx sends one when what it shares changes, and every half hour (#211).")
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ async function drawDirectorySettings(){
|
|||||||
<p class="hint">The Directory shows podcasts from a directory service that iPX servers share.
|
<p class="hint">The Directory shows podcasts from a directory service that iPX servers share.
|
||||||
Without one, people add feeds by their address.</p>
|
Without one, people add feeds by their address.</p>
|
||||||
<div class="field"><label for="durl">Directory address</label>
|
<div class="field"><label for="durl">Directory address</label>
|
||||||
<input type="text" inputmode="url" id="durl" value="${esc(g.directory_url)}" placeholder="https://ipx-dir.example.net" spellcheck="false"></div>
|
<input type="text" inputmode="url" id="durl" value="${esc(g.directory_url)}" placeholder="https://directory.example.net" spellcheck="false"></div>
|
||||||
<div class="field"><label for="dtoken">Access token</label>
|
<div class="field"><label for="dtoken">Access token</label>
|
||||||
<div class="inline"><input type="password" id="dtoken" autocomplete="off" spellcheck="false"
|
<div class="inline"><input type="password" id="dtoken" autocomplete="off" spellcheck="false"
|
||||||
placeholder="${g.directory_token_set ? 'Set. Paste a new one to replace it' : 'Paste the token the directory gave this server'}">
|
placeholder="${g.directory_token_set ? 'Set. Paste a new one to replace it' : 'Paste the token the directory gave this server'}">
|
||||||
|
|||||||
Reference in New Issue
Block a user