Every non-OK playback-state response marked the write dirty and retried it every five seconds,
so a stale manifest (412) or a position past the rounded track duration (400) looped for the rest
of the session. Each attempt also minted a new operationId, which defeated the server's replay
protection, and capturedAt was stamped at flush time rather than when the position was taken.
Capture the timestamp and one operationId with each position and reuse them across retries.
Retry only network failures, 5xx and 429, and drop other rejected writes. On 409, reload the
server state and keep the local write only if it is newer. On 412, hold the write and have the
player reload the manifest, resending once the revision changes. Clamp positions to the track
duration from the manifest.
The web audiobook player shared the reading session composable, which never sent a
sessionType, so the server stored every listening session as a read session and counted
audiobook time as reading in activity stats. Let callers pass an optional sessionType and
have the audiobook view send listen. Other readers keep omitting the field, so their
payloads are unchanged. Existing rows are not backfilled.
Let users hide request entry points and notifications while existing requests continue running. Keep the setting optional so older clients retain their current behavior.
Ref #1476
Allow 0.05% adjustments while preserving existing integer settings and the 98% default. Keep finish detection stable when progress is stored as PostgreSQL real.
* fix(auth): recover from expired edge auth proxy sessions
The service worker answered every page load from the precached app shell, so once an edge auth
proxy's session cookie (e.g. Cloudflare Access, OAuth2 Proxy) expired, the proxy never saw the
navigation and could not redirect to its login page. The app loaded from cache, its API calls were
redirected cross-origin and failed, and it fell back to its own login screen with no way out short
of clearing site data.
Serve navigations network-first and fall back to the precached shell only when offline.
vite-plugin-pwa defaults navigateFallback to index.html, which would register a cache-only route
ahead of this one, so disable it explicitly. Cache only 200 responses so a proxy's redirect is never
stored as the page.
Request API redirects manually so a proxy redirect is detected directly and triggers a full reload,
which the proxy can then send to its login page. Cap these reloads to one per 15 seconds so a reload
answered from cache again surfaces an error instead of looping.
Fixes#642
* fix(auth): harden edge auth proxy recovery against loops and cache growth
Force manual redirects after caller options so no request can opt back into following a proxy
redirect. Skip the reload when session storage is unavailable, since the loop guard cannot work
without it. Drop the navigation network timeout so a slow proxy redirect is never replaced by the
cached page, and cap the app shell cache, which stores one entry per visited URL.
Ref #642
* test(auth): isolate useLoginOptions module state between tests
The composable keeps its cached options, error, and in-flight request at module level, so they
carried over between tests in the file. Reset the module and import it fresh for each test so a
future test cannot pass or fail depending on what ran before it.
Ref #642
* fix(auth): let edge proxy handle sign-in navigations
Keep the offline shell under a distinct precache URL so Workbox cannot serve
root navigations before the proxy sees them. Route public auth requests through
the redirect-aware fetch path so expired edge sessions open reauthentication.
Ref #642
* fix(auth): keep offline shell off root URLs
Prevent Workbox from mapping root query variants to the precached offline shell so expired proxy sessions reach the network.
---------
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Refresh the web access cookie after a media load failure and resume from the last observed position. Limit retries so unrelated media errors surface promptly.
Closes#1536
Add a reusable pagination control and cover Book Dock navigation with focused tests. Align the client PDF dependency and lockfile for clean CI verification.
Use the browser origin when APP_URL points to a local address so setup links work through a public proxy. Preserve public canonical URLs and HTTPS.
Closes#1548
Every format chip, edition row and format menu now builds from shared helpers: content files
only, a read-along EPUB kept apart from a plain one, in the library's format priority, which
gains a read-along entry. Labels, colours and the chip each have one source, and cards, OPDS,
email, Kobo and the dashboard rank files the same way. Book detail file order is unchanged
because iOS builds its track queue from it.
The plugin Set up form reused the new-source editor, which always showed
an Indexer type select listing only torznab and newznab. Changing it
saved a torznab feed under the plugin's name, which is how a MyAnonaMouse
feed ended up pointed at the tracker's website.
"Read from here" and the page start resolved the range to the nearest element with any id and
matched SMIL entries against that id. Storyteller keeps the book's inline elements inside its
sentence spans with their ids intact (Kobo's kobo.N.N, calibre's calibre_link-N), so in those
books the nearest id named no entry and narration restarted at the top of the chapter, while
playback kept highlighting the same sentences correctly.
Start at the first SMIL entry whose element overlaps the range instead, looking each entry up the
way foliate resolves the highlight. Foliate already walks entries in reading order through the
start filter, so the vendored engine is unchanged.
Fixes#1528
Keep one cover slot per medium and show the one each surface needs: the
library's shape for the face, square art on audio screens, the ebook slot
for Kobo and OPDS. Legacy covers convert on upgrade or on first read, and a
write that names no slot keeps working for released iOS builds. Also read
the dev client origin from one constant.
Run the dev API on 6262 and Vite on 6263 with strictPort; production
defaults stay on 3000. Define the dev client origin once and read the
typed appConfig instead of repeating its fallback in every gateway.
Load read-aloud voices only once playback starts, so the app-wide player
no longer fires a 401 on public pages. Let the session-failure handler
clear auth without leaving a public route, which also keeps magic links
and the sign-in redirect intact.
Fixes#1503
Carry positions between sibling EPUBs even without an audiobook, and keep
narration markers on the read-along file only. Point the KOReader book panel
and hold release at the copy devices sync, and label EPUB-only sync in the
read-aloud panel.
Fixes#1508
Accept android as a reading-session source from clients and report it as
its own statistics bucket, labelled Android app, beside the iOS app and
Apple Watch. A generated migration adds it to the reading_sessions check
constraint, achievements count Android reading toward Full Orbit and Two
Worlds, and the web charts draw it in a new --pill-android token at hue 95.
Wait for the requested SMIL fragment to resolve before falling back, and invalidate stale audio loads so overlapping starts cannot orphan playback.
Fixes#1477
Keep pagination and the selection popup idle until native selection gestures finish, including cancelled touches.
Cover paginated and scrolled flows while retaining ordinary swipe snapping.
Closes#1487
The new assertions read `apiMock.mock.calls[0][1].body` straight off an `unknown`, which vitest
runs happily and `vue-tsc` rejects. Read them through one typed helper instead.
Every settings write from the reader PUT the whole object: the defaults write sent all twenty-one
EPUB fields and the per-book write sent the entire accumulated delta. PUT replaces the stored row,
so a write from this client threw away whatever another one had set since the page loaded.
That is a real conflict now rather than a theoretical one. The iOS reader owns the look fields and
keeps the layout fields on the device, so it writes a small subset and leaves the rest alone. A
full snapshot from a web tab reverted its look edits silently, and there was nothing either side
could do about it: last writer won the whole row.
Move the three writes to the PATCH routes, which merge field by field on the server, so two
clients editing different fields no longer race. An empty patch is dropped rather than sent,
because the server rejects an empty body.
Reset still deletes, on both routes: removing a customization is a whole-row operation and means
the same thing to every client.
Replace the hardcoded English in the TTS and narration components with
message keys, label every icon-only control, and add settings.common.nav.tts,
which the settings sidebar rendered as a raw key.
Stop requiring PODCAST_ENCRYPTION_KEY while APP_FEATURES.podcasts is off, and
skip podcast routes and permissions in the authorization matrix for the same
reason.
Pass the direct-download settle() helper the SETTLE_MS budget the file already
defines: vi.waitFor's one-second default cannot cover six DNS-resolved redirect
hops on a cold CI resolver.
Both failures broke Tests (Client) on main. reka-ui tooltip parts inject a provider
context that only App.vue supplies, so any spec mounting a subtree threw; the tooltip
primitive now stands in its own provider when none is in scope. The foliate click
handler's final else had no isMobile guard, letting a desktop middle-zone tap toggle
the reader chrome. Also record the v3.0.0 server floor beside the App Store badge.
Add an App Store link and an Android placeholder beside the existing
project links, and regroup the footer so support and client links sit
on opposite sides of the version label. Size the Android mark larger
than the Apple one because its head fills only 56% of its viewBox.
* feat(settings): add SABnzbd download client
Support API-key authentication, NZB uploads, queue monitoring, reconciliation, and completed-download imports through SABnzbd.
Expose the client in request settings and validate its required category and credentials.
Ref #1312
* feat(settings): add Prowlarr indexer management
Manage synchronized torrent and Usenet sources through a shared Prowlarr connection.
Ref #1312
* fix(settings): correct SABnzbd adoption and managed search retries
Address four defects found in review of the Prowlarr and SABnzbd work.
SABnzbd keeps a failed job in its history until somebody clears it, and BookOrbit removes a client
download only on an explicit action. Because the client key is a stable digest, re-grabbing the same
release adopted that dead history entry, never uploaded the NZB, and failed instantly and forever.
Adopt only an entry that is not already failed; a queue entry still wins, so a fresh upload reports
correctly.
The category-expansion retry reused the title-and-author query, making it strictly narrower on text
than the title-only attempt that had just returned nothing. A release matching only a title-only
search with expanded categories was therefore unreachable. Drop the author there so each rung of the
ladder is genuinely broader than the last.
Changing the download client type through the create form's select bypassed the picker's handler, so
a username typed for qBittorrent was still sent when SABnzbd was chosen. The server stores it, the
API-key form hides it, and it can no longer be edited away. Watch the credential kind instead, so
both controls clear it.
Finally, a swallowed managed-release refresh failure left nothing behind at all; give it its own
fail log while still reporting the original refusal to the caller.
* fix(requests): cast managed search health flags to boolean
The bulk search-health update bound each case branch as an untyped
parameter, so Postgres could not resolve it against the boolean column.
Cast every branch explicitly and assert the cast in the repository test.
* fix(requests): close review gaps in Prowlarr and SABnzbd support
Inventory the eight indexer-manager routes so the authorization matrix
probes them, resolve a managed indexer without reading its manager's
whole source list, keep an unreadable credential out of the test-failed
mapping, translate manager error codes, and show per-source search
health. Documents Prowlarr connections in docs/BOOK_REQUESTS.md.
* i18n(client): keep the retired sources empty-state key
Dropping bodyOneWay from en.json forced pruning it from five Crowdin
target catalogs, which the localization policy check rejects outright.
Leave the now-unused key in place; nothing validates unreferenced
English source keys, so Crowdin can retire it on its own schedule.
* db: regenerate the SABnzbd and Prowlarr migration onto main
Rebasing collided with main's own 0091 and 0092, which left this branch's
two migrations on disk with no journal entry referencing them. Drop both
and regenerate a single migration on main's lineage; the three affected
tables are schema-identical to the pre-rebase snapshot.
Sort groups by reclaimable space, pre-pick a keeper with a rule that states
its reasoning, and expand any row into a field-by-field comparison that shows
only what differs. Persist "not duplicates" so a judgement survives a rescan,
and review every copy before deleting. Reading progress no longer renders 0%.
Lift the bulk rename empty state out of the vertical centre of the
tools pane so it sits near the top instead of floating in the void.
Replace the hardcoded Cmd in the settings search hint with a shared
useModifierKey composable, so Windows and Linux see Ctrl+K. The
translated hint now takes the combo as a placeholder.
Reserve the clear button's 32px of right padding only while it renders,
which leaves the placeholder 124px instead of 102px at the default
sidebar width. Add text-ellipsis so longer translations end in an
ellipsis rather than a sliced glyph, and hide the native WebKit search
cancel button that doubled up with our own.
Add EPUB narration, shared media controls, and native device session integration.
Apply the generated migrations and update all first-party clients alongside the server.
BREAKING CHANGE: introduce incompatible database and API contracts for media playback
Keep unrelated folder selections in the order users add them, and make API reads deterministic so web and iOS defaults use the same first folder.
Fixes#1412
Route touch selection through EmbedPDF pointer mode while keeping swipe pagination limited to pan mode. Keep NestJS testing aligned with the application major and prevent incompatible standalone major updates.
Fixes#1388
Add configurable seed ratio and seed time goals to request indexers, supporting both static goals and tracker-advertised ratio/seed-time limits via torznab attributes. Enforce indexer-specific seed policy during request fulfillment and display active seed targets in the request seed panel.
Closes#1360
Add an in-app update service and UI controls for installed indexer plugins, verifying releases and fetching updates securely. Introduce configurable release profiles for fine-tuning score weights, format preferences, and rejection criteria in book request automation.
Support Usenet indexers via the built-in Newznab adapter and Usenet downloads via NZBGet. Generalize active download tracking from torrent client hashes to protocol-agnostic client keys, add NZB release scoring and file handling, and expose Newznab indexer and NZBGet client options in settings and grab flows.
Closes#1312