Linking ipx's 431 MB test binary took Tower, 31 GB with no swap and 4 GB free beside its containers, into page-fault thrashing for 54 minutes: load 160 with the CPU idle and 130 processes blocked. Line tables for our crates and no debug info for dependencies make it 109 MB, built in 2m14s with load at 20 and 6.5 GB left. CLAUDE.md says to watch memory beside the load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
397 lines
27 KiB
Markdown
397 lines
27 KiB
Markdown
# Working on ipx
|
|
|
|
Notes for whoever picks this up next. Read [docs/architecture.md](docs/architecture.md) for how the
|
|
thing is built; this file is about working on it without repeating mistakes that have already been
|
|
made here.
|
|
|
|
## Where things are
|
|
|
|
Production is the `iPX` container on Tower (192.168.1.130), the `ipx` service of the
|
|
Arcane project `ipx`: `/mnt/fast/arcane/projects/ipx/compose.yaml`. That file is what runs, and
|
|
the containers of the split (#207) join it there. `docker-compose.yml` in this repo is a sample for
|
|
others, ipx alone on SQLite; editing it changes nothing in production. Until 2026-10-12 ipx was a
|
|
service of the `content` project, beside audiobookshelf, immich and Plex (#208).
|
|
|
|
| | Host | In the container |
|
|
|---|---|---|
|
|
| Image | `192.168.1.130:5000/ipx:latest` | |
|
|
| Config | `/mnt/fast/appdata/ipx/config.toml`: bind address, token, trusted proxies, torrent, paths. The feeds and server settings are in the database | `/config/config.toml` |
|
|
| 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` |
|
|
| Downloads | `/mnt/user/ipx/downloads` | `/downloads` |
|
|
| Directory | `ipx-directory` in the same project, `192.168.1.130:8100`, public at `directory.ipxreader.app` through the tunnel with no Access in front: its pages are anyone's to browse, read only (#215), and its API (`/v1`) takes a token for everything. Until 2026-10-12 it was `ipx-dir.sdf1.net` (#214). 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`, with the address ipx reaches it at: `http://ipx-directory:8100`, inside Docker, not the public address, which went out through Cloudflare and the tunnel, 170-340 ms a call where the directory answers in 16 ms), 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` |
|
|
| Home page | `ipxreader.app` (and `www`): a static page from `site/`, on the Cloudflare Pages project `ipxreader`, not on Tower. See below | |
|
|
| 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`) |
|
|
|
|
Work to do lives in the Gitea issues at https://git.sdf1.net/rays/ipx/issues, not in a
|
|
`TODO.md`. `/src/tea` is logged in: `/src/tea issues list --login git.sdf1.net --repo rays/ipx`.
|
|
|
|
**Every problem found gets an issue, before it is fixed.** A bug, a gap or a security hole turned
|
|
up along the way (reading logs, a review, a test that fails for another reason) is filed as soon
|
|
as it is found, even if it is fixed a minute later, so there is a record of what was wrong and
|
|
when. Name the issue in the commit that fixes it (`(#40)` in the subject). Once the fix is on
|
|
`main` and pushed, comment on the issue with what changed and the commit, then close it:
|
|
|
|
```sh
|
|
R="--login git.sdf1.net --repo rays/ipx"
|
|
t() { timeout 30 /src/tea "$@" < /dev/null; }
|
|
t issues create $R -t "Web token printed in the startup log" -L bug -d "What is wrong, where, how it was found."
|
|
t comment $R 40 "Fixed in 3625cf4: the startup line says where the token is kept, not what it is. Deployed in 0.8.3."
|
|
t issues close $R 40
|
|
```
|
|
|
|
**Give tea a closed stdin and a timeout**, as `t` does. Without a terminal, `tea comment` waits on
|
|
stdin and never exits; a script closing seven issues sat hung for a day on the second (#39).
|
|
|
|
Labels: `bug` for something wrong, `enhancement` for something missing. A problem found and left
|
|
for later stays open, and that is how it gets picked up again.
|
|
|
|
Deploying a change is: build and push the image, then pull it and recreate the container.
|
|
|
|
```sh
|
|
docker buildx build --tag 192.168.1.130:5000/ipx:latest . --push
|
|
docker compose -f /mnt/fast/arcane/projects/ipx/compose.yaml pull ipx
|
|
docker compose -f /mnt/fast/arcane/projects/ipx/compose.yaml up -d ipx
|
|
docker logs --tail 20 iPX
|
|
```
|
|
|
|
The directory is the same, with its own image:
|
|
|
|
```sh
|
|
docker buildx build -f Dockerfile.directory --tag 192.168.1.130:5000/ipx-directory:latest . --push
|
|
docker compose -f /mnt/fast/arcane/projects/ipx/compose.yaml pull ipx-directory
|
|
docker compose -f /mnt/fast/arcane/projects/ipx/compose.yaml up -d ipx-directory
|
|
```
|
|
|
|
And the page, `ipx-web`, the same again; a change to `web/` alone needs only this one:
|
|
|
|
```sh
|
|
docker buildx build -f Dockerfile.web --tag 192.168.1.130:5000/ipx-web:latest . --push
|
|
docker compose -f /mnt/fast/arcane/projects/ipx/compose.yaml pull ipx-web
|
|
docker compose -f /mnt/fast/arcane/projects/ipx/compose.yaml up -d ipx-web
|
|
```
|
|
|
|
The home page (`site/`, #216) is not on Tower. `sh site/build.sh` puts it in `site/dist` with the
|
|
typeface, logo and icon from `web/`, and it goes to the Cloudflare Pages project `ipxreader` by
|
|
direct upload, since there is no wrangler login here: hash each file as wrangler does (BLAKE3 of
|
|
its base64 contents plus its extension, the first 32 hex digits, with the `blake3-wasm` package),
|
|
ask the project for an upload token (`GET .../pages/projects/ipxreader/upload-token`), send the
|
|
files to `/pages/assets/upload` with that token, then create a deployment
|
|
(`POST .../pages/projects/ipxreader/deployments`) with the manifest of paths and hashes and
|
|
`_headers`. Its screenshots (`site/shots`) come from a scratch iPX on public feeds, never
|
|
production's, which would show whose subscriptions are whose.
|
|
|
|
Testing sign-in from Tower itself proves nothing about the tunnel: Tower's own requests reach the
|
|
published port as `192.168.1.130`, and Caddy rightly strips their `Cf-Access-*`. Send them from a
|
|
container on another network, as cloudflared's are (#209).
|
|
|
|
The three containers resolve names through Cloudflare (`dns: [1.1.1.1, 1.0.0.1]` in the compose
|
|
file), not the router: the LAN's DNS is NextDNS, which answers for a domain it filters with its own
|
|
block page, and ipx then logged a feed as failing with `invalid peer certificate: UnknownIssuer`
|
|
(tomrenner.com, 2026-10-17). Docker's own DNS still answers for the services' names first.
|
|
|
|
**Name the service.** A bare `up -d` recreates every container in the project, which once the
|
|
split lands means the frontend and directory too. Run `pull` before `up`, because `up` reuses whatever `latest` the host already has.
|
|
|
|
**A build that fails with `429 Too Many Requests` on a base image** is Docker Hub rate-limiting
|
|
this host. There is no Docker Hub login here, and the build asks about `debian:trixie-slim` and
|
|
`rust:1-slim-trixie` every time unless they are already stored locally. Pull them from Google's
|
|
mirror and tag them; the build then uses the local copies without asking Docker Hub:
|
|
|
|
```sh
|
|
docker pull mirror.gcr.io/library/debian:trixie-slim
|
|
docker tag mirror.gcr.io/library/debian:trixie-slim debian:trixie-slim
|
|
docker pull mirror.gcr.io/library/rust:1-slim-trixie
|
|
docker tag mirror.gcr.io/library/rust:1-slim-trixie rust:1-slim-trixie
|
|
```
|
|
|
|
Run those again now and then, or the local copies go stale.
|
|
|
|
The containers have no Docker healthcheck (#218): Docker runs one with `exec`, which on Tower
|
|
fails whenever root is logged out, because elogind removes `/run/user/0`, where containerd puts
|
|
each exec's state. Whether ipx is well is Prometheus's question (`up`, the worker's job and time)
|
|
and `ipx status` against the control socket, which says the daemon answers and can read its
|
|
database. The socket answers `status` itself instead of queuing it behind the worker's current
|
|
job, so a worker stuck on one job still answers it. The containers restart on their own after a
|
|
reboot.
|
|
|
|
Before the container, ipx ran by hand in code-server, with its files in `/config/.config/ipx/` and
|
|
`/config/.local/share/ipx/`. Those are still there and the container does not read them. If you run
|
|
a daemon by hand for testing, **stop it by its own PID**: start it with `& echo $! > pid` and
|
|
`kill $(cat pid)`. Never `pkill -x ipx`: Tower sees the container's processes, so it kills
|
|
production's daemon as well (issue #38). Never `pkill -f ipx` either: `-f` matches the shell
|
|
running the command and kills the session (exit 144), which has happened more than once.
|
|
|
|
## Before you touch the page
|
|
|
|
There are three pages: the app (`web/index.html`), the admin page (`web/admin.html`, sent to
|
|
admins only) and sign-in (`web/login.html`). The app and admin pages share one stylesheet,
|
|
`web/app.css`, and their script is TypeScript in `web/src/`; `web/build.mjs` lists which files
|
|
make up each page's script. The page is its own image, **`ipx-web`** (#203): `web/build.mjs` uses
|
|
swc to strip the types and minify each script (`app.js`, `admin.js`, `login.js`), minifies the
|
|
pages, and puts them with the icons and typeface in `web/dist`, which Caddy serves
|
|
(`web/Caddyfile`, `Dockerfile.web`), passing `/api` and `/media` to ipx on the same origin. ipx
|
|
serves no page. The page loads its script as `/app.js?v=<hash of its contents>`, and `/app.css`
|
|
and `/boot.js` the same way: the page is served `no-cache` and those `immutable`, so a browser
|
|
keeps them until a deploy changes them and their names. So **every page change needs ipx-web
|
|
rebuilt and deployed**, not ipx, and building needs node and `npm ci` run once.
|
|
|
|
Caddy asks ipx before it serves a page (`forward_auth`): `/api/page-check` sends a browser that is
|
|
not signed in to `/login` and anyone but an admin away from `/admin` (and refuses them
|
|
`/admin.js`); `/api/login-check` sends someone Cloudflare Access vouches for past the form; a page
|
|
asked for with `?token=` goes to `/api/signin`, which sets the cookie. The page is the same file
|
|
for everyone, so what ipx used to write into it per person is the page's own job now:
|
|
`web/boot.js`, in `<head>`, puts the theme from its cookie and an admin's link from the last visit
|
|
on the page before the first frame, and `/api/me` brings the rest (version, the account's old
|
|
theme).
|
|
|
|
The files in `web/src` are not modules. They are one script split up, concatenated in the order
|
|
`web/build.mjs` lists them, sharing one top-level scope as the single inline script did; a new
|
|
file goes into that list. Top-level names are kept as they are, because markup calls some by
|
|
name (`onclick="closeModal()"`) and the browser tests reach others through `page.evaluate`.
|
|
|
|
After any edit to it:
|
|
|
|
```sh
|
|
npx tsc -p .
|
|
node tests/page-smoke.js
|
|
node --test tests/page-unit.js
|
|
```
|
|
|
|
The first type-checks `web/src` in TypeScript's `strict` mode: every parameter and variable has a
|
|
type, what the API sends is typed in `web/src/types.ts`, which mirrors the Rust structs and changes
|
|
with them, and a value that may be null has to be checked before use. `$<T>` returns the element
|
|
type it is given (`$<HTMLInputElement>('#nurl')`) and assumes the element is there; one that may
|
|
not be is written `$(...)?.`, and the page check below proves the ones wired at load exist. The second
|
|
builds the page as shipped and runs its script against a stub DOM, checking every selector it
|
|
wires at load actually exists. That check exists because a patch once anchored on a deleted
|
|
function, `String.replace` silently matched nothing, and the whole UI died with a
|
|
`ReferenceError` while every server-side test passed. The third loads the same built script and
|
|
calls its helpers directly (#234): a pure function added to the page gets its check there.
|
|
|
|
Patching that file by guessing an anchor string has failed repeatedly. Read the exact block first
|
|
(`sed -n 'START,ENDp'`), match it verbatim, and assert the replacement happened rather than hoping.
|
|
|
|
## Tests
|
|
|
|
```sh
|
|
cargo fmt --all --check # rustfmt, as rustfmt.toml says; the pre-commit hook refuses a commit without it
|
|
cargo clippy --workspace --all-targets -- -D warnings # no warnings, ever
|
|
cargo deny check # known vulnerabilities, licences, sources (deny.toml); reads Cargo.lock, builds nothing
|
|
cargo machete # dependencies a crate lists and does not use; now and then, it can be wrong
|
|
cargo test # ~80 tests: parsing, filters, retention, schedules, SQL, per-user state
|
|
npx tsc -p . # type-checks web/src
|
|
node tests/page-smoke.js
|
|
node --test tests/page-unit.js # the page's own functions, called directly, in a second
|
|
node tests/native-bridge.js # the page hands playback to a native shell
|
|
node tests/contrast.js # every theme's palette against WCAG AA
|
|
npx playwright test # 66 browser tests against a real daemon on fixture feeds
|
|
node tests/load/run.js # k6: many people at once against a scratch daemon with 1,500 feeds
|
|
```
|
|
|
|
**Builds share Tower with everything else, production included.** On 2026-10-16 a build of the
|
|
directory followed in the same command by its tests' compile took Tower's load past 200 and the
|
|
session was stopped. So:
|
|
|
|
* One cargo command at a time: a build, then the tests, as separate commands, never chained or
|
|
run in the background beside another, and never two crates' builds at once. Do not tune `-j`.
|
|
* Look at the load before and after each (`cat /proc/loadavg`). Above about 24, Tower's twelve
|
|
threads twice over, wait for it to come down before starting another.
|
|
* Build only the crate the change is in (`cargo build -p ipx-directory`), and run only its tests
|
|
until the change is done; the whole workspace's once, at the end.
|
|
* Builds and test binaries live under `target/`, a link to `/mnt/fast`; linking through
|
|
`/mnt/user` (FUSE) once took the load to 248 on its own.
|
|
* **Memory is the tighter limit.** Tower has 31 GB, no swap, and the containers leave about 4-7
|
|
GB. A build past that stalls everything on page faults: load 160 with the CPU idle and the
|
|
disks quiet, many processes blocked (`node_procs_blocked`, `node_vmstat_pgmajfault`). On
|
|
2026-10-18 linking a 431 MB test binary did it for 54 minutes. So `[profile.dev]` keeps only
|
|
line tables and dependencies no debug info; keep it that way, and look at `MemAvailable` beside
|
|
the load.
|
|
|
|
The Rust is formatted by rustfmt (`rustfmt.toml`) and kept free of clippy's warnings. A clone
|
|
turns on the hook once: `git config core.hooksPath .githooks`; and `git config blame.ignoreRevsFile
|
|
.git-blame-ignore-revs` keeps blame past the commit that first formatted everything. A clippy
|
|
suggestion is not always the same code: on adopting it, `fold(false, |any, id| remove(id) || any)`
|
|
was offered as `any(...)`, which stops at the first and would have removed one feed of many. A lint
|
|
that is wrong for a line gets `#[allow(clippy::...)]` there with a comment saying why.
|
|
|
|
Things about the browser suite that have cost time:
|
|
|
|
* It starts **its own daemon and database** under `/tmp/ipx-ui-test`, wiped once per run. Playwright
|
|
re-imports the config in every worker, so `prepare()` guards on `TEST_WORKER_INDEX` — without
|
|
that guard a worker deleted the database out from under the running daemon, which then kept
|
|
serving from the unlinked inode while everything else saw an empty file.
|
|
* Tests **share that daemon and run in order**. A test that opens an item marks it read and changes
|
|
what later tests see. Write assertions that do not depend on what ran before, or normalise the
|
|
state first.
|
|
* Fixture feeds must not share an enclosure URL, because `enclosures.url` is globally unique and
|
|
whichever feed is scanned first claims it.
|
|
* `webServer` starts **before** `globalSetup`, which is why the fixture config is written at
|
|
config-load time instead.
|
|
|
|
The load tests (`tests/load`, issue #133) are for what one browser cannot show: twenty-five people
|
|
browsing at once, a hundred players saving positions while a scan writes, fifty listeners
|
|
seeking through files, a flood of wrong passwords. Things about them:
|
|
|
|
* They need **k6**, which `/src/install.sh` installs from k6's own signed apt repository, and they
|
|
build and run a **release** binary: Argon2 in a debug build takes about a second a sign-in,
|
|
which would measure the build.
|
|
* `run.js` serves the feeds itself, generated, and starts **its own daemon** under
|
|
`/tmp/ipx-load`, wiped each run, on ports 8793 and 8794, so it can run beside the browser suite.
|
|
It stops that daemon by its PID. People sign in by the `X-Load-User` header, trusted from
|
|
127.0.0.1, as production trusts Cloudflare Access's; only the sign-in test uses a password.
|
|
* Each threshold is a budget well above what the request takes now: it is there to catch a query
|
|
that has started asking once per feed, or writers queueing on a lock, not a slow minute.
|
|
* `ipx status` runs every second throughout, and a run fails if one takes 5s.
|
|
* One at a time: `node tests/load/run.js signin`. All four take about six minutes.
|
|
* The daemon is on **SQLite** unless `--postgres`, which puts it in `IPX_TEST_DATABASE_URL`, in
|
|
a schema of its own (`ipx_load`) dropped and made again each run. That URL, in `/src/.envrc`, is
|
|
`ipodderx_test` on the `databases` project's server, production's, as `ipodderx`, taken from
|
|
`ipx.env`; the harness refuses a database whose name does not end in `_test`. Postgres's
|
|
numbers are what production would see, and a run loads the server production's database is on,
|
|
so not while someone is using iPX in earnest. Each budget has a value for each database.
|
|
* The Rust tests run on Postgres with the same URL: `. /src/.envrc && cargo test`. Each test
|
|
makes a schema `ipxt_<pid>_<n>`, and the next run drops those an earlier one left.
|
|
|
|
Non-trivial logic leaves one runnable check behind. Pure functions (`merge_policy`, `pick`,
|
|
`matches_keywords`, `parse_interval`) are the easiest place to put it.
|
|
|
|
## Production's database
|
|
|
|
The owner allows `EXPLAIN (ANALYZE, BUFFERS)` of any query against production's Postgres, to see
|
|
where a query's time goes; nothing else without asking. Run it read only, so nothing can change:
|
|
|
|
```sh
|
|
U=$(grep -E '^IPX_DATABASE_URL=' /mnt/fast/arcane/projects/ipx/ipx.env | cut -d= -f2- | tr -d '"')
|
|
psql "$U" -X -v ON_ERROR_STOP=1 <<'SQL'
|
|
BEGIN READ ONLY;
|
|
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
|
|
ROLLBACK;
|
|
SQL
|
|
```
|
|
|
|
Never print the URL: it carries the password. Write the query as ipx sends it (`$1` parameters
|
|
become `PREPARE`/`EXECUTE`, or values), for a real account. A query fast here and slow in ipx's
|
|
trace is time outside the query: on 2026-10-15 the feed list's count took 5 ms in Postgres and 366
|
|
ms in ipx, the difference being new connections, 80-330 ms each to open (#241).
|
|
|
|
## Things that are true and easy to get wrong
|
|
|
|
* **`enclosures.url` is globally UNIQUE.** It is the dedupe key and the reason one file serves every
|
|
subscriber. Two feeds publishing the same URL means only the first one scanned shows it.
|
|
* **Read state lives in `entry_state`, per user, and nowhere else.** `entries` had `read`, `flagged`
|
|
and `position` columns from before accounts; two bugs came from queries still reading them
|
|
(retention, and the entry pruner), and they were dropped in 0.5.
|
|
* **The catalogue and the server settings are in the database, not config.toml** (issue #18):
|
|
tables `catalogue` (each feed's `config::Feed` as JSON) and `settings` (`general`:
|
|
`config::Stored`). ipx still runs from one in-memory `Config`, config.toml for where things are
|
|
and who gets in, the database for the rest (`assemble_config`); a change goes through
|
|
`Ctx::store_cfg`, never a write to the file. The first start on a database without them imports
|
|
config.toml's and trims the file, keeping `config.toml.pre-database`. A feed exists once;
|
|
`subscriptions(user_id, feed_id)` says who wants it and with what settings. OPML children are
|
|
derived and never in the catalogue.
|
|
* **Postgres connections ask for no notices** (`client_min_messages=warning`, `db::url_for`).
|
|
Postgres sends one for every `CREATE ... IF NOT EXISTS` on something existing, sqlx logs each,
|
|
and tracing-subscriber's per-layer filters then dropped the next line ipx logged.
|
|
* **One fetch serves everyone**, so scan policy is a union of subscribers' wants (`merge_policy`).
|
|
Anyone wanting an item is enough to fetch it.
|
|
* **The UI hiding a control is not enforcement.** Admin-only actions check `user.is_admin` in the
|
|
handler and return `403`.
|
|
* **A `tokio::select!` only races its branches at the point of selection.** A long download has to
|
|
watch the shutdown channel itself; the daemon ignored SIGTERM for exactly this reason.
|
|
* Only one daemon per socket. Removing the socket file defeats the guard and you get two daemons
|
|
fighting over the database, with the stale one still holding the port.
|
|
* **The Grafana dashboard reads the log's fields** (`grafana/dashboard.py`). Production logs JSON
|
|
(`IPX_LOG_FORMAT=json`); the access log's `method`, `path`, `route`, `status`, `duration_ms` and
|
|
the events' `ev`, `feed`, `new`, `bytes`, `msg` (`log_event` in ipc.rs) are what the panels query.
|
|
Rename one and its panels go blank without an error; regenerate the dashboard to match.
|
|
* `/api/settings` answering `200` does **not** mean the daemon is well — the web server is a
|
|
different task. `ipx status` checks the control socket and the database; to see the worker
|
|
getting through its jobs, watch for `scan complete` in the log.
|
|
* **The database goes through SeaORM, and the entities in `crates/ipx/src/entity.rs` are the schema.**
|
|
`Db::open` creates any missing table or index from them (`create_missing`), on every `ipx`
|
|
command, the healthcheck's `ipx status` included, so it must never write when nothing is
|
|
missing: SeaORM's experimental schema sync dropped and remade an index on every open, the
|
|
write lock that took made `ipx status` time out behind a busy daemon, and it was removed for
|
|
it. A new column on an existing table needs its own `ALTER`; nothing adds one for you. Look for
|
|
the column first and alter only when it is missing, as `feeds.description` does.
|
|
* **SQL written by hand in `db.rs` has to run on SQLite and Postgres both** (issue #18): `$1`
|
|
parameters, bound only if used; `ON CONFLICT`, not `INSERT OR IGNORE`; yes/no columns tested
|
|
as themselves (`NOT coalesce(s.read, false)`) and written as `true`/`false`, never compared to
|
|
1; no `rowid`, `GLOB` or `UPDATE OR IGNORE`. `Args` in `db.rs` builds the parameters.
|
|
|
|
## House style
|
|
|
|
Comments explain **why**, not what. If a line looks odd, the comment says what went wrong without
|
|
it. No emoji, no exclamation marks, no "obviously". Prose in the UI and docs is plain English and
|
|
addressed to the person using it.
|
|
|
|
Every change gets one line under `## [Unreleased]` in the changelog of the service it changes, in
|
|
its [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) group: Added, Changed, Deprecated,
|
|
Removed, Fixed or Security. One changelog a service: [crates/ipx/CHANGELOG.md](crates/ipx/CHANGELOG.md)
|
|
(the daemon and its API; until 0.12.0 everything), [web/CHANGELOG.md](web/CHANGELOG.md) (the page
|
|
and ipx-web) and [crates/directory/CHANGELOG.md](crates/directory/CHANGELOG.md) (ipx-directory). A
|
|
change to more than one gets a line in each, each saying what that service does differently. The
|
|
monitoring (the Grafana dashboard, the prod-check skill) and the repository's own tooling are not
|
|
a service anyone uses and get no line. Say it the way someone using ipx would notice it. When there is more to
|
|
say, such as what was wrong before or what it cost to find out, it goes in the commit message's
|
|
body, where `git log` and `git blame` find it beside the change. (There was a long-form
|
|
`docs/history.md` until 0.7.0; it grew too large to be useful and was removed. It is in git.)
|
|
|
|
Each service has its own version and is released on its own, when its changelog has something to
|
|
release:
|
|
|
|
| service | version in | changelog | tag |
|
|
|---|---|---|---|
|
|
| ipx | `crates/ipx/Cargo.toml` | `crates/ipx/CHANGELOG.md` | `vX.Y.Z`, as every release before the split |
|
|
| ipx-directory | `crates/directory/Cargo.toml` | `crates/directory/CHANGELOG.md` | `directory-vX.Y.Z` |
|
|
| ipx-web, the page | `package.json` | `web/CHANGELOG.md` | `web-vX.Y.Z` |
|
|
|
|
`crates/feed` is a library the two Rust services build in, not released (`0.0.0`); a change to it
|
|
is a line in the changelog of each service it changes. The page shows ipx's version and its own in
|
|
the logo's tooltip; the directory logs its own at start; each binary answers `--version`.
|
|
|
|
Cutting a release of one: first read its `[Unreleased]` against its last release, not line by
|
|
line, and rewrite it as what changed between the two. Lines go in as the work happens, so they
|
|
record the way there: a feature added and then reworked in the same release is one line saying
|
|
what it is now, under Added; something added and taken out again before the release, or a fix for
|
|
a bug no release ever had, is not there at all; and "instead of" and "no longer" compare with the
|
|
last release, not with last week. Issue numbers stay, all of them, on the line that survives. Then
|
|
rename `[Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD` and open a new empty `[Unreleased]` above it,
|
|
set the service's version to `X.Y.Z` (dropping `-dev`), tag the commit with the service's tag, and
|
|
update the compare links at the bottom of its changelog. Push the commit and the tag, build and
|
|
deploy the service's image, then `sh scripts/release.sh ipx|directory|web` makes the Gitea
|
|
release: that version's changelog section as its notes, and a tarball of what the image runs (the
|
|
binary, or the page's files with the Caddyfile) attached, checked to be that version. Then, in the
|
|
next commit, set its version
|
|
to the next patch with `-dev` (after 0.9.0, `0.9.1-dev`), so a build between releases says so
|
|
instead of claiming to be the last release. The release that follows can still be a minor or major
|
|
one; `-dev` only says the work comes after `X.Y.Z`. A service never released is at `0.1.0-dev`,
|
|
working towards its first release, `0.1.0`.
|
|
|
|
**Traces first, logs second.** Anything that does work (a service, a handler, a job, a call to
|
|
another service) gets spans, and passes `traceparent` on to whatever it calls, so one trace in
|
|
Tempo follows a request or a job across ipx-web, ipx and the directory. A trace shows where the
|
|
time went and what called what; a log line only that something happened. Log what a trace cannot
|
|
carry: events worth counting on the dashboard, warnings and errors. A new service or route without
|
|
traces is not finished.
|
|
|
|
Deliberate simplifications get a `ponytail:` comment naming the ceiling and the upgrade path, e.g.
|
|
`// ponytail: global connection mutex, move to a pool if feed count makes it contend`.
|
|
|
|
## Known gaps
|
|
|
|
* They are the open issues in Gitea, not a list here: a limitation known and left in place is an
|
|
issue left open.
|
|
|
|
<!-- rtk-instructions v2 -->
|
|
# Command output
|
|
|
|
Command output here is condensed to save tokens, keeping every signal and
|
|
dropping costly noise. Treat it as the complete result: run commands
|
|
normally, and batch related commands into one call to avoid extra turns.
|
|
Truncated results state their recovery path in their own output. Re-run a
|
|
command as `rtk proxy <cmd>` only when its result is unusable: empty when
|
|
output was clearly expected, contradicting its exit code, or garbled.
|
|
<!-- /rtk-instructions --> |