# How ipx was built The long-form record, newest first: what landed each day, what was wrong before, and what it cost to find out. [CHANGELOG.md](../CHANGELOG.md) has the short version, by release; this is where the reasoning lives. New write-ups go at the top. See [README.md](../README.md) for what the thing is. ## 2026-09-12 — Trimming the state database An audit of the database layer, with a read-only copy of production to check it against. The data was already clean: no tables or indexes left from older versions, 47 free pages after the column drops earlier the same day, and one stray `entry_state` row. The code had five things: - `migrate()` still added eight columns to any table missing them. All eight shipped in 0.2.0 and upgrades now start from 0.3.0 at the oldest, so the list and its loop went; the `retired` drop list stays, since a database coming from 0.4.0 still has the old read columns. - `created` on `users`, `subscriptions` and `sessions` was written by every insert and read by nothing. They joined `retired`. The old-database test now builds all three tables, foreign keys included, since `DROP COLUMN` on a table that references another was the part worth proving. - `Db::subscribed_feed_ids` had no callers, though its doc said the scanner walked it. `Db::subscriber_count` had one caller asking whether it was above zero, which `subscriber_counts().contains_key` answers. `Managed.orphaned` was selected and never read. ## 2026-09-12 — Cutting what had outlived its reason A whole-repo audit for over-engineering listed twelve things to cut, and all of them went. - **Upgrades from before accounts.** `migrate_opml_children` moved OPML feeds that old versions wrote into `config.toml` out to the database, and ran at every daemon start to do nothing after the first. Production ran it in 0.3.0; anything older has to pass through 0.4.0. - **Half of the adoption, and not the other half.** The audit called `adopt_existing_library` a one-time migration and it was cut whole. It did two jobs: copy the old read state into `entry_state`, which was dead, and subscribe the first admin to the whole catalogue while nobody subscribed to anything, which is how a fresh install's first account gets `config.toml`'s feeds. The browser suite caught it at once, signing in to an empty sidebar; `cargo test` had no idea. The second job is back as `adopt_catalogue`, with a unit test of its own. - **The dead `entries` columns.** `read`, `flagged` and `position` moved to `entry_state` with accounts. The adoption's copy was their last reader, but `record_entry` still wrote them, and still reset `read` when a title changed, which nothing looked at. Two bugs came from queries reading them. `migrate()` now drops them from an existing database (SQLite has had `DROP COLUMN` since 3.35), and a test builds an old table to prove it. On production each drop rewrote the 66 MB `entries` table, about four seconds apiece, so the first start took thirteen. An `ipx status` run in that window failed with `migrating schema`: every `ipx` command migrates when it opens the database, and it collided with the daemon doing the same. A failed `ALTER TABLE` changes nothing, and the database had been copied to `backup/` first anyway. - **`interval_mins`**, which `schedule` replaced. An old config that still has the key loads; the key is ignored, and the config test carries it to keep that true. - **Three dependencies.** `infer` was only asked whether a file is a torrent, and the check after it already looked for `d8:announce`, which is what `infer` looks for. `dirs` was three lookups of `XDG_CONFIG_HOME`, `XDG_DATA_HOME` and `HOME`. `tokio-stream` wrapped the broadcast receiver for the event stream; `futures_util::stream::unfold` does the same, lagging clients included. - **Two token generators.** The web token came from a copy of the session-token code, with a clock fallback on top. It uses `auth::new_session_token` now, and is 64 characters. - **The icon inlined four times**, 23 KB of base64 each, into both pages. It is `/icon.png` now, outside the sign-in wall with `/login`, since the sign-in page shows it. - Also: the `contrib/` systemd units from before the container, `Db::entries` and `Db::count_entries` that only the tests called, three `logbuf` visitors that repeated the trait's defaults, and unused state, a helper and dead CSS in the page. ## 2026-09-11 — A design pass on the web UI A review against screenshots of every view in all three themes found that Dark and Light read as a generic dark dashboard: one pale blue did every job, most labels were bold, and nothing led. The list it produced, in `TODO.md`, was worked through in one go. What was worth knowing: - **Amber means new.** Badges, unread dots and download bars take the icon's EQ amber; blue is left for the primary action and links. Light's amber was `#b06f10`, which gives white text 4.1:1, short of AA for 11 px bold. It is `#9a5f0a` now, 5.2:1. - **EQ bars mark what is playing.** Three `` bars stand at 60, 100 and 40 % and animate only while `body.playing` is set. The first version left the animation on but paused, expecting each bar to hold a different frame. The frames it held were within a pixel of each other, and on screen the bars read as three dots. Under reduced motion one rule drops every animation and transition, which leaves the bars standing. - **The focus ring was clipped.** `.tgroup` and `#topbar` both set `overflow:hidden`, so a ring drawn outside a toolbar button was cut off. Rings inside clipping parents are inset instead. - **The feed list could not be used from the keyboard at all.** Rows were `
`s and the triangle a ``, so Tab went from the feed filter to Sign out. Rows now take focus, the triangle is a `