- All Subscriptions' header checks every feed and marks everything read
(POST /api/read-all, the same feeds the view lists); it asks first.
- Minus unsubscribes everywhere (the feed header's x read as "close"),
x only closes or cancels, plus adds/subscribes/imports, and a dialog's
confirm carries its action's icon. Remaining word buttons, the player
and the folder arrow are Font Awesome 7.3.1 icons.
- Toolbar grouped by what it acts on (add, unsubscribe, scan | play,
read, keep); read and keep show the selected item's state.
- The OPML subscription page uses the same header as a feed.
- Fixed: Escape ignored inside a dialog's text box (Add feed could not
be closed with it), white password box in the dark theme, stray dot
in an undated item's details.
- Tests: one action one icon across toolbar, page and all 8 dialogs;
All Subscriptions mark everything read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0173mGu6rK18Ne7UGTwAaVJV
- The separate check (and warning) beside a file's type icon is gone:
the type icon itself is green once the file is downloaded and red when
the download failed, with the details in its tooltip. One icon per row
keeps the column lined up. On Classic's blue selection they are a
lighter green and red rather than white.
- Mark unread under an item's title was a solid circle, which read as a
record button. It is Font Awesome's closed envelope now.
- Drops the unused circle-check, circle-exclamation and circle icons.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
- Every button's icon is drawn from one small SVG set (ICON), in bold
strokes of the button's own colour, instead of font characters: ⟳ ⤓ ↗
and the like came out thin and tiny and differed from font to font.
Static buttons name theirs with data-icon. Keep is a flag everywhere.
- A file's state is an icon: a check when downloaded, a warning with the
error in its tooltip when it failed, nothing while it waits. Its type
(audio, video, image, pdf, torrent, other) is an icon with the word in
its tooltip. The DOWNLOADED and PENDING chips are gone, which also
fixes them being hard to read on Classic's blue selection.
- Tests find state and type by their tooltips.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
- A Classic theme beside Dark and Light: brushed-metal toolbar and
status bar, a pale blue-grey source list, Aqua blue for whatever is
selected, red unread badges, a striped table with blue titles, a blue
bar behind the item's title, and Lucida Grande. The theme button steps
through all three and remembers the choice.
- The feed and OPML headers' buttons (scan, download latest, mark all
read, settings, unsubscribe) and the Settings, feed settings and
Download latest dialogs' buttons (save, download, cancel) are icons,
with the words in title and aria-label.
- Tests: the theme button reaches Classic and it survives a reload; the
header buttons are found by action.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
Save, delete, view and download in the Files pane, and mark read, keep
and open the original under an item's title, are icons now. The word is
in each one's title and aria-label, so it is still there on hover and
for screen readers. Tests find them by title or action, not text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
- Security: feeds from Patreon, Supercast, Supporting Cast, Glow and
Memberful are never listed in Popular or the Directory. A Supercast
feed keeps its key in the URL's path, which the query check missed, so
it was being listed.
- Adding a feed queues a scan of it, and an OPML import that added feeds
scans what is due, so items show without pressing Scan.
- A file deleted to save space, or by hand, looks as if it was never
downloaded: no "reaped" chip, just the Download button. The retention
summary says "deleted".
- The feed header keeps its title and stats to one line each and wraps
its buttons; a single feed's table drops the Feed column.
- Tests: adding a feed shows its item without Scan; a deleted file shows
no "reaped"; paid-feed hosts and acast public ids in the unit test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
- A toolbar across the window with the original's groups: add and
unsubscribe, play, mark read and keep for the selected item, scan, a
search box for what is showing, and Settings and Log (admins only).
- Directory, Popular and All Subscriptions sit at the top of the feed
list and open in the main pane; the Popular and Directory buttons and
their dialogs are gone.
- All Subscriptions lists every item from every feed you subscribe to:
GET /api/entries, the per-feed query with its scope widened. The
enclosure lookup after it matches files to rows by feed and guid, since
a page can now span feeds.
- Items are a table (unread, kept, item, feed, file, published) with a
Files pane beside it, the text below, and a status bar with totals. On
a phone the files follow the text and the table is title and date.
- Tests: enclosures are checked in #files; the toolbar's read, keep and
play act on the selected item; All Subscriptions holds only your feeds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
- GET /api/popular returns the ten most subscribed feeds. The new GET
/api/directory returns every feed that may be listed, sorted by name,
from the same list: everyone counted, you included, never a URL, never
a private feed or a feed inside an OPML. POST /api/popular/{id} still
subscribes to anything on it.
- A Directory button sits beside Popular; both open the same list
screen. The sidebar toolbar wraps rather than squeezing four buttons.
- Tests: the directory is A to Z, Popular is its top ten, Paid Show is
in neither, and subscribing works from the directory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
- A Popular button beside + Feed opens the popular list directly; the
Add feed dialog keeps it too.
- The list counts every subscriber, you included. Your own feeds stay on
it, marked Subscribed, and clicking one opens it. Private feeds and
feeds inside an OPML are still never listed, for anyone.
- GET /api/popular rows carry `subscribed`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
- Add feed lists what other accounts subscribe to, most subscribers
first, and subscribes you by id (GET /api/popular, POST
/api/popular/{id}). Rows never carry a URL. Feeds from an OPML and
anything that looks private (a login, credentials in the URL, a key
such as auth= or token=) are never listed, and the subscribe route
checks the id against the same list.
- CHANGELOG.md follows Keep a Changelog 1.1.0: 0.1.0 (2026-09-09, the
CLI), 0.2.0 (2026-09-10, the web UI), 0.3.0 (2026-09-11, accounts and
sharing). The long-form entries moved unchanged to docs/history.md.
- Cargo.toml is 0.3.0. CLAUDE.md says how to add an entry and cut a
release.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
- The import screen has a file picker beside the paste box. The page
reads the file, checks it looks like OPML before sending, and clears
the picker when it is refused and after it is imported. The file is
sent as text and never written to disk on the server.
- The server parses the OPML before touching anything and answers 400
"that is not an OPML file" (was a 500). subscribe_opml takes a parsed
document, so ipx import also refuses a non-OPML file by name.
- Tests: Settings' Export OPML download and paste import; uploading an
RSS file (refused) and a real OPML; the server's 400; the admin's
export round-tripped into a second account; ipx import/export in a
scratch config.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
Import predated accounts: it only added URLs missing from config.toml
and subscribed nobody. Importing another account's export did nothing
("Imported 0 feed(s)"), and a genuinely new feed had no subscriber, so
it was never scanned. Web and CLI import now share subscribe_opml,
which subscribes the caller (the CLI: the first admin) to every feed in
the file and reports new vs already-subscribed.
Export wrote the whole catalogue to anyone signed in, including other
people's private feed URLs. It now lists only your own subscriptions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0173mGu6rK18Ne7UGTwAaVJV
- Settings > Manage users: add an account (password, or none for proxy
sign-in), toggle admin, remove. Backed by GET/POST /api/users and
PATCH/DELETE /api/users/{id}, 403 for non-admins. The only admin
cannot be demoted or removed.
- GET /api/logs is admin-only and the Log button is hidden for others;
the log names every account, feed and failed sign-in.
- Feeds inside an OPML list those with unread items first, in the
sidebar folder and on the subscription's page.
- Deploying is now buildx --push to 192.168.1.130:5000 and recreating
the ipodderx service of the Arcane project content; CLAUDE.md and the
README's Docker section say so.
- Tests: Playwright for user admin, the last-admin guard, 403s for a
non-admin and the unread ordering (new Aardvark Radio fixture); a unit
test for last_admin; the smoke test drives usersModal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0173mGu6rK18Ne7UGTwAaVJV
A feed with other subscribers labels the button Delete for everyone and
names them in the confirmation. The server decides: if anyone else has
starred the item or not played it, DELETE returns 409 with the reason and
only ?force=true proceeds. A feed's header says when it is shared, which
answers why a file nobody here asked for exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
Read, starred and position move to entry_state; subscriptions carry each
person's keywords, auto-download, explicit and per-scan limit. The feed
list and unread counts are per person, and the existing library is
adopted by the admin on first start.
The feed URL, folder and schedule stay shared and admin-only: one file
serves everyone, so they describe the file rather than a preference.
Scanning merges subscribers' wants -- anyone wanting an item is enough --
via merge_policy, which is pure and tested.
Also: the test fixture wiped its data directory from every Playwright
worker, deleting the database out from under the running daemon.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
epAction's redraw closure called itself when handed a row, so Mark read
recursed until the stack blew; it now swaps that row in place. Opening an
item also marks it read, redrawn where it stands so nothing vanishes from
under the pointer on the Unread tab.
Step A of multi-user: users and sessions tables, Argon2id, a session
cookie, ipx user subcommands, and a trusted proxy header for Cloudflare
Zero Trust -- honoured only from a trusted_proxies address. The shared
token still works and is the admin. A new database starts with
admin/ipodderx.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
Half the library is text feeds, so the UI no longer assumes a podcast:
counts, search, the empty detail pane, the phone back button, retention
and per-feed settings, and the download dialog all say item. S1E1 badges
and the episode column stay -- those are the itunes:episode field, which
only appears when a feed publishes it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
The ☰ button lived in the player bar, which is hidden until something
plays, so the feed list was unreachable on a phone. It moves to a bar
that is always present, and the sidebar gets a scrim.
The reading pane takes the whole screen over the list with a back
button, the player stacks into two rows above it, and the page no longer
scrolls sideways -- a grid column is min-content wide by default, so one
long headline dragged everything off the right edge.
Covered by a Playwright case at 390x844.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
Marking the subscription read did nothing: its own row holds no entries.
read-all now resolves the feeds grouped under the id -- via
subscriptions(), so a child promoted to config is included -- and marks
those. Button sits before Unsubscribe, where every other feed keeps it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
Every row reserves the chevron slot, so artwork and titles share one
column instead of stepping left when a feed has no children. Children
keep a single icon size and read as nested from the indent alone. Labels
stack on one line-height, and the unread count has a min-width so a
three-digit feed doesn't shove its own title.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
The rss crate keeps at most one enclosure per item and, when a feed ships
several, silently keeps the last -- so a two-file item lost its first
file. enclosures_by_item reads them from the XML in document order,
unescaping attributes so a URL's & survives. The row summarises the
one you would act on and counts the rest; the pane below lists them all.
Non-media enclosures gain a View link opening in a new tab: the
publisher's URL, or the local copy once downloaded. A direct link, not a
proxy, so the daemon does not become a fetch-anything relay.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
The media-type filter stopped new image enclosures being fetched, but ones
already on disk still got a play button and an <audio> element, because
the UI tested for a path rather than for a playable type. Four places did
this, including play() itself, which picked the first downloaded enclosure
whatever it was.
isPlayable() checks audio/* or video/*, falling back to the extension when
a feed declares no type. A downloaded non-media file now shows as its kind
with Save and Delete, so it stays available without posing as an episode.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
Feeds beside, the feed's items above, and the selected item's text with
its enclosures below -- the shape iPodderX used. Selecting a row fills the
pane below instead of expanding inline; enclosures render there as a
player when the file is present and a labelled download when it is not.
The divider drags and its position is remembered.
The archived site kept no usable screenshot of the original window, only
marketing panels, so this follows the description rather than reference
art.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
The log view splits into All / Daemon I/O / Scans / HTTP. Daemon I/O is
the control protocol itself, logged where every command funnels through
so it covers socket clients, the CLI and the web UI alike. stderr and the
in-app buffer now have separate filters, so the UI can keep debug detail
the terminal should not carry.
Playwright drives a real browser against a daemon on fixture feeds. Eight
tests, each mapping to a bug that reached a user -- the Rust tests and the
stub-DOM smoke test cannot see a wrong selector or a dead handler.
It immediately found one: OPML folders rendered expanded by default,
because the code stored closed groups, so any folder never toggled counted
as open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh