Repository navigation
v0.48.0: every open issue and pull request, in one release - #110
Merged
Merged
Conversation
…ght to left (#102) `seriesDto` answered `readingDirection: 'WEBTOON'` for every series, so the reader's "Series default" never laid a page out right to left, a right-to-left double spread was never put back together, a downloaded chapter carried WEBTOON offline, and the Komga-compatible API reported every manga as a webtoon. All four read that one field; it is now the series' own. Stored (v0.48.0 block at the end of the DDL): `lib_series.reading_direction` (one of Komga's four, NULL = unknown, which still reads as WEBTOON) with `reading_direction_from` naming the evidence, and the admin's choice on `series_overrides.reading_direction`, which beats all of it like the other overrides. Learned from, most trusted first, and a weaker signal never overwrites a stronger one (learnDirection): - ComicInfo `<Manga>YesAndRightToLeft</Manga>`, read by every scan. `No` is deliberately not read as left to right: tools write it by default, and at the top rank it would pin a Japanese series left to right. - The source: MangaDex's `originalLanguage` (ja -> RIGHT_TO_LEFT, ko/zh -> WEBTOON), carried on `SourceSeries.readingDirection` and recorded when a series is added. - AniList's `countryOfOrigin`, from the art match every add and art job already makes (one more field in the same query). A title SEARCH answers with its best guess whatever it is asked -- a series called "No Direction" matched "Dear Green: Hitomi no Ounowa", from Japan -- so the country counts only when one of the entry's titles is the series' title (directionFromAniListMatch); a tracker link a person made is trusted. - Series added before any of this: the nightly repair's new eighth step, `directions`, asks MangaDex (100 ids a request) and AniList (50) about series nothing has spoken for, at most REPAIR_DIRECTIONS_MAX (500) each. Admins set it in Edit details ("Automatic -- Right to left, from the chapter files" says what automatic means and why) and on `PUT /api/admin/series/:id/meta`, where absent leaves it, null clears it, and an unknown value is a 400. `GET /api/series/:id` gives admins `detectedDirection`. Tests: directionSignals.test.ts (mappers, the title agreement, both batch lookups) and readingDirection.int.test.ts (scan, precedence, override, add, repair, and the reader/manifest/Komga surfaces), each guard proven by reintroducing its bug. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ht-to-left page turns cross chapters (#102) Pinned series settings swallowed the profile's direction. Since v0.46.0 (c575523, merging #95) any change of mode, theme or spread in the reader's sheet pinned `pagedDirection: n.pagedDirection` to the open title -- usually `series`, inherited from the profile -- so Profile -> Settings -> Reading direction never reached that title again, and its sheet showed "Series default" whatever the profile said. - seriesPinChange: a look change pins the look (mode, theme, spread) as before, and the direction only when the change WAS the direction, marked `directionChosen` so a deliberate "Series default" for one title survives a profile change. - normaliseSeriesPrefs, on every read of a title's memory: a stored `series` without the mark is what those builds wrote by accident and is ignored, locally and when it arrives from the account; the cleaned memory is what the next save pushes back. A stored `ltr`/`rtl` is kept -- it may be the default of the day, but it is also what choosing a direction for one title wrote then, the very workaround #102 describes. - withTitleLook: the reader's overlay of a title's memory, extracted so the whole chain has a unit test. Found while verifying the above in a browser: on a right-to-left track, turning a page as the next chapter was appended jumped fourteen pages into it (one page when only the Up Next card was added). Chrome keeps an in-flight smooth scroll's destination in physical pixels from the left, and an RTL track grows leftwards; a still track keeps its place. Reachable since v0.46.0 by choosing Right to left, and the default for every Japanese series once "Series default" works. The reader now appends to a right-to-left track only once it has been still for 250 ms (untilStill); left-to-right tracks are unchanged. web/test/e2e/walk48.mjs (npm run test:e2e:v048) drives all of it in Chrome against an instance of its own, with its fixtures written by `seed.py <lib> --rtl` so the main e2e library is untouched. USAGE.md describes the setting as it now behaves, the Edit details field and the repair step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"a chapter with a repeated credit page collapsed nothing" failed 3 full runs of 5 on 2026-09-25 while the
feature itself was fine. It was never the preference. Instrumented, the failing run's reader tab was a sign-in
page: no aria-labels at all, `junkPages: collapse` in localStorage, `{}` in /api/settings.
The session died a minute earlier. Puppeteer reloads the page whenever `isMobile` or `hasTouch` changes, and
`reload()` resolves on `load`, before the app boots. The app's first act is POST /auth/refresh, which rotates
the refresh cookie. The phone block switched back to 1440 and navigated at once, abandoning that request after
the server had rotated the token but before its Set-Cookie reached the jar. The jar kept the old token, the
60 s rotation grace (REFRESH_GRACE_MS) kept answering it without rotating, and the first refresh after that
minute was refused and cleared the cookie. Whichever tab booted next was signed out: the repeated-pages tab
(fail) or, a few seconds later, the deep link (recovered by ensureSignedIn, so the run passed). The same
rotation was lost in every run examined, passing or failing; pass or fail was a matter of seconds.
- setViewportSettled(): every isMobile/hasTouch switch waits until the refresh request the RELOADED document
sent has finished before anything navigates (not any refresh response: one the old document had in flight
can be answered after the reload commits). Driven alone, 8 switches left 7-8 of 16 refreshes unanswered and
the session gone 62 s later; settled (about 100 ms each), none, and still signed in.
- The check sets what it depends on instead of inheriting it: `junkPages: collapse` and the series' mode
`vertical` on the server AND in localStorage (the reader adopts /api/settings and the server copy wins),
written before the tab opens so the reader loads once, not twice (the second navigation could abandon the
first one's refresh the same way). A signed-out tab signs back in and says so instead of being judged.
- A failure now says what the reader was doing: signed out, mode, page, the preference it saw, the labels.
The assertion is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /auth/refresh rotates yomi_rt. When the browser abandons that request after the server has rotated -- a reload or navigation while the boot refresh is in flight, a closed tab, a dropped response -- the jar keeps the superseded token. The grace window answered it for 60 s and then refused it, so the next refresh (a tab switch was enough) signed the device out. Reproduced in a headful Chrome with real keypresses: F5 twice 200 ms apart over a 200 ms round trip, or with DevTools' Slow 4G preset; not on a LAN. planRefresh judges a superseded token by what happened after it in its session's chain, using a new refresh_tokens.used_at (the holder presented it and got the next one): 1. chain ends in a revoked token -> refused, before the grace, so sign-out-everywhere and an admin revoke end a session at once; 2. superseded < 60 s ago -> the race answer, cookie untouched (as before); 3. superseded by a recovery before its holder used it, after the grace -> refused and the session ended: two jars hold one session; 4. a later token was used -> old token, refused (as before); 5. nothing later ever used, newest token past the grace -> recovered: the head is superseded by a new token, which is set. It inherits the head's expiry, so a stale token never extends the session. Rotation is now one statement (the insert happens only if the revoke did), so two concurrent refreshes can no longer both insert and fork a device into two live sessions. Logout walks forward from the presented token, so signing out from a jar that holds a superseded token ends the session too. refreshLost.int.test.ts mounts the real auth routes; 6 of its 7 subtests fail on main on their named assertions, and each of seven single mutations of the fix fails exactly its own subtest. refreshRace's "older than the grace window" test never presented the successor -- a lost answer -- so it now uses it first, keeping its intent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…string-sanitization) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#108 lets a token that was rotated away recover the session once nothing newer in it was ever used -- which is what keeps a device signed in when a refresh answer is lost. Unbounded, it also let a copy of an old cookie (a backed-up or synced browser profile) take the session over for as long as the real device stayed away. A lost answer is redeemed by the device's next refresh: minutes while the app is open, the next launch otherwise. REFRESH_RECOVER_MS caps the recovery at a day; past it the device signs in again, as it did before #108. Reintroduced by dropping the check: "a lost answer older than a day is not recovered" gets a 200. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dependabot #96 brought undici 8.11.0, which left gzip bodies undecoded for Node's built-in fetch over HTTP/2 -- every built-in source came back empty. 8.11.2 passes bff/test/globalFetch.test.ts and MangaDex answers normally with it loaded (24 latest, 7 search results), where 8.11.0 still fails the test. Also the rest of #96's group: @fastify/swagger 9.9.0, tsx 4.23.15. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two Unraid installs reported chapters on disk that never reached the library:
a Fetch showed "Fetching 1 chapters" for a second and added nothing, a
series added from Discover downloaded but never appeared, a manual Library
Scan changed nothing, and nothing was logged -- while the same files moved
into the library folder were picked up at once.
persistScan ran every folder in one pass with no isolation, and every caller
swallowed its throw (`persistScan().catch(() => {})`). One folder it could not
index ended the pass on every run, and the downloads root is walked second,
so every chapter this server fetched stayed unindexed. A Fetch then found the
file already there, skipped it, and ended `done` in a second.
- persistScan steps over a folder that throws (its transaction rolls back),
logs it, and reports it; Admin -> Health gets a Library scan check naming
each folder and the scanner's reason.
- A NUL in a ComicInfo field is one real way to get there: Postgres refuses
it in any text value, and the downloader copied a source's description into
the file verbatim. XML-forbidden control characters are stripped on read
and on write.
- A folder with a deleted twin in another library goes to the live row: the
lookup was unordered, so the deleted one could come back first and the
folder was skipped for good.
- A Fetch whose chapter is on disk but still not in the library after the
scan says so on its card instead of ending quietly.
- Scan errors are logged at every call site.
Each guard was checked by reintroducing its bug: scanResilience.int.test.ts
fails the named test every time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#101) @Squeaks72's proposal. Admin -> Health only speaks to somebody who already decided to open it, and what it finds -- a source blocked for a week, chapters failing their retries, folders the scan cannot index -- goes unnoticed for exactly that reason. - While the last report found something, an admin's top bar shows a mark beside the Updates bell (amber, red for a problem) whose tooltip is the worst check's own sentence. - A one-line banner says it once per finding set, at every screen size, with Take a look / Not now. The key is WHICH checks found something and how badly, so a count moving inside one check does not bring it back; never on the admin console itself. - The header never runs the ten checks: GET /api/admin/health/summary answers from server_settings.health_summary, written by the Health route whenever it runs and by the server every six hours. - Admins only: the route is behind requireAdmin, and nothing renders for other accounts. Also moves #108's refresh_tokens.used_at into the v0.48.0 migration block, at the end of the DDL where each release's columns go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Squeaks72's proposal, built as its own first slice: one menu primitive, the series card menu, and the series page's two hand-rolled ⋯ popovers moved onto it as its first callers. - components/ContextMenu.tsx: one menu opened four ways -- right-click, a press-and-hold on a touchscreen (with a movement threshold, and the click its release would send swallowed), the ⋯ button, Shift+F10 / the Menu key. Portalled to <body>, flips at the viewport edges, closes on scroll, resize and a press elsewhere; role="menu" with arrow keys, Home/End, and Escape back to where it was opened from. An open menu counts as modal to type-to-search. - A series card, wherever it appears: Open in a new tab and Copy link (the cards are links, so these stand in for the browser's menu), Favourite, Mark all read / unread through the Library's bulk routes, and for an admin Check for new chapters. Greyed offline; offered by role. - The browser's menu is left alone on selected text, in a text field and with Shift held; Profile -> Settings -> Appearance -> Right-click menus turns them off per device. Answers to the proposal's questions: right-click is taken only where the browser's menu offers nothing worth keeping; the menus are short; Select mode and the reader's pages are left for a follow-up (the reader's taps and zoom already share that surface). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Downloads that never reached the library (#109), a direction for every series so "Series default" can read right to left (#102, #105), right-click menus (#100), an admin who is told when the library needs attention (#101), a device that is no longer signed out after a lost refresh answer (#108, capped at a day), the e2e flake fixed at its cause (#107), and undici 8.11.2 in place of the 8.11.0 that #96 held back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every open issue and pull request as of 2026-09-26, in one release.
REFRESH_RECOVER_MS), so a copy of an old cookie cannot take a session over for as long as the real device stays away.used_atmoved into the v0.48.0 migration block.GET /api/admin/health/summaryreads a stored summary, never runs the checks; six-hourly server run.ContextMenuprimitive, series card menu, the series page's two ⋯ popovers moved onto it. Per-device switch.globalFetch.test.tspasses, MangaDex answers) + the group's swagger/tsx bumps.Verified locally: bff full suite 1953/1954 (the one: a guard asking for the new scheduler to be listed — done in d66acc9), web 508/508, browser e2e 72/72 with no dropped sessions, #105's walk48 24/24, a v0.48.0 walk of #109→#101 and #100 in Chrome (menus by right-click, keyboard, touch; banner and marker), and v0.47.1 starting on the upgraded database. Each new guard was checked by reintroducing its bug.
Merged by fast-forward push once green; #105, #107 and #108 then show as merged.
🤖 Generated with Claude Code