Skip to content

refactor(cache): improve resource caching strategies and add new endpoints for menu counts and user preferences#806

Open
balazs-szucs wants to merge 3 commits intogrimmory-tools:developfrom
balazs-szucs:cacheing
Open

refactor(cache): improve resource caching strategies and add new endpoints for menu counts and user preferences#806
balazs-szucs wants to merge 3 commits intogrimmory-tools:developfrom
balazs-szucs:cacheing

Conversation

@balazs-szucs
Copy link
Copy Markdown
Member

@balazs-szucs balazs-szucs commented Apr 23, 2026

Description

Linked Issue: Fixes #

Changes

The new endpoints will be used. Right now they aren't. I know it.

This pull request introduces several improvements across caching strategy, authentication handling, API endpoints, and model mapping in the backend. The most significant changes are the addition of fine-grained cache-control headers for static resources and API responses, improved handling of authentication edge cases, and the introduction of a new lightweight endpoint for retrieving menu counts. Several DTOs and mappers are also updated to support new features.

Caching and Static Resource Handling

  • Implemented detailed cache-control policies for static resources in WebMvcConfig, ensuring fingerprinted assets are cached for one year, service worker files and index.html are always revalidated, and third-party assets are cached for one day. This improves frontend performance and update reliability.
  • Added cache-control headers to API responses for book lists (private, max-age=60) and user profile (no-cache, no-store, must-revalidate) to optimize client-side caching and data freshness.

Authentication and Security Improvements

  • Allowed unauthenticated access to the /api/v1/app/bootstrap endpoint by updating both the security config and authentication filter, ensuring smoother client bootstrapping.
  • Improved robustness in AuthenticationService: now safely returns null for unauthenticated or anonymous users instead of throwing exceptions.
  • Enhanced validation and error logging for the token refresh endpoint in AuthenticationController, providing clearer error messages for invalid requests.

API and Model Enhancements

  • Added a new /books/menu-counts endpoint that returns lightweight counts for libraries, shelves, and magic-shelves, reducing load on the client when rendering sidebars. Introduced the MenuCountsResponse DTO and supporting service.
  • Updated the BookMapper interface and related DTOs (PdfViewerPreferences, NewPdfViewerPreferences, CbxViewerPreferences) to include userId fields, supporting user-specific viewer preferences.

Repository Improvements

  • Added a new query method findAllIds() to LibraryRepository to efficiently fetch all library IDs, supporting the new menu counts feature.

Summary by CodeRabbit

  • New Features

    • Lightweight /menu-counts endpoint for sidebar book counts.
    • Unauthenticated bootstrap endpoint for initial app data.
    • Viewer preferences now include user association for personalized settings.
  • Bug Fixes

    • Improved refresh-token validation, logging and error responses.
    • Authentication flow bypass added for bootstrap requests.
    • User profile endpoint now disables caching to protect sensitive data.
  • Performance

    • HTTP caching policies added for static assets and book list responses.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

Adds HTTP caching policies for static assets and API responses; exposes a bootstrap endpoint that bypasses JWT auth; extends viewer-preference DTOs with userId; introduces a read-only MenuCountsService and permission-aware service overloads; various controllers/services adjusted for caching and null-safety.

Changes

Cohort / File(s) Summary
Web MVC & Caching
backend/src/main/java/org/booklore/config/WebMvcConfig.java
Registers multiple resource handlers with explicit CacheControl rules for fingerprinted bundles, /media, SPA fallback/index, service worker files, and selected non-fingerprinted assets.
Security & Auth
backend/src/main/java/org/booklore/config/security/SecurityConfig.java, backend/src/main/java/org/booklore/config/security/filter/AuthenticationCheckFilter.java, backend/src/main/java/org/booklore/config/security/service/AuthenticationService.java
Whitelists /api/v1/app/bootstrap; auth filter short-circuits for that path; getAuthenticatedUser() returns null for missing/not-authenticated principals and no longer throws when principal type differs.
Auth Controller
backend/src/main/java/org/booklore/controller/AuthenticationController.java
/refresh now validates request and token presence, returns ResponseEntity<?>, logs and rethrows exceptions, and returns 400 with fixed JSON when refresh token missing.
Book & User Controllers
backend/src/main/java/org/booklore/controller/BookController.java, backend/src/main/java/org/booklore/controller/UserController.java
Adds GET /api/v1/books/menu-counts; sets Cache-Control: private, max-age=60 for books list; /me response now sets no-cache headers.
Menu Counts & Services
backend/src/main/java/org/booklore/service/MenuCountsService.java, backend/src/main/java/org/booklore/service/ShelfService.java, backend/src/main/java/org/booklore/service/library/LibraryService.java
Adds MenuCountsService computing per-library, per-shelf, per-magic-shelf counts and totals with visibility/admin filtering; adds null-safe overloads that accept BookLoreUser and delegates existing methods to them.
DTOs & Mapping
backend/src/main/java/org/booklore/model/dto/PdfViewerPreferences.java, backend/src/main/java/org/booklore/model/dto/NewPdfViewerPreferences.java, backend/src/main/java/org/booklore/model/dto/CbxViewerPreferences.java, backend/src/main/java/org/booklore/model/dto/response/MenuCountsResponse.java, backend/src/main/java/org/booklore/mapper/BookMapper.java
Adds userId field to viewer-preference DTOs; introduces MenuCountsResponse record; extends BookMapper with MapStruct mappings for viewer-preference entities → DTOs.
Repository
backend/src/main/java/org/booklore/repository/LibraryRepository.java
Adds findAllIds() JPQL query method returning list of library IDs.
Book Service
backend/src/main/java/org/booklore/service/book/BookService.java
Populates userId into built viewer-preference DTOs when present.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Controller as BookController
    participant Service as MenuCountsService
    participant RepoB as BookRepository
    participant RepoS as ShelfRepository
    participant MSvc as MagicShelfBookService

    Client->>Controller: GET /api/v1/books/menu-counts
    Controller->>Service: getMenuCounts()
    Service->>RepoB: query grouped book counts by library (visibility spec)
    Service->>RepoS: query grouped book counts by shelf (criteria tuple)
    Service->>MSvc: compute/count books per magic-shelf (per-shelf spec)
    RepoB-->>Service: library counts map
    RepoS-->>Service: shelf counts map
    MSvc-->>Service: magic-shelf counts (or 0 on error)
    Service-->>Controller: MenuCountsResponse
    Controller-->>Client: 200 OK (JSON)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

backend

Suggested reviewers

  • imajes
  • zachyale

Poem

🐰 Hopping through headers and counts so spry,

Bootstrap left open for a token-free try,
Viewer prefs wear user ids with pride,
Shelves and libraries tallied side by side,
A rabbit cheers: "Cache smart and routes apply!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title follows the conventional commit format with a clear type (refactor) and scope (cache), accurately summarizing the main changes including caching improvements, new endpoints, and user preference features.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all major changes across caching, authentication, API endpoints, and repository improvements. Required sections (Description and Changes) are present with detailed explanations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/src/main/java/org/booklore/service/ShelfService.java (1)

113-116: ⚠️ Potential issue | 🟡 Minor

getAuthenticatedUserId() still dereferences a potentially null user.

Now that authenticationService.getAuthenticatedUser() can return null, user.getId() on line 115 will NPE instead of throwing the previous explicit IllegalStateException. In practice createShelf/getUserKoboShelf are behind .authenticated() so this is mostly defensive, but a clearer error keeps the contract explicit.

