Reported: the unread count on the icon only changed when the app was
opened. Two reasons:
- Items read in the page never reached it, since only the player's own
mark-read refreshed the badge.
- The background refresh that should bring new items runs when iOS
chooses, which for this app has been rarely.
The page now sends All Subscriptions' count ({t:"unread", n}) each time
its list is drawn with a new one, so reading, and new items arriving
over the event stream while the app is open, set it at once. While an
episode plays the app is awake with the screen locked, so the player's
tick also re-checks every quarter of an hour.
With the app closed and nothing playing, the count still waits on iOS's
refresh. Push (#433) is what makes that exact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page asks the app for a haptic ({t:"haptic", kind}) and the app
plays the phone's own. A light tap for play, pause, flag and pull to
refresh; the system's success for marking read, one item or all; its
warning with an error toast. A browser has no host to ask, and an
older app ignores the message, so there is nothing to gate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sleep timer pauses after 5 minutes to an hour, fading out over the
last ten seconds. In the iOS app the phone's player keeps the time:
with the screen locked the web view is frozen, and a timer in the page
would not fire until the app was opened again. A browser plays on in a
background tab, so there the page keeps it.
The app says at ready what it does itself ({t:"host", can}), so a page
deployed ahead of the app falls back to its own timer and shows no
AirPlay button an older app would ignore. This deploy order problem
came up with #425.
AirPlay opens AVRoutePickerView by pressing its button from code; there
is no API to present it. On iOS 27 the picker opens on the button's
primary action, not touch-up, so that is what is sent, with touch-up
for versions that have nothing on it. The simulator has no AirPlay
devices and never showed the picker, so this is unverified until it
runs on a phone.
On a phone the player's one line left the title two letters wide once
the two controls were added, so the title takes the top line and the
controls flank play below it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A WKWebView runs no service worker, so the page's Keep button (#412)
was not shown in the app and everything played from the server. Now the
page asks the app to keep the file (keep, unkeep, clearKept), and the
app answers with what it holds (kept, keepFailed) whether or not
anything plays. The app fetches with a background URLSession, so a
download goes on with the app closed, into Application Support (not
backed up; the files can be fetched again), and Playback plays from the
file when there is one. The DeadAir copy is kept under its own path, as
the browser keeps it. Places and finishes reached offline wait in an
outbox, newest per address, and go when the network is back.
Checked in the simulator against a local server: kept /media/42 and
/media/42/deadair, then with the server refusing /media, a fresh launch
played each from the phone and asked the server for nothing. The page
itself still needs a connection to open (#435).
Deploy the app (build 6) before the page: a new page in an old app
shows a Keep button nothing answers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
page-unit.ts and native-bridge.ts run the page's script outside a
browser, so the page's coverage counted none of what they test, and
native.ts read as mostly untested. In a coverage run they now run the
script from a file of its own, named as the browser names it, under
NODE_V8_COVERAGE (scripts/coverage-node.sh, before the browser suite in
npm run coverage:page and coverage.sh api), and the report's teardown
takes their record in, mapped to web/src by the build's source map: the
page's lines went from 73.3% to 80.0% of 3,043, native.ts to 93 of 120.
The cargo tests in coverage.sh's container run on Postgres, as
production does, when IPX_TEST_DATABASE_URL names the _test database,
from the shell or else /src/.envrc (that variable only); on SQLite
without it. The integration tests' own files are left out of the
totals; a crate's inline mod tests cannot be on stable Rust.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page and the service worker were strict TypeScript; the tests, the
build, the home page's generator, the load tests, boot.js and the
directory's site.js were plain JavaScript that nothing checked. All of
it is TypeScript now, in five programs, each with its own globals, all
checked by npm run typecheck: the page (tsconfig.json, with boot.ts and
the directory's site.ts), the worker, Node's (tsconfig.tools.json: the
Node tests, the browser suite, the build, the site generator, the load
harness), and k6's (tsconfig.k6.json, with @types/k6).
Node 24 runs the .ts files by stripping their types, so they hold only
TypeScript that erases (erasableSyntaxOnly) and import each other by
.ts name; package.json says "type": "module" for it, and Dockerfile.web
moves to Node 24 to run build.mts. k6 2.3 strips its scripts' types
itself.
The browser suite's page.evaluate calls reach the page's globals; the
page's tsc run now also writes its declarations to target/page-types,
which the suite's program includes, so those calls are typed by the
page's own code rather than by hand-written copies.
boot.ts is compiled by build.mts with swc, as the worker is, and named
by the hash of what is served. The directory's build has no Node, so
site.js is compiled from site.ts and committed (node web/build.mts
--directory-script), and tests/directory-script.ts fails if they drift.
Checked: the site generator's 139 pages are byte for byte what the
JavaScript made; the page build's app.js is identical and only boot.js
(now minified) and its hash differ; contrast, page-smoke, page-unit,
native-bridge, the browser suite (96 passed, 2 skipped) and the
directory's and ipx's Rust tests pass; k6 parses each load script. The
load tests themselves did not run: they build a release binary outside
the capped builder. .agents/skills/security-audit is an installed
skill's, not ours, and is left as it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>