Commit Graph

18 Commits

Author SHA1 Message Date
ec2b9e87b8 Page type-check: noImplicitAny, every parameter and variable typed (#235)
290 places the type-check could not see into, mostly parameters, now typed with web/src/types.ts
(feeds, items, files, accounts, log lines, the directory's rows and items). Two changes run
differently in form only: the seek and volume inputs divide Number(value) where they divided
the string, and esc's table has a name. Null checks are #236.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:57:05 +00:00
af6eae9baf Page type-check: types for what the API sends, and for S (#235)
web/src/types.ts mirrors FeedRow, EntryRow, EncRow, /api/me and PopularRow; types only, so
swc strips it and it is not in build.mjs's list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:52:22 +00:00
78704e40f2 Page type-check: strict's flags that cost nothing, and catch variables as unknown (#235)
why(e) gives a caught error's message; each catch read e.message as any.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:51:41 +00:00
724764c8df Page: catch background fetches, no HTML in errors, a trace ends after a quiet minute (#219)
The page reported NetworkError rejections from the live update's list refresh and the first
feed load, which had no catch, whenever the network was gone (a laptop asleep). A Cloudflare
502 page became a request's error message, since api() used whatever body came back. And
the trace id lived until the next tap, so background requests nine hours apart shared one
trace. The id now ends after a minute without a request; a play's position saves, every ten
seconds, keep it going.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:16:50 +00:00
6c931d0485 The page reports its errors and playback timings, and its requests carry a trace (#190)
Nothing the page did in the browser reached the logs: a DeadAir playback
failure on the phone was a toast and nothing more. POST /api/client takes
page errors, unhandled rejections, hls.js and media errors, the time from
play to sound and the length of a stall, and logs them as ev lines under
ipx::client, inside the request's span. Only those kinds are taken, the
text cut to 500 characters, and the page sends each at most once a minute.

The page's API requests send traceparent, a new trace id for each tap or
key, and the server's request span takes it as its parent, so one action's
requests are one trace in Tempo, the report included. The stub DOM the
smoke and native-bridge tests run in gains crypto and performance, which
every browser the page runs in has.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:15:20 +00:00
d6c0e3e7a6 Show that DeadAir is still working on an episode (#176)
A new download in a show with DeadAir on plays at full length until its
copy is made, and nothing said so. Each file now carries
deadair_pending while DeadAir is on for its show and the copy is still
to come (not for one heard through, which it passes over), and the
episode's line and details say "Cutting dead air…". The maker sends a
deadair_done event when it finishes a file, and the page takes the list
again, so the line becomes how much shorter the episode is without a
reload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:05:18 +00:00
111a0e29a4 DeadAir's icon: the silence open when off, closed up when on (#172)
Outlined bars against solid ones read the wrong way round at 20 px: the
outlines ran together and the off icon looked the heavier. The off icon
now has the gap in its middle, the on icon has it closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:27:42 +00:00
827cc7489f DeadAir in the page: a switch per show, one beside the speed, and the time saved (#172)
A DeadAir switch in each show's settings, and the same switch beside
the speed for the show that is playing, drawn as a waveform with its gap
closed up, outlined when off and solid when on, with aria-pressed. A
file the server gives a copy for plays from /media/{id}/deadair. An
episode's line says how much shorter it is, and Settings says how much
DeadAir has saved you.

Turning it on or off mid-episode saves the place with a request it waits
for, not a beacon, then asks for the entry again from the new
GET /api/entries/{feed}/{guid}, whose position is in the clock of the
version it now plays, and carries on there. A copy not made yet plays
from the next time. Turning a show on wakes the copy maker, so the copy
is there in seconds rather than at its next pass.

The browser test plays a real MP3 with long silences, quiet.mp3, from a
show it adds and removes itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:18:41 +00:00
95acde3046 Lay the Directory out as Apple's is, with the most subscribed in it (#124, #126, #128)
The Directory was one grid of every listed feed, 1,472 in production, under Podcasts and Blogs
tabs, a wall of about 30 category buttons and a sort menu, with a picked category's
subcategories appearing as a second row of buttons that looked like the first. It read as a
list to scroll, and the two rows were hard to tell apart.

It opens now on the ten most subscribed feeds, ranked, and the categories as tiles, each with
three of its shows' covers fanned in its corner on the tint its initials would get, so the
shows colour it and no theme's palette changes. A category has a page of its own: its most
subscribed, its subcategories with how many each holds, and its grid. See all is every feed,
as the grid was. Podcasts or Blogs holds across every page. Popular, the ten most subscribed,
had a place of its own in the feed list; it is the Directory's first section instead, and its
key, g p, is gone. /api/popular stays for scripts.

The search box finds a feed in the Directory by name (#126). It said "Search items…" there and
did nothing, since loadEntries returns early for a place that lists feeds.

A feed you do not subscribe to opens a page of its own (#128): its cover, category (a link to
that category's page), how many here subscribe, a subscribe button and its latest twenty items,
each with its title, linking to the post where it has one, its first lines, date and length.
Before, a click on it did nothing; only its + did. The items come from GET /api/directory/{id},
which answers only for a feed the Directory lists, so a guessed id reaches nothing private, and
carries no file or its address. The feed's own description would belong at the top, but the
database does not keep one.

Every tile and chart row takes the keyboard, as the feed list's rows do. Feed text made plain
keeps a space at a line break or a paragraph's end, which glued "2010)Recorded" together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:19:38 +00:00
a00516687a Keep artwork on disk and serve every image from iPX (#111)
The page loaded artwork from each publisher's server, or through /api/art, fetched every time,
for http-only hosts. Nothing was kept, every visit asked every publisher, and artwork went when
a publisher's server did.

- The page draws every image a feed or item names from /api/art. The first time, iPX fetches
  it (only an address a feed or item names, only an image, up to 5 MB, within the feed timeout)
  and keeps it in art/ beside the database, under a hash of its address with its type beside
  it (src/art.rs). Later it comes from disk, which marks it as used.
- art_cache_mb, a server setting on the admin page, 500 by default, caps what is kept: the
  sweep before each scan drops the least recently shown until it fits. 0 keeps nothing, and
  artwork is fetched through iPX each time. Settings saved before it get the default.
- Served from iPX's own address, someone else's image must stay an image: nosniff, and a CSP
  with sandbox, so an SVG opened on its own runs no script as iPX.
- The fixture server sends .jpg as image/jpeg, which /api/art requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:50:46 +00:00
36b16c7f5d Fetch artwork offered only over http for the https page (#90)
Through ipodderx.sdf1.net the page is https, the browser upgrades an http:// image to https,
and a host with no https, such as The Secret Cabal's CDN, answers nothing, so no artwork. On an
https page, the page now asks /api/art for those, and ipx fetches them. It only fetches an
address some feed or entry names as its artwork, and only an image, up to 5 MB, so the route
cannot be pointed at anything else on the network.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:19:06 +00:00
a5de4ae06c Toggle state in the icon's shape, not the theme's colours (#74)
4ed2d59 drew a pressed toggle in each theme's accent colour; the themes'
colours were not to change. Back as they were, pinned rows included. The
read button carries its state in its shape instead, as the pin does with
outline and solid: a tick when read, the envelope when not, where before
it showed the action (the envelope on a read item).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:38:59 +00:00
4ed2d59311 Toggles show their state the same way everywhere (#74, #75)
A sweep of all 24 theme palettes, measuring each icon's drawn colour,
found pinned in three colours: accent in the feed list, the text colour
on an item's row, and uncoloured on the toolbar, beside the title and on
the feed page. The read button showed the action (an envelope on a read
item) beside a pin showing the state. Now each toggle shows what is, with
aria-pressed, and a pressed one is the accent colour; Classic needs its
own rule, as its buttons set their colour at higher specificity. The
contrast test checks the accent on the button grounds, where it now draws.

Directory's Subscribe button carried the Subscribed tick; it is a plus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:36:23 +00:00
380a552913 Share a feed, an item or a file (#48)
A share button in the feed's header, beside an item's "Open the original", and on each file.
It uses the Web Share API where the browser has it, which is the share sheet on a phone, and
copies the link where it does not. A file is shared by the publisher's address, not /media/,
which only someone signed in here can open.

Paid feeds carry the subscriber's access in their address, Patreon's in a token, so sharing one
gives the subscription away. An address that looks like that asks first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:17:52 +00:00
43ffafe7ac A UI pass: contrast in every theme, and a sign-in page that fits
Measured every theme's palette against WCAG AA. The unread badge's count
on its --accent2 fill fell as low as 2.6:1 (Flat Remix light), and the
unread dot with it; each theme's --accent2 is taken down until white on
it clears 4.5. Tags were --warn or --bad on --raise, short of AA in most
light themes, so they are outlined on the row's own ground instead.

Nordic dark had --line equal to --panel2, so bordered buttons on a
panel2 ground drew no edge at all. Classic's selected row left the
row's icon buttons grey on the blue. A zero badge on a selected row was
--raise on --raise and vanished.

login.html never had a doctype or viewport meta, so it rendered in
quirks mode and at desktop width on a phone, and its light palette sat
under data-theme="light", which nothing ever set. It follows
prefers-color-scheme now, signed out having no account to ask.

Dropped the unused log and users icons. The Classic theme's label is
now just "Classic".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 01:37:21 +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
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
26802d2b23 The page's script is TypeScript in web/src, built and minified with swc
- web/src/*.ts: the script that was inline in index.html and login.html, split along its
  existing sections. Still one scope, concatenated in order, not modules.
- web/build.mjs strips the types, puts the script in the page and minifies it with swc;
  build.rs runs it into OUT_DIR and web.rs include_str!s the result. 137 KB -> 106 KB.
- npx tsc -p . type-checks web/src, loosely; the handful of annotations it needed
  change no behaviour.
- The Docker build installs node and swc (npm ci --omit=dev).
- Two list requests racing no longer let the older one win, and switching tabs clears
  the selection it closes, which made a browser test flaky.

Closes #23, #24.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:41:34 +00:00