Proposed fix
     private Long getAuthenticatedUserId() {
         BookLoreUser user = authenticationService.getAuthenticatedUser();
+        if (user == null || user.getId() == null) {
+            throw new AccessDeniedException("No authenticated user");
+        }
         return user.getId();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/booklore/service/ShelfService.java` around lines
113 - 116, The helper getAuthenticatedUserId currently dereferences a possibly
null result from authenticationService.getAuthenticatedUser(); change it to
null-check the returned BookLoreUser and throw a clear IllegalStateException (or
similar runtime exception) with a descriptive message if it's null so callers
like createShelf and getUserKoboShelf keep the explicit contract; update
getAuthenticatedUserId to call authenticationService.getAuthenticatedUser(),
verify non-null, then return user.getId() otherwise throw.
backend/src/main/java/org/booklore/config/security/service/AuthenticationService.java (1)

82-96: ⚠️ Potential issue | 🔴 Critical

Breaking change: nullable return mixes inconsistent null-handling across callsites.

getAuthenticatedUser() now returns null for unauthenticated/non-BookLoreUser principals, but this contract change was only partially addressed at call sites:

Updated callsites (proper null checks added):

  • MenuCountsService.getMenuCounts() — guards with if (user == null || user.getId() == null)
  • ShelfService.getShelves() — guards with if (user == null || user.getId() == null)

Unhandled callsites (no null checks, dereference immediately):

  • ReadingSessionService.recordSession() line 95 — authenticatedUser.getId()
  • ReadingSessionService.getSessionHeatmapForYear() line 122 — getAuthenticatedUser().getId()
  • LibraryService.createLibrary() line 155 — bookLoreUser.getId()
  • AuthorMetadataService.getAllAuthors() line 58 — user.getPermissions().isAdmin()
  • ShelfService.getAuthenticatedUserId()user.getId() (helper used by getUserKoboShelf, createShelf)

While these endpoints are currently protected by .authenticated() in SecurityConfig, the missing @Nullable annotation means the type system provides no protection against future misuse. Any new bootstrap-style permit-all endpoint calling these services will silently NPE.

Recommended fix: Add @Nullable to the method signature and audit the unhandled callsites, or revert to a throwing contract with a separate getAuthenticatedUserOrNull() for the null-aware callers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/booklore/config/security/service/AuthenticationService.java`
around lines 82 - 96, getAuthenticatedUser() changed to return null for
unauthenticated principals but lacks `@Nullable` and several callers
(ReadingSessionService.recordSession,
ReadingSessionService.getSessionHeatmapForYear, LibraryService.createLibrary,
AuthorMetadataService.getAllAuthors, ShelfService.getAuthenticatedUserId) still
dereference the result causing potential NPEs; either annotate
AuthenticationService.getAuthenticatedUser() with `@Nullable` and update the
listed callsites to null-check before calling getId()/getPermissions(), or
revert to a non-null contract by restoring a throwing variant (e.g.,
getAuthenticatedUserOrThrow()) and update null-aware callers to call the new
nullable helper (or vice versa) so the contract is explicit and all usages are
audited and fixed.
🧹 Nitpick comments (7)
backend/src/main/java/org/booklore/mapper/BookMapper.java (1)

41-45: Migrate callers to the new MapStruct methods.

The new mapper methods are the right direction, but BookService still hand-builds PdfViewerPreferences, NewPdfViewerPreferences, EbookViewerPreferences, and CbxViewerPreferences via .builder() (lines 211, 229, 236, and 246). The new mapper methods remain unused, which means:

  1. Any future entity field addition still has to be mirrored manually in these builders.
  2. Manual builders are error-prone and don't benefit from MapStruct's automatic mapping.

Replace the manual builders with the mapper methods: bookMapper.toPdfViewerPreferences(pdfPref), bookMapper.toNewPdfViewerPreferences(pdfPref), bookMapper.toEbookViewerPreferences(epubPref), and bookMapper.toCbxViewerPreferences(cbxPref).

This aligns with the guideline: "Use MapStruct for entity/DTO mapping in backend code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/booklore/mapper/BookMapper.java` around lines 41 -
45, BookService is still constructing PdfViewerPreferences,
NewPdfViewerPreferences, EbookViewerPreferences, and CbxViewerPreferences via
manual builders; replace those manual constructions with the MapStruct mapper
methods to avoid duplicated mapping logic. Concretely, in the places currently
calling the builders (the Pdf/NewPdf/Ebook/Cbx preference creation in
BookService) swap the builder code for calls to
bookMapper.toPdfViewerPreferences(pdfPref),
bookMapper.toNewPdfViewerPreferences(pdfPref),
bookMapper.toEbookViewerPreferences(epubPref), and
bookMapper.toCbxViewerPreferences(cbxPref) respectively, ensuring you pass the
same source entity variables (pdfPref/epubPref/cbxPref) and remove the
now-redundant manual field assignments. Ensure imports and the BookMapper field
usage remain correct and run tests to verify behavior.
backend/src/main/java/org/booklore/config/WebMvcConfig.java (2)

68-97: Consider moving index.html off the SPA fallback handler.

Currently index.html is only served via the /** fallback (line 101-116) which sets NO_CACHE_REVALIDATE. That's correct, but it couples the revalidate behavior for index.html to whatever else lands on the fallback. An explicit handler (e.g. registering "/", "/index.html" before the /** fallback and pointing at classpath:/static/) would make the policy for the entry document explicit and match the intent described in the comment on lines 99-100. Optional — current behavior is correct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/booklore/config/WebMvcConfig.java` around lines 68
- 97, Add an explicit resource handler for the SPA entry document in
WebMvcConfig.addResourceHandlers: register "/" and "/index.html" (or equivalent
root path) before the existing /** fallback handler and point it to
"classpath:/static/" with NO_CACHE_REVALIDATE so the cache policy for index.html
is explicit and not coupled to the generic /** fallback handler; update the
handler ordering in addResourceHandlers so the new handler runs before the
existing /** fallback registration.

92-97: Consider optimizing resource handler location order for root files.

The handler registers /manifest.webmanifest and /favicon.* but checks classpath:/static/assets/ and classpath:/static/icons/ before classpath:/static/. Since these files reside only in the root static directory, Spring performs unnecessary existence checks for the first two locations on each request. Either reorder locations to put classpath:/static/ first, or register root-level files in a dedicated handler with only the root location.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/booklore/config/WebMvcConfig.java` around lines 92
- 97, The resource handler registration in registry.addResourceHandler currently
lists "classpath:/static/assets/" and "classpath:/static/icons/" before
"classpath:/static/", causing extra file-existence checks for root files; fix by
either reordering addResourceLocations to put "classpath:/static/" first or
(preferred) register a separate handler for the root files (e.g.,
registry.addResourceHandler("/manifest.webmanifest", "/favicon.*") with
addResourceLocations("classpath:/static/") and setCacheControl(ONE_DAY_PUBLIC)),
leaving the existing "/assets/**" and "/icons/**" handler pointing to their
specific locations.
backend/src/main/java/org/booklore/controller/UserController.java (1)

33-37: Consider using the typed CacheControl API.

Minor: Spring offers .cacheControl(CacheControl.noStore()) on ResponseEntity builder, which avoids the raw header string and keeps consistency with WebMvcConfig's use of CacheControl. Either works.

Proposed refactor
-        return ResponseEntity.ok()
-                .header("Cache-Control", "no-cache, no-store, must-revalidate")
-                .body(userService.getMyself());
+        return ResponseEntity.ok()
+                .cacheControl(CacheControl.noStore().mustRevalidate())
+                .body(userService.getMyself());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/booklore/controller/UserController.java` around
lines 33 - 37, Replace the raw Cache-Control header in the
UserController.getMyself() response with Spring's typed CacheControl API: call
ResponseEntity.ok().cacheControl(CacheControl.noStore().mustRevalidate()) (or
CacheControl.noStore() to match project conventions) and remove the manual
.header(...) usage; ensure the CacheControl class
(org.springframework.http.CacheControl) is imported so getMyself() returns the
same semantic cache policy via the typed API.
backend/src/main/java/org/booklore/controller/AuthenticationController.java (1)

73-76: Consider lowering the failure log level and narrowing the catch.

log.error for every refresh failure will be noisy since expired/invalid/revoked refresh tokens are expected user-triggered outcomes (see AuthenticationService.refreshToken which throws APIException via ApiError.INVALID_CREDENTIALS for not-found/expired/revoked). A log.warn is more appropriate here, and catching Exception broadly then rethrowing adds little beyond what a dedicated exception handler or the service-level logging could do. If the intent is purely observability, consider letting the global exception handler log it, or at minimum use WARN.

Suggested change
-        try {
-            return authenticationService.refreshToken(request.getRefreshToken());
-        } catch (Exception e) {
-            log.error("[Auth] Token refresh failed: {}", e.getMessage());
-            throw e;
-        }
+        try {
+            return authenticationService.refreshToken(request.getRefreshToken());
+        } catch (RuntimeException e) {
+            log.warn("[Auth] Token refresh failed: {}", e.getMessage());
+            throw e;
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/booklore/controller/AuthenticationController.java`
around lines 73 - 76, The catch in AuthenticationController (around the token
refresh handling) is too broad and logs expected user-driven failures at ERROR;
change the catch to only handle the specific service exception (e.g., catch
APIException or the specific exception thrown by
AuthenticationService.refreshToken/ApiError.INVALID_CREDENTIALS) and lower the
log level to WARN (log.warn(...)) or remove logging so the global exception
handler can record it; ensure other unexpected exceptions still propagate (do
not swallow) so higher-level handlers can deal with them.
backend/src/main/java/org/booklore/service/MenuCountsService.java (2)

136-156: Per-shelf count query may become a hotspot for users with many magic shelves.

Each magic shelf triggers a separate count(Specification) SQL query, and each Specification is built by parsing the shelf's JSON rules (MagicShelfBookService.toSpecification deserializes filterJson on every call). For a user with N magic shelves this is N round-trips plus N JSON parses on every sidebar refresh, and the response is cached for only 60 seconds. Worth benchmarking with a realistic shelf count; if it shows up, consider caching the parsed specs or computing counts in a single aggregate query.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/booklore/service/MenuCountsService.java` around
lines 136 - 156, fetchMagicShelfCounts performs one bookRepository.count(spec)
per shelf and calls MagicShelfBookService.toSpecification for each, causing N
SQL round-trips and N JSON deserializations; fix by avoiding per-shelf
round-trips: either (1) cache parsed Specifications/parsed filterJson keyed by
shelf id (e.g., use a short-lived in-memory cache or a method-local map to call
toSpecification once per shelf and reuse on repeated refreshes) and reuse them
when building counts, or (2) compute counts in a single aggregate query by
translating each shelf spec into a predicate and combining them into one query
that returns counts grouped by shelf id (so replace repeated
bookRepository.count calls with one batched query). Update fetchMagicShelfCounts
to use the chosen approach and adjust error handling around
MagicShelfBookService.toSpecification and bookRepository so parsing failures for
one shelf don’t trigger repeated parsing or queries for others.

50-54: Consider cache-loading the user's libraries/shelves via a single call path.

getMenuCounts triggers authenticationService.getAuthenticatedUser() once here, but libraryService.getLibraries(user) and shelfService.getShelves(user) each do their own userRepository.findByIdWithLibraries(...) / shelf lookups. For a single-page menu endpoint this is fine, but if this is hit on every navigation consider either caching per-request or having callers pass a resolved BookLoreUserEntity to avoid the repeated DB lookups.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/booklore/service/MenuCountsService.java` around
lines 50 - 54, getMenuCounts calls authenticationService.getAuthenticatedUser(),
then libraryService.getLibraries(user) and shelfService.getShelves(user), which
each re-query the user's libraries/shelves; fix by loading the user with
libraries/shelves once and reusing it: retrieve a fully-initialized
BookLoreUserEntity (e.g. via userRepository.findByIdWithLibraries(user.getId())
or an auth method that returns the entity with associations) and pass that
single resolved entity into libraryService and shelfService (or add overloads
like getLibrariesForEntity/getShelvesForEntity) so you avoid duplicate DB
lookups and/or implement a per-request cache for the resolved user entity used
by getMenuCounts, authenticationService.getAuthenticatedUser(),
libraryService.getLibraries(), shelfService.getShelves(), and
userRepository.findByIdWithLibraries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/src/main/java/org/booklore/controller/AuthenticationController.java`:
- Line 64: Add the javax.validation.Valid annotation to the RefreshTokenRequest
parameter in the refreshToken method (i.e., change the signature to use `@Valid`
`@RequestBody` RefreshTokenRequest request), ensure the import for
javax.validation.Valid is present, and remove the redundant manual null/blank
checks in refreshToken so the framework enforces the `@NotBlank` constraint
declared on RefreshTokenRequest (consistent with registerUser and loginUser).

In `@backend/src/main/java/org/booklore/model/dto/CbxViewerPreferences.java`:
- Line 15: The new field userId in CbxViewerPreferences is left null because
CbxViewerPreferences.builder() call sites (notably in BookService within the
method that builds viewer prefs) do not set .userId(...); update those builders
to pass the current user's id (e.g., user.getId()) when constructing
CbxViewerPreferences, mirroring how EbookViewerPreferences.builder() populates
userId, and audit other CbxViewerPreferences.builder() usages to ensure
.userId(...) is always set.

In `@backend/src/main/java/org/booklore/model/dto/NewPdfViewerPreferences.java`:
- Line 15: The NewPdfViewerPreferences builder call inside BookService (the
lambda passed to
newPdfViewerPreferencesRepository.findByBookIdAndUserId(...).ifPresent(...)) is
missing the userId field; update the builder used in the
settingsBuilder.newPdfSettings(...) invocation to include .userId(user.getId())
so the DTO mirrors EbookViewerPreferences.builder() where userId is set,
ensuring the resulting NewPdfViewerPreferences has the correct userId populated.

In `@backend/src/main/java/org/booklore/model/dto/PdfViewerPreferences.java`:
- Line 14: The PdfViewerPreferences instances are missing userId because
PdfViewerPreferences.builder() call sites (notably in BookService where the PDF
response is constructed) do not set it; update the builder invocation to call
.userId(user.getId()) when creating the PdfViewerPreferences, or replace that
builder usage with the new BookMapper.toPdfViewerPreferences(...) which
correctly populates userId; locate the construction in BookService (around the
PDF response creation) and add .userId(user.getId()) or swap to
BookMapper.toPdfViewerPreferences(...) to ensure userId is serialized.

In `@backend/src/main/java/org/booklore/service/MenuCountsService.java`:
- Around line 105-134: fetchShelfCounts currently groups by shelves.id but only
restricts books by visibleLibraryIds/isAdmin, allowing shelf IDs belonging to
other users to appear; restrict the shelf side by intersecting the joined shelf
IDs with the set of shelves the caller can see (the IDs produced by
shelfService.getShelves(user) that is computed earlier) — i.e., obtain the
allowedShelfIds and add a predicate like shelfIdPath.in(allowedShelfIds) (or
apply the same filter when joining) to the CriteriaQuery so only user's/public
shelves are counted; update fetchShelfCounts to accept or compute
allowedShelfIds when calling shelfService.getShelves and include that predicate
alongside visiblePredicate and notDeletedPredicate.

---

Outside diff comments:
In
`@backend/src/main/java/org/booklore/config/security/service/AuthenticationService.java`:
- Around line 82-96: getAuthenticatedUser() changed to return null for
unauthenticated principals but lacks `@Nullable` and several callers
(ReadingSessionService.recordSession,
ReadingSessionService.getSessionHeatmapForYear, LibraryService.createLibrary,
AuthorMetadataService.getAllAuthors, ShelfService.getAuthenticatedUserId) still
dereference the result causing potential NPEs; either annotate
AuthenticationService.getAuthenticatedUser() with `@Nullable` and update the
listed callsites to null-check before calling getId()/getPermissions(), or
revert to a non-null contract by restoring a throwing variant (e.g.,
getAuthenticatedUserOrThrow()) and update null-aware callers to call the new
nullable helper (or vice versa) so the contract is explicit and all usages are
audited and fixed.

In `@backend/src/main/java/org/booklore/service/ShelfService.java`:
- Around line 113-116: The helper getAuthenticatedUserId currently dereferences
a possibly null result from authenticationService.getAuthenticatedUser(); change
it to null-check the returned BookLoreUser and throw a clear
IllegalStateException (or similar runtime exception) with a descriptive message
if it's null so callers like createShelf and getUserKoboShelf keep the explicit
contract; update getAuthenticatedUserId to call
authenticationService.getAuthenticatedUser(), verify non-null, then return
user.getId() otherwise throw.

---

Nitpick comments:
In `@backend/src/main/java/org/booklore/config/WebMvcConfig.java`:
- Around line 68-97: Add an explicit resource handler for the SPA entry document
in WebMvcConfig.addResourceHandlers: register "/" and "/index.html" (or
equivalent root path) before the existing /** fallback handler and point it to
"classpath:/static/" with NO_CACHE_REVALIDATE so the cache policy for index.html
is explicit and not coupled to the generic /** fallback handler; update the
handler ordering in addResourceHandlers so the new handler runs before the
existing /** fallback registration.
- Around line 92-97: The resource handler registration in
registry.addResourceHandler currently lists "classpath:/static/assets/" and
"classpath:/static/icons/" before "classpath:/static/", causing extra
file-existence checks for root files; fix by either reordering
addResourceLocations to put "classpath:/static/" first or (preferred) register a
separate handler for the root files (e.g.,
registry.addResourceHandler("/manifest.webmanifest", "/favicon.*") with
addResourceLocations("classpath:/static/") and setCacheControl(ONE_DAY_PUBLIC)),
leaving the existing "/assets/**" and "/icons/**" handler pointing to their
specific locations.

In `@backend/src/main/java/org/booklore/controller/AuthenticationController.java`:
- Around line 73-76: The catch in AuthenticationController (around the token
refresh handling) is too broad and logs expected user-driven failures at ERROR;
change the catch to only handle the specific service exception (e.g., catch
APIException or the specific exception thrown by
AuthenticationService.refreshToken/ApiError.INVALID_CREDENTIALS) and lower the
log level to WARN (log.warn(...)) or remove logging so the global exception
handler can record it; ensure other unexpected exceptions still propagate (do
not swallow) so higher-level handlers can deal with them.

In `@backend/src/main/java/org/booklore/controller/UserController.java`:
- Around line 33-37: Replace the raw Cache-Control header in the
UserController.getMyself() response with Spring's typed CacheControl API: call
ResponseEntity.ok().cacheControl(CacheControl.noStore().mustRevalidate()) (or
CacheControl.noStore() to match project conventions) and remove the manual
.header(...) usage; ensure the CacheControl class
(org.springframework.http.CacheControl) is imported so getMyself() returns the
same semantic cache policy via the typed API.

In `@backend/src/main/java/org/booklore/mapper/BookMapper.java`:
- Around line 41-45: BookService is still constructing PdfViewerPreferences,
NewPdfViewerPreferences, EbookViewerPreferences, and CbxViewerPreferences via
manual builders; replace those manual constructions with the MapStruct mapper
methods to avoid duplicated mapping logic. Concretely, in the places currently
calling the builders (the Pdf/NewPdf/Ebook/Cbx preference creation in
BookService) swap the builder code for calls to
bookMapper.toPdfViewerPreferences(pdfPref),
bookMapper.toNewPdfViewerPreferences(pdfPref),
bookMapper.toEbookViewerPreferences(epubPref), and
bookMapper.toCbxViewerPreferences(cbxPref) respectively, ensuring you pass the
same source entity variables (pdfPref/epubPref/cbxPref) and remove the
now-redundant manual field assignments. Ensure imports and the BookMapper field
usage remain correct and run tests to verify behavior.

In `@backend/src/main/java/org/booklore/service/MenuCountsService.java`:
- Around line 136-156: fetchMagicShelfCounts performs one
bookRepository.count(spec) per shelf and calls
MagicShelfBookService.toSpecification for each, causing N SQL round-trips and N
JSON deserializations; fix by avoiding per-shelf round-trips: either (1) cache
parsed Specifications/parsed filterJson keyed by shelf id (e.g., use a
short-lived in-memory cache or a method-local map to call toSpecification once
per shelf and reuse on repeated refreshes) and reuse them when building counts,
or (2) compute counts in a single aggregate query by translating each shelf spec
into a predicate and combining them into one query that returns counts grouped
by shelf id (so replace repeated bookRepository.count calls with one batched
query). Update fetchMagicShelfCounts to use the chosen approach and adjust error
handling around MagicShelfBookService.toSpecification and bookRepository so
parsing failures for one shelf don’t trigger repeated parsing or queries for
others.
- Around line 50-54: getMenuCounts calls
authenticationService.getAuthenticatedUser(), then
libraryService.getLibraries(user) and shelfService.getShelves(user), which each
re-query the user's libraries/shelves; fix by loading the user with
libraries/shelves once and reusing it: retrieve a fully-initialized
BookLoreUserEntity (e.g. via userRepository.findByIdWithLibraries(user.getId())
or an auth method that returns the entity with associations) and pass that
single resolved entity into libraryService and shelfService (or add overloads
like getLibrariesForEntity/getShelvesForEntity) so you avoid duplicate DB
lookups and/or implement a per-request cache for the resolved user entity used
by getMenuCounts, authenticationService.getAuthenticatedUser(),
libraryService.getLibraries(), shelfService.getShelves(), and
userRepository.findByIdWithLibraries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: df542915-67d7-44cb-b679-919165141d8e

📥 Commits

Reviewing files that changed from the base of the PR and between 03389d7 and 87c9e4b.

📒 Files selected for processing (16)
  • backend/src/main/java/org/booklore/config/WebMvcConfig.java
  • backend/src/main/java/org/booklore/config/security/SecurityConfig.java
  • backend/src/main/java/org/booklore/config/security/filter/AuthenticationCheckFilter.java
  • backend/src/main/java/org/booklore/config/security/service/AuthenticationService.java
  • backend/src/main/java/org/booklore/controller/AuthenticationController.java
  • backend/src/main/java/org/booklore/controller/BookController.java
  • backend/src/main/java/org/booklore/controller/UserController.java
  • backend/src/main/java/org/booklore/mapper/BookMapper.java
  • backend/src/main/java/org/booklore/model/dto/CbxViewerPreferences.java
  • backend/src/main/java/org/booklore/model/dto/NewPdfViewerPreferences.java
  • backend/src/main/java/org/booklore/model/dto/PdfViewerPreferences.java
  • backend/src/main/java/org/booklore/model/dto/response/MenuCountsResponse.java
  • backend/src/main/java/org/booklore/repository/LibraryRepository.java
  • backend/src/main/java/org/booklore/service/MenuCountsService.java
  • backend/src/main/java/org/booklore/service/ShelfService.java
  • backend/src/main/java/org/booklore/service/library/LibraryService.java
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Test Suite / Frontend Tests
  • GitHub Check: Test Suite / Backend Tests
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (java-kotlin)
  • GitHub Check: Frontend Lint Threshold Check
🧰 Additional context used
📓 Path-based instructions (1)
backend/src/**/*.java

📄 CodeRabbit inference engine (AGENTS.md)

backend/src/**/*.java: Use 4-space indentation and match surrounding Java style in backend code
Prefer constructor injection via Lombok patterns already used in the codebase. Do not introduce @Autowired field injection in backend code
Use MapStruct for entity/DTO mapping in backend code
Keep JPA entities on the *Entity suffix in backend code

Files:

  • backend/src/main/java/org/booklore/config/security/SecurityConfig.java
  • backend/src/main/java/org/booklore/model/dto/CbxViewerPreferences.java
  • backend/src/main/java/org/booklore/config/security/filter/AuthenticationCheckFilter.java
  • backend/src/main/java/org/booklore/model/dto/NewPdfViewerPreferences.java
  • backend/src/main/java/org/booklore/repository/LibraryRepository.java
  • backend/src/main/java/org/booklore/controller/UserController.java
  • backend/src/main/java/org/booklore/config/security/service/AuthenticationService.java
  • backend/src/main/java/org/booklore/model/dto/response/MenuCountsResponse.java
  • backend/src/main/java/org/booklore/mapper/BookMapper.java
  • backend/src/main/java/org/booklore/controller/AuthenticationController.java
  • backend/src/main/java/org/booklore/service/ShelfService.java
  • backend/src/main/java/org/booklore/service/library/LibraryService.java
  • backend/src/main/java/org/booklore/service/MenuCountsService.java
  • backend/src/main/java/org/booklore/controller/BookController.java
  • backend/src/main/java/org/booklore/model/dto/PdfViewerPreferences.java
  • backend/src/main/java/org/booklore/config/WebMvcConfig.java
🧠 Learnings (6)
📚 Learning: 2026-04-10T08:15:37.436Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 449
File: booklore-api/src/main/java/org/booklore/service/book/BookDownloadService.java:139-145
Timestamp: 2026-04-10T08:15:37.436Z
Learning: When using Spring `ContentDisposition.builder(...).filename(name, StandardCharsets.UTF_8).build()` (i.e., explicitly providing UTF-8), the resulting header value should include both the quoted `filename="=?UTF-8?..."` and the RFC 5987 `filename*=` parameters. In this case, any extra ASCII fallback computation (e.g., deriving an ASCII `fallbackFilename` via `NON_ASCII_PATTERN` and calling `.filename(fallbackFilename)`) is likely redundant—prefer calling only `.filename(fallbackName?, StandardCharsets.UTF_8)` as appropriate and let Spring handle the UTF-8 header parameters. Verify by comparing the emitted header for `filename` and `filename*` before deciding to keep an ASCII fallback.

Applied to files:

  • backend/src/main/java/org/booklore/config/security/SecurityConfig.java
  • backend/src/main/java/org/booklore/model/dto/CbxViewerPreferences.java
  • backend/src/main/java/org/booklore/config/security/filter/AuthenticationCheckFilter.java
  • backend/src/main/java/org/booklore/model/dto/NewPdfViewerPreferences.java
  • backend/src/main/java/org/booklore/repository/LibraryRepository.java
  • backend/src/main/java/org/booklore/controller/UserController.java
  • backend/src/main/java/org/booklore/config/security/service/AuthenticationService.java
  • backend/src/main/java/org/booklore/model/dto/response/MenuCountsResponse.java
  • backend/src/main/java/org/booklore/mapper/BookMapper.java
  • backend/src/main/java/org/booklore/controller/AuthenticationController.java
  • backend/src/main/java/org/booklore/service/ShelfService.java
  • backend/src/main/java/org/booklore/service/library/LibraryService.java
  • backend/src/main/java/org/booklore/service/MenuCountsService.java
  • backend/src/main/java/org/booklore/controller/BookController.java
  • backend/src/main/java/org/booklore/model/dto/PdfViewerPreferences.java
  • backend/src/main/java/org/booklore/config/WebMvcConfig.java
📚 Learning: 2026-04-14T12:43:08.698Z
Learnt from: balazs-szucs
Repo: grimmory-tools/grimmory PR: 502
File: booklore-api/src/main/java/org/booklore/service/reader/ChapterCacheService.java:0-0
Timestamp: 2026-04-14T12:43:08.698Z
Learning: For this codebase (booklore-api), target Java 25 with `--enable-preview`, so `_` is intentionally used as an unnamed/ignored variable (e.g., lambda parameter or pattern variable) per Java’s preview feature JEP 456. Do not flag `_` in those contexts as an invalid/reserved identifier; only flag it if it’s used in a non-supported position (e.g., where an unnamed variable is not applicable for the Java preview rules).

Applied to files:

  • backend/src/main/java/org/booklore/config/security/SecurityConfig.java
  • backend/src/main/java/org/booklore/model/dto/CbxViewerPreferences.java
  • backend/src/main/java/org/booklore/config/security/filter/AuthenticationCheckFilter.java
  • backend/src/main/java/org/booklore/model/dto/NewPdfViewerPreferences.java
  • backend/src/main/java/org/booklore/repository/LibraryRepository.java
  • backend/src/main/java/org/booklore/controller/UserController.java
  • backend/src/main/java/org/booklore/config/security/service/AuthenticationService.java
  • backend/src/main/java/org/booklore/model/dto/response/MenuCountsResponse.java
  • backend/src/main/java/org/booklore/mapper/BookMapper.java
  • backend/src/main/java/org/booklore/controller/AuthenticationController.java
  • backend/src/main/java/org/booklore/service/ShelfService.java
  • backend/src/main/java/org/booklore/service/library/LibraryService.java
  • backend/src/main/java/org/booklore/service/MenuCountsService.java
  • backend/src/main/java/org/booklore/controller/BookController.java
  • backend/src/main/java/org/booklore/model/dto/PdfViewerPreferences.java
  • backend/src/main/java/org/booklore/config/WebMvcConfig.java
📚 Learning: 2026-04-22T01:56:39.495Z
Learnt from: CR
Repo: grimmory-tools/grimmory PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-22T01:56:39.495Z
Learning: Applies to backend/src/**/*.java : Use MapStruct for entity/DTO mapping in backend code

Applied to files:

  • backend/src/main/java/org/booklore/mapper/BookMapper.java
📚 Learning: 2026-03-25T19:09:09.638Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 189
File: booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java:113-116
Timestamp: 2026-03-25T19:09:09.638Z
Learning: In `booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`, when deciding whether to forward the `Authorization` header to the upstream Kobo server proxy on the `/v1/library/sync` endpoint, a simple null check (`request.getHeader(HttpHeaders.AUTHORIZATION) != null`) is the correct guard. The header is either absent or present — if present it may or may not be a valid token, but validity is determined by the upstream Kobo server, not by Grimmory. Blank/whitespace intermediate states do not occur in practice, and pre-validating the token value before forwarding is not the responsibility of this service.

Applied to files:

  • backend/src/main/java/org/booklore/controller/AuthenticationController.java
📚 Learning: 2026-04-04T15:36:56.558Z
Learnt from: balazs-szucs
Repo: grimmory-tools/grimmory PR: 372
File: booklore-api/src/main/java/org/booklore/app/service/AppBookService.java:357-359
Timestamp: 2026-04-04T15:36:56.558Z
Learning: In `booklore-api/src/main/java/org/booklore/app/service/AppBookService.java`, `shelfId` and `magicShelfId` are mutually exclusive navigation contexts in `getFilterOptions`. A request will never supply both at the same time, so the `else if (shelfId != null)` branch after the `magicBookIds != null` check is intentional and correct — there is no missing shelf-∩-magicShelf intersection case. Do not flag this pattern as a bug.

Applied to files:

  • backend/src/main/java/org/booklore/service/ShelfService.java
  • backend/src/main/java/org/booklore/service/MenuCountsService.java
📚 Learning: 2026-04-16T19:53:00.375Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 507
File: booklore-api/src/main/java/org/booklore/controller/KoboController.java:219-220
Timestamp: 2026-04-16T19:53:00.375Z
Learning: In `booklore-api/src/main/java/org/booklore/controller/KoboController.java`, the `/v1/products/{bookId}/nextread` Kobo endpoint should be registered as GET only (`GetMapping`), not POST. Although calibre-web's kobo.py registers this route with both GET and POST methods as a proxy implementation choice, the Kobo device itself only sends GET requests to this endpoint per imnotjames (PR `#507`).

Applied to files:

  • backend/src/main/java/org/booklore/controller/BookController.java
🔇 Additional comments (9)
backend/src/main/java/org/booklore/mapper/BookMapper.java (1)

41-44: Field names align correctly — no mapping issues detected.

Verification confirms that all entity field names match their corresponding DTO field names exactly. MapStruct's implicit name-based mapping will work correctly for these methods:

  • PdfViewerPreferencesEntity → fields: userId, bookId, zoom, spread, isDarkTheme all match DTO
  • NewPdfViewerPreferencesEntity → fields: userId, bookId, pageSpread, pageViewMode, fitMode, scrollMode, backgroundColor all match DTO
  • EbookViewerPreferenceEntity → fields: userId, bookId, fontFamily, fontSize, gap, hyphenate, isDark, justify, lineHeight, maxBlockSize, maxColumnCount, maxInlineSize, theme, flow all match DTO
  • CbxViewerPreferencesEntity → fields: userId, bookId, pageSpread, pageViewMode, fitMode, scrollMode, backgroundColor all match DTO

No runtime null values will occur from field name mismatches.

backend/src/main/java/org/booklore/repository/LibraryRepository.java (1)

19-20: LGTM — consistent with existing pattern.

Matches the findAllIds() pattern already established in BookdropFileRepository for optimized ID-only retrieval.

backend/src/main/java/org/booklore/model/dto/response/MenuCountsResponse.java (1)

5-11: LGTM.

Clean record DTO for the menu counts aggregate.

backend/src/main/java/org/booklore/config/security/SecurityConfig.java (1)

284-284: LGTM.

Whitelisting /api/v1/app/bootstrap via permitAll() is correct here since this chain's securityMatcher doesn't exclude the path (it's intentionally not added to whitelistedPatterns), so the JWT filter still populates the security context when a token is present, but unauthenticated access is allowed through.

backend/src/main/java/org/booklore/service/ShelfService.java (1)

78-88: LGTM — null-safe overload is a good pattern.

The getShelves(BookLoreUser) overload correctly short-circuits to List.of() when the user or its id is missing, matching the new nullable contract of AuthenticationService.getAuthenticatedUser().

backend/src/main/java/org/booklore/config/security/filter/AuthenticationCheckFilter.java (1)

21-25: This filter is disabled and has no functional impact — consider removing the disabled code.

The @FilterRegistration(enabled = false) annotation means this filter never executes, making the path-matching logic irrelevant. The actual authorization for /api/v1/app/bootstrap is handled by SecurityConfig.requestMatchers() on line 284, which is the only active source of truth. Since this disabled filter has no effect on request handling, either re-enable it with proper integration or remove it to reduce code clutter.

			> Likely an incorrect or invalid review comment.
backend/src/main/java/org/booklore/service/library/LibraryService.java (1)

234-250: LGTM.

The new overload integrates cleanly with the AuthenticationService.getAuthenticatedUser() null-return behavior, and the existing permission-based flow is preserved. Exposing a user-parameter overload also lets callers like MenuCountsService share an already-resolved principal without a second lookup.

backend/src/main/java/org/booklore/controller/BookController.java (1)

84-92: LGTM on the new menu-counts endpoint.

Delegation to MenuCountsService plus the short private cache header is consistent with the other list endpoint change. The service handles unauthenticated users by returning empty/zero aggregates, matching the behavior pattern used by LibraryController.getLibraries().

backend/src/main/java/org/booklore/service/MenuCountsService.java (1)

59-59: No action needed — accessor is correct.

The BookLoreUser.UserPermissions DTO exposes isAdmin() (generated by Lombok from the boolean isAdmin field), and this method is used consistently across the codebase, including in MenuCountsService line 59. The method carries the expected semantics for checking admin privilege. There is no inconsistency between the DTO accessor and other parts of the code.

public ResponseEntity<Map<String, String>> refreshToken(
@Parameter(description = "Refresh token request") @Valid @RequestBody RefreshTokenRequest request) {
return authenticationService.refreshToken(request.getRefreshToken());
public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -e java RefreshTokenRequest.java --exec cat {}

Repository: grimmory-tools/grimmory

Length of output: 398


🏁 Script executed:

#!/bin/bash
# Find and examine the AuthenticationController.java file
fd -e java AuthenticationController.java --exec cat -n {}

Repository: grimmory-tools/grimmory

Length of output: 6241


Restore @Valid annotation for consistency with other endpoints in this controller.

The @Valid annotation is missing from the RefreshTokenRequest parameter. While a manual null/blank check is present (line 66), RefreshTokenRequest declares a @NotBlank constraint that isn't being enforced by the validation framework. Other endpoints in this controller—registerUser (line 48) and loginUser (line 57)—both use @Valid on their request DTOs. Restoring @Valid here maintains consistency and allows the framework to handle validation rather than relying on manual checks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/booklore/controller/AuthenticationController.java`
at line 64, Add the javax.validation.Valid annotation to the RefreshTokenRequest
parameter in the refreshToken method (i.e., change the signature to use `@Valid`
`@RequestBody` RefreshTokenRequest request), ensure the import for
javax.validation.Valid is present, and remove the redundant manual null/blank
checks in refreshToken so the framework enforces the `@NotBlank` constraint
declared on RefreshTokenRequest (consistent with registerUser and loginUser).

Comment thread backend/src/main/java/org/booklore/service/MenuCountsService.java Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
backend/src/main/java/org/booklore/controller/AuthenticationController.java (2)

64-69: Redundant manual validation now that @Valid is restored.

With @Valid on the parameter and @NotBlank declared on RefreshTokenRequest.refreshToken, Spring will reject missing/blank tokens with MethodArgumentNotValidException before this method body executes. The request == null branch is also effectively unreachable for @RequestBody (a missing body raises HttpMessageNotReadableException). Keeping the manual check produces a response shape ({"message": "..."}) that differs from your global validation error handler, yielding inconsistent 400 payloads for the same class of error.

Consider removing the manual guard and letting the framework handle it:

♻️ Proposed simplification
-    public ResponseEntity<?> refreshToken(`@RequestBody` `@Valid` RefreshTokenRequest request) {
-        log.debug("[Auth] Refresh request received. Request object exists: {}", request != null);
-        if (request == null || request.getRefreshToken() == null || request.getRefreshToken().isBlank()) {
-            log.warn("[Auth] Refresh request failed: missing or empty refresh token");
-            return ResponseEntity.badRequest().body(Map.of("message", "Refresh token is missing"));
-        }
-        
-        try {
+    public ResponseEntity<Map<String, String>> refreshToken(`@RequestBody` `@Valid` RefreshTokenRequest request) {
+        try {
             return authenticationService.refreshToken(request.getRefreshToken());
         } catch (Exception e) {
-            log.error("[Auth] Token refresh failed: {}", e.getMessage());
+            log.error("[Auth] Token refresh failed", e);
             throw e;
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/booklore/controller/AuthenticationController.java`
around lines 64 - 69, The manual null/blank guard inside
AuthenticationController.refreshToken (the check on request == null ||
request.getRefreshToken() == null || isBlank() and the
ResponseEntity.badRequest(Map.of("message", ...))) should be removed because
`@RequestBody` `@Valid` plus `@NotBlank` on RefreshTokenRequest.refreshToken will let
Spring's validation and your global exception handlers produce a consistent 400
payload; delete the conditional and its custom log/response, keep the initial
debug log if desired, and rely on your existing
MethodArgumentNotValidException/HttpMessageNotReadableException handlers to
handle missing/blank body cases.

64-64: Return type widened from ResponseEntity<Map<String, String>> to ResponseEntity<?>.

AuthenticationService.refreshToken(...) still returns ResponseEntity<Map<String, String>>. Widening to ? here loses the response contract in the signature and weakens generated OpenAPI/type info for clients. Once the manual error branch is removed (see adjacent comment), the specific generic can be restored.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/booklore/controller/AuthenticationController.java`
at line 64, The controller method AuthenticationController.refreshToken
currently widens its return type to ResponseEntity<?>; change its signature back
to ResponseEntity<Map<String, String>> to match the service method
AuthenticationService.refreshToken(...) so the response contract and
OpenAPI/type information are preserved, and ensure any manual error-branch
handling is reconciled with the specific generic return type (and update imports
if necessary).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/src/main/java/org/booklore/controller/AuthenticationController.java`:
- Around line 73-75: In AuthenticationController's token refresh catch block,
change the logging to pass the Throwable itself instead of only e.getMessage()
so SLF4J will include the stacktrace (i.e., call log.error with the exception as
the last argument for the catch block that currently logs "[Auth] Token refresh
failed: {}"), and consider narrowing the caught exception type or removing the
local catch in favor of a global `@ControllerAdvice` handler if the block only
exists for diagnostics.

In `@backend/src/main/java/org/booklore/service/MenuCountsService.java`:
- Around line 90-94: Replace the duplicated inline "not deleted" Criteria
predicate in fetchShelfCounts with the existing notDeleted() Specification
helper by calling notDeleted().toPredicate(root, query, cb) and using that
Predicate instead of recreating the IS NULL OR = false logic; leave
fetchLibraryCounts as-is (it would need a larger refactor to switch from JPQL to
Criteria) and ensure BookEntity.deleted nullable semantics are preserved by
using the helper's NULL-or-false logic.
- Around line 181-183: Change unshelved() to accept the caller's visibleShelfIds
and build a predicate that excludes books that are on any of those shelves:
update the method signature to unshelved(Set<Long> visibleShelfIds) and replace
the cb.isEmpty(root.get("shelves")) check with a left-join/exists-style
predicate that ensures no joined ShelfEntity id is in visibleShelfIds (i.e. the
book has zero shelves among the user's visibleShelfIds). Then update the
unshelvedBookCount query call to pass visibleShelfIds. Reference symbols:
unshelved(), unshelvedBookCount, visibleShelfIds, BookEntity.shelves.

---

Nitpick comments:
In `@backend/src/main/java/org/booklore/controller/AuthenticationController.java`:
- Around line 64-69: The manual null/blank guard inside
AuthenticationController.refreshToken (the check on request == null ||
request.getRefreshToken() == null || isBlank() and the
ResponseEntity.badRequest(Map.of("message", ...))) should be removed because
`@RequestBody` `@Valid` plus `@NotBlank` on RefreshTokenRequest.refreshToken will let
Spring's validation and your global exception handlers produce a consistent 400
payload; delete the conditional and its custom log/response, keep the initial
debug log if desired, and rely on your existing
MethodArgumentNotValidException/HttpMessageNotReadableException handlers to
handle missing/blank body cases.
- Line 64: The controller method AuthenticationController.refreshToken currently
widens its return type to ResponseEntity<?>; change its signature back to
ResponseEntity<Map<String, String>> to match the service method
AuthenticationService.refreshToken(...) so the response contract and
OpenAPI/type information are preserved, and ensure any manual error-branch
handling is reconciled with the specific generic return type (and update imports
if necessary).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e1fde587-71da-4820-b09b-4721238b6230

📥 Commits

Reviewing files that changed from the base of the PR and between 87c9e4b and 40aaded.

📒 Files selected for processing (3)
  • backend/src/main/java/org/booklore/controller/AuthenticationController.java
  • backend/src/main/java/org/booklore/service/MenuCountsService.java
  • backend/src/main/java/org/booklore/service/book/BookService.java
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test Suite / Frontend Tests
  • GitHub Check: Test Suite / Backend Tests
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (java-kotlin)
🧰 Additional context used
📓 Path-based instructions (1)
backend/src/**/*.java

📄 CodeRabbit inference engine (AGENTS.md)

backend/src/**/*.java: Use 4-space indentation and match surrounding Java style in backend code
Prefer constructor injection via Lombok patterns already used in the codebase. Do not introduce @Autowired field injection in backend code
Use MapStruct for entity/DTO mapping in backend code
Keep JPA entities on the *Entity suffix in backend code

Files:

  • backend/src/main/java/org/booklore/service/book/BookService.java
  • backend/src/main/java/org/booklore/controller/AuthenticationController.java
  • backend/src/main/java/org/booklore/service/MenuCountsService.java
🧠 Learnings (5)
📚 Learning: 2026-03-24T18:46:47.249Z
Learnt from: pedronave
Repo: grimmory-tools/grimmory PR: 173
File: booklore-api/src/main/java/org/booklore/service/hardcover/HardcoverSyncService.java:188-199
Timestamp: 2026-03-24T18:46:47.249Z
Learning: In grimmory-tools/grimmory (HardcoverSyncService.java), when a `hardcoverBookId` is stored in book metadata but is malformed (non-numeric), the preferred behavior is to return null and skip the sync rather than falling back to ISBN-based lookup. The reasoning is that an explicitly set bookId represents deliberate user intent, and silently resolving via ISBN could sync progress to a different book than intended.

Applied to files:

  • backend/src/main/java/org/booklore/service/book/BookService.java
📚 Learning: 2026-04-10T08:15:37.436Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 449
File: booklore-api/src/main/java/org/booklore/service/book/BookDownloadService.java:139-145
Timestamp: 2026-04-10T08:15:37.436Z
Learning: When using Spring `ContentDisposition.builder(...).filename(name, StandardCharsets.UTF_8).build()` (i.e., explicitly providing UTF-8), the resulting header value should include both the quoted `filename="=?UTF-8?..."` and the RFC 5987 `filename*=` parameters. In this case, any extra ASCII fallback computation (e.g., deriving an ASCII `fallbackFilename` via `NON_ASCII_PATTERN` and calling `.filename(fallbackFilename)`) is likely redundant—prefer calling only `.filename(fallbackName?, StandardCharsets.UTF_8)` as appropriate and let Spring handle the UTF-8 header parameters. Verify by comparing the emitted header for `filename` and `filename*` before deciding to keep an ASCII fallback.

Applied to files:

  • backend/src/main/java/org/booklore/service/book/BookService.java
  • backend/src/main/java/org/booklore/controller/AuthenticationController.java
  • backend/src/main/java/org/booklore/service/MenuCountsService.java
📚 Learning: 2026-04-14T12:43:08.698Z
Learnt from: balazs-szucs
Repo: grimmory-tools/grimmory PR: 502
File: booklore-api/src/main/java/org/booklore/service/reader/ChapterCacheService.java:0-0
Timestamp: 2026-04-14T12:43:08.698Z
Learning: For this codebase (booklore-api), target Java 25 with `--enable-preview`, so `_` is intentionally used as an unnamed/ignored variable (e.g., lambda parameter or pattern variable) per Java’s preview feature JEP 456. Do not flag `_` in those contexts as an invalid/reserved identifier; only flag it if it’s used in a non-supported position (e.g., where an unnamed variable is not applicable for the Java preview rules).

Applied to files:

  • backend/src/main/java/org/booklore/service/book/BookService.java
  • backend/src/main/java/org/booklore/controller/AuthenticationController.java
  • backend/src/main/java/org/booklore/service/MenuCountsService.java
📚 Learning: 2026-03-25T19:09:09.638Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 189
File: booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java:113-116
Timestamp: 2026-03-25T19:09:09.638Z
Learning: In `booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`, when deciding whether to forward the `Authorization` header to the upstream Kobo server proxy on the `/v1/library/sync` endpoint, a simple null check (`request.getHeader(HttpHeaders.AUTHORIZATION) != null`) is the correct guard. The header is either absent or present — if present it may or may not be a valid token, but validity is determined by the upstream Kobo server, not by Grimmory. Blank/whitespace intermediate states do not occur in practice, and pre-validating the token value before forwarding is not the responsibility of this service.

Applied to files:

  • backend/src/main/java/org/booklore/controller/AuthenticationController.java
📚 Learning: 2026-04-04T15:36:56.558Z
Learnt from: balazs-szucs
Repo: grimmory-tools/grimmory PR: 372
File: booklore-api/src/main/java/org/booklore/app/service/AppBookService.java:357-359
Timestamp: 2026-04-04T15:36:56.558Z
Learning: In `booklore-api/src/main/java/org/booklore/app/service/AppBookService.java`, `shelfId` and `magicShelfId` are mutually exclusive navigation contexts in `getFilterOptions`. A request will never supply both at the same time, so the `else if (shelfId != null)` branch after the `magicBookIds != null` check is intentional and correct — there is no missing shelf-∩-magicShelf intersection case. Do not flag this pattern as a bug.

Applied to files:

  • backend/src/main/java/org/booklore/service/MenuCountsService.java
🔇 Additional comments (3)
backend/src/main/java/org/booklore/service/book/BookService.java (1)

231-250: LGTM!

The userId is correctly sourced from the authenticated user and populated consistently across PDF, new PDF, and CBX preference builders, matching the pattern already used for the EPUB branch above.

backend/src/main/java/org/booklore/service/MenuCountsService.java (2)

108-140: Shelf visibility filter is now correctly applied.

The prior-review concern about shelf counts leaking other users' shelf IDs is addressed: visibleShelfIds is threaded through and used in the shelfVisibilityPredicate, and the early-return also bails out when the non-admin user has no visible shelves. Looks good.


142-162: ⚠️ Potential issue | 🟠 Major

Per-magic-shelf count() loop can become an N+1 hot spot on a sidebar endpoint.

For each magic shelf, this loop:

  1. Calls magicShelfBookService.toSpecification(userId, shelfId), which internally invokes validateMagicShelfAccessmagicShelfRepository.findById(...) and userRepository.findByIdWithDetails(userId) (per context snippet from MagicShelfBookService).
  2. Issues a separate bookRepository.count(spec.and(notDeleted())) query.

So a user with N magic shelves triggers roughly 3N DB round-trips (user fetch + shelf fetch + count), every time the sidebar is refreshed. Given the PR description markets this endpoint as "lightweight counts for libraries, shelves, and magic-shelves", this is likely to dominate its cost.

Suggestions (any one is a big win):

  • Resolve the user once and pass an overload that skips the per-call userRepository.findByIdWithDetails in validateMagicShelfAccess.
  • Batch all magic shelves in a single query where possible, or at minimum cache access.user() / GroupRule parsing across the loop.
  • Consider short-TTL caching of the menu-counts response itself (it's already backed by the new private, max-age=60 policy on the book endpoints per the PR summary — make sure this endpoint has a similar cache-control header).
⛔ Skipped due to learnings
Learnt from: balazs-szucs
Repo: grimmory-tools/grimmory PR: 372
File: booklore-api/src/main/java/org/booklore/app/service/AppBookService.java:357-359
Timestamp: 2026-04-04T15:36:56.558Z
Learning: In `booklore-api/src/main/java/org/booklore/app/service/AppBookService.java`, `shelfId` and `magicShelfId` are mutually exclusive navigation contexts in `getFilterOptions`. A request will never supply both at the same time, so the `else if (shelfId != null)` branch after the `magicBookIds != null` check is intentional and correct — there is no missing shelf-∩-magicShelf intersection case. Do not flag this pattern as a bug.
Learnt from: pedronave
Repo: grimmory-tools/grimmory PR: 247
File: booklore-api/src/main/java/org/booklore/service/hardcover/HardcoverSyncService.java:156-159
Timestamp: 2026-04-01T17:50:06.817Z
Learning: In grimmory-tools/grimmory (HardcoverSyncService.java), the Hardcover API already returns `user_book_reads` in a consistent, ordered fashion (ascending by recency), so `getLast()` reliably retrieves the most recent read entry. Adding an explicit `order_by` to the `user_book_reads` query is unnecessary.

Comment thread backend/src/main/java/org/booklore/service/MenuCountsService.java
Comment thread backend/src/main/java/org/booklore/service/MenuCountsService.java Outdated
…visible shelf IDs and streamline not deleted predicate
@coderabbitai coderabbitai Bot added backend and removed feature labels Apr 23, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
backend/src/main/java/org/booklore/service/MenuCountsService.java (1)

141-161: Magic-shelf counts are N+1 — one count query (plus an access/parse round-trip) per shelf.

Each iteration calls magicShelfBookService.toSpecification(userId, shelfId) (which does validateMagicShelfAccess + JSON parse on the filter spec) and then bookRepository.count(spec.and(notDeleted())). For users (especially admins) with many magic shelves this fans out into a lot of sequential DB round-trips on what is intended to be a cheap "menu counts" endpoint.

Given magic shelf rules are arbitrary specifications, collapsing to a single query is not trivial. Two pragmatic options to keep this endpoint snappy:

  • Cache MenuCountsResponse per user with a short TTL (seconds), since this endpoint is now behind the private, max-age=60 cache header introduced elsewhere in this PR.
  • If the count for a magic shelf isn't strictly needed on first render, consider lazily fetching per-shelf counts from the frontend on demand.

Not blocking, but worth a follow-up if magic-shelf counts become a hot path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/booklore/service/MenuCountsService.java` around
lines 141 - 161, fetchMagicShelfCounts currently performs N+1 work by calling
magicShelfBookService.toSpecification(userId, shelf.getId()) and
bookRepository.count(...) for each MagicShelf, causing many DB/parse
round-trips; fix by introducing a short-lived per-user cache for the whole
MenuCountsResponse (or at least the magic-shelf counts) so fetchMagicShelfCounts
can return cached Map<Long,Long> for the user when present, falling back to the
current computation only on cache miss and populating the cache with a TTL
(e.g., seconds); reference fetchMagicShelfCounts,
magicShelfBookService.toSpecification, bookRepository.count and the
MenuCountsResponse payload when adding the cache key and TTL logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/src/main/java/org/booklore/service/MenuCountsService.java`:
- Around line 115-139: fetchShelfCounts currently uses root.join("shelves") (an
INNER JOIN) so shelves with zero books are omitted; to ensure every visible
shelf is present, pre-seed the counts Map<Long,Long> with visibleShelfIds mapped
to 0L (using the visibleShelfIds collection) before iterating query results and
overlaying actual counts, or alternatively change the join to a LEFT JOIN on
"shelves" and adjust predicates to preserve empty shelves; update the logic
around join("shelves"), visibleShelfIds and counts to guarantee keys for all
visible shelves.

---

Nitpick comments:
In `@backend/src/main/java/org/booklore/service/MenuCountsService.java`:
- Around line 141-161: fetchMagicShelfCounts currently performs N+1 work by
calling magicShelfBookService.toSpecification(userId, shelf.getId()) and
bookRepository.count(...) for each MagicShelf, causing many DB/parse
round-trips; fix by introducing a short-lived per-user cache for the whole
MenuCountsResponse (or at least the magic-shelf counts) so fetchMagicShelfCounts
can return cached Map<Long,Long> for the user when present, falling back to the
current computation only on cache miss and populating the cache with a TTL
(e.g., seconds); reference fetchMagicShelfCounts,
magicShelfBookService.toSpecification, bookRepository.count and the
MenuCountsResponse payload when adding the cache key and TTL logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 133fe2c7-f84c-42b9-bf13-15fdc403043d

📥 Commits

Reviewing files that changed from the base of the PR and between 40aaded and db9fa3d.

📒 Files selected for processing (1)
  • backend/src/main/java/org/booklore/service/MenuCountsService.java
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Test Suite / Frontend Tests
  • GitHub Check: Test Suite / Backend Tests
  • GitHub Check: Analyze (java-kotlin)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Frontend Lint Threshold Check
🧰 Additional context used
📓 Path-based instructions (1)
backend/src/**/*.java

📄 CodeRabbit inference engine (AGENTS.md)

backend/src/**/*.java: Use 4-space indentation and match surrounding Java style in backend code
Prefer constructor injection via Lombok patterns already used in the codebase. Do not introduce @Autowired field injection in backend code
Use MapStruct for entity/DTO mapping in backend code
Keep JPA entities on the *Entity suffix in backend code

Files:

  • backend/src/main/java/org/booklore/service/MenuCountsService.java
🧠 Learnings (6)
📚 Learning: 2026-04-04T15:36:56.558Z
Learnt from: balazs-szucs
Repo: grimmory-tools/grimmory PR: 372
File: booklore-api/src/main/java/org/booklore/app/service/AppBookService.java:357-359
Timestamp: 2026-04-04T15:36:56.558Z
Learning: In `booklore-api/src/main/java/org/booklore/app/service/AppBookService.java`, `shelfId` and `magicShelfId` are mutually exclusive navigation contexts in `getFilterOptions`. A request will never supply both at the same time, so the `else if (shelfId != null)` branch after the `magicBookIds != null` check is intentional and correct — there is no missing shelf-∩-magicShelf intersection case. Do not flag this pattern as a bug.

Applied to files:

  • backend/src/main/java/org/booklore/service/MenuCountsService.java
📚 Learning: 2026-03-24T18:46:47.249Z
Learnt from: pedronave
Repo: grimmory-tools/grimmory PR: 173
File: booklore-api/src/main/java/org/booklore/service/hardcover/HardcoverSyncService.java:188-199
Timestamp: 2026-03-24T18:46:47.249Z
Learning: In grimmory-tools/grimmory (HardcoverSyncService.java), when a `hardcoverBookId` is stored in book metadata but is malformed (non-numeric), the preferred behavior is to return null and skip the sync rather than falling back to ISBN-based lookup. The reasoning is that an explicitly set bookId represents deliberate user intent, and silently resolving via ISBN could sync progress to a different book than intended.

Applied to files:

  • backend/src/main/java/org/booklore/service/MenuCountsService.java
📚 Learning: 2026-04-01T17:50:06.817Z
Learnt from: pedronave
Repo: grimmory-tools/grimmory PR: 247
File: booklore-api/src/main/java/org/booklore/service/hardcover/HardcoverSyncService.java:156-159
Timestamp: 2026-04-01T17:50:06.817Z
Learning: In grimmory-tools/grimmory (HardcoverSyncService.java), the Hardcover API already returns `user_book_reads` in a consistent, ordered fashion (ascending by recency), so `getLast()` reliably retrieves the most recent read entry. Adding an explicit `order_by` to the `user_book_reads` query is unnecessary.

Applied to files:

  • backend/src/main/java/org/booklore/service/MenuCountsService.java
📚 Learning: 2026-03-26T03:22:24.500Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 209
File: booklore-api/src/main/java/org/booklore/util/FileUtils.java:32-38
Timestamp: 2026-03-26T03:22:24.500Z
Learning: In `grimmory-tools/grimmory`, `BookFileEntity.fileSubPath` (in `booklore-api/src/main/java/org/booklore/model/entity/BookFileEntity.java`) is declared with `Column(name = "file_sub_path", length = 512, nullable = false)`, meaning it is never null. Do not flag null-guard concerns for this field.

Applied to files:

  • backend/src/main/java/org/booklore/service/MenuCountsService.java
📚 Learning: 2026-04-10T08:15:37.436Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 449
File: booklore-api/src/main/java/org/booklore/service/book/BookDownloadService.java:139-145
Timestamp: 2026-04-10T08:15:37.436Z
Learning: When using Spring `ContentDisposition.builder(...).filename(name, StandardCharsets.UTF_8).build()` (i.e., explicitly providing UTF-8), the resulting header value should include both the quoted `filename="=?UTF-8?..."` and the RFC 5987 `filename*=` parameters. In this case, any extra ASCII fallback computation (e.g., deriving an ASCII `fallbackFilename` via `NON_ASCII_PATTERN` and calling `.filename(fallbackFilename)`) is likely redundant—prefer calling only `.filename(fallbackName?, StandardCharsets.UTF_8)` as appropriate and let Spring handle the UTF-8 header parameters. Verify by comparing the emitted header for `filename` and `filename*` before deciding to keep an ASCII fallback.

Applied to files:

  • backend/src/main/java/org/booklore/service/MenuCountsService.java
📚 Learning: 2026-04-14T12:43:08.698Z
Learnt from: balazs-szucs
Repo: grimmory-tools/grimmory PR: 502
File: booklore-api/src/main/java/org/booklore/service/reader/ChapterCacheService.java:0-0
Timestamp: 2026-04-14T12:43:08.698Z
Learning: For this codebase (booklore-api), target Java 25 with `--enable-preview`, so `_` is intentionally used as an unnamed/ignored variable (e.g., lambda parameter or pattern variable) per Java’s preview feature JEP 456. Do not flag `_` in those contexts as an invalid/reserved identifier; only flag it if it’s used in a non-supported position (e.g., where an unnamed variable is not applicable for the Java preview rules).

Applied to files:

  • backend/src/main/java/org/booklore/service/MenuCountsService.java

Comment thread backend/src/main/java/org/booklore/service/MenuCountsService.java
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant