[Perf Tracks] Prevent crash when accessing $$typeof - #35679
Merged
eps1lon merged 4 commits intoFeb 3, 2026
Merged
Conversation
eps1lon
commented
Feb 3, 2026
eps1lon
force-pushed
the
sebbie/02-03-_perf_tracks_skip_diffing_html-like_objects
branch
3 times, most recently
from
February 3, 2026 10:58
9f43236 to
f435c5c
Compare
eps1lon
force-pushed
the
sebbie/02-03-_perf_tracks_skip_diffing_html-like_objects
branch
from
February 3, 2026 10:59
f435c5c to
736819e
Compare
eps1lon
force-pushed
the
sebbie/02-03-_perf_tracks_skip_diffing_html-like_objects
branch
from
February 3, 2026 11:00
736819e to
45ac560
Compare
eps1lon
marked this pull request as ready for review
February 3, 2026 11:07
hoxyq
approved these changes
Feb 3, 2026
eps1lon
deleted the
sebbie/02-03-_perf_tracks_skip_diffing_html-like_objects
branch
February 3, 2026 16:54
This was referenced Feb 5, 2026
github-actions Bot
pushed a commit
to code/lib-react
that referenced
this pull request
Feb 9, 2026
DiffTrain build for [6853d7a](react@6853d7a)
github-actions Bot
pushed a commit
to code/lib-react
that referenced
this pull request
Feb 9, 2026
DiffTrain build for [6853d7a](react@6853d7a)
mbret
added a commit
to mbret/oboku
that referenced
this pull request
Jul 25, 2026
…n frames React 19.2's dev-only performance track deep-walks changed props and throws a SecurityError when it reaches the reader iframe's cross-origin window, crashing the commit (react/react#34840). Guard the prop walkers so unreadable values log as [inaccessible] instead of throwing. Fixed upstream by react/react#35679 (19.3 canary only); remove with `pnpm patch-remove react-dom@19.2.8` once on a release containing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mbret
added a commit
to mbret/oboku
that referenced
this pull request
Jul 25, 2026
* feat: upgrade @prose-reader/* to 1.332.0 (#503) * feat: upgrade @prose-reader/* to 1.332.0 Bump every @prose-reader/* dependency from 1.324.0 to 1.332.0 across web, api and archive-metadata, and migrate the web reader to the breaking API changes shipped in that range. Reader lifecycle (prose #229/#230/#231): - createReader() now takes the manifest (and optional cfi) at construction and mount(containerElement) is a one-shot DOM attachment; reader.load() is gone. useCreateReader now creates + mounts + destroys the reader in a single effect (destroy() is the true inverse of create + mount, so the effect is strict-mode safe), and useLoadReader is removed. - The restored reading location (cfi) is read through a live ref so later progress writes flowing back into the book query never destroy/recreate the reader. - reader.state$ ("idle"|"ready") is replaced by reader.mounted$; the book loading overlay now keys off !mounted. Pin react/react-dom to 19.1.8 in the web app only: - react-dom 19.2 dev builds recursively read component props and access $$typeof on every object, throwing SecurityError on our gapi cross-origin iframe (react/react#34840). The uncaught throw aborts passive-effect mounts (dead reader quick-menu) and corrupts the work loop (crash on back navigation). Prod is unaffected; dev is unusable. - 19.1.x predates the offending logger and the web app uses no 19.2-only APIs. admin/landing stay on 19.2.x (@Mantine 9.4 needs useEffectEvent), so the pin is web-only, held together by resolve.dedupe in vite.config. - Documented in AGENTS.md with removal steps for when the react bug is fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(reader): read book once via enabled gate instead of useLiveRef The enabled predicate already stops observing after the first result, so progress-sync writes to the same book document never change the value. Drop the redundant useLiveRef, read bookOnce directly, and name the enabled predicate to state the intent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: unpin apps/web react and bump react to 19.2.7 project-wide apps/web was pinned to react 19.1.8 to dodge a dev-only React 19.2 logger crash with the gapi iframe. The pin diverged web from the rest of the repo and left @types on 19.2.x, creating a runtime/types skew. Align everything to the latest 19.2.7 (react/react-dom) and 19.2.17 (@types/react) across web, admin and landing. Remove the now-unneeded scaffolding: the vite resolve.dedupe entry and the AGENTS.md "React version pin" section. The dev-only reader crash returns at parity with develop/master; production is unaffected (logger is dev-only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * chore(deps): bump @nestjs/platform-express to 11.1.28 to fix multer DoS (#507) Updates @nestjs/platform-express 11.1.27 -> 11.1.28, which pulls multer 2.1.1 -> 2.2.0 (multer is consumed only by platform-express). Resolves the two multer denial-of-service advisories on the API file-upload path: - GHSA-72gw-mp4g-v24j (high): DoS via deeply nested field names - GHSA-3p4h-7m6x-2hcm (moderate): DoS via incomplete cleanup of aborted uploads Claude-Session: https://claude.ai/code/session_01WKoPAepDc2xhSFFDhHTHe5 Co-authored-by: Claude <noreply@anthropic.com> * fix: lock * fix: biome * fix: types * feat: migrate to pnpm * fix: dedupe * fix(deps): patch react-dom 19.2.8 dev perf-track crash on cross-origin frames React 19.2's dev-only performance track deep-walks changed props and throws a SecurityError when it reaches the reader iframe's cross-origin window, crashing the commit (react/react#34840). Guard the prop walkers so unreadable values log as [inaccessible] instead of throwing. Fixed upstream by react/react#35679 (19.3 canary only); remove with `pnpm patch-remove react-dom@19.2.8` once on a release containing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dev-proxy): stop 502 bursts from unreachable IPv6 host.docker.internal peer Docker Desktop writes both IPv4 and IPv6 host-gateway entries, so nginx expands host.docker.internal into two peers; the IPv6 one is unreachable from the IPv4-only compose network and its failure accounting could mark the whole implicit upstream down (no live upstreams -> 502 bursts during replication). An explicit upstream with max_fails=0 lets a failed connect fall through to the reachable address instead of disabling the group. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vercel): bootstrap pnpm 11 in install command to match lockfile Vercel's lockfile heuristic picks pnpm 9, which can't read patchedDependencies from pnpm-workspace.yaml and fails with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. The bootstrapped pnpm 11 defers to the packageManager pin in package.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vercel): invoke bootstrapped pnpm by absolute path Vercel prepends its detected pnpm 9 to PATH, shadowing the npm-installed pnpm 11, so the install still hit ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
mbret
added a commit
to mbret/oboku
that referenced
this pull request
Jul 26, 2026
* feat: upgrade @prose-reader/* to 1.332.0 (#503) * feat: upgrade @prose-reader/* to 1.332.0 Bump every @prose-reader/* dependency from 1.324.0 to 1.332.0 across web, api and archive-metadata, and migrate the web reader to the breaking API changes shipped in that range. Reader lifecycle (prose #229/#230/#231): - createReader() now takes the manifest (and optional cfi) at construction and mount(containerElement) is a one-shot DOM attachment; reader.load() is gone. useCreateReader now creates + mounts + destroys the reader in a single effect (destroy() is the true inverse of create + mount, so the effect is strict-mode safe), and useLoadReader is removed. - The restored reading location (cfi) is read through a live ref so later progress writes flowing back into the book query never destroy/recreate the reader. - reader.state$ ("idle"|"ready") is replaced by reader.mounted$; the book loading overlay now keys off !mounted. Pin react/react-dom to 19.1.8 in the web app only: - react-dom 19.2 dev builds recursively read component props and access $$typeof on every object, throwing SecurityError on our gapi cross-origin iframe (react/react#34840). The uncaught throw aborts passive-effect mounts (dead reader quick-menu) and corrupts the work loop (crash on back navigation). Prod is unaffected; dev is unusable. - 19.1.x predates the offending logger and the web app uses no 19.2-only APIs. admin/landing stay on 19.2.x (@Mantine 9.4 needs useEffectEvent), so the pin is web-only, held together by resolve.dedupe in vite.config. - Documented in AGENTS.md with removal steps for when the react bug is fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(reader): read book once via enabled gate instead of useLiveRef The enabled predicate already stops observing after the first result, so progress-sync writes to the same book document never change the value. Drop the redundant useLiveRef, read bookOnce directly, and name the enabled predicate to state the intent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: unpin apps/web react and bump react to 19.2.7 project-wide apps/web was pinned to react 19.1.8 to dodge a dev-only React 19.2 logger crash with the gapi iframe. The pin diverged web from the rest of the repo and left @types on 19.2.x, creating a runtime/types skew. Align everything to the latest 19.2.7 (react/react-dom) and 19.2.17 (@types/react) across web, admin and landing. Remove the now-unneeded scaffolding: the vite resolve.dedupe entry and the AGENTS.md "React version pin" section. The dev-only reader crash returns at parity with develop/master; production is unaffected (logger is dev-only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * chore(deps): bump @nestjs/platform-express to 11.1.28 to fix multer DoS (#507) Updates @nestjs/platform-express 11.1.27 -> 11.1.28, which pulls multer 2.1.1 -> 2.2.0 (multer is consumed only by platform-express). Resolves the two multer denial-of-service advisories on the API file-upload path: - GHSA-72gw-mp4g-v24j (high): DoS via deeply nested field names - GHSA-3p4h-7m6x-2hcm (moderate): DoS via incomplete cleanup of aborted uploads Claude-Session: https://claude.ai/code/session_01WKoPAepDc2xhSFFDhHTHe5 Co-authored-by: Claude <noreply@anthropic.com> * fix: lock * fix: biome * fix: types * feat: migrate to pnpm * fix: dedupe * fix(deps): patch react-dom 19.2.8 dev perf-track crash on cross-origin frames React 19.2's dev-only performance track deep-walks changed props and throws a SecurityError when it reaches the reader iframe's cross-origin window, crashing the commit (react/react#34840). Guard the prop walkers so unreadable values log as [inaccessible] instead of throwing. Fixed upstream by react/react#35679 (19.3 canary only); remove with `pnpm patch-remove react-dom@19.2.8` once on a release containing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dev-proxy): stop 502 bursts from unreachable IPv6 host.docker.internal peer Docker Desktop writes both IPv4 and IPv6 host-gateway entries, so nginx expands host.docker.internal into two peers; the IPv6 one is unreachable from the IPv4-only compose network and its failure accounting could mark the whole implicit upstream down (no live upstreams -> 502 bursts during replication). An explicit upstream with max_fails=0 lets a failed connect fall through to the reachable address instead of disabling the group. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vercel): bootstrap pnpm 11 in install command to match lockfile Vercel's lockfile heuristic picks pnpm 9, which can't read patchedDependencies from pnpm-workspace.yaml and fails with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. The bootstrapped pnpm 11 defers to the packageManager pin in package.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vercel): invoke bootstrapped pnpm by absolute path Vercel prepends its detected pnpm 9 to PATH, shadowing the npm-installed pnpm 11, so the install still hit ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(docker): copy patches into image for frozen install pnpm-workspace.yaml references patches/react-dom@19.2.8.patch, so the frozen install in the base stage fails with ENOENT without it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(vercel): set node 24 via engines for web and landing engines.node overrides the dashboard Node version; >=24 resolves to the latest 24.x on Vercel while staying satisfied by the node 25 used in CI, docker and local shells. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): declare express as a direct dependency main.ts value-imports json/urlencoded from express, which pnpm's isolated node_modules can't resolve through @nestjs/platform-express; dist/main crashed with MODULE_NOT_FOUND on a clean install. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(landing): ignore vercel cli .env.local Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reader): freeze first book result to keep reader mounted useBook is a live rxdb-backed query and the book finished dialog keeps an always-on observer of the same key, so the disabled observer in useCreateReader still received every progress-sync cache update and the bookOnce effect dependency remounted the reader on each write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): derive go-back availability from the router history index (#514) The back arrow fell through to home whenever a screen had performed a replace-only navigation, such as switching tab on the book optimize screen. react-router rebuilds the whole history state object on replace, so the `__obokuCanGoBack` flag was dropped, and TrackHistoryCanGoBack only re-stamped it on pathname changes. react-router already tracks the entry position under `history.state.idx`, preserved across replace, which removes the need for a custom flag, the `__obokuFallbackBack` marker and the tracker component. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
mbret
added a commit
to mbret/oboku
that referenced
this pull request
Jul 26, 2026
* feat: upgrade @prose-reader/* to 1.332.0 (#503) * feat: upgrade @prose-reader/* to 1.332.0 Bump every @prose-reader/* dependency from 1.324.0 to 1.332.0 across web, api and archive-metadata, and migrate the web reader to the breaking API changes shipped in that range. Reader lifecycle (prose #229/#230/#231): - createReader() now takes the manifest (and optional cfi) at construction and mount(containerElement) is a one-shot DOM attachment; reader.load() is gone. useCreateReader now creates + mounts + destroys the reader in a single effect (destroy() is the true inverse of create + mount, so the effect is strict-mode safe), and useLoadReader is removed. - The restored reading location (cfi) is read through a live ref so later progress writes flowing back into the book query never destroy/recreate the reader. - reader.state$ ("idle"|"ready") is replaced by reader.mounted$; the book loading overlay now keys off !mounted. Pin react/react-dom to 19.1.8 in the web app only: - react-dom 19.2 dev builds recursively read component props and access $$typeof on every object, throwing SecurityError on our gapi cross-origin iframe (react/react#34840). The uncaught throw aborts passive-effect mounts (dead reader quick-menu) and corrupts the work loop (crash on back navigation). Prod is unaffected; dev is unusable. - 19.1.x predates the offending logger and the web app uses no 19.2-only APIs. admin/landing stay on 19.2.x (@Mantine 9.4 needs useEffectEvent), so the pin is web-only, held together by resolve.dedupe in vite.config. - Documented in AGENTS.md with removal steps for when the react bug is fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(reader): read book once via enabled gate instead of useLiveRef The enabled predicate already stops observing after the first result, so progress-sync writes to the same book document never change the value. Drop the redundant useLiveRef, read bookOnce directly, and name the enabled predicate to state the intent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: unpin apps/web react and bump react to 19.2.7 project-wide apps/web was pinned to react 19.1.8 to dodge a dev-only React 19.2 logger crash with the gapi iframe. The pin diverged web from the rest of the repo and left @types on 19.2.x, creating a runtime/types skew. Align everything to the latest 19.2.7 (react/react-dom) and 19.2.17 (@types/react) across web, admin and landing. Remove the now-unneeded scaffolding: the vite resolve.dedupe entry and the AGENTS.md "React version pin" section. The dev-only reader crash returns at parity with develop/master; production is unaffected (logger is dev-only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * chore(deps): bump @nestjs/platform-express to 11.1.28 to fix multer DoS (#507) Updates @nestjs/platform-express 11.1.27 -> 11.1.28, which pulls multer 2.1.1 -> 2.2.0 (multer is consumed only by platform-express). Resolves the two multer denial-of-service advisories on the API file-upload path: - GHSA-72gw-mp4g-v24j (high): DoS via deeply nested field names - GHSA-3p4h-7m6x-2hcm (moderate): DoS via incomplete cleanup of aborted uploads Claude-Session: https://claude.ai/code/session_01WKoPAepDc2xhSFFDhHTHe5 Co-authored-by: Claude <noreply@anthropic.com> * fix: lock * fix: biome * fix: types * feat: migrate to pnpm * fix: dedupe * fix(deps): patch react-dom 19.2.8 dev perf-track crash on cross-origin frames React 19.2's dev-only performance track deep-walks changed props and throws a SecurityError when it reaches the reader iframe's cross-origin window, crashing the commit (react/react#34840). Guard the prop walkers so unreadable values log as [inaccessible] instead of throwing. Fixed upstream by react/react#35679 (19.3 canary only); remove with `pnpm patch-remove react-dom@19.2.8` once on a release containing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dev-proxy): stop 502 bursts from unreachable IPv6 host.docker.internal peer Docker Desktop writes both IPv4 and IPv6 host-gateway entries, so nginx expands host.docker.internal into two peers; the IPv6 one is unreachable from the IPv4-only compose network and its failure accounting could mark the whole implicit upstream down (no live upstreams -> 502 bursts during replication). An explicit upstream with max_fails=0 lets a failed connect fall through to the reachable address instead of disabling the group. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vercel): bootstrap pnpm 11 in install command to match lockfile Vercel's lockfile heuristic picks pnpm 9, which can't read patchedDependencies from pnpm-workspace.yaml and fails with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. The bootstrapped pnpm 11 defers to the packageManager pin in package.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vercel): invoke bootstrapped pnpm by absolute path Vercel prepends its detected pnpm 9 to PATH, shadowing the npm-installed pnpm 11, so the install still hit ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(docker): copy patches into image for frozen install pnpm-workspace.yaml references patches/react-dom@19.2.8.patch, so the frozen install in the base stage fails with ENOENT without it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(vercel): set node 24 via engines for web and landing engines.node overrides the dashboard Node version; >=24 resolves to the latest 24.x on Vercel while staying satisfied by the node 25 used in CI, docker and local shells. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): declare express as a direct dependency main.ts value-imports json/urlencoded from express, which pnpm's isolated node_modules can't resolve through @nestjs/platform-express; dist/main crashed with MODULE_NOT_FOUND on a clean install. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(landing): ignore vercel cli .env.local Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reader): freeze first book result to keep reader mounted useBook is a live rxdb-backed query and the book finished dialog keeps an always-on observer of the same key, so the disabled observer in useCreateReader still received every progress-sync cache update and the bookOnce effect dependency remounted the reader on each write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): derive go-back availability from the router history index (#514) The back arrow fell through to home whenever a screen had performed a replace-only navigation, such as switching tab on the book optimize screen. react-router rebuilds the whole history state object on replace, so the `__obokuCanGoBack` flag was dropped, and TrackHistoryCanGoBack only re-stamped it on pathname changes. react-router already tracks the entry position under `history.state.idx`, preserved across replace, which removes the need for a custom flag, the `__obokuFallbackBack` marker and the tracker component. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * feat(web): generalize first-result snapshot into useEnsureQueryData$ Extracts createBookQueryOptions alongside the existing createLinkQueryOptions pattern and rebuilds the reader's frozen book snapshot on top of a reusable hook that resolves a query's first result and ignores later cache updates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): rebuild useEnsureQueryData$ on an ensureQueryData$ mechanism Splits the one-shot idea into an observable flavour of queryClient.ensureQueryData (cache read, else first emission seeding the empty cache, never touching the tanstack fetch machinery) and a thin snapshot hook on top. Drops the inert observer, so consumers no longer re-render on post-capture cache writes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): memoize useEnsureQueryData$ snapshot in a private query Replaces the effect+state capture with a vanilla useQuery over a prefix-namespaced key whose queryFn is ensureQueryData$: staleTime Infinity freezes the snapshot, gcTime 0 scopes it to the consumers' lifetime and concurrent consumers dedupe into a single resolution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): default useEnsureQueryData$ to networkMode always The private useQuery dropped the source query's networkMode, so offline the default online mode paused resolution entirely — blocking even the warm-cache read and leaving locally available books on the loading screen. Resolution is cache-first, so always is the correct default; network-bound sources can pass online explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
mbret
added a commit
to mbret/oboku
that referenced
this pull request
Jul 26, 2026
* feat: upgrade @prose-reader/* to 1.332.0 (#503) * feat: upgrade @prose-reader/* to 1.332.0 Bump every @prose-reader/* dependency from 1.324.0 to 1.332.0 across web, api and archive-metadata, and migrate the web reader to the breaking API changes shipped in that range. Reader lifecycle (prose #229/#230/#231): - createReader() now takes the manifest (and optional cfi) at construction and mount(containerElement) is a one-shot DOM attachment; reader.load() is gone. useCreateReader now creates + mounts + destroys the reader in a single effect (destroy() is the true inverse of create + mount, so the effect is strict-mode safe), and useLoadReader is removed. - The restored reading location (cfi) is read through a live ref so later progress writes flowing back into the book query never destroy/recreate the reader. - reader.state$ ("idle"|"ready") is replaced by reader.mounted$; the book loading overlay now keys off !mounted. Pin react/react-dom to 19.1.8 in the web app only: - react-dom 19.2 dev builds recursively read component props and access $$typeof on every object, throwing SecurityError on our gapi cross-origin iframe (react/react#34840). The uncaught throw aborts passive-effect mounts (dead reader quick-menu) and corrupts the work loop (crash on back navigation). Prod is unaffected; dev is unusable. - 19.1.x predates the offending logger and the web app uses no 19.2-only APIs. admin/landing stay on 19.2.x (@Mantine 9.4 needs useEffectEvent), so the pin is web-only, held together by resolve.dedupe in vite.config. - Documented in AGENTS.md with removal steps for when the react bug is fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(reader): read book once via enabled gate instead of useLiveRef The enabled predicate already stops observing after the first result, so progress-sync writes to the same book document never change the value. Drop the redundant useLiveRef, read bookOnce directly, and name the enabled predicate to state the intent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: unpin apps/web react and bump react to 19.2.7 project-wide apps/web was pinned to react 19.1.8 to dodge a dev-only React 19.2 logger crash with the gapi iframe. The pin diverged web from the rest of the repo and left @types on 19.2.x, creating a runtime/types skew. Align everything to the latest 19.2.7 (react/react-dom) and 19.2.17 (@types/react) across web, admin and landing. Remove the now-unneeded scaffolding: the vite resolve.dedupe entry and the AGENTS.md "React version pin" section. The dev-only reader crash returns at parity with develop/master; production is unaffected (logger is dev-only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * chore(deps): bump @nestjs/platform-express to 11.1.28 to fix multer DoS (#507) Updates @nestjs/platform-express 11.1.27 -> 11.1.28, which pulls multer 2.1.1 -> 2.2.0 (multer is consumed only by platform-express). Resolves the two multer denial-of-service advisories on the API file-upload path: - GHSA-72gw-mp4g-v24j (high): DoS via deeply nested field names - GHSA-3p4h-7m6x-2hcm (moderate): DoS via incomplete cleanup of aborted uploads Claude-Session: https://claude.ai/code/session_01WKoPAepDc2xhSFFDhHTHe5 Co-authored-by: Claude <noreply@anthropic.com> * fix: lock * fix: biome * fix: types * feat: migrate to pnpm * fix: dedupe * fix(deps): patch react-dom 19.2.8 dev perf-track crash on cross-origin frames React 19.2's dev-only performance track deep-walks changed props and throws a SecurityError when it reaches the reader iframe's cross-origin window, crashing the commit (react/react#34840). Guard the prop walkers so unreadable values log as [inaccessible] instead of throwing. Fixed upstream by react/react#35679 (19.3 canary only); remove with `pnpm patch-remove react-dom@19.2.8` once on a release containing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dev-proxy): stop 502 bursts from unreachable IPv6 host.docker.internal peer Docker Desktop writes both IPv4 and IPv6 host-gateway entries, so nginx expands host.docker.internal into two peers; the IPv6 one is unreachable from the IPv4-only compose network and its failure accounting could mark the whole implicit upstream down (no live upstreams -> 502 bursts during replication). An explicit upstream with max_fails=0 lets a failed connect fall through to the reachable address instead of disabling the group. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vercel): bootstrap pnpm 11 in install command to match lockfile Vercel's lockfile heuristic picks pnpm 9, which can't read patchedDependencies from pnpm-workspace.yaml and fails with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. The bootstrapped pnpm 11 defers to the packageManager pin in package.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vercel): invoke bootstrapped pnpm by absolute path Vercel prepends its detected pnpm 9 to PATH, shadowing the npm-installed pnpm 11, so the install still hit ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(docker): copy patches into image for frozen install pnpm-workspace.yaml references patches/react-dom@19.2.8.patch, so the frozen install in the base stage fails with ENOENT without it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(vercel): set node 24 via engines for web and landing engines.node overrides the dashboard Node version; >=24 resolves to the latest 24.x on Vercel while staying satisfied by the node 25 used in CI, docker and local shells. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): declare express as a direct dependency main.ts value-imports json/urlencoded from express, which pnpm's isolated node_modules can't resolve through @nestjs/platform-express; dist/main crashed with MODULE_NOT_FOUND on a clean install. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(landing): ignore vercel cli .env.local Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reader): freeze first book result to keep reader mounted useBook is a live rxdb-backed query and the book finished dialog keeps an always-on observer of the same key, so the disabled observer in useCreateReader still received every progress-sync cache update and the bookOnce effect dependency remounted the reader on each write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): derive go-back availability from the router history index (#514) The back arrow fell through to home whenever a screen had performed a replace-only navigation, such as switching tab on the book optimize screen. react-router rebuilds the whole history state object on replace, so the `__obokuCanGoBack` flag was dropped, and TrackHistoryCanGoBack only re-stamped it on pathname changes. react-router already tracks the entry position under `history.state.idx`, preserved across replace, which removes the need for a custom flag, the `__obokuFallbackBack` marker and the tracker component. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * feat(web): generalize first-result snapshot into useEnsureQueryData$ Extracts createBookQueryOptions alongside the existing createLinkQueryOptions pattern and rebuilds the reader's frozen book snapshot on top of a reusable hook that resolves a query's first result and ignores later cache updates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): rebuild useEnsureQueryData$ on an ensureQueryData$ mechanism Splits the one-shot idea into an observable flavour of queryClient.ensureQueryData (cache read, else first emission seeding the empty cache, never touching the tanstack fetch machinery) and a thin snapshot hook on top. Drops the inert observer, so consumers no longer re-render on post-capture cache writes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): memoize useEnsureQueryData$ snapshot in a private query Replaces the effect+state capture with a vanilla useQuery over a prefix-namespaced key whose queryFn is ensureQueryData$: staleTime Infinity freezes the snapshot, gcTime 0 scopes it to the consumers' lifetime and concurrent consumers dedupe into a single resolution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): default useEnsureQueryData$ to networkMode always The private useQuery dropped the source query's networkMode, so offline the default online mode paused resolution entirely — blocking even the warm-cache read and leaving locally available books on the loading screen. Resolution is cache-first, so always is the correct default; network-bound sources can pass online explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): let users protect storage from browser eviction (#516) Origin storage is best-effort by default, so the browser may evict oboku's whole bucket under disk pressure — the RxDB library, the Dexie downloads and the covers cache together. Most of it re-syncs from CouchDB afterwards, but books uploaded straight from a device exist nowhere else and are lost for good. Nothing in the app ever called `navigator.storage.persist()`, so no user was protected. - `useIsStoragePersisted` / `useRequestStoragePersistence` under `storage/` - a row in Manage storage showing a green or red shield, tappable to request protection, with a "Read more" link to the new guide - an inbox notification while storage is evictable, pointing at that screen - `guides/storage.md` explaining what it is, why it matters and how to fix it The inbox notification is deliberately not dismissible: Chrome refuses the request for most users in a plain tab, and installing the app both satisfies its heuristics and silences the warning, so the nag and the fix are the same action. `LocalNotificationCard` hardcoded a login icon, which only worked while `session_expired` was the sole local notification; the icon now comes from the notification's action. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
mbret
added a commit
to mbret/oboku
that referenced
this pull request
Aug 1, 2026
* feat: upgrade @prose-reader/* to 1.332.0 (#503) * feat: upgrade @prose-reader/* to 1.332.0 Bump every @prose-reader/* dependency from 1.324.0 to 1.332.0 across web, api and archive-metadata, and migrate the web reader to the breaking API changes shipped in that range. Reader lifecycle (prose #229/#230/#231): - createReader() now takes the manifest (and optional cfi) at construction and mount(containerElement) is a one-shot DOM attachment; reader.load() is gone. useCreateReader now creates + mounts + destroys the reader in a single effect (destroy() is the true inverse of create + mount, so the effect is strict-mode safe), and useLoadReader is removed. - The restored reading location (cfi) is read through a live ref so later progress writes flowing back into the book query never destroy/recreate the reader. - reader.state$ ("idle"|"ready") is replaced by reader.mounted$; the book loading overlay now keys off !mounted. Pin react/react-dom to 19.1.8 in the web app only: - react-dom 19.2 dev builds recursively read component props and access $$typeof on every object, throwing SecurityError on our gapi cross-origin iframe (react/react#34840). The uncaught throw aborts passive-effect mounts (dead reader quick-menu) and corrupts the work loop (crash on back navigation). Prod is unaffected; dev is unusable. - 19.1.x predates the offending logger and the web app uses no 19.2-only APIs. admin/landing stay on 19.2.x (@Mantine 9.4 needs useEffectEvent), so the pin is web-only, held together by resolve.dedupe in vite.config. - Documented in AGENTS.md with removal steps for when the react bug is fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(reader): read book once via enabled gate instead of useLiveRef The enabled predicate already stops observing after the first result, so progress-sync writes to the same book document never change the value. Drop the redundant useLiveRef, read bookOnce directly, and name the enabled predicate to state the intent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: unpin apps/web react and bump react to 19.2.7 project-wide apps/web was pinned to react 19.1.8 to dodge a dev-only React 19.2 logger crash with the gapi iframe. The pin diverged web from the rest of the repo and left @types on 19.2.x, creating a runtime/types skew. Align everything to the latest 19.2.7 (react/react-dom) and 19.2.17 (@types/react) across web, admin and landing. Remove the now-unneeded scaffolding: the vite resolve.dedupe entry and the AGENTS.md "React version pin" section. The dev-only reader crash returns at parity with develop/master; production is unaffected (logger is dev-only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * chore(deps): bump @nestjs/platform-express to 11.1.28 to fix multer DoS (#507) Updates @nestjs/platform-express 11.1.27 -> 11.1.28, which pulls multer 2.1.1 -> 2.2.0 (multer is consumed only by platform-express). Resolves the two multer denial-of-service advisories on the API file-upload path: - GHSA-72gw-mp4g-v24j (high): DoS via deeply nested field names - GHSA-3p4h-7m6x-2hcm (moderate): DoS via incomplete cleanup of aborted uploads Claude-Session: https://claude.ai/code/session_01WKoPAepDc2xhSFFDhHTHe5 Co-authored-by: Claude <noreply@anthropic.com> * fix: lock * fix: biome * fix: types * feat: migrate to pnpm * fix: dedupe * fix(deps): patch react-dom 19.2.8 dev perf-track crash on cross-origin frames React 19.2's dev-only performance track deep-walks changed props and throws a SecurityError when it reaches the reader iframe's cross-origin window, crashing the commit (react/react#34840). Guard the prop walkers so unreadable values log as [inaccessible] instead of throwing. Fixed upstream by react/react#35679 (19.3 canary only); remove with `pnpm patch-remove react-dom@19.2.8` once on a release containing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dev-proxy): stop 502 bursts from unreachable IPv6 host.docker.internal peer Docker Desktop writes both IPv4 and IPv6 host-gateway entries, so nginx expands host.docker.internal into two peers; the IPv6 one is unreachable from the IPv4-only compose network and its failure accounting could mark the whole implicit upstream down (no live upstreams -> 502 bursts during replication). An explicit upstream with max_fails=0 lets a failed connect fall through to the reachable address instead of disabling the group. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vercel): bootstrap pnpm 11 in install command to match lockfile Vercel's lockfile heuristic picks pnpm 9, which can't read patchedDependencies from pnpm-workspace.yaml and fails with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. The bootstrapped pnpm 11 defers to the packageManager pin in package.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vercel): invoke bootstrapped pnpm by absolute path Vercel prepends its detected pnpm 9 to PATH, shadowing the npm-installed pnpm 11, so the install still hit ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(docker): copy patches into image for frozen install pnpm-workspace.yaml references patches/react-dom@19.2.8.patch, so the frozen install in the base stage fails with ENOENT without it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(vercel): set node 24 via engines for web and landing engines.node overrides the dashboard Node version; >=24 resolves to the latest 24.x on Vercel while staying satisfied by the node 25 used in CI, docker and local shells. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): declare express as a direct dependency main.ts value-imports json/urlencoded from express, which pnpm's isolated node_modules can't resolve through @nestjs/platform-express; dist/main crashed with MODULE_NOT_FOUND on a clean install. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(landing): ignore vercel cli .env.local Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reader): freeze first book result to keep reader mounted useBook is a live rxdb-backed query and the book finished dialog keeps an always-on observer of the same key, so the disabled observer in useCreateReader still received every progress-sync cache update and the bookOnce effect dependency remounted the reader on each write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): derive go-back availability from the router history index (#514) The back arrow fell through to home whenever a screen had performed a replace-only navigation, such as switching tab on the book optimize screen. react-router rebuilds the whole history state object on replace, so the `__obokuCanGoBack` flag was dropped, and TrackHistoryCanGoBack only re-stamped it on pathname changes. react-router already tracks the entry position under `history.state.idx`, preserved across replace, which removes the need for a custom flag, the `__obokuFallbackBack` marker and the tracker component. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * feat(web): generalize first-result snapshot into useEnsureQueryData$ Extracts createBookQueryOptions alongside the existing createLinkQueryOptions pattern and rebuilds the reader's frozen book snapshot on top of a reusable hook that resolves a query's first result and ignores later cache updates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): rebuild useEnsureQueryData$ on an ensureQueryData$ mechanism Splits the one-shot idea into an observable flavour of queryClient.ensureQueryData (cache read, else first emission seeding the empty cache, never touching the tanstack fetch machinery) and a thin snapshot hook on top. Drops the inert observer, so consumers no longer re-render on post-capture cache writes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): memoize useEnsureQueryData$ snapshot in a private query Replaces the effect+state capture with a vanilla useQuery over a prefix-namespaced key whose queryFn is ensureQueryData$: staleTime Infinity freezes the snapshot, gcTime 0 scopes it to the consumers' lifetime and concurrent consumers dedupe into a single resolution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): default useEnsureQueryData$ to networkMode always The private useQuery dropped the source query's networkMode, so offline the default online mode paused resolution entirely — blocking even the warm-cache read and leaving locally available books on the loading screen. Resolution is cache-first, so always is the correct default; network-bound sources can pass online explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): let users protect storage from browser eviction (#516) Origin storage is best-effort by default, so the browser may evict oboku's whole bucket under disk pressure — the RxDB library, the Dexie downloads and the covers cache together. Most of it re-syncs from CouchDB afterwards, but books uploaded straight from a device exist nowhere else and are lost for good. Nothing in the app ever called `navigator.storage.persist()`, so no user was protected. - `useIsStoragePersisted` / `useRequestStoragePersistence` under `storage/` - a row in Manage storage showing a green or red shield, tappable to request protection, with a "Read more" link to the new guide - an inbox notification while storage is evictable, pointing at that screen - `guides/storage.md` explaining what it is, why it matters and how to fix it The inbox notification is deliberately not dismissible: Chrome refuses the request for most users in a plain tab, and installing the app both satisfies its heuristics and silences the warning, so the nag and the fix are the same action. `LocalNotificationCard` hardcoded a login icon, which only worked while `session_expired` was the sole local notification; the icon now comes from the notification's action. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * fix(shared): match reader file extensions case-insensitively (#508) isFileSupported compared the raw extension against the lowercase READER_ACCEPTED_EXTENSIONS list, so files like Book.EPUB or comic.CBZ were reported as unsupported and skipped by synology-drive/webdav sync and greyed out in file browsers. Claude-Session: https://claude.ai/code/session_01DqCq7ako5cdiYdvpJAKgKJ Co-authored-by: Claude <noreply@anthropic.com> * refactor: consolidate duplicated web plugin helpers (#506) Unify the three byte-identical connector-based useRefreshMetadata hooks (server, webdav, synology-drive) behind a shared createConnectorRefreshMetadata factory in plugins/common, and remove the orphaned duplicate of the tokenValidity module that was superseded by the plugins/common copy every caller already imports. Claude-Session: https://claude.ai/code/session_01TznNj9nDaE3ezbXP9ssXTa Co-authored-by: Claude <noreply@anthropic.com> * refactor(web): consolidate incremental book mutation pipeline (#509) The three incremental book mutation hooks (useIncrementalBookModify, useIncrementalBookPatch, useIncrementalBookUpdate) each duplicated the same document-resolution pipeline: resolve a book doc from either an id or a document, guard against a missing document, then apply the rxdb incremental operation. Only the operation itself differed. Extract that shared pipeline into incrementalBookMutation and have each hook pass its operation as a callback. Public hook APIs are unchanged. Claude-Session: https://claude.ai/code/session_01MjDUJXpyhzCwpJJ73YcGdn Co-authored-by: Claude <noreply@anthropic.com> * fix(web): make the not-interested-only filter match not interested books (#522) * fix(web): make the not-interested-only filter match not interested books The isNotInterested: "only" branch in useCollections intersected collection.books with the bookIds query param instead of the computed notInterestedBookIds. The search screen never passes bookIds, so intersection(collection.books, undefined) degenerated to the full book list and the "Show not interested contents: Only" filter returned every non-empty collection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BKdyaYuEezwdEQ8XCJZdQ * refactor(web): colocate not-interested filter predicate in useCollections Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EveXE8CpZGCHmoSJwWHqdd --------- Co-authored-by: Claude <noreply@anthropic.com> * chore(deps): bump rxdb to 17.4.0 to fix ws DoS advisory (#511) rxdb 17.3.0 pinned ws@8.20.1, which is affected by GHSA-96hv-2xvq-fx4p (high-severity memory-exhaustion DoS). rxdb 17.4.0 requires ws@8.21.0, clearing the advisory in @oboku/web. Patch bump within the existing ^17 range; the lockfile also dedupes seven nested transitive copies that rxdb 17.4.0 no longer pins. Claude-Session: https://claude.ai/code/session_01KN1rjmw23NgWRyXVvuBiqu Co-authored-by: Claude <noreply@anthropic.com> * perf: compute book title sort key once per book in alpha sort (#510) The alpha branch of sortBooksBy called getMetadataFromBook twice inside the sort comparator, so an expensive metadata merge ran ~2·n·log(n) times per sort. Precompute each book's title once (n calls) via a decorate-sort-undecorate, turning the getMetadataFromBook cost from O(n·log n) into O(n). Output order is unchanged. Claude-Session: https://claude.ai/code/session_017dkihQVZGC8NhYJzQcdoav Co-authored-by: Claude <noreply@anthropic.com> * Upgrade workspace dependencies and read API book metadata through prose-reader (#524) * chore(deps): upgrade workspace dependencies Brings prose-reader to 1.334, plus MUI, Sentry, react-query, vite, workbox and the rest of the tree up to current. dexie is pinned to the exact version rxdb resolves: the two share a single IndexedDB connection and break when they drift. A postinstall check fails the install if they ever disagree again. The API reads book metadata through prose-reader's resolveArchive rather than assembling it from the OPF and ComicInfo documents itself, and reports the sources it could not parse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MQjPncXbBGcwudKbfo7Rph * refactor(archive-metadata): move the metadata writer out of the src root `patchArchiveMetadata` composes the ComicInfo and OPF container modules the same way those modules compose their own read/write halves, so it belongs beside them rather than at the package root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MQjPncXbBGcwudKbfo7Rph --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
mbret
added a commit
to mbret/oboku
that referenced
this pull request
Aug 1, 2026
* feat: upgrade @prose-reader/* to 1.332.0 (#503) * feat: upgrade @prose-reader/* to 1.332.0 Bump every @prose-reader/* dependency from 1.324.0 to 1.332.0 across web, api and archive-metadata, and migrate the web reader to the breaking API changes shipped in that range. Reader lifecycle (prose #229/#230/#231): - createReader() now takes the manifest (and optional cfi) at construction and mount(containerElement) is a one-shot DOM attachment; reader.load() is gone. useCreateReader now creates + mounts + destroys the reader in a single effect (destroy() is the true inverse of create + mount, so the effect is strict-mode safe), and useLoadReader is removed. - The restored reading location (cfi) is read through a live ref so later progress writes flowing back into the book query never destroy/recreate the reader. - reader.state$ ("idle"|"ready") is replaced by reader.mounted$; the book loading overlay now keys off !mounted. Pin react/react-dom to 19.1.8 in the web app only: - react-dom 19.2 dev builds recursively read component props and access $$typeof on every object, throwing SecurityError on our gapi cross-origin iframe (react/react#34840). The uncaught throw aborts passive-effect mounts (dead reader quick-menu) and corrupts the work loop (crash on back navigation). Prod is unaffected; dev is unusable. - 19.1.x predates the offending logger and the web app uses no 19.2-only APIs. admin/landing stay on 19.2.x (@Mantine 9.4 needs useEffectEvent), so the pin is web-only, held together by resolve.dedupe in vite.config. - Documented in AGENTS.md with removal steps for when the react bug is fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(reader): read book once via enabled gate instead of useLiveRef The enabled predicate already stops observing after the first result, so progress-sync writes to the same book document never change the value. Drop the redundant useLiveRef, read bookOnce directly, and name the enabled predicate to state the intent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: unpin apps/web react and bump react to 19.2.7 project-wide apps/web was pinned to react 19.1.8 to dodge a dev-only React 19.2 logger crash with the gapi iframe. The pin diverged web from the rest of the repo and left @types on 19.2.x, creating a runtime/types skew. Align everything to the latest 19.2.7 (react/react-dom) and 19.2.17 (@types/react) across web, admin and landing. Remove the now-unneeded scaffolding: the vite resolve.dedupe entry and the AGENTS.md "React version pin" section. The dev-only reader crash returns at parity with develop/master; production is unaffected (logger is dev-only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * chore(deps): bump @nestjs/platform-express to 11.1.28 to fix multer DoS (#507) Updates @nestjs/platform-express 11.1.27 -> 11.1.28, which pulls multer 2.1.1 -> 2.2.0 (multer is consumed only by platform-express). Resolves the two multer denial-of-service advisories on the API file-upload path: - GHSA-72gw-mp4g-v24j (high): DoS via deeply nested field names - GHSA-3p4h-7m6x-2hcm (moderate): DoS via incomplete cleanup of aborted uploads Claude-Session: https://claude.ai/code/session_01WKoPAepDc2xhSFFDhHTHe5 Co-authored-by: Claude <noreply@anthropic.com> * fix: lock * fix: biome * fix: types * feat: migrate to pnpm * fix: dedupe * fix(deps): patch react-dom 19.2.8 dev perf-track crash on cross-origin frames React 19.2's dev-only performance track deep-walks changed props and throws a SecurityError when it reaches the reader iframe's cross-origin window, crashing the commit (react/react#34840). Guard the prop walkers so unreadable values log as [inaccessible] instead of throwing. Fixed upstream by react/react#35679 (19.3 canary only); remove with `pnpm patch-remove react-dom@19.2.8` once on a release containing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dev-proxy): stop 502 bursts from unreachable IPv6 host.docker.internal peer Docker Desktop writes both IPv4 and IPv6 host-gateway entries, so nginx expands host.docker.internal into two peers; the IPv6 one is unreachable from the IPv4-only compose network and its failure accounting could mark the whole implicit upstream down (no live upstreams -> 502 bursts during replication). An explicit upstream with max_fails=0 lets a failed connect fall through to the reachable address instead of disabling the group. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vercel): bootstrap pnpm 11 in install command to match lockfile Vercel's lockfile heuristic picks pnpm 9, which can't read patchedDependencies from pnpm-workspace.yaml and fails with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. The bootstrapped pnpm 11 defers to the packageManager pin in package.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vercel): invoke bootstrapped pnpm by absolute path Vercel prepends its detected pnpm 9 to PATH, shadowing the npm-installed pnpm 11, so the install still hit ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(docker): copy patches into image for frozen install pnpm-workspace.yaml references patches/react-dom@19.2.8.patch, so the frozen install in the base stage fails with ENOENT without it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(vercel): set node 24 via engines for web and landing engines.node overrides the dashboard Node version; >=24 resolves to the latest 24.x on Vercel while staying satisfied by the node 25 used in CI, docker and local shells. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): declare express as a direct dependency main.ts value-imports json/urlencoded from express, which pnpm's isolated node_modules can't resolve through @nestjs/platform-express; dist/main crashed with MODULE_NOT_FOUND on a clean install. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(landing): ignore vercel cli .env.local Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reader): freeze first book result to keep reader mounted useBook is a live rxdb-backed query and the book finished dialog keeps an always-on observer of the same key, so the disabled observer in useCreateReader still received every progress-sync cache update and the bookOnce effect dependency remounted the reader on each write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): derive go-back availability from the router history index (#514) The back arrow fell through to home whenever a screen had performed a replace-only navigation, such as switching tab on the book optimize screen. react-router rebuilds the whole history state object on replace, so the `__obokuCanGoBack` flag was dropped, and TrackHistoryCanGoBack only re-stamped it on pathname changes. react-router already tracks the entry position under `history.state.idx`, preserved across replace, which removes the need for a custom flag, the `__obokuFallbackBack` marker and the tracker component. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * feat(web): generalize first-result snapshot into useEnsureQueryData$ Extracts createBookQueryOptions alongside the existing createLinkQueryOptions pattern and rebuilds the reader's frozen book snapshot on top of a reusable hook that resolves a query's first result and ignores later cache updates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): rebuild useEnsureQueryData$ on an ensureQueryData$ mechanism Splits the one-shot idea into an observable flavour of queryClient.ensureQueryData (cache read, else first emission seeding the empty cache, never touching the tanstack fetch machinery) and a thin snapshot hook on top. Drops the inert observer, so consumers no longer re-render on post-capture cache writes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): memoize useEnsureQueryData$ snapshot in a private query Replaces the effect+state capture with a vanilla useQuery over a prefix-namespaced key whose queryFn is ensureQueryData$: staleTime Infinity freezes the snapshot, gcTime 0 scopes it to the consumers' lifetime and concurrent consumers dedupe into a single resolution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): default useEnsureQueryData$ to networkMode always The private useQuery dropped the source query's networkMode, so offline the default online mode paused resolution entirely — blocking even the warm-cache read and leaving locally available books on the loading screen. Resolution is cache-first, so always is the correct default; network-bound sources can pass online explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): let users protect storage from browser eviction (#516) Origin storage is best-effort by default, so the browser may evict oboku's whole bucket under disk pressure — the RxDB library, the Dexie downloads and the covers cache together. Most of it re-syncs from CouchDB afterwards, but books uploaded straight from a device exist nowhere else and are lost for good. Nothing in the app ever called `navigator.storage.persist()`, so no user was protected. - `useIsStoragePersisted` / `useRequestStoragePersistence` under `storage/` - a row in Manage storage showing a green or red shield, tappable to request protection, with a "Read more" link to the new guide - an inbox notification while storage is evictable, pointing at that screen - `guides/storage.md` explaining what it is, why it matters and how to fix it The inbox notification is deliberately not dismissible: Chrome refuses the request for most users in a plain tab, and installing the app both satisfies its heuristics and silences the warning, so the nag and the fix are the same action. `LocalNotificationCard` hardcoded a login icon, which only worked while `session_expired` was the sole local notification; the icon now comes from the notification's action. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * fix(shared): match reader file extensions case-insensitively (#508) isFileSupported compared the raw extension against the lowercase READER_ACCEPTED_EXTENSIONS list, so files like Book.EPUB or comic.CBZ were reported as unsupported and skipped by synology-drive/webdav sync and greyed out in file browsers. Claude-Session: https://claude.ai/code/session_01DqCq7ako5cdiYdvpJAKgKJ Co-authored-by: Claude <noreply@anthropic.com> * refactor: consolidate duplicated web plugin helpers (#506) Unify the three byte-identical connector-based useRefreshMetadata hooks (server, webdav, synology-drive) behind a shared createConnectorRefreshMetadata factory in plugins/common, and remove the orphaned duplicate of the tokenValidity module that was superseded by the plugins/common copy every caller already imports. Claude-Session: https://claude.ai/code/session_01TznNj9nDaE3ezbXP9ssXTa Co-authored-by: Claude <noreply@anthropic.com> * refactor(web): consolidate incremental book mutation pipeline (#509) The three incremental book mutation hooks (useIncrementalBookModify, useIncrementalBookPatch, useIncrementalBookUpdate) each duplicated the same document-resolution pipeline: resolve a book doc from either an id or a document, guard against a missing document, then apply the rxdb incremental operation. Only the operation itself differed. Extract that shared pipeline into incrementalBookMutation and have each hook pass its operation as a callback. Public hook APIs are unchanged. Claude-Session: https://claude.ai/code/session_01MjDUJXpyhzCwpJJ73YcGdn Co-authored-by: Claude <noreply@anthropic.com> * fix(web): make the not-interested-only filter match not interested books (#522) * fix(web): make the not-interested-only filter match not interested books The isNotInterested: "only" branch in useCollections intersected collection.books with the bookIds query param instead of the computed notInterestedBookIds. The search screen never passes bookIds, so intersection(collection.books, undefined) degenerated to the full book list and the "Show not interested contents: Only" filter returned every non-empty collection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BKdyaYuEezwdEQ8XCJZdQ * refactor(web): colocate not-interested filter predicate in useCollections Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EveXE8CpZGCHmoSJwWHqdd --------- Co-authored-by: Claude <noreply@anthropic.com> * chore(deps): bump rxdb to 17.4.0 to fix ws DoS advisory (#511) rxdb 17.3.0 pinned ws@8.20.1, which is affected by GHSA-96hv-2xvq-fx4p (high-severity memory-exhaustion DoS). rxdb 17.4.0 requires ws@8.21.0, clearing the advisory in @oboku/web. Patch bump within the existing ^17 range; the lockfile also dedupes seven nested transitive copies that rxdb 17.4.0 no longer pins. Claude-Session: https://claude.ai/code/session_01KN1rjmw23NgWRyXVvuBiqu Co-authored-by: Claude <noreply@anthropic.com> * perf: compute book title sort key once per book in alpha sort (#510) The alpha branch of sortBooksBy called getMetadataFromBook twice inside the sort comparator, so an expensive metadata merge ran ~2·n·log(n) times per sort. Precompute each book's title once (n calls) via a decorate-sort-undecorate, turning the getMetadataFromBook cost from O(n·log n) into O(n). Output order is unchanged. Claude-Session: https://claude.ai/code/session_017dkihQVZGC8NhYJzQcdoav Co-authored-by: Claude <noreply@anthropic.com> * Upgrade workspace dependencies and read API book metadata through prose-reader (#524) * chore(deps): upgrade workspace dependencies Brings prose-reader to 1.334, plus MUI, Sentry, react-query, vite, workbox and the rest of the tree up to current. dexie is pinned to the exact version rxdb resolves: the two share a single IndexedDB connection and break when they drift. A postinstall check fails the install if they ever disagree again. The API reads book metadata through prose-reader's resolveArchive rather than assembling it from the OPF and ComicInfo documents itself, and reports the sources it could not parse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MQjPncXbBGcwudKbfo7Rph * refactor(archive-metadata): move the metadata writer out of the src root `patchArchiveMetadata` composes the ComicInfo and OPF container modules the same way those modules compose their own read/write halves, so it belongs beside them rather than at the package root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MQjPncXbBGcwudKbfo7Rph --------- Co-authored-by: Claude <noreply@anthropic.com> * fix(docker): copy scripts into the install stage (#528) The root postinstall runs scripts/check-dexie-version.mjs, but the base stage only copied manifests, so pnpm install --frozen-lockfile died with MODULE_NOT_FOUND and every image build failed. Claude-Session: https://claude.ai/code/session_01S828yJM3UYrYT8CoGsdtxu Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
mbret
added a commit
to mbret/oboku
that referenced
this pull request
Aug 5, 2026
* feat: upgrade @prose-reader/* to 1.332.0 (#503) * feat: upgrade @prose-reader/* to 1.332.0 Bump every @prose-reader/* dependency from 1.324.0 to 1.332.0 across web, api and archive-metadata, and migrate the web reader to the breaking API changes shipped in that range. Reader lifecycle (prose #229/#230/#231): - createReader() now takes the manifest (and optional cfi) at construction and mount(containerElement) is a one-shot DOM attachment; reader.load() is gone. useCreateReader now creates + mounts + destroys the reader in a single effect (destroy() is the true inverse of create + mount, so the effect is strict-mode safe), and useLoadReader is removed. - The restored reading location (cfi) is read through a live ref so later progress writes flowing back into the book query never destroy/recreate the reader. - reader.state$ ("idle"|"ready") is replaced by reader.mounted$; the book loading overlay now keys off !mounted. Pin react/react-dom to 19.1.8 in the web app only: - react-dom 19.2 dev builds recursively read component props and access $$typeof on every object, throwing SecurityError on our gapi cross-origin iframe (react/react#34840). The uncaught throw aborts passive-effect mounts (dead reader quick-menu) and corrupts the work loop (crash on back navigation). Prod is unaffected; dev is unusable. - 19.1.x predates the offending logger and the web app uses no 19.2-only APIs. admin/landing stay on 19.2.x (@Mantine 9.4 needs useEffectEvent), so the pin is web-only, held together by resolve.dedupe in vite.config. - Documented in AGENTS.md with removal steps for when the react bug is fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(reader): read book once via enabled gate instead of useLiveRef The enabled predicate already stops observing after the first result, so progress-sync writes to the same book document never change the value. Drop the redundant useLiveRef, read bookOnce directly, and name the enabled predicate to state the intent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: unpin apps/web react and bump react to 19.2.7 project-wide apps/web was pinned to react 19.1.8 to dodge a dev-only React 19.2 logger crash with the gapi iframe. The pin diverged web from the rest of the repo and left @types on 19.2.x, creating a runtime/types skew. Align everything to the latest 19.2.7 (react/react-dom) and 19.2.17 (@types/react) across web, admin and landing. Remove the now-unneeded scaffolding: the vite resolve.dedupe entry and the AGENTS.md "React version pin" section. The dev-only reader crash returns at parity with develop/master; production is unaffected (logger is dev-only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * chore(deps): bump @nestjs/platform-express to 11.1.28 to fix multer DoS (#507) Updates @nestjs/platform-express 11.1.27 -> 11.1.28, which pulls multer 2.1.1 -> 2.2.0 (multer is consumed only by platform-express). Resolves the two multer denial-of-service advisories on the API file-upload path: - GHSA-72gw-mp4g-v24j (high): DoS via deeply nested field names - GHSA-3p4h-7m6x-2hcm (moderate): DoS via incomplete cleanup of aborted uploads Claude-Session: https://claude.ai/code/session_01WKoPAepDc2xhSFFDhHTHe5 Co-authored-by: Claude <noreply@anthropic.com> * fix: lock * fix: biome * fix: types * feat: migrate to pnpm * fix: dedupe * fix(deps): patch react-dom 19.2.8 dev perf-track crash on cross-origin frames React 19.2's dev-only performance track deep-walks changed props and throws a SecurityError when it reaches the reader iframe's cross-origin window, crashing the commit (react/react#34840). Guard the prop walkers so unreadable values log as [inaccessible] instead of throwing. Fixed upstream by react/react#35679 (19.3 canary only); remove with `pnpm patch-remove react-dom@19.2.8` once on a release containing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dev-proxy): stop 502 bursts from unreachable IPv6 host.docker.internal peer Docker Desktop writes both IPv4 and IPv6 host-gateway entries, so nginx expands host.docker.internal into two peers; the IPv6 one is unreachable from the IPv4-only compose network and its failure accounting could mark the whole implicit upstream down (no live upstreams -> 502 bursts during replication). An explicit upstream with max_fails=0 lets a failed connect fall through to the reachable address instead of disabling the group. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vercel): bootstrap pnpm 11 in install command to match lockfile Vercel's lockfile heuristic picks pnpm 9, which can't read patchedDependencies from pnpm-workspace.yaml and fails with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. The bootstrapped pnpm 11 defers to the packageManager pin in package.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vercel): invoke bootstrapped pnpm by absolute path Vercel prepends its detected pnpm 9 to PATH, shadowing the npm-installed pnpm 11, so the install still hit ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(docker): copy patches into image for frozen install pnpm-workspace.yaml references patches/react-dom@19.2.8.patch, so the frozen install in the base stage fails with ENOENT without it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(vercel): set node 24 via engines for web and landing engines.node overrides the dashboard Node version; >=24 resolves to the latest 24.x on Vercel while staying satisfied by the node 25 used in CI, docker and local shells. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): declare express as a direct dependency main.ts value-imports json/urlencoded from express, which pnpm's isolated node_modules can't resolve through @nestjs/platform-express; dist/main crashed with MODULE_NOT_FOUND on a clean install. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(landing): ignore vercel cli .env.local Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reader): freeze first book result to keep reader mounted useBook is a live rxdb-backed query and the book finished dialog keeps an always-on observer of the same key, so the disabled observer in useCreateReader still received every progress-sync cache update and the bookOnce effect dependency remounted the reader on each write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): derive go-back availability from the router history index (#514) The back arrow fell through to home whenever a screen had performed a replace-only navigation, such as switching tab on the book optimize screen. react-router rebuilds the whole history state object on replace, so the `__obokuCanGoBack` flag was dropped, and TrackHistoryCanGoBack only re-stamped it on pathname changes. react-router already tracks the entry position under `history.state.idx`, preserved across replace, which removes the need for a custom flag, the `__obokuFallbackBack` marker and the tracker component. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * feat(web): generalize first-result snapshot into useEnsureQueryData$ Extracts createBookQueryOptions alongside the existing createLinkQueryOptions pattern and rebuilds the reader's frozen book snapshot on top of a reusable hook that resolves a query's first result and ignores later cache updates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): rebuild useEnsureQueryData$ on an ensureQueryData$ mechanism Splits the one-shot idea into an observable flavour of queryClient.ensureQueryData (cache read, else first emission seeding the empty cache, never touching the tanstack fetch machinery) and a thin snapshot hook on top. Drops the inert observer, so consumers no longer re-render on post-capture cache writes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): memoize useEnsureQueryData$ snapshot in a private query Replaces the effect+state capture with a vanilla useQuery over a prefix-namespaced key whose queryFn is ensureQueryData$: staleTime Infinity freezes the snapshot, gcTime 0 scopes it to the consumers' lifetime and concurrent consumers dedupe into a single resolution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): default useEnsureQueryData$ to networkMode always The private useQuery dropped the source query's networkMode, so offline the default online mode paused resolution entirely — blocking even the warm-cache read and leaving locally available books on the loading screen. Resolution is cache-first, so always is the correct default; network-bound sources can pass online explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): let users protect storage from browser eviction (#516) Origin storage is best-effort by default, so the browser may evict oboku's whole bucket under disk pressure — the RxDB library, the Dexie downloads and the covers cache together. Most of it re-syncs from CouchDB afterwards, but books uploaded straight from a device exist nowhere else and are lost for good. Nothing in the app ever called `navigator.storage.persist()`, so no user was protected. - `useIsStoragePersisted` / `useRequestStoragePersistence` under `storage/` - a row in Manage storage showing a green or red shield, tappable to request protection, with a "Read more" link to the new guide - an inbox notification while storage is evictable, pointing at that screen - `guides/storage.md` explaining what it is, why it matters and how to fix it The inbox notification is deliberately not dismissible: Chrome refuses the request for most users in a plain tab, and installing the app both satisfies its heuristics and silences the warning, so the nag and the fix are the same action. `LocalNotificationCard` hardcoded a login icon, which only worked while `session_expired` was the sole local notification; the icon now comes from the notification's action. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * fix(shared): match reader file extensions case-insensitively (#508) isFileSupported compared the raw extension against the lowercase READER_ACCEPTED_EXTENSIONS list, so files like Book.EPUB or comic.CBZ were reported as unsupported and skipped by synology-drive/webdav sync and greyed out in file browsers. Claude-Session: https://claude.ai/code/session_01DqCq7ako5cdiYdvpJAKgKJ Co-authored-by: Claude <noreply@anthropic.com> * refactor: consolidate duplicated web plugin helpers (#506) Unify the three byte-identical connector-based useRefreshMetadata hooks (server, webdav, synology-drive) behind a shared createConnectorRefreshMetadata factory in plugins/common, and remove the orphaned duplicate of the tokenValidity module that was superseded by the plugins/common copy every caller already imports. Claude-Session: https://claude.ai/code/session_01TznNj9nDaE3ezbXP9ssXTa Co-authored-by: Claude <noreply@anthropic.com> * refactor(web): consolidate incremental book mutation pipeline (#509) The three incremental book mutation hooks (useIncrementalBookModify, useIncrementalBookPatch, useIncrementalBookUpdate) each duplicated the same document-resolution pipeline: resolve a book doc from either an id or a document, guard against a missing document, then apply the rxdb incremental operation. Only the operation itself differed. Extract that shared pipeline into incrementalBookMutation and have each hook pass its operation as a callback. Public hook APIs are unchanged. Claude-Session: https://claude.ai/code/session_01MjDUJXpyhzCwpJJ73YcGdn Co-authored-by: Claude <noreply@anthropic.com> * fix(web): make the not-interested-only filter match not interested books (#522) * fix(web): make the not-interested-only filter match not interested books The isNotInterested: "only" branch in useCollections intersected collection.books with the bookIds query param instead of the computed notInterestedBookIds. The search screen never passes bookIds, so intersection(collection.books, undefined) degenerated to the full book list and the "Show not interested contents: Only" filter returned every non-empty collection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BKdyaYuEezwdEQ8XCJZdQ * refactor(web): colocate not-interested filter predicate in useCollections Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EveXE8CpZGCHmoSJwWHqdd --------- Co-authored-by: Claude <noreply@anthropic.com> * chore(deps): bump rxdb to 17.4.0 to fix ws DoS advisory (#511) rxdb 17.3.0 pinned ws@8.20.1, which is affected by GHSA-96hv-2xvq-fx4p (high-severity memory-exhaustion DoS). rxdb 17.4.0 requires ws@8.21.0, clearing the advisory in @oboku/web. Patch bump within the existing ^17 range; the lockfile also dedupes seven nested transitive copies that rxdb 17.4.0 no longer pins. Claude-Session: https://claude.ai/code/session_01KN1rjmw23NgWRyXVvuBiqu Co-authored-by: Claude <noreply@anthropic.com> * perf: compute book title sort key once per book in alpha sort (#510) The alpha branch of sortBooksBy called getMetadataFromBook twice inside the sort comparator, so an expensive metadata merge ran ~2·n·log(n) times per sort. Precompute each book's title once (n calls) via a decorate-sort-undecorate, turning the getMetadataFromBook cost from O(n·log n) into O(n). Output order is unchanged. Claude-Session: https://claude.ai/code/session_017dkihQVZGC8NhYJzQcdoav Co-authored-by: Claude <noreply@anthropic.com> * Upgrade workspace dependencies and read API book metadata through prose-reader (#524) * chore(deps): upgrade workspace dependencies Brings prose-reader to 1.334, plus MUI, Sentry, react-query, vite, workbox and the rest of the tree up to current. dexie is pinned to the exact version rxdb resolves: the two share a single IndexedDB connection and break when they drift. A postinstall check fails the install if they ever disagree again. The API reads book metadata through prose-reader's resolveArchive rather than assembling it from the OPF and ComicInfo documents itself, and reports the sources it could not parse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MQjPncXbBGcwudKbfo7Rph * refactor(archive-metadata): move the metadata writer out of the src root `patchArchiveMetadata` composes the ComicInfo and OPF container modules the same way those modules compose their own read/write halves, so it belongs beside them rather than at the package root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MQjPncXbBGcwudKbfo7Rph --------- Co-authored-by: Claude <noreply@anthropic.com> * fix(docker): copy scripts into the install stage (#528) The root postinstall runs scripts/check-dexie-version.mjs, but the base stage only copied manifests, so pnpm install --frozen-lockfile died with MODULE_NOT_FOUND and every image build failed. Claude-Session: https://claude.ai/code/session_01S828yJM3UYrYT8CoGsdtxu Co-authored-by: Claude <noreply@anthropic.com> * refactor(api): consolidate synology-drive link session resolution (#526) getFileMetadata, getFolderMetadata and download each repeated the same ~18-line preamble: validate connectorId/credentials/db, load the connector, open a Synology Drive session and read fileId from the link. Extract it into a single openSynologyDriveSessionForLink helper so the validation and error handling live in one place. Claude-Session: https://claude.ai/code/session_01SiouS4DW4vVoe9wfNAN761 Co-authored-by: Claude <noreply@anthropic.com> * perf: compute search title once and hoist search regex in book search (#525) useBooksForSearch recomputes on every keystroke over the whole library. It compiled a fresh RegExp inside the .filter callback (once per book) and called the expensive getMetadataFromBook twice inside the sort comparator (~2·n·log(n) metadata merges). Hoist the regex to one compilation per search and precompute each book's title once (n calls) via decorate-sort-undecorate. Results and order are unchanged. Benchmark at 3000 books: 149ms -> 15.5ms per search (~9.6x). Claude-Session: https://claude.ai/code/session_01WTpoRzFz6L5YQ1MXtHE2Vf Co-authored-by: Claude <noreply@anthropic.com> * chore(deps): bump next to 16.2.12 to fix DoS/SSRF/proxy-bypass advisories (#527) Updates next and its lockstep companion eslint-config-next from 16.1.6 to 16.2.12 in @oboku/landing. Minor bump within major 16; patches ~9 high severity advisories in the internet-facing landing app (Server Components DoS, SSRF via WebSocket upgrades, App/Pages Router middleware & proxy bypasses, i18n bypass). Both were exact-pinned to 16.1.6; kept exact-pin style and moved them together since eslint-config-next version-tracks next. Claude-Session: https://claude.ai/code/session_01W73Y7fJypdhxKPp6YtHi65 Co-authored-by: Claude <noreply@anthropic.com> * feat: ugprade vercel * feat: upgrade * chore(deps): bump sharp to 0.35.3 to fix libvips security advisory (#540) Updates @oboku/api's sharp dependency from ^0.34.5 to ^0.35.3 to remediate the high-severity advisory affecting sharp <0.35.0 (inherited libvips CVEs). sharp 0.35 raises the minimum Node.js to >=20.9.0, satisfied by the pinned Node 25 runtime. The image pipeline in covers.service.ts uses only stable APIs (resize, toFormat, jpeg, webp, toBuffer), so no code changes are needed. Claude-Session: https://claude.ai/code/session_014kuujQqjWVajzvvhrmqyuB Co-authored-by: Claude <noreply@anthropic.com> * refactor(web): consolidate duplicated plugin link-info hooks (#539) The dropbox, google, synology-drive and one-drive plugins each shipped a byte-identical `useLinkInfo` that derives a `ID: <fileId>` label, and the server and webdav plugins shipped an identical `filePath`-based one. Move both implementations into `plugins/common/linkInfo.ts` as `useFileIdLinkInfo` and `useFilePathLinkInfo`, and point each plugin at the shared export. Behavior is unchanged; a plugin can still diverge later by providing its own implementation. Claude-Session: https://claude.ai/code/session_01KNBTACP3nnNjfsGvFh8i9z Co-authored-by: Claude <noreply@anthropic.com> * chore: remove strict dead code (unused exports) (#537) Remove exports that knip flagged and that a repo-wide grep confirmed are never imported or referenced anywhere (each appears only at its own declaration; no dynamic/string/test references): - apps/web/src/tags/helpers.ts: getProtectedTags, getTagsByIds (both @deprecated), useTagsByIds - apps/web/src/reader/states.ts: reader$, usePagination - apps/web/src/collections/dbHelpers.ts: getCollections - apps/api/src/lib/utils.ts: switchMapMergeOuter - apps/api/src/lib/google/googleBooksApi.ts: findSeriesByTitle - apps/api/src/lib/couch/dbHelpers.ts: findAllDataSources Also drop the now-unused type imports left behind (Database, MangoResponse, DataSourceDocType) and the rxjs/reactjrx imports that only those symbols used. Claude-Session: https://claude.ai/code/session_013Xb8Rr8xPgnjmexMzNbZBW Co-authored-by: Claude <noreply@anthropic.com> * perf(web): linearize covers cache cleanup passes (#538) The service-worker covers cache cleanup runs every 10 minutes (and on startup) and had two super-linear passes over the cover cache: - obsolete-cover detection matched each cache key against the book and collection lists with `.some` (O(cacheKeys * (books + collections))) - outdated-version detection compared every cache key against every other key, re-parsing request headers each time (O(cacheKeys^2)) Replace the membership scan with a Set lookup and the pairwise scan with a single max-time-per-coverId map. Both passes now parse each request's headers once and produce byte-for-byte identical removal sets. Claude-Session: https://claude.ai/code/session_01RNbzoWKvYxCqMT7PLKPTHQ Co-authored-by: Claude <noreply@anthropic.com> * refactor: single product version, automatic releases on master, versioned docker tags (#531) * chore: align license metadata with GPL-3.0 The root LICENCE file is the full GNU GPL v3 text, but the README and package manifests advertised MIT (and UNLICENSED for the API). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X91x1vJ1CJ45Gwk9WmrFaa * ci: publish a GitHub release when the root version changes Tags were pushed by the web version bump, but no GitHub release was ever created, so the repository exposed no installable version to pin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X91x1vJ1CJ45Gwk9WmrFaa * ci: tag docker images with the version and commit sha Images were published as :latest only, so an instance could not be pinned to a known build. Version resolution moves to a shared job now that the docker builds and the release both need it. Release notes rewrite changelog links relative to the changelog into blob URLs at the release tag, since relative targets do not resolve on a release page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X91x1vJ1CJ45Gwk9WmrFaa * refactor: single product version for the whole repo The web app carried its own version line, bumped on every master push, which was accidental rather than intended. Root package.json is now the only version: the web app reads it through the __APP_VERSION__ build constant, and the private unpublished manifests no longer declare one. The persisted query cache moves to __BUILD_ID__ so it keeps busting per build; keying it on a version that now changes only per release would let a newer build rehydrate state persisted by an older one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X91x1vJ1CJ45Gwk9WmrFaa * docs: document the release-notes script contract Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X91x1vJ1CJ45Gwk9WmrFaa * refactor: mark workspace packages private and drop their versions @oboku/shared was last published in 2022 and no publish flow exists, so these are internal workspace packages like the apps: no version to carry, and private stops an accidental npm publish. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X91x1vJ1CJ45Gwk9WmrFaa * ci: bump the version automatically on every master push The release version is now derived from conventional commits since the last released tag (breaking -> major, feat -> minor, otherwise patch) and committed back to master, so merging develop is the only manual step. Downstream jobs build from the bump commit so the web app embeds the version it ships under. A hand-edited, not-yet-released version in package.json still wins, which also covers the pending 1.2.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X91x1vJ1CJ45Gwk9WmrFaa * fix: tighten the breaking-change footer detection The conventional commits spec allows both BREAKING CHANGE and BREAKING-CHANGE as footer tokens, and requiring the colon stops prose mentions from triggering a major bump. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X91x1vJ1CJ45Gwk9WmrFaa * ci: replace hand-rolled version bump and release with semantic-release The bump computation, tagging, release creation and notes generation move to semantic-release with the conventionalcommits preset. A catch-all releaseRules entry keeps the release-on-every-master-push behavior that the default rules would skip for chore/docs-only pushes. Upgrade instructions now travel in BREAKING CHANGE commit footers, which release-notes-generator renders on the release page, so the gitbook changelog becomes a pointer to GitHub releases and both custom release scripts are deleted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X91x1vJ1CJ45Gwk9WmrFaa --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 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.
Alternate to #34840
Fixes #34840
Closes #34840
Summary
Guards dotting into
$$typeofunconditionally. The try-catch proposed in #34840 feels too heavy.Original plan was to skip diffing DOM nodes entirely since they're mutable boxes anyway. That felt like too much of a stretch unless we move the check to the host config. However, that wouldn't fix the linked issue. The linked issue is caused by having a cross-origin window in props not the iframe. Perf tracks wouldn't diff
contentWindowsince it's not enumerable.The proposed implementation is host agnostic and feels like a better fit overall. And it guards against cases where accessing an unknown
$$typeofproperty would also throw.Test plan