Commit Graph

64 Commits

Author SHA1 Message Date
35b57fa997 Settings and the shortcuts list close from the corner
Both have nothing to confirm, so their only button was a lone X at the
foot of the card, below the fold of a long Settings card. A dialog with a
confirm keeps Cancel beside it at the bottom, where the pair belongs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 01:45:02 +00:00
45cbd3a239 The scan spinner takes the unread count's place
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 01:21:49 +00:00
905dfa0b02 A spinner on the feed being checked, not toasts; check only your own feeds
The scan's events reach everyone, so every browser showed "<feed>: N new" and
"Scanning…" toasts, and refreshed, for everyone's feeds. Now a feed's row, and
its folder's, carries a spinner between feed_start and its done, skip or error;
the list refreshes only for the reader's own feeds; the scan toasts are gone, and
"Downloaded" is said only for a file on screen.

"Check every feed" from the web UI sent a scan of every feed on the server.
Command::Fetch takes an optional `feeds` list -- those feeds and the feeds
inside any OPML among them -- and the web fills it with the asker's
subscriptions. The schedule and the CLI send none, meaning every feed.

Closes #37.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 01:17:40 +00:00
f09bb4a11c Revert pinned items rising to the top of their list
Sorting by the pin column, or the Pinned tab, was enough. order_sql loses its
pinned_first option, pinning no longer reloads the list, and the tests and
changelog line for #35 go. The NULLS FIRST/LAST ordering from the Postgres work
stays.

Closes #36.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:08:37 +00:00
c99e17bd80 A pinned item goes to the top of its list
order_sql takes pinned_first, which puts coalesce(s.flagged, 0) DESC ahead of
the chosen sort, so pins lead every list in whatever order is asked for and on
every page of it. Not when sorting by the pin column itself, where the direction
is the point, and not for Currently Listening. Pinning now asks for the list again
so the row moves at once, instead of redrawing it where it stood.

Closes #35.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 17:51:38 +00:00
aeb686163b A separate admin page: server settings, accounts and the log
/admin, with Server, Accounts and Log sections chosen by the URL's hash. The
server sends the page and /admin.js to admins only (anyone else asking for the
page goes back to the app, and the script is 403), and removes the header's link
to it from everyone else's page rather than hiding it. The API keeps refusing
all of it to non-admins as before.

Settings becomes personal: theme, OPML import and export, and the schedule and
download folder to read. The server fields, the Users dialog and the Log dialog
move out of dialogs.ts into admin.ts.

The CSS moves out of index.html into web/app.css, which both pages load as
/app.css?v=<hash>, served immutable like the scripts. The smoke test checks both
pages.

Closes #19.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:28:28 +00:00
2d158a4540 Pin a feed to the top of the feed list
subscriptions.pinned, per person, set by PATCH /api/feeds/{id} {pinned} and
returned as FeedRow.pinned. Kept out of Sub, which the scanner merges into its
policy; set_subscription names its columns, so saving a feed's settings leaves
the pin alone (tested).

Pinned feeds come first in the list, a pin before the name and a rule under the
block: a pinned folder with its feeds under it, a feed from inside one lifted out
of it. The pin button is on both the feed and the folder page.

Closes #33.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:14:47 +00:00
fc425bffa6 Play/pause test: play without decoding the fixture
The fixture file does not reliably decode in the test browser; the load error
paused the player, which rightly turned the buttons back to play, and the test
failed in the full run. The test now fakes play and pause, events included, so it
checks what the buttons do and nothing else. The previous commit went up with
this test failing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:09:26 +00:00
42a1136e2e Every play button for what is playing shows pause, and pauses it
Only the player bar's button changed; the files pane's, the row's and the
toolbar's kept showing play while it played. play() now pauses when asked to play
what is already playing, which makes each of them a toggle, and syncPlayButtons()
repaints them on play, pause and ended and whenever the list or reader is drawn.

Closes #34.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:01:03 +00:00
53341264a7 On a phone, no "No files" box above an item that has none
The files sit over the text on a phone, so an item without any showed a box
saying so before its text. Nothing is shown now; the desktop files pane already
hid itself when empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 14:50:36 +00:00
9c16408d04 Keep the theme on the account, not in the browser
- users.theme and users.theme_mode, added by migrate(); GET /api/me returns them
  and PATCH /api/me saves them, refusing anything but a plain name and
  light/dark/auto, since index() writes them into the page's <html> tag.
- The page arrives with data-theme and data-choice already on <html> (and
  data-mode unless Auto), so it is drawn in the account's theme from the start.
- A theme a browser kept in localStorage goes up to the account once, the first
  time an account with none loads the page.
- Saves go one at a time, each with the choice as it stands: sent all at once, a
  quick run through the list could land out of order and keep a theme passed on
  the way. The browser test caught it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 14:33:59 +00:00
3b00e2721a Touch gestures: pull to check for new items, swipe between items
- Pull the item list down from its top: checks the feed (or every feed, on All
  Subscriptions) for new items, which arrive as they do from the scan button.
  overscroll-behavior keeps the browser's own pull-to-reload out of it.
- Swipe the item you are reading left for the next, right for the one before,
  or back to the list from the first. A vertical move is a scroll; something
  that scrolls sideways, or takes typing, keeps its own swipe.

Touch events only, so a mouse never sets them off.

Closes #22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 14:13:28 +00:00
fc09e8a6b7 Favicon, level file icons, and a feed error mark in the triangle's column
- The logo as favicon, squared up (it is 128x121), at /favicon.png and at
  /favicon.ico outside the auth layer, where a browser asking on its own got a
  401; an apple-touch-icon on white (#32).
- An item not yet downloaded had its download bar on a line of its own under the
  file icon, lifting the icon above its row's; the bar now sits under it without
  taking space (#31).
- A feed error is Font Awesome's exclamation, hung in the margin where a folder's
  triangle is, in the same column; a folder holding a failing feed has its
  triangle turn red.

Closes #31, #32.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 14:09:38 +00:00
5483355021 Serve the script as /app.js, cached until a deploy changes it
The page loaded its script inline. It now names /app.js?v=<hash> (login.js for
the sign-in page), the hash of the script's contents: the script is served
immutable for a year and the page no-cache, so a browser fetches the script
again only when a deploy changes it and so its name.

Also fixes a race in the mark-everything-read test: it waited on a badge that
was seldom 0 to begin with, so a mark-unread still in flight could land after
the read-all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 13:03:25 +00:00
e2969bcee8 Themes: Dracula, Material, Adwaita, Flat Remix, Paper, Nordic, each light, dark or Auto
The theme picker lists Modern (the old Dark and Light), Classic and six new
palettes, from Dracula's spec (with Alucard), Material 3's baseline scheme,
libadwaita's CSS variables, Flat Remix's _colors.scss, Paper and Nord. A second
setting picks Light, Dark or Auto where a theme has both; Classic and Paper do
not, so it is hidden for them.

The page gets data-mode, light or dark, and Auto is worked out in theme.ts from
the system, so each palette is written once instead of again under a media
query. Every new palette clears WCAG AA for text on its backgrounds. An old
ipx.theme of dark, light or auto carries over as Modern.

Closes #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:53:25 +00:00
fbba447ca6 Add a feed without Popular; a phone shows an item's files above its notes
- The Add a feed dialog no longer lists Popular; the sidebar has it (#30).
- On a phone the files, with play and delete, come before the show notes. Below
  them, long notes buried the delete button and it looked missing on iOS (#21).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:21:44 +00:00
d7a4a0b663 Fix the open bugs: read state, Unread tab, feed errors, theme button, log button, relative images
- Opening an item stays read: a list refresh that crossed with the write no longer
  puts the unread dot back (#16).
- On the Unread tab the item you were reading goes when you move to the next (#17).
- Feed errors mark the feed with a red ! instead of a toast per failure (#20).
- The theme is chosen in Settings only (#15).
- The server leaves the Log button out of a non-admin's page, so it no longer flashes (#29).
- Relative images and links in a post resolve against the post's link (#28).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:10:05 +00:00
b9e0d9f3cb Save a position only from a player that has played since its last save
A tab left paused further into an episode saved its older place as it
reloaded, over where the listener had got to since, and the episode
dropped out of Currently Listening. A jump back is now saved at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 18:29:10 +00:00
8daa7a1991 Currently Listening: the EQ bars mark what is playing
The row in the player gets the amber EQ bars, as the item list's does,
and its progress rail and time left move as it plays. Rows say how
much is left, and their buttons are quiet so that row stands out.

savePos no longer saves before the file has loaded: currentTime is 0
then, and a failed load or an early pause wiped the saved position.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 18:08:21 +00:00
f89ca2aceb Currently Listening: a cross takes an episode off the list
It forgets the saved position, which is what puts an episode on the
list. The one in the player is closed without saving first, or its
next save would put it straight back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 17:54:17 +00:00
57a6fb5daa Currently Listening: finished is 90% played, not read
Opening an episode marks it read, so filtering on read hid every
episode anyone had started. The player now also reports the length it
measured, filling in one the feed left out. Fixes #14.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 17:47:57 +00:00
49dafedbbc Inter, one file where WordPress listed two, and a pin heading on line
Inter (#11): the pages are set in Inter's variable font, served from the
binary at /inter.woff2 as the icon is, with its OFL licence beside it in
web/. Classic keeps Lucida Grande, the 2004 app's face.

Double audio (#12): WordPress numbers each audio player on a page by
adding ?_=N to its file's URL, so a post that embeds the file it encloses
listed it twice, and it was downloaded twice. The parser keeps the first
of an item's enclosures that differ only by that number. At startup the
repeats already stored fold into the first; where only the repeat had
been downloaded its file moves to the first rather than being deleted.

Pin heading (#13): the rows' icon buttons kept the browser's side
padding, which pushed their 16px icon 3px right of centre, and the
heading's icon sat at the left of its column. Both are centred now, and
the heading row takes the pixel of border the rows have, so every
heading sits over its column.

Closes #11, closes #12, closes #13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 17:31:03 +00:00
b83523fc92 Directory: let an admin give a blog its category
Almost no blog names a category the Directory can use, so a feed can
carry one of its own in config.toml, set by an admin in the feed's
settings and used when the feed names none. The feed's own iTunes
category still wins. The field offers the categories the Directory
already shows, so a blog about games joins Games rather than starting a
second chip. Setting it on a feed from an OPML promotes it to config, as
any other shared setting does.

Closes #10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 17:19:59 +00:00
8ae5c332c3 Pin, not flag
Keeping an item is pinning it now: a thumbtack where the flag was, and
Pin, Pinned and Unpin where Keep, Kept and Stop keeping were, on the
toolbar, the item's own buttons, the filter tab, the table column, the
retention hint and the warning before deleting a shared file. Pinned is
the solid thumbtack and not pinned the same shape outlined, as the flag
had its regular and solid pair. The API and database keep `flagged`.

The icon test compared glyphs by their path alone, which the two pins
share; it compares the whole glyph now.

Closes #9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 17:14:59 +00:00
420f9eb9a0 Keyboard shortcuts, after Feedly's
j/n and k/p step through the items, Shift-J and Shift-K through the feed
list, and g with a letter goes to a place: All Subscriptions, Directory,
Popular, Currently Listening, Settings. o plays the selected item, m marks
it read or unread and s keeps it, each by pressing the toolbar's own
button; v opens the original, Shift-A marks all read, r refreshes, [ hides
the feed list, and ? lists them all. None fire while typing, with a
modifier held, or with a dialog open.

Closes #8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 17:08:54 +00:00
eaea520020 Give Currently Listening its own place, below Popular
It was a section at the bottom of the Popular page, so finding the
episode you were halfway through meant opening a list of feeds first.
It is a place in the feed list now, between Popular and All
Subscriptions, with its own page and count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 16:49:11 +00:00
cbd16ce3f6 Directory: Podcasts and Blogs as a filter, beside the category chips
What a feed is and what it is about are two questions, so they are two
controls that combine: the same .tabs the item filters use for All,
Podcasts and Blogs, and a row of category chips that is always there,
offering only the categories among the feeds the filter lets through. A
picked chip lifts on a second press, so there is no second All.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 16:30:59 +00:00
954173cacf Directory: chips by kind and category over a grid of cover art
Feeds take their channel's first <itunes:category> into a new feeds.category
column; the migration drops ETag and Last-Modified once so every feed re-reads
on its normal schedule and picks one up. /api/popular and /api/directory carry
category and podcast (any audio or video enclosure). Directory becomes a grid of
cover-art tiles under a chip rail: All, Podcasts, Blogs, and a podcast's
categories once Podcasts is picked. Popular and Add a feed keep their rows.

Closes #4, closes #5, closes #6, closes #7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 16:09:47 +00:00
af38583b53 Give listFeeds its container: Add a feed loads its Popular list again
The dialog and the Directory/Popular pane both rendered into id="popular", and
listFeeds looked it up by id, so the dialog's list landed in the pane behind it.

Fixes #1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SZbKERNSt4vQfyGV8rvkqp
2026-09-14 21:28:49 +00:00
a30edff248 Release 0.5.4: remembered view, Auto theme, Currently Listening
- Remember the feed/place and tab across a reload or new visit; an
  unknown or unsubscribed one lands on All Subscriptions instead of the
  first feed alphabetically.
- Add an Auto theme that follows the system's light/dark setting, and
  move Dark/Light/Classic/Auto into Settings as a dropdown alongside the
  header button's toggle.
- Add Currently Listening below Popular: episodes started and not
  finished, across every subscribed feed, one tap to resume. Reuses the
  existing entries/filter machinery (Filter::InProgress) rather than a
  new endpoint.
- Likely fix for the iOS bug where the topbar stopped responding to taps
  until a hard refresh (100vh -> 100dvh); unverified on a real device.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DmQfE1eFPApnXWyPHBWqUA
2026-09-14 15:38:03 +00:00
be3820bbbd Release 0.5.3: OPML orphan scan, feed error UI, small UI fixes
- Stop scanning an OPML/Patreon feed's derived rows once nobody subscribes
  to it; retire them (drop or orphan) the way sync_group already does when
  the list itself drops one. This is what let 922 defunct davewiner feeds
  keep scanning hourly after the OPML left config.
- Repair feed XML with a bare `&`, and give a plain reason (moved web page
  with its new address when linked, or nothing yet for an empty body)
  instead of a raw parser error.
- Show a failing feed's plain-English reason and next step (Unsubscribe /
  Use the new address) in the sidebar and on its own page, once it has
  been down a day.
- Fix four small UI bugs: show-note links open in a new tab, video files
  play as video, an opened item no longer disappears from the Unread tab,
  and Subscribe/Unsubscribe get their own icons.
- Fix Settings disappearing for non-admin accounts: it was hiding the
  whole modal instead of just the admin-only parts (Users, the editable
  schedule/quota, Save), which are the only parts the server actually
  refuses them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DmQfE1eFPApnXWyPHBWqUA
2026-09-14 14:53:45 +00:00
b94a74ef15 Sign out through the proxy when the proxy signed you in
Sign out cleared ipx's cookies and showed its password page, while
Cloudflare Access still vouched for the person: nothing was signed out,
and the page looked like the wrong login. /api/me now says, for someone
the proxy signed in, where to go instead ([web] sign_out_url, which is
/cdn-cgi/access/logout behind Access), and /login sends anyone the proxy
vouches for on to their feeds. The header check both use is one function.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAC7sLVqfKmY6rsTLXzNgk
2026-09-12 14:10:30 +00:00
2ba83c3aed Keep when each account was added and when it last signed in
users.created comes back, beside a new last_login, for whoever maintains
the server. A password sign-in, the token link and a request through the
proxy all count, recorded to the hour so the proxy's per-request vouching
is not a write each time. Settings -> Users and ipx user list show both.
The three user queries now share one row mapping.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAC7sLVqfKmY6rsTLXzNgk
2026-09-12 13:37:09 +00:00
a958f7cb37 Trim the state database; Popular lists feeds the way Directory does
Drops the created columns on users, subscriptions and sessions, which were
written by every insert and read by nothing, and migrate()'s add list, whose
columns all predate 0.3.0. Removes Db::subscribed_feed_ids (no callers),
Db::subscriber_count (one caller wanting > 0) and Managed.orphaned (never
read). The old-database test now builds the tables with foreign keys on.

Popular now lists the feeds inside an OPML or a Patreon creator, never the
collection, as Directory does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAC7sLVqfKmY6rsTLXzNgk
2026-09-12 13:15:55 +00:00
457a58dcc5 Directory lists the feeds inside an OPML, not the OPML
Popular still counts an OPML as one feed, since everyone subscribed to it
counts for every feed inside and they would bury the rest. The directory is
for finding a show, so it lists them one by one and never the OPML. A feed
inside an OPML that looks private is hidden with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAC7sLVqfKmY6rsTLXzNgk
2026-09-12 02:26:52 +00:00
dc63d6acaf Cut what the audit found: dead columns, one-time upgrades, three deps
Works through TODO.md from the 2026-09-12 over-engineering audit. Drops the
entries.read/flagged/position columns (migrate() removes them from older
databases), migrate_opml_children, the legacy interval_mins key, the
contrib/ systemd units, test-only Db wrappers, a duplicate token generator,
redundant logbuf visitors, unused page state and CSS, and the infer, dirs
and tokio-stream dependencies. The icon is served once as /icon.png instead
of inlined four times, taking about 94 KB off the two pages.

The adoption's subscription half was not dead: it gives a fresh install's
first admin the config's feeds. It stays as adopt_catalogue, now tested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAC7sLVqfKmY6rsTLXzNgk
2026-09-12 01:55:44 +00:00
0990f2a90d Design pass on the web UI: amber for new, EQ bars, keyboard feed list
Works through TODO.md from the 2026-09-11 review. Unread badges, dots and
download bars take the icon's amber; the playing item is marked by EQ bars
that move only while it plays. The feed list is usable from the keyboard,
focus rings show everywhere, and folders get a mosaic of their shows' art
with the triangle hung in the margin. Sentence-case labels, fewer bold
weights, tinted initials tiles, shorter header lines, "Kept" everywhere,
and the list gets the room the empty panes had. Reduced motion is honoured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAC7sLVqfKmY6rsTLXzNgk
2026-09-11 22:24:41 +00:00
669e8b5124 Files pane plays through the player bar, once
The pane drew its own <audio controls> for a downloaded file, and its
onplay also started the player bar, so one click played the same file
twice at once. The pane now has a play button (with the file's type
icon, like the other rows) that hands that exact file to the player
bar, the only player. play() takes the file, so another of an item's
files starts from its top instead of resuming the first. Dead CSS for
the pane's <audio> removed.

Test: play in the Files pane leaves one <audio> on the page, the bar's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0173mGu6rK18Ne7UGTwAaVJV
2026-09-11 17:30:03 +00:00
5362436766 Sortable item table, Size in its own column, Subscribed as an icon
- Every column heading sorts (kept, title, feed, file type, size,
  published); a second click reverses it. The server sorts through a
  fixed whitelist (order_sql), so it covers the whole list, not the
  fifty loaded; the choice is remembered in the browser.
- Size is its own column and shows KB for small files instead of
  "0 MB". The Item heading is Title.
- Popular/Directory/Add feed: Subscribed is a green circle-check.
- Tests: every sort column runs and orders both ways (db); the table
  sorts by title both ways and remembers across a reload (browser).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0173mGu6rK18Ne7UGTwAaVJV
2026-09-11 17:17:16 +00:00
0efc49519c One icon per meaning across the UI; mark everything read in All Subscriptions
- 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
2026-09-11 17:05:47 +00:00
57dcba2d1a One file icon, green when downloaded; mark unread is an envelope
- 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
2026-09-11 15:20:32 +00:00
7295be8b25 Drawn icons throughout; file state and type as icons, no PENDING
- 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
2026-09-11 15:11:15 +00:00
ae47e31a97 Classic theme after the 2004 Mac app; header and dialog buttons as icons
- 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
2026-09-11 15:00:30 +00:00
8a309eb652 Files pane and item buttons are icons, with the words in tooltips
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
2026-09-11 14:57:03 +00:00
d7fac2d0e7 Never list paid-feed services; scan on add; no "reaped"; slimmer header
- 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
2026-09-11 14:47:03 +00:00
df9b7645d6 The original iPodderX layout: toolbar, places, item table, Files pane
- 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
2026-09-11 14:39:05 +00:00
f3825cfc57 Popular is a top 10; a Directory lists every listed feed A to Z
- 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
2026-09-11 14:13:16 +00:00
c0f4b0bcb2 Popular button in the sidebar; the popular list counts everyone
- 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
2026-09-11 14:05:24 +00:00
8b8b48302b Popular on this server; changelog follows Keep a Changelog; 0.3.0
- 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
2026-09-11 13:56:57 +00:00
5d3fdde4da Upload an OPML file to import; tests for every way in and out
- 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
2026-09-11 13:42:38 +00:00