Skip to content

Conversation

@ildyria
Copy link
Member

@ildyria ildyria commented Jan 8, 2026

Summary by CodeRabbit

  • New Features

    • New album endpoints: head, paginated child-albums, and paginated photos; frontend can fetch head/albums/photos separately. New pagination components and composables (infinite scroll, load-more, page navigation).
  • Configuration

    • Admin-configurable pagination UI modes, per-page counts, thresholds; new strict title/description sorting options.
  • UI/UX

    • Album-level access controls affect photo delivery (size downgrades when restricted). Removed deprecated is_accessible flag from config.
  • Tests & Docs

    • Expanded unit/feature/integration tests and full pagination documentation.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

Adds paginated album endpoints and frontend pagination UI/state, permission-driven photo downgrade flags, repository layers, new resources and requests, pagination config/enum and migrations, SizeVariants/Watermarker refactors, many controller/resource call-site updates, tests, and extensive documentation.

Changes

Cohort / File(s) Summary
API Controllers & Routes
app/Http/Controllers/Gallery/AlbumHeadController.php, app/Http/Controllers/Gallery/AlbumChildrenController.php, app/Http/Controllers/Gallery/AlbumPhotosController.php, routes/api_v2.php
New controllers and routes for album head, paginated albums and photos; DI and middleware applied.
Requests / Validation
app/Http/Requests/Album/GetAlbumHeadRequest.php, app/Http/Requests/Album/GetAlbumChildrenRequest.php, app/Http/Requests/Album/GetAlbumPhotosRequest.php, app/Contracts/Http/Requests/RequestAttribute.php
New FormRequests with authorization (AlbumPolicy), page handling, album resolution; new PAGE_ATTRIBUTE constant.
Repositories & Sorting
app/Repositories/AlbumRepository.php, app/Repositories/PhotoRepository.php, app/Models/Extensions/SortingDecorator.php, app/Enum/ColumnSorting*.php
New Album/Photo repositories for paginated queries; SortingDecorator split into SQL/PHP phases; added strict sorting enum cases.
Resources: Head & Pagination
app/Http/Resources/Models/Head*, app/Http/Resources/Collections/PaginatedAlbumsResource.php, app/Http/Resources/Collections/PaginatedPhotosResource.php, app/Http/Resources/GalleryConfigs/InitConfig.php, app/Enum/PaginationMode.php
New Spatie Data head resources and paginated collection resources; InitConfig gains pagination properties and PaginationMode enum added.
Photo / SizeVariants / Embed refactor
app/Http/Resources/Models/PhotoResource.php, app/Http/Resources/Models/SizeVariantsResouce.php, app/Http/Resources/Embed/*, app/Http/Resources/Search/ResultsResource.php, app/Http/Resources/Collections/PositionDataResource.php
Resource constructors changed to accept album_id and should_downgrade flags; removed photo rights propagation; embed/search/position data updated accordingly.
Controllers & Actions updates
app/Http/Controllers/Gallery/AlbumController.php, app/Http/Controllers/Gallery/FrameController.php, app/Http/Controllers/Gallery/PhotoController.php, app/Http/Controllers/Gallery/EmbedController.php, app/Actions/*
Replaced positional resource calls with named args; introduced Gate/ConfigManager checks to compute downgrade flags; AlbumController flow simplified.
Frontend pagination components & composables
resources/js/components/pagination/*.vue, resources/js/composables/pagination/usePagination.ts
New Pagination component with Infinite/LoadMore/PageNav modes and a usePagination hook implementing pagination state/actions.
Frontend stores, services & UI wiring
resources/js/stores/AlbumState.ts, resources/js/stores/PhotosState.ts, resources/js/stores/PhotoState.ts, resources/js/stores/LycheeState.ts, resources/js/services/album-service.ts
Album store adopts Head* types, adds paginated state/actions; PhotosState gains append/rebuild navigation; PhotoState removes rights getter; LycheeState and AlbumService updated for new endpoints and cache keys.
Frontend components & composables updates
resources/js/components/gallery/*, resources/js/views/*, resources/js/composables/*, resources/js/composables/contextMenus/contextMenu.ts
Replaced old paginator usage, switched many permission checks from photo rights to albumStore.rights, adjusted composable signatures (canInteractPhoto, useBuyMeActions), narrowed context menu types to Head* variants.
Watermarker & Middleware
app/Image/Watermarker.php, app/Http/Middleware/ResolveConfigs.php, app/Actions/Photo/Pipes/Standalone/ApplyWatermark.php, app/Http/Resources/Rights/ModulesRightsResource.php, tests under ImageProcessing
Watermarker refactored (is_watermark_enabled, check_watermark_image, can_watermark); middleware binds Watermarker; call-sites/tests updated to use can_watermark().
Factories, Relations & small model changes
app/Factories/AlbumFactory.php, app/Relations/HasAlbumThumb.php, app/Models/Order.php
Eager-load access_permissions earlier in factory; HasAlbumThumb uses default sorting; Order model now eager-loads user.
Migrations & Config keys
database/migrations/2026_01_07_add_pagination_config_keys.php, database/migrations/2026_01_10_212900_add_strict_ordering.php, various migration hygiene changes
Added pagination config keys and migration for strict ordering; several migrations made idempotent via dropIfExists.
Tests
tests/Feature_v2/Album/*, tests/Feature_v2/PaginationIntegrationTest.php, tests/Unit/Repositories/AlbumRepositoryTest.php, tests/ImageProcessing/*
New endpoint/unit/integration tests for paginated endpoints and repositories; many tests updated to reflect removed is_accessible and Watermarker API change.
Docs, specs & i18n
docs/specs/4-architecture/features/007-pagination/*, docs/specs/2-how-to/configure-pagination.md, docs/specs/3-reference/api-design.md, multiple lang/* files
Full pagination feature plan, spec, tasks and how-to docs added; many locale files extended with pagination and new settings translations; open questions resolved.
Misc & config tweaks
config/debugbar.php, app/Http/Resources/GalleryConfigs/AlbumConfig.php, app/Actions/Album/PositionData.php, others
Debugbar backtrace numeric tweak; AlbumConfig drops is_accessible; small action/resource call-site adjustments and formatting changes across codebase.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hopped through routes and pages late at night,
I taught the photos when to hide from light.
I stitched configs, tests, and components in a row,
I paged and cached and let the tiny interfaces grow.
A rabbit counts pages — nudge — and whispers: all is right.

🚥 Pre-merge checks | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.

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


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.

@codecov
Copy link

codecov bot commented Jan 8, 2026

Codecov Report

❌ Patch coverage is 95.55556% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.45%. Comparing base (c8bcd60) to head (6b42070).
⚠️ Report is 5 commits behind head on master.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

ildyria and others added 8 commits January 9, 2026 11:14
…points

- Add config property to AlbumHeadResource for frontend rendering
- Create album-service methods: getHead(), getAlbums(), getPhotos()
- Extend AlbumState store with pagination state and actions:
  - loadHead(), loadAlbums(), loadPhotos() for new endpoints
  - loadMorePhotos(), loadMoreAlbums() for infinite scroll/load more
  - loadPaginated() to orchestrate parallel loading
- Add PhotosState.appendPhotos() for timeline-aware photo appending
- Create PaginationLoadMore.vue and PaginationInfiniteScroll.vue components
- Create usePagination.ts composable for shared pagination logic
- Integrate PaginationLoadMore into AlbumPanel for photos and albums
- Update tasks.md with implementation progress

Spec impact: docs/specs/4-architecture/features/007-pagination/tasks.md updated

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@ildyria ildyria marked this pull request as ready for review January 10, 2026 16:19
@ildyria ildyria requested a review from a team as a code owner January 10, 2026 16:19
Copy link

@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: 16

Caution

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

⚠️ Outside diff range comments (3)
docs/specs/4-architecture/open-questions.md (1)

17-1343: Remove resolved questions per coding guidelines.

The coding guidelines explicitly state: "Track unresolved high- and medium-impact questions here. Remove each row as soon as it is resolved, ensuring the answer is captured first in the governing spec's normative sections."

Currently, the Question Details section (lines 17-1343) contains numerous resolved questions marked with strikethrough. These should be removed entirely now that they're resolved and captured in the governing specs. Keeping resolved questions in this file:

  • Violates the documented process
  • Makes the file unnecessarily large and harder to navigate
  • Obscures any future active questions

The Active Questions table correctly shows "No active questions" — the Question Details section should reflect this by removing all resolved entries.

Based on coding guidelines: "Remove each row as soon as it is resolved, ensuring the answer is captured first in the governing spec's normative sections."

app/Http/Resources/Models/PhotoResource.php (1)

68-93: Constructor signature change correctly applied; however, fix eager-loading of tags in FlowItemResource.

The constructor signature updates are in place across all call sites (PhotoController, HasPrepPhotoCollection, TimelineResource, ResultsResource, GetTagWithPhotos, FrameController). However, FlowItemResource.setPhotos() at line 110 loads ['all_photos', 'all_photos.size_variants', 'all_photos.palette', 'all_photos.statistics'] but omits 'all_photos.tags'. Since PhotoResource.__construct() accesses $photo->tags->pluck('name')->all(), this will trigger N+1 queries when flow_include_photos_from_children is true. Add 'all_photos.tags' to the load() call.

resources/js/composables/contextMenus/contextMenu.ts (1)

109-131: Type cast to AlbumResource should be HeadAlbumResource.

The Selectors.album type was updated to use HeadAlbumResource | HeadTagAlbumResource | HeadSmartAlbumResource, but line 110 still casts to AlbumResource. This type mismatch could cause TypeScript errors or runtime issues.

🔧 Suggested fix
 		if (selectors.config?.value?.is_model_album === true && selectors.album !== undefined) {
-			const parent_album = selectors.album.value as App.Http.Resources.Models.AlbumResource;
+			const parent_album = selectors.album.value as App.Http.Resources.Models.HeadAlbumResource;
🧹 Nitpick comments (25)
resources/js/components/forms/album/AlbumVisibility.vue (1)

73-73: Optional: Fix typo in variable name.

The variable name can_pasword_protect has a typo (missing 's' in 'password'). Consider renaming to can_password_protect for correctness.

♻️ Suggested refactor

Update the variable name on line 161:

-const can_pasword_protect = ref<boolean>(albumStore.album?.rights.can_pasword_protect ?? false);
+const can_password_protect = ref<boolean>(albumStore.album?.rights.can_password_protect ?? false);

And update the references on lines 73 and 91:

-		v-if="!can_pasword_protect && is_password_required"
+		v-if="!can_password_protect && is_password_required"
-		v-else-if="can_pasword_protect"
+		v-else-if="can_password_protect"

Note: This may also require updating the backend property name if it uses the same typo.

Also applies to: 91-91, 161-161

resources/js/views/gallery-panels/Tag.vue (1)

28-28: Remove or annotate commented-out code.

Multiple sections are commented out: PhotoEdit component (line 28), its import (line 93), toggleEdit function (lines 197-203), and privileged keyboard shortcuts (lines 232-234).

Leaving commented code in the codebase creates maintenance burden. Consider either:

  1. Removing the code entirely if no longer needed
  2. Adding a TODO comment explaining why it's temporarily disabled and when it should be restored

Also applies to: 93-93, 197-203, 232-234

resources/js/stores/PhotosState.ts (1)

42-48: Consider maintaining chronological order for appended timeline groups.

When appending new timeline groups that don't match existing headers, they are pushed to the end of the array (line 47). If photos are loaded in paginated batches, this should maintain order. However, if appended photos have timeline dates that precede existing groups, the display order may be incorrect.

If chronological ordering is required, consider sorting photosTimeline by the timeline date after merging.

docs/specs/4-architecture/features/007-pagination/tasks.md (1)

60-64: Consider unit test coverage for maintainability.

Multiple layers lack unit test coverage:

  • Repository pagination methods (T-007-07)
  • Service methods (T-007-21)
  • Store pagination logic (T-007-23)
  • Pagination UI components (T-007-28)

While integration tests provide some coverage, unit tests improve:

  • Faster test execution
  • Easier debugging of failures
  • Better documentation of expected behavior
  • Safer refactoring

Consider creating at least unit tests for the repository and store logic as a recommended follow-up.

Also applies to: 157-161, 171-175, 204-208

app/Http/Resources/Timeline/TimelineResource.php (1)

79-80: Consider injecting ConfigManager instead of resolving on every call.

The resolve() call happens on every invocation of fromData. Since this is a static factory method that could be called frequently during pagination, consider either:

  • Injecting ConfigManager as a constructor parameter if TimelineResource becomes a service
  • Using request()->configs() directly (already available in this codebase pattern)
♻️ Proposed refactor using request()->configs()
-	$config_manager = resolve(ConfigManager::class);
-	$should_downgrade = !$config_manager->getValueAsBool('grants_full_photo_access');
+	$should_downgrade = !request()->configs()->getValueAsBool('grants_full_photo_access');
resources/js/components/forms/album/ConfirmSharingDialog.vue (1)

4-4: LGTM: Refactored to use Tailwind spacing units.

The width classes changed from arbitrary values md:w-[500px] max-w-[500px] to standard Tailwind spacing md:w-125 max-w-125. Both resolve to 500px (125 × 4px), so this is functionally equivalent while following Tailwind conventions.

app/Http/Controllers/Gallery/AlbumController.php (1)

89-98: Dead assignment on line 89.

The $album_resource = null; assignment is immediately overwritten by the match expression and can be removed.

🧹 Suggested cleanup
-		$album_resource = null;
-
-		$album_resource = match (true) {
+		$album_resource = match (true) {
			$request->album() instanceof BaseSmartAlbum => new SmartAlbumResource($request->album()),
			$request->album() instanceof TagAlbum => new TagAlbumResource($request->album()),
			$request->album() instanceof Album => new AlbumResource($request->album()),
			// @codeCoverageIgnoreStart
			default => throw new LycheeLogicException('This should not happen'),
			// @codeCoverageIgnoreEnd
		};
resources/js/lychee.d.ts (1)

582-589: TypeScript generation creates redundant null | null type union.

The statistics property in HeadSmartAlbumResource and SmartAlbumResource is generated as null | null, which is redundant and should be simplified to null. This occurs because the PHP source intentionally declares public null $statistics = null; to unify API responses across resource types, and the Spatie TypeScriptTransformer generates the union type redundantly. While this doesn't cause functional issues, consider:

  • Filing an issue with Spatie TypeScriptTransformer if this is a bug in their null type handling, or
  • Adding a type replacement rule in config/typescript-transformer.php if there's a workaround available.
resources/js/components/pagination/PaginationLoadMore.vue (1)

1-5: Consider simplifying Button props.

The Button has both :loading="loading" and :disabled="loading". PrimeVue's Button component typically disables interactions automatically when in loading state, making the explicit :disabled prop potentially redundant.

♻️ Potential simplification
-		<Button :label="buttonLabel" :loading="loading" :disabled="loading" severity="secondary" class="rounded" @click="emit('loadMore')" />
+		<Button :label="buttonLabel" :loading="loading" severity="secondary" class="rounded" @click="emit('loadMore')" />
app/Http/Resources/Embed/EmbedAlbumResource.php (1)

37-41: Policy-driven downgrade logic correctly applied.

The integration of Gate-based access control for embedded photos is correct and consistent with the PR's broader refactoring pattern.

Minor observation: The negation here uses !Gate::check(...) while app/Actions/Album/PositionData.php uses === false. Both are functionally correct for boolean results, but consistent style across the codebase would be slightly cleaner.

app/Http/Controllers/Gallery/AlbumChildrenController.php (1)

23-27: Remove misleading constructor comment.

The comment "Prevent instantiation" is incorrect. This is a standard Laravel dependency injection pattern that enables instantiation with the injected repository. The constructor should either have no comment or a meaningful one describing its purpose.

📝 Proposed fix
 	public function __construct(
 		private AlbumRepository $album_repository,
 	) {
-		// Prevent instantiation
 	}
app/Http/Requests/Album/GetAlbumChildrenRequest.php (1)

68-68: Optional: Remove redundant type coercion.

The intval() call is redundant since the validation rule 'integer' already ensures the value is an integer type, and the ?? 1 fallback provides an integer default.

♻️ Simplify type handling
-		$this->page = intval($values[RequestAttribute::PAGE_ATTRIBUTE] ?? 1);
+		$this->page = $values[RequestAttribute::PAGE_ATTRIBUTE] ?? 1;
app/Http/Controllers/Gallery/AlbumHeadController.php (2)

48-48: Optional: Remove unnecessary null initialization.

The $album_resource variable is initialized to null but immediately overwritten by the match expression on line 50. The initialization can be removed since the match expression is exhaustive and always assigns a value.

♻️ Simplify variable initialization
 		$config = new AlbumConfig($request->album());
-		$album_resource = null;
-
-		$album_resource = match (true) {
+
+		$album_resource = match (true) {
 			$request->album() instanceof BaseSmartAlbum => new HeadSmartAlbumResource($request->album()),
 			$request->album() instanceof TagAlbum => new HeadTagAlbumResource($request->album()),
 			$request->album() instanceof Album => new HeadAlbumResource($request->album()),

59-59: Optional: Cache album reference to avoid repeated method calls.

The $request->album() method is called three times on line 59. While this is a minor performance consideration, caching the reference improves readability.

♻️ Cache album reference
 		$config = new AlbumConfig($request->album());
+		$album = $request->album();

-		$album_resource = match (true) {
-			$request->album() instanceof BaseSmartAlbum => new HeadSmartAlbumResource($request->album()),
-			$request->album() instanceof TagAlbum => new HeadTagAlbumResource($request->album()),
-			$request->album() instanceof Album => new HeadAlbumResource($request->album()),
+		$album_resource = match (true) {
+			$album instanceof BaseSmartAlbum => new HeadSmartAlbumResource($album),
+			$album instanceof TagAlbum => new HeadTagAlbumResource($album),
+			$album instanceof Album => new HeadAlbumResource($album),
 			// @codeCoverageIgnoreStart
 			default => throw new LycheeLogicException('This should not happen'),
 			// @codeCoverageIgnoreEnd
 		};

-		AlbumVisit::dispatchIf((MetricsController::shouldMeasure() && $request->album() instanceof BaseAlbum), $this->visitorId(), $request->album()->get_id());
+		AlbumVisit::dispatchIf((MetricsController::shouldMeasure() && $album instanceof BaseAlbum), $this->visitorId(), $album->get_id());

 		return new HeadAbstractAlbumResource($config, $album_resource);
database/migrations/2026_01_07_add_pagination_config_keys.php (1)

20-32: Consider using POSITIVE type_range for per-page values.

Both albums_per_page and photos_per_page use self::INT which allows zero or negative values. A value of 0 or negative would likely cause issues with pagination. Consider using self::POSITIVE to enforce valid positive integers.

Proposed fix
 			[
 				'key' => 'albums_per_page',
 				'value' => '30',
 				'cat' => self::MOD_GALLERY,
-				'type_range' => self::INT,
+				'type_range' => self::POSITIVE,
 				'description' => 'Number of sub-albums per page.',
 			[
 				'key' => 'photos_per_page',
 				'value' => '100',
 				'cat' => self::MOD_GALLERY,
-				'type_range' => self::INT,
+				'type_range' => self::POSITIVE,
 				'description' => 'Number of photos per page.',

Also applies to: 59-71

resources/js/components/pagination/Pagination.vue (1)

56-56: totalPages computed property is redundant.

The totalPages computed is just an alias for props.lastPage. Consider using props.lastPage directly in the template condition (Line 22) to reduce indirection.

Proposed simplification
-<div v-else-if="mode === 'page_navigation' && totalPages > 1" class="flex justify-center w-full py-4">
+<div v-else-if="mode === 'page_navigation' && lastPage > 1" class="flex justify-center w-full py-4">

And remove:

-const totalPages = computed(() => props.lastPage);
resources/js/components/gallery/albumModule/AlbumPanel.vue (1)

221-229: goTo*Page() should ideally await loads (or return the Promise) for smoother UI coordination.
If Pagination.vue wants to show loading state based on the handler’s lifetime, returning/awaiting the Promise makes it easier to chain UI behavior (e.g., scroll-to-top after data arrives).

Possible tweak
-function goToPhotosPage(page: number) {
+function goToPhotosPage(page: number): Promise<void> {
 	photosStore.reset();
-	albumStore.loadPhotos(page, false);
+	return albumStore.loadPhotos(page, false);
 }
 
-function goToAlbumsPage(page: number) {
+function goToAlbumsPage(page: number): Promise<void> {
 	albumsStore.reset();
-	albumStore.loadAlbums(page, false);
+	return albumStore.loadAlbums(page, false);
 }
resources/js/composables/pagination/usePagination.ts (1)

26-88: Solid, minimal pagination composable; consider the “unknown lastPage” UX edge.
If lastPage=0 means “unknown yet”, goToPage(1) currently no-ops; that’s fine if initial load is always handled elsewhere, but worth confirming.

resources/js/services/album-service.ts (1)

67-76: Fix typo and consider centralizing hard-coded page limits.

The comment on line 64 has a typo: "we now" should be "we know". Additionally, hard-coded page ceilings (legacy: 10, new: 50) risk leaving cached entries stale if users browse beyond those limits. The axios-cache-interceptor library doesn't provide a built-in prefix-based eviction API, so consider extracting page limits to named constants (e.g., MAX_LEGACY_PAGES = 10, MAX_PAGINATED_PAGES = 50) to make future updates easier, or implement a custom storage adapter if broader prefix-based eviction becomes necessary.

tests/Feature_v2/Album/AlbumPhotosEndpointTest.php (1)

26-127: Tests are solid, but hard-coding default per_page and exact validation messages is brittle

per_page => 100 and 'The album id field is required.' will break if defaults/localization change. If there’s an existing helper in BaseApiWithDataTest for pagination assertions (or a seeded config accessor), prefer that over hard-coded values/messages.

tests/Feature_v2/Album/AlbumChildrenEndpointTest.php (1)

26-108: Same brittleness here: avoid hard-coding per_page => 30 and exact validation message

Consider asserting the pagination shape (keys present, ints, totals) or deriving the expected defaults from the seeded config/test fixtures rather than embedding literals.

resources/js/components/pagination/PaginationInfiniteScroll.vue (1)

95-104: Observer isn’t recreated if threshold, resourceType, or rootMargin change

If any of those props can change during the component lifetime, you’ll keep an observer with stale options. Consider watching them and recreating the observer (disconnect + setup).

app/Http/Resources/Models/HeadAlbumResource.php (1)

52-53: Uninitialized nullable property $editable may cause issues.

The $editable property is declared as nullable but is only assigned when $this->rights->can_edit is true. Without explicit initialization to null, Spatie Data may encounter issues when serializing.

♻️ Suggested fix
 	public ?EditableBaseAlbumResource $editable;
+	public ?EditableBaseAlbumResource $editable = null;
app/Http/Resources/Models/HeadTagAlbumResource.php (1)

47-47: Uninitialized nullable property $editable.

Same as HeadAlbumResource - the nullable $editable property should be explicitly initialized to null.

♻️ Suggested fix
-	public ?EditableBaseAlbumResource $editable;
+	public ?EditableBaseAlbumResource $editable = null;
resources/js/stores/AlbumState.ts (1)

152-152: Debug logging statements present.

Lines 152 and 187 contain console.debug statements. These are useful for development but consider removing them before final release or gating behind a debug flag.

Also applies to: 187-187

Copy link

@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: 16

Caution

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

⚠️ Outside diff range comments (2)
database/migrations/2025_12_22_163233_create_xhprof_table.php (1)

28-28: Column name with space violates Laravel and PSR-4 conventions.

The column name 'server name' (line 28) contains a space, which violates Laravel's snake_case convention for database columns. While Laravel's Schema builder will quote this identifier automatically, this creates technical debt:

  • Raw SQL queries require manual backtick quoting
  • External database tools may have issues
  • Inconsistent with other columns in the schema
  • Violates PSR-4 coding standards specified in the guidelines

As per coding guidelines, variable and column names should use snake_case.

🔧 Rename column to use snake_case
-		$table->char('server name', 64)->nullable();
+		$table->char('server_name', 64)->nullable();

And update the composite index:

-		$table->index(['server name', 'timestamp']);
+		$table->index(['server_name', 'timestamp']);

Also applies to: 46-46

resources/js/stores/AlbumState.ts (1)

283-364: Bug: load() sets config after loading photos for tag/smart albums.
That means loadPhotos() sees this.config as undefined and may disable timeline merging incorrectly. Set this.config before calling loadPhotos() (and consider reusing loadHead() to reduce duplication).

Proposed fix
 				if (data.data.config.is_model_album) {
 					this.config = data.data.config;
 					await Promise.all([this.loadAlbums(1, false), this.loadPhotos(1, false)]);
 					this.modelAlbum = data.data.resource as App.Http.Resources.Models.HeadAlbumResource;
 				} else {
 					// Tag/Smart albums: Only load photos (they don't have child albums)
-					await this.loadPhotos(1, false);
 					this.config = data.data.config;
+					await this.loadPhotos(1, false);
 					if (data.data.config.is_base_album) {
 						// Tag album: Photos filtered by tag
 						this.tagAlbum = data.data.resource as App.Http.Resources.Models.HeadTagAlbumResource;
 					} else {
 						// Smart album: Recent, Starred, On This Day, Unsorted, Untagged
 						this.smartAlbum = data.data.resource as App.Http.Resources.Models.HeadSmartAlbumResource;
 					}
 				}
🧹 Nitpick comments (16)
app/Relations/HasAlbumThumb.php (1)

226-229: Consider caching the sorting criterion if performance becomes a concern.

The change from using a per-instance $this->sorting property to calling PhotoSortingCriterion::createDefault() on each getResults() invocation ensures that sorting always reflects the current configuration. However, this means resolving the ConfigManager from the service container and creating a new PhotoSortingCriterion instance on every call.

While the ConfigManager likely caches configuration values and this method may not be called frequently, you could optimize by caching the result if profiling indicates this is a hot path.

♻️ Optional optimization: cache the default sorting criterion

If performance profiling indicates this is beneficial, you could cache the default sorting criterion as a static property or memoize it within the method:

public function getResults(): ?Thumb
{
    // ... existing code ...
    
    static $defaultSorting = null;
    if ($defaultSorting === null) {
        $defaultSorting = PhotoSortingCriterion::createDefault();
    }
    
    return Thumb::createFromQueryable(
        $this->getRelationQuery(),
        $defaultSorting
    );
}

Note: Only apply this optimization if the current approach proves to be a performance bottleneck, as the change appears intentional to ensure consistency with the pagination and sorting refactoring in this PR.

app/Http/Middleware/ResolveConfigs.php (1)

33-34: Consider lazy instantiation to avoid unnecessary DB queries.

The Watermarker is instantiated eagerly on every request, which may trigger a database query in its constructor (via check_watermark_image()) if watermarking is enabled—even when the Watermarker is never used in that request. This contradicts the PR's goal of reducing queries.

⚡ Proposed fix for lazy instantiation
-		$watermarker = new Watermarker();
-		app()->scoped(Watermarker::class, fn () => $watermarker);
+		app()->scoped(Watermarker::class, fn () => new Watermarker());

This ensures the Watermarker is only instantiated when first resolved from the container, deferring the database query until it's actually needed.

app/Image/Watermarker.php (2)

186-192: Consider using can_watermark() for consistency.

The do() method calls is_watermark_enabled() and check_watermark_image() separately, but the newly introduced public method can_watermark() combines both checks. Using can_watermark() would make the code more consistent and reduce duplication.

♻️ Optional refactor
-		if (!$this->is_watermark_enabled()) {
-			return;
-		}
-
-		if (!$this->check_watermark_image()) {
+		if (!$this->can_watermark()) {
			return;
		}

194-194: Inject ConfigManager via constructor for consistency.

The do() method resolves ConfigManager directly using resolve(), which is inconsistent with the dependency injection pattern used elsewhere in the codebase (e.g., ApplyWatermark constructor injects both ConfigManager and Watermarker). Injecting it via the constructor would improve testability and consistency. As per coding guidelines, prefer constructor injection over service location.

♻️ Proposed refactor

Add ConfigManager to the constructor:

+	public function __construct(
+		private readonly ConfigManager $config_manager,
+	) {
-	public function __construct()
-	{
		$this->naming_strategy = new WatermarkGroupedWithRandomSuffixNamingStrategy();

		if (!$this->is_watermark_enabled()) {
			return;
		}

Update method calls to use the injected instance:

-		$config_manager = resolve(ConfigManager::class);
-		$is_enabled = $config_manager->getValueAsBool('watermark_enabled');
+		$is_enabled = $this->config_manager->getValueAsBool('watermark_enabled');

Apply similar changes to all resolve(ConfigManager::class) calls in the class (lines 52, 136, 162, 194).

database/migrations/2025_12_22_163233_create_xhprof_table.php (1)

17-19: Consider consolidating redundant driver checks.

The MySQL driver check at lines 17-19 causes an early return for non-MySQL databases, making the second check at lines 49-53 redundant. While this defensive coding may prevent issues if the early return is removed, it's currently unreachable for non-MySQL drivers.

♻️ Consolidate driver checks
 public function up()
 {
 	if (DB::connection()->getDriverName() !== 'mysql') {
 		return;
 	}

 	Schema::dropIfExists('details');
 	Schema::create('details', function (Blueprint $table) {
 		$table->id('idcount');
 		$table->char('id', 64);
 		$table->char('url', 255)->nullable();
 		$table->char('c_url', 255)->nullable();
 		$table->timestamp('timestamp')->useCurrent()->useCurrentOnUpdate();
 		$table->char('server name', 64)->nullable();
 		$table->binary('perfdata')->nullable();
 		$table->tinyInteger('type')->nullable();
 		$table->binary('cookie')->nullable();
 		$table->binary('post')->nullable();
 		$table->binary('get')->nullable();
 		$table->integer('pmu')->nullable();
 		$table->integer('wt')->nullable();
 		$table->integer('cpu')->nullable();
 		$table->char('server_id', 64)->nullable();
 		$table->char('aggregateCalls_include', 255)->nullable();

 		$table->index('url');
 		$table->index('c_url');
 		$table->index('cpu');
 		$table->index('wt');
 		$table->index('pmu');
 		$table->index('timestamp');
 		$table->index(['server name', 'timestamp']);
 	});

-	if (DB::connection()->getDriverName() === 'mysql') {
-		DB::statement('ALTER TABLE details MODIFY COLUMN `perfdata` LONGBLOB');
-		DB::statement('ALTER TABLE details MODIFY COLUMN `cookie` LONGBLOB');
-		DB::statement('ALTER TABLE details MODIFY COLUMN `post` LONGBLOB');
-	}
+	DB::statement('ALTER TABLE details MODIFY COLUMN `perfdata` LONGBLOB');
+	DB::statement('ALTER TABLE details MODIFY COLUMN `cookie` LONGBLOB');
+	DB::statement('ALTER TABLE details MODIFY COLUMN `post` LONGBLOB');
 }

Also applies to: 49-53

app/Enum/ColumnSortingType.php (1)

23-26: Clarify semantics of “STRICT” vs non-strict (DB vs natural sorting) to avoid user surprise.

Enums are fine, but the comment alone may not be enough for consumers/UI text: ensure it’s clear that TITLE/DESCRIPTION may still use non-DB “natural” sorting (potentially per-page with pagination), while *_STRICT is DB ordering (globally consistent across pages). Based on learnings, this trade-off is acceptable, but it should be documented/config-disclaimed.

docs/specs/4-architecture/features/007-pagination/tasks.md (1)

245-259: Duplicate task IDs: T-007-35 and T-007-35a..f collide with T-007-35 – Run full frontend quality gate.

Consider renumbering to keep IDs unique and stable for cross-references.

app/Http/Requests/Album/GetAlbumPhotosRequest.php (2)

71-78: Consider excluding 'access_permissions' relation.

The code excludes several heavy relations like 'owner' and 'statistics', but doesn't exclude 'access_permissions' which could involve additional joins. Consider whether this relation is needed for the photos endpoint.


39-42: Add PasswordRequiredException handling to match similar request classes.

GetAlbumHeadRequest and GetAlbumRequest both throw PasswordRequiredException in their authorize() methods to display the password dialog when accessing password-protected albums. GetAlbumPhotosRequest should include the same pattern for consistent UX behavior:

public function authorize(): bool
{
    $result = Gate::check(AlbumPolicy::CAN_ACCESS, [AbstractAlbum::class, $this->album]);
    
    // In case of a password protected album, we must throw an exception
    // with a special error message ("Password required") such that the
    // front-end shows the password dialog if a password is set, but
    // does not show the dialog otherwise.
    if (
        !$result &&
        $this->album instanceof BaseAlbum &&
        $this->album->public_permissions()?->password !== null
    ) {
        throw new PasswordRequiredException();
    }
    
    return $result;
}
tests/Unit/Repositories/AlbumRepositoryTest.php (3)

81-98: Pagination coverage is fine, but consider asserting item identity too.
Right now it only asserts counts; optionally assert specific IDs/titles to ensure page boundaries are correct.


133-157: Sorting assertions are OK, but keep in mind title/description sorting may be per-page.
Since you keep results within one page, this won’t conflict with the known “PHP-level natural sort after pagination” trade-off. Based on learnings, consider adding a short comment here to prevent future expansion of this test into multi-page title-sorting expectations.


159-197: Visibility tests are valuable; consider reducing unrelated setup.
$otherAlbum is created but never used—optional to remove to keep the test focused.

tests/Feature_v2/PaginationIntegrationTest.php (1)

211-232: Default-page behavior test is fine; consider using assertSame for strictness.
Mostly a style/readability improvement for metadata fields (current_page etc.).

resources/js/stores/AlbumState.ts (3)

10-69: State additions are coherent; reset() doesn’t reset legacy paging fields.
If anything still reads current_page/last_page/per_page/total, consider resetting them too (or remove them if fully migrated).


82-143: loadHead() looks OK, but error handling is duplicated and can be simplified.
The 401/403 + "Password required" branches are identical; consider collapsing with a single condition + optional chaining.


155-191: loadAlbums() append mode risks duplicates if pages overlap/reorder.
If backend sorting can change between calls (e.g., concurrent updates), consider de-duping by id when appending.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 161551e and 551d660.

📒 Files selected for processing (34)
  • app/Actions/Photo/Pipes/Standalone/ApplyWatermark.php
  • app/Enum/ColumnSortingAlbumType.php
  • app/Enum/ColumnSortingPhotoType.php
  • app/Enum/ColumnSortingType.php
  • app/Http/Controllers/Gallery/AlbumChildrenController.php
  • app/Http/Controllers/Gallery/AlbumHeadController.php
  • app/Http/Controllers/Gallery/AlbumPhotosController.php
  • app/Http/Middleware/ResolveConfigs.php
  • app/Http/Requests/Album/GetAlbumPhotosRequest.php
  • app/Http/Resources/Models/HeadTagAlbumResource.php
  • app/Http/Resources/Rights/ModulesRightsResource.php
  • app/Image/Watermarker.php
  • app/Models/Extensions/SortingDecorator.php
  • app/Relations/HasAlbumThumb.php
  • app/Repositories/AlbumRepository.php
  • config/debugbar.php
  • database/migrations/2025_08_27_203030_create_orders_table.php
  • database/migrations/2025_12_22_163233_create_xhprof_table.php
  • database/migrations/2025_12_27_034137_create_photo_ratings_table.php
  • database/migrations/2026_01_02_203124_create_album_size_statistics_table.php
  • database/migrations/2026_01_07_add_pagination_config_keys.php
  • database/migrations/2026_01_10_212900_add_strict_ordering.php
  • docs/specs/2-how-to/configure-pagination.md
  • docs/specs/3-reference/api-design.md
  • docs/specs/4-architecture/features/007-pagination/tasks.md
  • docs/specs/4-architecture/knowledge-map.md
  • resources/js/components/pagination/PaginationInfiniteScroll.vue
  • resources/js/composables/pagination/usePagination.ts
  • resources/js/config/constants.ts
  • resources/js/lychee.d.ts
  • resources/js/stores/AlbumState.ts
  • resources/js/stores/PhotosState.ts
  • tests/Feature_v2/PaginationIntegrationTest.php
  • tests/Unit/Repositories/AlbumRepositoryTest.php
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/Http/Controllers/Gallery/AlbumHeadController.php
  • resources/js/composables/pagination/usePagination.ts
  • resources/js/components/pagination/PaginationInfiniteScroll.vue
  • app/Repositories/AlbumRepository.php
  • app/Http/Controllers/Gallery/AlbumChildrenController.php
🧰 Additional context used
📓 Path-based instructions (13)
**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.php: Any new PHP file should contain the license header and have a single blank line after the opening PHP tag
Variable names should be in snake_case in PHP
Apply the PSR-4 coding standard in PHP
Use in_array() with true as the third parameter in PHP
Only use booleans in if statements, not integers or strings
Use strict comparison (===) instead of loose comparison (==)
Avoid code duplication in both if and else statements
Do not use empty() in PHP
Use the moneyphp/money library for handling monetary values in PHP
Never use floats or doubles to represent monetary values; use integers representing the smallest currency unit (e.g., cents for USD)

**/*.php: Write or extend executable specifications (unit, behaviour, or scenario tests) ahead of implementation, confirm they fail, and then drive code to green before refactoring. List the expected success, validation, and failure branches and add thin failing tests for each path.
For PHP code, adhere to conventions: license headers in new files, strict comparison (===), no empty(), in_array() with third parameter true, snake_case variables, PSR-4 standard, test base classes (AbstractTestCase for Unit, BaseApiWithDataTest for Feature_v2).
Always run phpunit tests. If a test remains red, disable it with a TODO, note the reason, and capture the follow-up in the relevant plan.
Spotless now uses Palantir Java Format 2.78.0 with a 120-character wrap; configure IDE formatters to match before pushing code changes.
Keep each increment's control flow flat by delegating validation/normalisation into tiny pure helpers that return simple enums or result records, then compose them instead of introducing inline branching that inflates the branch count per change.
When introducing new helpers/utilities or editing files prone to style violations (records, DTOs, generated adapters), run the narrowest applicable lint target (for example phpstan) before the full pipeline. Note the command in the related plan/task.
For PHP changes, ru...

Files:

  • app/Actions/Photo/Pipes/Standalone/ApplyWatermark.php
  • database/migrations/2025_12_22_163233_create_xhprof_table.php
  • database/migrations/2025_08_27_203030_create_orders_table.php
  • app/Enum/ColumnSortingType.php
  • database/migrations/2026_01_02_203124_create_album_size_statistics_table.php
  • app/Relations/HasAlbumThumb.php
  • app/Enum/ColumnSortingAlbumType.php
  • app/Enum/ColumnSortingPhotoType.php
  • database/migrations/2026_01_10_212900_add_strict_ordering.php
  • tests/Feature_v2/PaginationIntegrationTest.php
  • database/migrations/2025_12_27_034137_create_photo_ratings_table.php
  • config/debugbar.php
  • tests/Unit/Repositories/AlbumRepositoryTest.php
  • app/Http/Resources/Rights/ModulesRightsResource.php
  • app/Http/Requests/Album/GetAlbumPhotosRequest.php
  • app/Models/Extensions/SortingDecorator.php
  • database/migrations/2026_01_07_add_pagination_config_keys.php
  • app/Http/Resources/Models/HeadTagAlbumResource.php
  • app/Http/Controllers/Gallery/AlbumPhotosController.php
  • app/Image/Watermarker.php
  • app/Http/Middleware/ResolveConfigs.php
**/*.md

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.md: Use Markdown format for documentation
At the bottom of documentation files, add an hr line followed by "Last updated: [date of the update]"

Files:

  • docs/specs/2-how-to/configure-pagination.md
  • docs/specs/4-architecture/features/007-pagination/tasks.md
  • docs/specs/4-architecture/knowledge-map.md
  • docs/specs/3-reference/api-design.md
docs/specs/4-architecture/features/**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

docs/specs/4-architecture/features/**/*.md: Author new specifications, feature plans, and task checklists using docs/specs/templates/feature-spec-template.md, docs/specs/templates/feature-plan-template.md, and docs/specs/templates/feature-tasks-template.md to keep structure, metadata, and verification notes uniform across features.
For any new UI feature or modification, include an ASCII mock-up in the specification (see docs/specs/4-architecture/spec-guidelines/ui-ascii-mockups.md).
When revising a specification, only document fallback or compatibility behaviour if the user explicitly asked for it; if instructions are unclear, pause and request confirmation instead of assuming a fallback.
For every task, refresh the relevant feature plan and note open questions; only move forward once the plan reflects the desired change. Update specs before code.
Update feature specs, feature plans, and tasks documents as progress is made and sync context to disk.
Capture prompt summaries, command sequences, and rationale in the active feature plan or an appendix referenced from it so downstream reviewers know how the change was produced (intent logging).
Track upcoming additions for contract tests, mutation analysis, and security/red-team prompt suites in the plans until automated jobs exist (quality gates).
Publish prompt and tool usage notes alongside the feature plan update so future agents understand how the iteration unfolded.

Files:

  • docs/specs/4-architecture/features/007-pagination/tasks.md
docs/specs/4-architecture/features/**/{spec,plan,tasks}.md

📄 CodeRabbit inference engine (AGENTS.md)

docs/specs/4-architecture/features/**/{spec,plan,tasks}.md: Start every feature by updating or creating its specification at docs/specs/4-architecture/features/-/spec.md, followed by plan and tasks documents after clarifications are resolved.
Generate or refresh the feature plan only after the specification is current and high-/medium-impact clarifications are resolved and recorded in the spec (plus ADRs where required).

Files:

  • docs/specs/4-architecture/features/007-pagination/tasks.md
docs/specs/4-architecture/features/**/tasks.md

📄 CodeRabbit inference engine (AGENTS.md)

docs/specs/4-architecture/features/**/tasks.md: Maintain a per-feature tasks checklist that mirrors the plan, orders tests before code, and keeps planned increments ≤90 minutes by preferring finer-grained entries and documenting sub-steps when something nears the limit.
Mark each task [x] in the feature's tasks.md as soon as it passes verification. Do not batch task completions—update the checklist after every individual task so progress is always visible.
Before committing, confirm every completed task in tasks.md is marked [x] and the roadmap status reflects current progress (validate task checklist).

Files:

  • docs/specs/4-architecture/features/007-pagination/tasks.md
tests/Feature_v2/**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Tests in the tests/Feature_v2 directory should extend from BaseApiWithDataTest

Files:

  • tests/Feature_v2/PaginationIntegrationTest.php
tests/**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

No need to mock the database in tests; use the in-memory SQLite database instead

Files:

  • tests/Feature_v2/PaginationIntegrationTest.php
  • tests/Unit/Repositories/AlbumRepositoryTest.php
docs/specs/4-architecture/knowledge-map.md

📄 CodeRabbit inference engine (AGENTS.md)

docs/specs/4-architecture/knowledge-map.md: Skim docs/specs/4-architecture/knowledge-map.md and the up-to-date module snapshot in docs/specs/architecture-graph.json before planning so new work reinforces the architectural relationships already captured there.
Maintain the knowledge map by adding, adjusting, or pruning entries whenever new modules, dependencies, or contracts appear.

Files:

  • docs/specs/4-architecture/knowledge-map.md
tests/Unit/**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Tests in the tests/Unit directory should extend from AbstractTestCase

Files:

  • tests/Unit/Repositories/AlbumRepositoryTest.php
**/*.{vue,ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{vue,ts,js}: For Vue3/TypeScript frontend code, follow coding conventions defined in docs/specs/3-reference/coding-conventions.md for style, naming, and testing practices.
For frontend changes, run npm run check to ensure all frontend tests pass before committing.

Files:

  • resources/js/config/constants.ts
  • resources/js/stores/AlbumState.ts
  • resources/js/lychee.d.ts
  • resources/js/stores/PhotosState.ts
**/*.{vue,ts,js,css}

📄 CodeRabbit inference engine (AGENTS.md)

For frontend changes, run npm run format to apply frontend code formatting with Prettier before committing.

Files:

  • resources/js/config/constants.ts
  • resources/js/stores/AlbumState.ts
  • resources/js/lychee.d.ts
  • resources/js/stores/PhotosState.ts
**/*Resource.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Resource classes should extend from Spatie Data instead of JsonResource

Files:

  • app/Http/Resources/Rights/ModulesRightsResource.php
  • app/Http/Resources/Models/HeadTagAlbumResource.php
**/*Request.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*Request.php: In Request classes, if a user is provided by the query, place it in $this->user2
In Request classes, reserve $this->user for the user making the request

Files:

  • app/Http/Requests/Album/GetAlbumPhotosRequest.php
🧠 Learnings (32)
📓 Common learnings
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3718
File: app/Models/Extensions/SortingDecorator.php:191-0
Timestamp: 2025-10-06T21:24:16.072Z
Learning: In the Lychee codebase (LycheeOrg/Lychee), when implementing pagination with the SortingDecorator class, it's acceptable for PHP-level sorting (used for natural sorting of title/description columns) to occur after pagination rather than before, even though this means sorting is per-page rather than globally correct across all pages. This is an intentional trade-off to avoid memory issues from loading all items before pagination. A config disclaimer should document this limitation.
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/OrderService.php:65-73
Timestamp: 2025-09-16T21:56:01.607Z
Learning: The validation for album-photo membership in OrderService::addPhotoToOrder() is implemented within PurchasableService::getEffectivePurchasableForPhoto() using a whereHas('albums') constraint that ensures the photo belongs to the specified album_id before applying pricing logic.
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/OrderService.php:65-73
Timestamp: 2025-09-16T21:56:01.607Z
Learning: The validation for album-photo membership in PurchasableService::getEffectivePurchasableForPhoto() uses a JOIN query with the photo_album pivot table to ensure that only purchasables for albums that actually contain the specified photo are returned, preventing price spoofing attacks where clients could use arbitrary album_ids.
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/PurchasableService.php:150-168
Timestamp: 2025-09-18T07:09:17.176Z
Learning: In PurchasablePhotoRequest, the validation for photo-album membership is implemented at the request validation level rather than in the service layer, using a count comparison to ensure all photos belong to the target album before creating purchasables.
📚 Learning: 2025-08-22T18:56:08.725Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3642
File: resources/js/components/gallery/albumModule/AlbumHero.vue:173-183
Timestamp: 2025-08-22T18:56:08.725Z
Learning: In the needSizeVariantsWatermark function in resources/js/components/gallery/albumModule/AlbumHero.vue, the original variant is intentionally excluded from the watermark check. This is a deliberate design decision to not consider original files when determining if the watermark button should be shown.

Applied to files:

  • app/Actions/Photo/Pipes/Standalone/ApplyWatermark.php
  • app/Image/Watermarker.php
📚 Learning: 2025-12-28T18:12:55.752Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3901
File: app/Providers/AppServiceProvider.php:0-0
Timestamp: 2025-12-28T18:12:55.752Z
Learning: When using Laravel Octane's tick API, Octane::tick(...) returns an InvokeTickCallable that only has ->seconds(int) and ->immediate() methods. There is no ->every(N) method. Use the correct usage: Octane::tick('name', fn() => ...)->seconds(N) or Octane::tick('name', fn() => ..., N). Apply this guideline to PHP files across the project (not just AppServiceProvider.php).

Applied to files:

  • app/Actions/Photo/Pipes/Standalone/ApplyWatermark.php
  • database/migrations/2025_12_22_163233_create_xhprof_table.php
  • database/migrations/2025_08_27_203030_create_orders_table.php
  • app/Enum/ColumnSortingType.php
  • database/migrations/2026_01_02_203124_create_album_size_statistics_table.php
  • app/Relations/HasAlbumThumb.php
  • app/Enum/ColumnSortingAlbumType.php
  • app/Enum/ColumnSortingPhotoType.php
  • database/migrations/2026_01_10_212900_add_strict_ordering.php
  • tests/Feature_v2/PaginationIntegrationTest.php
  • database/migrations/2025_12_27_034137_create_photo_ratings_table.php
  • config/debugbar.php
  • tests/Unit/Repositories/AlbumRepositoryTest.php
  • app/Http/Resources/Rights/ModulesRightsResource.php
  • app/Http/Requests/Album/GetAlbumPhotosRequest.php
  • app/Models/Extensions/SortingDecorator.php
  • database/migrations/2026_01_07_add_pagination_config_keys.php
  • app/Http/Resources/Models/HeadTagAlbumResource.php
  • app/Http/Controllers/Gallery/AlbumPhotosController.php
  • app/Image/Watermarker.php
  • app/Http/Middleware/ResolveConfigs.php
📚 Learning: 2025-08-20T20:32:03.676Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3637
File: database/migrations/2025_08_19_160144_create_renamer_rules_table.php:106-108
Timestamp: 2025-08-20T20:32:03.676Z
Learning: In the Lychee codebase, ildyria intentionally uses destructive migration patterns where up() calls down() to ensure clean state, as indicated by "Working as intended" and "No mercy" comments in migrations like database/migrations/2025_08_19_160144_create_renamer_rules_table.php.

Applied to files:

  • database/migrations/2025_12_22_163233_create_xhprof_table.php
  • database/migrations/2025_08_27_203030_create_orders_table.php
  • database/migrations/2026_01_10_212900_add_strict_ordering.php
  • database/migrations/2025_12_27_034137_create_photo_ratings_table.php
📚 Learning: 2025-10-06T21:24:16.072Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3718
File: app/Models/Extensions/SortingDecorator.php:191-0
Timestamp: 2025-10-06T21:24:16.072Z
Learning: In the Lychee codebase (LycheeOrg/Lychee), when implementing pagination with the SortingDecorator class, it's acceptable for PHP-level sorting (used for natural sorting of title/description columns) to occur after pagination rather than before, even though this means sorting is per-page rather than globally correct across all pages. This is an intentional trade-off to avoid memory issues from loading all items before pagination. A config disclaimer should document this limitation.

Applied to files:

  • docs/specs/2-how-to/configure-pagination.md
  • resources/js/config/constants.ts
  • app/Models/Extensions/SortingDecorator.php
  • docs/specs/3-reference/api-design.md
📚 Learning: 2025-08-20T20:32:36.930Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3637
File: database/migrations/2025_08_19_160144_create_renamer_rules_table.php:133-134
Timestamp: 2025-08-20T20:32:36.930Z
Learning: In the Lychee codebase, ildyria intentionally designs migrations to be destructive where up() calls down() first to ensure a clean state before inserting data. This approach makes migrations idempotent through destruction and recreation rather than through upsert operations, as confirmed by the "Working as intended" response to suggestions about using insertOrIgnore for config seeding.

Applied to files:

  • database/migrations/2025_08_27_203030_create_orders_table.php
  • database/migrations/2026_01_10_212900_add_strict_ordering.php
📚 Learning: 2025-08-18T10:19:04.946Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3626
File: database/migrations/2025_06_07_144157_photo_tags_to_table.php:87-105
Timestamp: 2025-08-18T10:19:04.946Z
Learning: In the Lychee photo management system, the migration `2025_06_07_144157_photo_tags_to_table.php` runs on data that only contains comma-separated tag names in tag_albums.show_tags - OR/AND expressions do not exist at this migration time, so handling them is unnecessary.

Applied to files:

  • database/migrations/2026_01_02_203124_create_album_size_statistics_table.php
  • database/migrations/2026_01_10_212900_add_strict_ordering.php
  • database/migrations/2025_12_27_034137_create_photo_ratings_table.php
  • database/migrations/2026_01_07_add_pagination_config_keys.php
📚 Learning: 2025-09-16T21:56:01.607Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/OrderService.php:65-73
Timestamp: 2025-09-16T21:56:01.607Z
Learning: The validation for album-photo membership in OrderService::addPhotoToOrder() is implemented within PurchasableService::getEffectivePurchasableForPhoto() using a whereHas('albums') constraint that ensures the photo belongs to the specified album_id before applying pricing logic.

Applied to files:

  • app/Relations/HasAlbumThumb.php
  • app/Http/Requests/Album/GetAlbumPhotosRequest.php
  • app/Http/Controllers/Gallery/AlbumPhotosController.php
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to docs/specs/4-architecture/features/**/tasks.md : Maintain a per-feature tasks checklist that mirrors the plan, orders tests before code, and keeps planned increments ≤90 minutes by preferring finer-grained entries and documenting sub-steps when something nears the limit.

Applied to files:

  • docs/specs/4-architecture/features/007-pagination/tasks.md
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to docs/specs/4-architecture/features/**/*.md : For every task, refresh the relevant feature plan and note open questions; only move forward once the plan reflects the desired change. Update specs before code.

Applied to files:

  • docs/specs/4-architecture/features/007-pagination/tasks.md
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to docs/specs/4-architecture/features/**/tasks.md : Mark each task [x] in the feature's tasks.md as soon as it passes verification. Do not batch task completions—update the checklist after every individual task so progress is always visible.

Applied to files:

  • docs/specs/4-architecture/features/007-pagination/tasks.md
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to docs/specs/4-architecture/features/**/*.md : Update feature specs, feature plans, and tasks documents as progress is made and sync context to disk.

Applied to files:

  • docs/specs/4-architecture/features/007-pagination/tasks.md
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to docs/specs/4-architecture/features/**/tasks.md : Before committing, confirm every completed task in tasks.md is marked [x] and the roadmap status reflects current progress (validate task checklist).

Applied to files:

  • docs/specs/4-architecture/features/007-pagination/tasks.md
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to docs/specs/4-architecture/roadmap.md : Keep high-level plans in docs/specs/4-architecture/roadmap.md, store each feature's spec/plan/tasks inside docs/specs/4-architecture/features/<NNN>-<feature-name>/, and remove plans once work is complete.

Applied to files:

  • docs/specs/4-architecture/features/007-pagination/tasks.md
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to docs/specs/4-architecture/features/**/*.md : Track upcoming additions for contract tests, mutation analysis, and security/red-team prompt suites in the plans until automated jobs exist (quality gates).

Applied to files:

  • docs/specs/4-architecture/features/007-pagination/tasks.md
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to docs/specs/4-architecture/features/**/{spec,plan,tasks}.md : Start every feature by updating or creating its specification at docs/specs/4-architecture/features/<NNN>-<feature-name>/spec.md, followed by plan and tasks documents after clarifications are resolved.

Applied to files:

  • docs/specs/4-architecture/features/007-pagination/tasks.md
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to docs/specs/4-architecture/features/**/*.md : Publish prompt and tool usage notes alongside the feature plan update so future agents understand how the iteration unfolded.

Applied to files:

  • docs/specs/4-architecture/features/007-pagination/tasks.md
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to docs/specs/4-architecture/features/**/*.md : Author new specifications, feature plans, and task checklists using docs/specs/templates/feature-spec-template.md, docs/specs/templates/feature-plan-template.md, and docs/specs/templates/feature-tasks-template.md to keep structure, metadata, and verification notes uniform across features.

Applied to files:

  • docs/specs/4-architecture/features/007-pagination/tasks.md
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to docs/specs/4-architecture/open-questions.md : Update or close entries in docs/specs/4-architecture/open-questions.md after completing work.

Applied to files:

  • docs/specs/4-architecture/features/007-pagination/tasks.md
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to docs/specs/4-architecture/features/**/{spec,plan,tasks}.md : Generate or refresh the feature plan only after the specification is current and high-/medium-impact clarifications are resolved and recorded in the spec (plus ADRs where required).

Applied to files:

  • docs/specs/4-architecture/features/007-pagination/tasks.md
📚 Learning: 2025-12-22T14:11:17.217Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-22T14:11:17.217Z
Learning: Applies to tests/Feature_v2/**/*.php : Tests in the tests/Feature_v2 directory should extend from BaseApiWithDataTest

Applied to files:

  • tests/Feature_v2/PaginationIntegrationTest.php
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to docs/specs/4-architecture/knowledge-map.md : Maintain the knowledge map by adding, adjusting, or pruning entries whenever new modules, dependencies, or contracts appear.

Applied to files:

  • docs/specs/4-architecture/knowledge-map.md
📚 Learning: 2025-09-18T07:09:17.176Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/PurchasableService.php:150-168
Timestamp: 2025-09-18T07:09:17.176Z
Learning: In PurchasablePhotoRequest, the validation for photo-album membership is implemented at the request validation level rather than in the service layer, using a count comparison to ensure all photos belong to the target album before creating purchasables.

Applied to files:

  • app/Http/Requests/Album/GetAlbumPhotosRequest.php
  • app/Http/Controllers/Gallery/AlbumPhotosController.php
📚 Learning: 2025-08-12T11:31:09.853Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3605
File: docs/Lifecycle-of-a-request-photo-upload.md:144-151
Timestamp: 2025-08-12T11:31:09.853Z
Learning: In Lychee, the BaseApiRequest class has been modified to change Laravel's default execution order - processValidatedValues() runs BEFORE authorize(), not after. This means that in Lychee request classes, $this->album and other processed properties are available during authorization checks, unlike standard Laravel behavior.

Applied to files:

  • app/Http/Requests/Album/GetAlbumPhotosRequest.php
📚 Learning: 2025-09-16T21:56:01.607Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/OrderService.php:65-73
Timestamp: 2025-09-16T21:56:01.607Z
Learning: The validation for album-photo membership in PurchasableService::getEffectivePurchasableForPhoto() uses a JOIN query with the photo_album pivot table to ensure that only purchasables for albums that actually contain the specified photo are returned, preventing price spoofing attacks where clients could use arbitrary album_ids.

Applied to files:

  • app/Http/Requests/Album/GetAlbumPhotosRequest.php
  • app/Http/Controllers/Gallery/AlbumPhotosController.php
📚 Learning: 2025-09-22T12:35:38.842Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3706
File: tests/Unit/Http/Controllers/ImportFromServerControllerTest.php:59-66
Timestamp: 2025-09-22T12:35:38.842Z
Learning: In the Lychee codebase, the Configs class extends Laravel's Model, making config values database-backed. Tests using the DatabaseTransactions trait automatically have config changes rolled back without requiring manual restoration in tearDown() methods.

Applied to files:

  • database/migrations/2026_01_07_add_pagination_config_keys.php
📚 Learning: 2025-09-28T12:04:53.277Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3721
File: resources/js/stores/TagState.ts:34-36
Timestamp: 2025-09-28T12:04:53.277Z
Learning: In the Lychee codebase, different API services have different response structures. TagsService.get() returns TagWithPhotosResource directly with fields id, name, and photos (no "resource" wrapper field), while AlbumService.get() returns a response with data.resource that can be null. Always check the actual TypeScript definitions before assuming response structure.

Applied to files:

  • app/Http/Resources/Models/HeadTagAlbumResource.php
  • resources/js/stores/AlbumState.ts
  • resources/js/lychee.d.ts
📚 Learning: 2025-09-28T12:43:29.852Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3721
File: resources/js/views/gallery-panels/Album.vue:200-204
Timestamp: 2025-09-28T12:43:29.852Z
Learning: The photoStore.load() method in resources/js/stores/PhotoState.ts already handles undefined photoId cases internally with a short circuit exit, so conditional checks before calling photoStore.load() are unnecessary.

Applied to files:

  • app/Http/Resources/Models/HeadTagAlbumResource.php
  • resources/js/stores/AlbumState.ts
  • resources/js/stores/PhotosState.ts
📚 Learning: 2025-08-14T10:17:35.082Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3616
File: app/Actions/Album/PositionData.php:38-39
Timestamp: 2025-08-14T10:17:35.082Z
Learning: PositionDataResource uses toPhotoResources() method to convert photos to PhotoResource instances, and PhotoResource accesses the tags relationship ($photo->tags->pluck('name')->all()), making the 'tags' eager-loading in PositionData necessary to avoid N+1 queries.

Applied to files:

  • app/Http/Resources/Models/HeadTagAlbumResource.php
  • app/Http/Controllers/Gallery/AlbumPhotosController.php
  • resources/js/lychee.d.ts
📚 Learning: 2025-09-28T08:27:38.332Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3721
File: resources/js/composables/album/photoActions.ts:39-43
Timestamp: 2025-09-28T08:27:38.332Z
Learning: The user ildyria (repository maintainer) prefers to skip error handling for the rotate photo functions (rotatePhotoCW and rotatePhotoCCW) in resources/js/composables/album/photoActions.ts.

Applied to files:

  • app/Http/Resources/Models/HeadTagAlbumResource.php
📚 Learning: 2025-09-13T14:01:20.039Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/OrderService.php:0-0
Timestamp: 2025-09-13T14:01:20.039Z
Learning: In PurchasableService::getEffectivePurchasableForPhoto(), both the photo-specific and album-level SQL queries include ->where('is_active', true), so the returned purchasable is guaranteed to be active and doesn't need additional is_active checks in calling code.

Applied to files:

  • app/Http/Controllers/Gallery/AlbumPhotosController.php
📚 Learning: 2025-09-28T08:46:37.299Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3721
File: resources/js/stores/UserState.ts:7-11
Timestamp: 2025-09-28T08:46:37.299Z
Learning: In Lychee codebase, the type `App.Enum.OauthProvidersType` is properly defined within the `App.Enum` namespace in `resources/js/lychee.d.ts` file. References to `App.Enum.OauthProvidersType` in TypeScript files are valid and resolve correctly.

Applied to files:

  • resources/js/lychee.d.ts
🧬 Code graph analysis (10)
app/Actions/Photo/Pipes/Standalone/ApplyWatermark.php (1)
app/Image/Watermarker.php (1)
  • can_watermark (108-111)
app/Relations/HasAlbumThumb.php (3)
resources/js/lychee.d.ts (1)
  • PhotoSortingCriterion (6-9)
app/DTO/AlbumSortingCriterion.php (1)
  • createDefault (23-35)
app/DTO/PhotoSortingCriterion.php (1)
  • createDefault (23-35)
tests/Feature_v2/PaginationIntegrationTest.php (2)
app/Models/Configs.php (1)
  • Configs (61-234)
tests/Feature_v2/Base/BaseApiWithDataTest.php (1)
  • BaseApiWithDataTest (93-266)
tests/Unit/Repositories/AlbumRepositoryTest.php (3)
app/Repositories/AlbumRepository.php (2)
  • AlbumRepository (24-67)
  • getChildrenPaginated (43-66)
tests/Traits/RequiresEmptyUsers.php (2)
  • setUpRequiresEmptyUsers (27-31)
  • tearDownRequiresEmptyUsers (33-37)
database/factories/AccessPermissionFactory.php (2)
  • public (55-63)
  • visible (146-153)
resources/js/config/constants.ts (1)
resources/js/lychee.d.ts (1)
  • PaginationMode (128-128)
app/Http/Resources/Rights/ModulesRightsResource.php (1)
app/Image/Watermarker.php (2)
  • Watermarker (23-245)
  • can_watermark (108-111)
app/Http/Requests/Album/GetAlbumPhotosRequest.php (4)
app/Factories/AlbumFactory.php (2)
  • AlbumFactory (28-236)
  • createSmartAlbum (224-235)
app/Http/Requests/BaseApiRequest.php (1)
  • BaseApiRequest (30-172)
app/Rules/AlbumIDRule.php (1)
  • AlbumIDRule (15-45)
app/Http/Requests/Album/GetAlbumHeadRequest.php (3)
  • authorize (29-46)
  • rules (51-56)
  • processValidatedValues (61-65)
app/Http/Resources/Models/HeadTagAlbumResource.php (6)
resources/js/lychee.d.ts (5)
  • EditableBaseAlbumResource (260-278)
  • AlbumProtectionPolicy (841-849)
  • PreFormattedAlbumData (859-870)
  • AlbumRightsResource (905-918)
  • AlbumStatisticsResource (544-548)
app/Models/TagAlbum.php (1)
  • TagAlbum (58-180)
app/Http/Resources/Models/HeadSmartAlbumResource.php (3)
  • TypeScript (19-47)
  • __construct (31-41)
  • fromModel (43-46)
app/Http/Resources/Models/HeadAlbumResource.php (3)
  • TypeScript (23-100)
  • __construct (57-94)
  • fromModel (96-99)
app/Http/Resources/Models/Utils/AlbumProtectionPolicy.php (1)
  • ofBaseAlbum (51-62)
app/Http/Resources/Traits/HasHeaderUrl.php (1)
  • getHeaderUrl (22-38)
app/Image/Watermarker.php (4)
app/Assets/WatermarkGroupedWithRandomSuffixNamingStrategy.php (1)
  • WatermarkGroupedWithRandomSuffixNamingStrategy (22-70)
app/Actions/Photo/Pipes/Standalone/ApplyWatermark.php (1)
  • __construct (23-27)
app/Actions/Diagnostics/Pipes/Checks/WatermarkerEnabledCheck.php (1)
  • __construct (23-26)
app/Image/Handlers/ImageHandler.php (1)
  • __construct (41-48)
app/Http/Middleware/ResolveConfigs.php (1)
app/Image/Watermarker.php (1)
  • Watermarker (23-245)
🪛 LanguageTool
docs/specs/2-how-to/configure-pagination.md

[style] ~160-~160: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...UX 3. Monitor browser memory usage with very large collections 4. Consider `page_navigatio...

(EN_WEAK_ADJECTIVE)

docs/specs/4-architecture/features/007-pagination/tasks.md

[style] ~252-~252: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...s/components/settings/ConfigGroup.vue. Added translation keys in lang/en/gallery.ph...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~436-~436: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...re: Explore cursor-based pagination for very large albums (10000+ photos)

(EN_WEAK_ADJECTIVE)

⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: 2️⃣ PHP dist / 8.5 - sqlite
  • GitHub Check: 2️⃣ PHP dist / 8.4 - postgresql
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Precomputing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Precomputing
  • GitHub Check: 1️⃣ PHP tests / 8.4 - mariadb -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.4 - postgresql -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Precomputing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Unit

Copy link

@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: 0

🧹 Nitpick comments (2)
tests/ImageProcessing/Image/WatermarkerTest.php (2)

62-62: Use strict boolean assertions instead of assertEquals.

The method call conversion looks correct. However, per coding guidelines requiring strict comparison (===), boolean assertions should use assertTrue/assertFalse instead of assertEquals with boolean literals, as assertEquals performs loose comparison by default.

♻️ Refactor to use strict boolean assertions
-		self::assertEquals(false, $watermarker->can_watermark());
+		self::assertFalse($watermarker->can_watermark());

Apply this pattern to all boolean assertions at lines 62, 70, 79, 88, 118, and 184. For true assertions:

-		self::assertEquals(true, $watermarker->can_watermark());
+		self::assertTrue($watermarker->can_watermark());

As per coding guidelines, PHP code should use strict comparison (===) instead of loose comparison (==).

Also applies to: 70-70, 79-79, 88-88, 118-118, 184-184


113-114: Remove duplicate Configs::set calls.

The same configuration key is set twice consecutively. The second call is redundant.

♻️ Remove duplicate configuration

At lines 113-114:

 		Configs::set('watermark_photo_id', $photoId);
-		Configs::set('watermark_photo_id', $photoId);

At lines 178-179:

 		Configs::set('watermark_photo_id', $photoId);
-		Configs::set('watermark_photo_id', $photoId);

Also applies to: 178-179

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 551d660 and 50a83f7.

📒 Files selected for processing (1)
  • tests/ImageProcessing/Image/WatermarkerTest.php
🧰 Additional context used
📓 Path-based instructions (2)
**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.php: Any new PHP file should contain the license header and have a single blank line after the opening PHP tag
Variable names should be in snake_case in PHP
Apply the PSR-4 coding standard in PHP
Use in_array() with true as the third parameter in PHP
Only use booleans in if statements, not integers or strings
Use strict comparison (===) instead of loose comparison (==)
Avoid code duplication in both if and else statements
Do not use empty() in PHP
Use the moneyphp/money library for handling monetary values in PHP
Never use floats or doubles to represent monetary values; use integers representing the smallest currency unit (e.g., cents for USD)

**/*.php: Write or extend executable specifications (unit, behaviour, or scenario tests) ahead of implementation, confirm they fail, and then drive code to green before refactoring. List the expected success, validation, and failure branches and add thin failing tests for each path.
For PHP code, adhere to conventions: license headers in new files, strict comparison (===), no empty(), in_array() with third parameter true, snake_case variables, PSR-4 standard, test base classes (AbstractTestCase for Unit, BaseApiWithDataTest for Feature_v2).
Always run phpunit tests. If a test remains red, disable it with a TODO, note the reason, and capture the follow-up in the relevant plan.
Spotless now uses Palantir Java Format 2.78.0 with a 120-character wrap; configure IDE formatters to match before pushing code changes.
Keep each increment's control flow flat by delegating validation/normalisation into tiny pure helpers that return simple enums or result records, then compose them instead of introducing inline branching that inflates the branch count per change.
When introducing new helpers/utilities or editing files prone to style violations (records, DTOs, generated adapters), run the narrowest applicable lint target (for example phpstan) before the full pipeline. Note the command in the related plan/task.
For PHP changes, ru...

Files:

  • tests/ImageProcessing/Image/WatermarkerTest.php
tests/**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

No need to mock the database in tests; use the in-memory SQLite database instead

Files:

  • tests/ImageProcessing/Image/WatermarkerTest.php
🧠 Learnings (2)
📓 Common learnings
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3718
File: app/Models/Extensions/SortingDecorator.php:191-0
Timestamp: 2025-10-06T21:24:16.072Z
Learning: In the Lychee codebase (LycheeOrg/Lychee), when implementing pagination with the SortingDecorator class, it's acceptable for PHP-level sorting (used for natural sorting of title/description columns) to occur after pagination rather than before, even though this means sorting is per-page rather than globally correct across all pages. This is an intentional trade-off to avoid memory issues from loading all items before pagination. A config disclaimer should document this limitation.
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/PurchasableService.php:150-168
Timestamp: 2025-09-18T07:09:17.176Z
Learning: In PurchasablePhotoRequest, the validation for photo-album membership is implemented at the request validation level rather than in the service layer, using a count comparison to ensure all photos belong to the target album before creating purchasables.
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/OrderService.php:65-73
Timestamp: 2025-09-16T21:56:01.607Z
Learning: The validation for album-photo membership in OrderService::addPhotoToOrder() is implemented within PurchasableService::getEffectivePurchasableForPhoto() using a whereHas('albums') constraint that ensures the photo belongs to the specified album_id before applying pricing logic.
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/OrderService.php:65-73
Timestamp: 2025-09-16T21:56:01.607Z
Learning: The validation for album-photo membership in PurchasableService::getEffectivePurchasableForPhoto() uses a JOIN query with the photo_album pivot table to ensure that only purchasables for albums that actually contain the specified photo are returned, preventing price spoofing attacks where clients could use arbitrary album_ids.
📚 Learning: 2025-12-28T18:12:55.752Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3901
File: app/Providers/AppServiceProvider.php:0-0
Timestamp: 2025-12-28T18:12:55.752Z
Learning: When using Laravel Octane's tick API, Octane::tick(...) returns an InvokeTickCallable that only has ->seconds(int) and ->immediate() methods. There is no ->every(N) method. Use the correct usage: Octane::tick('name', fn() => ...)->seconds(N) or Octane::tick('name', fn() => ..., N). Apply this guideline to PHP files across the project (not just AppServiceProvider.php).

Applied to files:

  • tests/ImageProcessing/Image/WatermarkerTest.php
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: 2️⃣ PHP dist / 8.5 - postgresql
  • GitHub Check: 1️⃣ PHP tests / 8.4 - postgresql -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.4 - mariadb -- Install

Copy link

@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: 6

🧹 Nitpick comments (7)
app/Image/Watermarker.php (2)

25-43: Avoid eager DB lookup in the constructor (can add a baseline query per request).
If Watermarker is resolved even on requests that never watermark, __construct() calling check_watermark_image() can introduce unnecessary DB load. Prefer lazy initialization (keep caching in check_watermark_image()), and let callers trigger the check via can_watermark() / do().

Proposed diff
 public function __construct()
 {
     $this->naming_strategy = new WatermarkGroupedWithRandomSuffixNamingStrategy();
-
-    if (!$this->is_watermark_enabled()) {
-        return;
-    }
-
-    if (!$this->check_watermark_image()) {
-        return;
-    }
 }

50-72: Good consolidation of enablement checks; consider caching ConfigManager to reduce repeated resolve() calls.
is_watermark_enabled() is now a single source of truth and uses strict comparisons—nice. Minor maintainability/perf: resolve(ConfigManager::class) is repeated across the class; a constructor-injected (or cached) ConfigManager property would simplify and reduce container lookups.

resources/js/stores/AlbumState.ts (3)

156-194: Consider adding _loadingAlbumId check for consistency with loadHead().

The loadAlbums() method only guards against navigation away (this.albumId !== requestedAlbumId) but doesn't track which specific load request is in flight using _loadingAlbumId. This differs from the more robust race condition handling in loadHead() (lines 93, 108, 139).

While loadMoreAlbums() guards against concurrent calls with the albums_loading flag, loadAlbums() can still be called directly with different page numbers, potentially causing race conditions if pages load out of order.

♻️ Consider consistent race condition handling
 loadAlbums(page: number = 1, append: boolean = false): Promise<void> {
 	const albumsStore = useAlbumsStore();
 
 	if (this.albumId === ALL || this.albumId === undefined) {
 		return Promise.resolve();
 	}
 
 	// Capture current album ID to detect navigation during loading
 	const requestedAlbumId = this.albumId;
+	const requestToken = `${requestedAlbumId}_${page}`;
 	this.albums_loading = true;
+	this._loadingPage = page;
 
 	return AlbumService.getAlbums(requestedAlbumId, page)
 		.then((data) => {
 			// Race condition guard: Don't update state if user navigated away
-			if (this.albumId !== requestedAlbumId) {
+			if (this.albumId !== requestedAlbumId || this._loadingPage !== page) {
 				return;
 			}

206-244: Consider adding page-level race condition tracking.

Similar to loadAlbums(), the loadPhotos() method only guards against navigation changes but doesn't track which specific page load is in flight. If multiple page loads are triggered (e.g., user rapidly clicks pagination buttons), responses could arrive out of order and overwrite newer data with older results.

Consider tracking the requested page similar to the pattern suggested for loadAlbums() to ensure that only the most recent page load updates the state.


288-369: Consider refactoring load() to use loadHead() to reduce duplication.

The load() method duplicates significant logic from loadHead() (lines 83-144):

  • Both call AlbumService.getHead() with the same album ID
  • Both handle the response data mapping (lines 312-347 vs 97-123)
  • Both have identical error handling for password protection (lines 349-362 vs 124-137)
  • Both manage race conditions using _loadingAlbumId
♻️ Refactor to eliminate duplication

Consider refactoring load() to call loadHead() first, then load children/photos:

async load(): Promise<void> {
	const togglableState = useTogglablesStateStore();
	const photosState = usePhotosStore();
	const albumsStore = useAlbumsStore();

	if (this.albumId === ALL || this.albumId === undefined) {
		return Promise.resolve();
	}

	// Do not reload fully if we are already on the right album.
	if (this.albumId === this.album?.id && this.isLoaded) {
		return Promise.resolve();
	}

	// Exit early if we are already loading this album
	if (this._loadingAlbumId === this.albumId) {
		return Promise.resolve();
	}

	const requestedAlbumId = this.albumId;

	// Clear existing data to avoid showing stale content
	albumsStore.reset();
	photosState.reset();

	// Load head metadata first
	await this.loadHead();

	// If navigation occurred during loadHead, abort
	if (this.albumId !== requestedAlbumId) {
		return;
	}

	// Load children and/or photos based on album type
	if (this.config?.is_model_album) {
		await Promise.all([this.loadAlbums(1, false), this.loadPhotos(1, false)]);
	} else {
		await this.loadPhotos(1, false);
	}
}

This approach:

  • Eliminates ~70 lines of duplicated code
  • Makes the loading flow more explicit and easier to maintain
  • Reuses the robust race condition handling from loadHead()
  • Maintains the same functionality
resources/js/components/pagination/PaginationInfiniteScroll.vue (2)

14-20: Consider adding validation or documentation for the rootMargin prop format.

The rootMargin prop accepts any string, but the parsing logic at line 158 uses parseInt(getRootMargin(), 10), which assumes a format like "{number}px". If a user provides a different format (e.g., "50%" or "1em"), the parsing will fail silently or produce incorrect results.

🛡️ Suggestions to improve robustness

Option 1: Add JSDoc or TypeScript comment documenting the expected format:

 const props = defineProps<{
 	loading: boolean;
 	hasMore: boolean;
+	/** Root margin in pixels (e.g., "300px"). Other units are not supported. */
 	rootMargin?: string;
 	threshold?: number;
 	resourceType?: "photos" | "albums";
 }>();

Option 2: Add runtime validation when parsing (at line 158):

function parseRootMarginPx(margin: string): number {
	const parsed = parseInt(margin, 10);
	if (isNaN(parsed)) {
		console.warn(`[InfiniteScroll] Invalid rootMargin format: ${margin}, falling back to 0`);
		return 0;
	}
	return parsed;
}
// Then use: const rootMarginPx = parseRootMarginPx(getRootMargin());

172-181: Optional: Extract duplicate emission pattern into a helper function.

The emission logic at lines 176-180 duplicates the pattern from lines 49-55 (setting isEmitting, emitting loadMore, and resetting via requestAnimationFrame). Extracting this into a helper function would improve maintainability.

♻️ Refactoring suggestion
+function emitLoadMore() {
+	isEmitting = true;
+	emit("loadMore");
+	requestAnimationFrame(() => {
+		isEmitting = false;
+	});
+}
+
 function handleIntersect(entries: IntersectionObserverEntry[]) {
 	const entry = entries[0];
 	console.debug(
 		`[InfiniteScroll] intersect: isIntersecting=${entry.isIntersecting}, hasMore=${props.hasMore}, loading=${props.loading}, isEmitting=${isEmitting}`,
 	);
 	if (entry.isIntersecting && props.hasMore && !props.loading && !isEmitting) {
-		isEmitting = true;
-		emit("loadMore");
-		requestAnimationFrame(() => {
-			isEmitting = false;
-		});
+		emitLoadMore();
 	}
 }

Then apply the same change at lines 176-180 in the post-load watcher.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50a83f7 and fd54d75.

📒 Files selected for processing (6)
  • app/Image/Watermarker.php
  • app/Models/Extensions/SortingDecorator.php
  • config/debugbar.php
  • database/migrations/2026_01_07_add_pagination_config_keys.php
  • resources/js/components/pagination/PaginationInfiniteScroll.vue
  • resources/js/stores/AlbumState.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • database/migrations/2026_01_07_add_pagination_config_keys.php
  • config/debugbar.php
🧰 Additional context used
📓 Path-based instructions (4)
**/*.vue

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.vue: Use TypeScript in composition API for Vue3 and use PrimeVue for UI components
Do not use await async calls in Vue3; use .then() instead
Do not use const function = () => {} syntax; use function functionName() {} instead in Vue3
In Vue3 components, the structure should be first, then <script lang="ts">, then <style>

Files:

  • resources/js/components/pagination/PaginationInfiniteScroll.vue
**/*.{vue,ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{vue,ts,js}: For Vue3/TypeScript frontend code, follow coding conventions defined in docs/specs/3-reference/coding-conventions.md for style, naming, and testing practices.
For frontend changes, run npm run check to ensure all frontend tests pass before committing.

Files:

  • resources/js/components/pagination/PaginationInfiniteScroll.vue
  • resources/js/stores/AlbumState.ts
**/*.{vue,ts,js,css}

📄 CodeRabbit inference engine (AGENTS.md)

For frontend changes, run npm run format to apply frontend code formatting with Prettier before committing.

Files:

  • resources/js/components/pagination/PaginationInfiniteScroll.vue
  • resources/js/stores/AlbumState.ts
**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.php: Any new PHP file should contain the license header and have a single blank line after the opening PHP tag
Variable names should be in snake_case in PHP
Apply the PSR-4 coding standard in PHP
Use in_array() with true as the third parameter in PHP
Only use booleans in if statements, not integers or strings
Use strict comparison (===) instead of loose comparison (==)
Avoid code duplication in both if and else statements
Do not use empty() in PHP
Use the moneyphp/money library for handling monetary values in PHP
Never use floats or doubles to represent monetary values; use integers representing the smallest currency unit (e.g., cents for USD)

**/*.php: Write or extend executable specifications (unit, behaviour, or scenario tests) ahead of implementation, confirm they fail, and then drive code to green before refactoring. List the expected success, validation, and failure branches and add thin failing tests for each path.
For PHP code, adhere to conventions: license headers in new files, strict comparison (===), no empty(), in_array() with third parameter true, snake_case variables, PSR-4 standard, test base classes (AbstractTestCase for Unit, BaseApiWithDataTest for Feature_v2).
Always run phpunit tests. If a test remains red, disable it with a TODO, note the reason, and capture the follow-up in the relevant plan.
Spotless now uses Palantir Java Format 2.78.0 with a 120-character wrap; configure IDE formatters to match before pushing code changes.
Keep each increment's control flow flat by delegating validation/normalisation into tiny pure helpers that return simple enums or result records, then compose them instead of introducing inline branching that inflates the branch count per change.
When introducing new helpers/utilities or editing files prone to style violations (records, DTOs, generated adapters), run the narrowest applicable lint target (for example phpstan) before the full pipeline. Note the command in the related plan/task.
For PHP changes, ru...

Files:

  • app/Image/Watermarker.php
  • app/Models/Extensions/SortingDecorator.php
🧠 Learnings (10)
📓 Common learnings
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3718
File: app/Models/Extensions/SortingDecorator.php:191-0
Timestamp: 2025-10-06T21:24:16.072Z
Learning: In the Lychee codebase (LycheeOrg/Lychee), when implementing pagination with the SortingDecorator class, it's acceptable for PHP-level sorting (used for natural sorting of title/description columns) to occur after pagination rather than before, even though this means sorting is per-page rather than globally correct across all pages. This is an intentional trade-off to avoid memory issues from loading all items before pagination. A config disclaimer should document this limitation.
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/OrderService.php:65-73
Timestamp: 2025-09-16T21:56:01.607Z
Learning: The validation for album-photo membership in OrderService::addPhotoToOrder() is implemented within PurchasableService::getEffectivePurchasableForPhoto() using a whereHas('albums') constraint that ensures the photo belongs to the specified album_id before applying pricing logic.
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/OrderService.php:65-73
Timestamp: 2025-09-16T21:56:01.607Z
Learning: The validation for album-photo membership in PurchasableService::getEffectivePurchasableForPhoto() uses a JOIN query with the photo_album pivot table to ensure that only purchasables for albums that actually contain the specified photo are returned, preventing price spoofing attacks where clients could use arbitrary album_ids.
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3683
File: app/Actions/Shop/PurchasableService.php:150-168
Timestamp: 2025-09-18T07:09:17.176Z
Learning: In PurchasablePhotoRequest, the validation for photo-album membership is implemented at the request validation level rather than in the service layer, using a count comparison to ensure all photos belong to the target album before creating purchasables.
📚 Learning: 2025-08-16T10:57:51.480Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3504
File: resources/js/views/gallery-panels/Albums.vue:11-13
Timestamp: 2025-08-16T10:57:51.480Z
Learning: In resources/js/views/gallery-panels/Albums.vue, the SelectDrag component is intentionally configured with :with-scroll="false" even though the #galleryView container has overflow-y-auto. This is working as intended according to the maintainer, who suggested that overflow-y-auto might be removed instead.

Applied to files:

  • resources/js/components/pagination/PaginationInfiniteScroll.vue
📚 Learning: 2026-01-10T17:13:49.763Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3952
File: resources/js/components/forms/album/AlbumProperties.vue:20-20
Timestamp: 2026-01-10T17:13:49.763Z
Learning: PrimeVue v4.0+ deprecated inputId for Select and related form components in favor of labelId. In Vue templates, use kebab-case label-id (label-id="...") to set the labelId prop. Also replace inputStyle and inputClass with labelStyle and labelClass. Apply these changes to all Vue components that interact with PrimeVue form controls, not just a single file.

Applied to files:

  • resources/js/components/pagination/PaginationInfiniteScroll.vue
📚 Learning: 2025-09-28T12:43:29.852Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3721
File: resources/js/views/gallery-panels/Album.vue:200-204
Timestamp: 2025-09-28T12:43:29.852Z
Learning: The photoStore.load() method in resources/js/stores/PhotoState.ts already handles undefined photoId cases internally with a short circuit exit, so conditional checks before calling photoStore.load() are unnecessary.

Applied to files:

  • resources/js/stores/AlbumState.ts
📚 Learning: 2025-09-28T12:04:53.277Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3721
File: resources/js/stores/TagState.ts:34-36
Timestamp: 2025-09-28T12:04:53.277Z
Learning: In the Lychee codebase, different API services have different response structures. TagsService.get() returns TagWithPhotosResource directly with fields id, name, and photos (no "resource" wrapper field), while AlbumService.get() returns a response with data.resource that can be null. Always check the actual TypeScript definitions before assuming response structure.

Applied to files:

  • resources/js/stores/AlbumState.ts
📚 Learning: 2025-09-28T08:39:34.280Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3721
File: resources/js/stores/NsfwConsentedState.ts:3-5
Timestamp: 2025-09-28T08:39:34.280Z
Learning: In Lychee codebase, all Pinia store files follow the pattern of declaring the type alias before the store definition (e.g., `export type StoreType = ReturnType<typeof useStore>; export const useStore = defineStore(...)`). This pattern compiles successfully with TypeScript 5.9 and vue-tsc.

Applied to files:

  • resources/js/stores/AlbumState.ts
📚 Learning: 2025-11-30T17:19:00.773Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3837
File: resources/js/composables/checkout/useStepTwo.ts:8-16
Timestamp: 2025-11-30T17:19:00.773Z
Learning: In the Lychee project's checkout composables (resources/js/composables/checkout/), module-level refs that cause state sharing across component instances are intentional architectural choices, not issues to flag.

Applied to files:

  • resources/js/stores/AlbumState.ts
📚 Learning: 2025-08-22T18:56:08.725Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3642
File: resources/js/components/gallery/albumModule/AlbumHero.vue:173-183
Timestamp: 2025-08-22T18:56:08.725Z
Learning: In the needSizeVariantsWatermark function in resources/js/components/gallery/albumModule/AlbumHero.vue, the original variant is intentionally excluded from the watermark check. This is a deliberate design decision to not consider original files when determining if the watermark button should be shown.

Applied to files:

  • app/Image/Watermarker.php
📚 Learning: 2025-12-28T18:12:55.752Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3901
File: app/Providers/AppServiceProvider.php:0-0
Timestamp: 2025-12-28T18:12:55.752Z
Learning: When using Laravel Octane's tick API, Octane::tick(...) returns an InvokeTickCallable that only has ->seconds(int) and ->immediate() methods. There is no ->every(N) method. Use the correct usage: Octane::tick('name', fn() => ...)->seconds(N) or Octane::tick('name', fn() => ..., N). Apply this guideline to PHP files across the project (not just AppServiceProvider.php).

Applied to files:

  • app/Image/Watermarker.php
  • app/Models/Extensions/SortingDecorator.php
📚 Learning: 2025-10-06T21:24:16.072Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3718
File: app/Models/Extensions/SortingDecorator.php:191-0
Timestamp: 2025-10-06T21:24:16.072Z
Learning: In the Lychee codebase (LycheeOrg/Lychee), when implementing pagination with the SortingDecorator class, it's acceptable for PHP-level sorting (used for natural sorting of title/description columns) to occur after pagination rather than before, even though this means sorting is per-page rather than globally correct across all pages. This is an intentional trade-off to avoid memory issues from loading all items before pagination. A config disclaimer should document this limitation.

Applied to files:

  • app/Models/Extensions/SortingDecorator.php
🧬 Code graph analysis (2)
resources/js/stores/AlbumState.ts (5)
resources/js/config/constants.ts (1)
  • ALL (2-2)
resources/js/stores/AlbumsState.ts (1)
  • useAlbumsStore (14-101)
resources/js/stores/LycheeState.ts (1)
  • useLycheeStateStore (6-221)
resources/js/stores/PhotosState.ts (1)
  • usePhotosStore (7-84)
resources/js/lychee.d.ts (3)
  • HeadAlbumResource (580-600)
  • HeadSmartAlbumResource (601-608)
  • HeadTagAlbumResource (609-621)
app/Image/Watermarker.php (2)
app/Models/SizeVariant.php (1)
  • SizeVariant (71-239)
app/Actions/Diagnostics/Pipes/Checks/WatermarkerEnabledCheck.php (1)
  • __construct (23-26)
🪛 GitHub Check: 1️⃣ PHP 8.4 - PHPStan
app/Models/Extensions/SortingDecorator.php

[failure] 214-214:
Variable $column_sorting_name might not be defined.

⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: 2️⃣ PHP dist / 8.4 - mariadb
  • GitHub Check: 2️⃣ PHP dist / 8.4 - sqlite
  • GitHub Check: 2️⃣ PHP dist / 8.5 - mariadb
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Precomputing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Precomputing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Precomputing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.4 - sqlite -- Install
🔇 Additional comments (6)
app/Models/Extensions/SortingDecorator.php (2)

152-157: LGTM! Correct implementation of strict suffix stripping.

The SQL sorting logic correctly strips the _strict suffix before applying the orderBy clause.


175-189: LGTM! PHP-level sorting correctly handles strict variants.

The two-step sanitization (removing photos. prefix, then _strict suffix) correctly prepares the column name for type resolution and sorting.

app/Image/Watermarker.php (1)

108-111: can_watermark() API looks clean and correctly composes the two checks.
This is a straightforward replacement for the old public flag and should read well at call sites.

resources/js/components/pagination/PaginationInfiniteScroll.vue (3)

1-6: LGTM: Template structure is clean and correct.

The sentinel element and conditional loading indicator are properly implemented. The sentinel's fixed height (h-4) is minimal and appropriate for an invisible trigger element.


41-57: LGTM: Race condition prevention is well-implemented.

The isEmitting flag combined with the loading prop check provides solid protection against duplicate emissions during rapid intersection callbacks. The use of requestAnimationFrame to reset the flag is appropriate.


65-74: LGTM: Observer lifecycle and edge case handling are excellent.

The implementation demonstrates strong attention to detail:

  • findScrollContainer correctly identifies the scroll parent by traversing the DOM tree
  • Observer lifecycle management properly sets up on mount, cleans up on unmount, and responds to hasMore changes
  • Post-load watcher (lines 149-184) elegantly handles the critical edge case where loaded content is insufficient to fill the viewport, preventing the user from needing to manually scroll to trigger more loading

The verbose debug logging is well-gated behind LycheeState.is_debug_enabled and will be valuable for troubleshooting in production.

Also applies to: 94-133, 149-184

Copy link

@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: 0

🧹 Nitpick comments (2)
resources/js/stores/AlbumState.ts (1)

285-366: Consider extracting duplicated getHead() logic.

The load() method duplicates significant logic from loadHead() (lines 80-141), including:

  • AlbumService.getHead() call and response handling
  • Race condition checks
  • Password protection handling
  • Type casting to Head*Resource variants

While the duplication may be intentional for different orchestration patterns, extracting the common logic into a shared helper would improve maintainability and reduce the risk of inconsistent behavior if one is updated without the other.

♻️ Refactoring approach

Consider extracting the shared getHead() call and response handling into a private method, or having load() call this.loadHead() followed by conditional loading of albums/photos. The race condition tracking would need careful consideration in either approach.

resources/js/components/gallery/albumModule/AlbumPanel.vue (1)

223-229: Consider adding user feedback for page navigation errors.

While the store methods handle errors internally, providing user-facing feedback at the component level (e.g., a toast notification) would improve UX if a page load fails. Currently, users would see no indication of failure.

📋 Optional enhancement
 function goToPhotosPage(page: number) {
-	albumStore.loadPhotos(page, false);
+	albumStore.loadPhotos(page, false).catch(() => {
+		toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to load photos page', life: 3000 });
+	});
 }

 function goToAlbumsPage(page: number) {
-	albumStore.loadAlbums(page, false);
+	albumStore.loadAlbums(page, false).catch(() => {
+		toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to load albums page', life: 3000 });
+	});
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 010536f and 45c02df.

📒 Files selected for processing (3)
  • app/Models/Extensions/SortingDecorator.php
  • resources/js/components/gallery/albumModule/AlbumPanel.vue
  • resources/js/stores/AlbumState.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.vue

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.vue: Use TypeScript in composition API for Vue3 and use PrimeVue for UI components
Do not use await async calls in Vue3; use .then() instead
Do not use const function = () => {} syntax; use function functionName() {} instead in Vue3
In Vue3 components, the structure should be first, then <script lang="ts">, then <style>

Files:

  • resources/js/components/gallery/albumModule/AlbumPanel.vue
**/*.{vue,ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{vue,ts,js}: For Vue3/TypeScript frontend code, follow coding conventions defined in docs/specs/3-reference/coding-conventions.md for style, naming, and testing practices.
For frontend changes, run npm run check to ensure all frontend tests pass before committing.

Files:

  • resources/js/components/gallery/albumModule/AlbumPanel.vue
  • resources/js/stores/AlbumState.ts
**/*.{vue,ts,js,css}

📄 CodeRabbit inference engine (AGENTS.md)

For frontend changes, run npm run format to apply frontend code formatting with Prettier before committing.

Files:

  • resources/js/components/gallery/albumModule/AlbumPanel.vue
  • resources/js/stores/AlbumState.ts
**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.php: Any new PHP file should contain the license header and have a single blank line after the opening PHP tag
Variable names should be in snake_case in PHP
Apply the PSR-4 coding standard in PHP
Use in_array() with true as the third parameter in PHP
Only use booleans in if statements, not integers or strings
Use strict comparison (===) instead of loose comparison (==)
Avoid code duplication in both if and else statements
Do not use empty() in PHP
Use the moneyphp/money library for handling monetary values in PHP
Never use floats or doubles to represent monetary values; use integers representing the smallest currency unit (e.g., cents for USD)

**/*.php: Write or extend executable specifications (unit, behaviour, or scenario tests) ahead of implementation, confirm they fail, and then drive code to green before refactoring. List the expected success, validation, and failure branches and add thin failing tests for each path.
For PHP code, adhere to conventions: license headers in new files, strict comparison (===), no empty(), in_array() with third parameter true, snake_case variables, PSR-4 standard, test base classes (AbstractTestCase for Unit, BaseApiWithDataTest for Feature_v2).
Always run phpunit tests. If a test remains red, disable it with a TODO, note the reason, and capture the follow-up in the relevant plan.
Spotless now uses Palantir Java Format 2.78.0 with a 120-character wrap; configure IDE formatters to match before pushing code changes.
Keep each increment's control flow flat by delegating validation/normalisation into tiny pure helpers that return simple enums or result records, then compose them instead of introducing inline branching that inflates the branch count per change.
When introducing new helpers/utilities or editing files prone to style violations (records, DTOs, generated adapters), run the narrowest applicable lint target (for example phpstan) before the full pipeline. Note the command in the related plan/task.
For PHP changes, ru...

Files:

  • app/Models/Extensions/SortingDecorator.php
🧠 Learnings (14)
📓 Common learnings
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3718
File: app/Models/Extensions/SortingDecorator.php:191-0
Timestamp: 2025-10-06T21:24:16.072Z
Learning: In the Lychee codebase (LycheeOrg/Lychee), when implementing pagination with the SortingDecorator class, it's acceptable for PHP-level sorting (used for natural sorting of title/description columns) to occur after pagination rather than before, even though this means sorting is per-page rather than globally correct across all pages. This is an intentional trade-off to avoid memory issues from loading all items before pagination. A config disclaimer should document this limitation.
📚 Learning: 2025-08-16T10:57:51.480Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3504
File: resources/js/views/gallery-panels/Albums.vue:11-13
Timestamp: 2025-08-16T10:57:51.480Z
Learning: In resources/js/views/gallery-panels/Albums.vue, the SelectDrag component is intentionally configured with :with-scroll="false" even though the #galleryView container has overflow-y-auto. This is working as intended according to the maintainer, who suggested that overflow-y-auto might be removed instead.

Applied to files:

  • resources/js/components/gallery/albumModule/AlbumPanel.vue
📚 Learning: 2025-08-16T10:55:08.753Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3504
File: resources/js/components/gallery/albumModule/thumbs/AlbumThumbImage.vue:41-42
Timestamp: 2025-08-16T10:55:08.753Z
Learning: In AlbumThumbImage.vue, the isSelectable prop is intentionally optional without a default value. When undefined, the template condition `isDragging && !isSelectable ? '' : props.class` will strip props.class during dragging by design. Components should explicitly pass isSelectable={true} if they want to preserve styling during drag operations.

Applied to files:

  • resources/js/components/gallery/albumModule/AlbumPanel.vue
📚 Learning: 2025-08-16T10:56:04.248Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3504
File: resources/js/composables/album/dragAndSelect.ts:68-74
Timestamp: 2025-08-16T10:56:04.248Z
Learning: In the Lychee drag-and-select implementation (resources/js/composables/album/dragAndSelect.ts), the X offset handling is intentionally not implemented as it's not needed for the functionality. The current coordinate system mixing pageY with viewport-based rects has been tested and works as expected.

Applied to files:

  • resources/js/components/gallery/albumModule/AlbumPanel.vue
📚 Learning: 2025-11-30T17:19:00.773Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3837
File: resources/js/composables/checkout/useStepTwo.ts:8-16
Timestamp: 2025-11-30T17:19:00.773Z
Learning: In the Lychee project's checkout composables (resources/js/composables/checkout/), module-level refs that cause state sharing across component instances are intentional architectural choices, not issues to flag.

Applied to files:

  • resources/js/components/gallery/albumModule/AlbumPanel.vue
  • resources/js/stores/AlbumState.ts
📚 Learning: 2025-08-16T14:00:53.808Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3504
File: resources/js/composables/selections/selections.ts:145-147
Timestamp: 2025-08-16T14:00:53.808Z
Learning: In the Lychee codebase, the maintainer ildyria has indicated that bounds checking for array access in selectables.albums.value[idx] within the albumClick function in resources/js/composables/selections/selections.ts is not necessary, suggesting there are adequate safeguards elsewhere in the code or the access pattern is guaranteed to be safe.

Applied to files:

  • resources/js/components/gallery/albumModule/AlbumPanel.vue
  • resources/js/stores/AlbumState.ts
📚 Learning: 2025-09-28T12:43:29.852Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3721
File: resources/js/views/gallery-panels/Album.vue:200-204
Timestamp: 2025-09-28T12:43:29.852Z
Learning: The photoStore.load() method in resources/js/stores/PhotoState.ts already handles undefined photoId cases internally with a short circuit exit, so conditional checks before calling photoStore.load() are unnecessary.

Applied to files:

  • resources/js/components/gallery/albumModule/AlbumPanel.vue
  • resources/js/stores/AlbumState.ts
📚 Learning: 2026-01-10T17:13:49.763Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3952
File: resources/js/components/forms/album/AlbumProperties.vue:20-20
Timestamp: 2026-01-10T17:13:49.763Z
Learning: PrimeVue v4.0+ deprecated inputId for Select and related form components in favor of labelId. In Vue templates, use kebab-case label-id (label-id="...") to set the labelId prop. Also replace inputStyle and inputClass with labelStyle and labelClass. Apply these changes to all Vue components that interact with PrimeVue form controls, not just a single file.

Applied to files:

  • resources/js/components/gallery/albumModule/AlbumPanel.vue
📚 Learning: 2025-10-06T21:24:16.072Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3718
File: app/Models/Extensions/SortingDecorator.php:191-0
Timestamp: 2025-10-06T21:24:16.072Z
Learning: In the Lychee codebase (LycheeOrg/Lychee), when implementing pagination with the SortingDecorator class, it's acceptable for PHP-level sorting (used for natural sorting of title/description columns) to occur after pagination rather than before, even though this means sorting is per-page rather than globally correct across all pages. This is an intentional trade-off to avoid memory issues from loading all items before pagination. A config disclaimer should document this limitation.

Applied to files:

  • app/Models/Extensions/SortingDecorator.php
📚 Learning: 2025-09-21T20:07:11.825Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3702
File: app/Http/Resources/Shop/CatalogResource.php:11-16
Timestamp: 2025-09-21T20:07:11.825Z
Learning: Spatie\LaravelData\DataCollection does NOT extend Illuminate\Support\Collection - it only implements some collection methods. To use Collection type, you must explicitly collect with Collection::class as the second parameter.

Applied to files:

  • app/Models/Extensions/SortingDecorator.php
📚 Learning: 2025-09-21T20:07:11.825Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3702
File: app/Http/Resources/Shop/CatalogResource.php:11-16
Timestamp: 2025-09-21T20:07:11.825Z
Learning: Spatie\LaravelData\DataCollection extends Illuminate\Support\Collection, so DataCollection instances can be assigned to Collection-typed properties without runtime errors.

Applied to files:

  • app/Models/Extensions/SortingDecorator.php
📚 Learning: 2025-12-28T18:12:55.752Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3901
File: app/Providers/AppServiceProvider.php:0-0
Timestamp: 2025-12-28T18:12:55.752Z
Learning: When using Laravel Octane's tick API, Octane::tick(...) returns an InvokeTickCallable that only has ->seconds(int) and ->immediate() methods. There is no ->every(N) method. Use the correct usage: Octane::tick('name', fn() => ...)->seconds(N) or Octane::tick('name', fn() => ..., N). Apply this guideline to PHP files across the project (not just AppServiceProvider.php).

Applied to files:

  • app/Models/Extensions/SortingDecorator.php
📚 Learning: 2025-09-28T12:04:53.277Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3721
File: resources/js/stores/TagState.ts:34-36
Timestamp: 2025-09-28T12:04:53.277Z
Learning: In the Lychee codebase, different API services have different response structures. TagsService.get() returns TagWithPhotosResource directly with fields id, name, and photos (no "resource" wrapper field), while AlbumService.get() returns a response with data.resource that can be null. Always check the actual TypeScript definitions before assuming response structure.

Applied to files:

  • resources/js/stores/AlbumState.ts
📚 Learning: 2025-09-28T08:39:34.280Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3721
File: resources/js/stores/NsfwConsentedState.ts:3-5
Timestamp: 2025-09-28T08:39:34.280Z
Learning: In Lychee codebase, all Pinia store files follow the pattern of declaring the type alias before the store definition (e.g., `export type StoreType = ReturnType<typeof useStore>; export const useStore = defineStore(...)`). This pattern compiles successfully with TypeScript 5.9 and vue-tsc.

Applied to files:

  • resources/js/stores/AlbumState.ts
🧬 Code graph analysis (2)
app/Models/Extensions/SortingDecorator.php (3)
app/Models/Order.php (1)
  • items (121-124)
resources/js/lychee.d.ts (2)
  • ColumnSortingType (41-52)
  • OrderSortingType (127-127)
app/Enum/Traits/DecorateBackedEnum.php (1)
  • values (46-49)
resources/js/stores/AlbumState.ts (5)
resources/js/config/constants.ts (1)
  • ALL (2-2)
resources/js/stores/AlbumsState.ts (1)
  • useAlbumsStore (14-101)
resources/js/stores/LycheeState.ts (1)
  • useLycheeStateStore (6-221)
resources/js/stores/PhotosState.ts (1)
  • usePhotosStore (7-101)
resources/js/lychee.d.ts (3)
  • HeadAlbumResource (580-600)
  • HeadSmartAlbumResource (601-608)
  • HeadTagAlbumResource (609-621)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: 2️⃣ PHP dist / 8.5 - mariadb
  • GitHub Check: 2️⃣ PHP dist / 8.4 - postgresql
  • GitHub Check: 2️⃣ PHP dist / 8.4 - sqlite
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Precomputing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Precomputing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Precomputing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.4 - sqlite -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Unit
🔇 Additional comments (14)
app/Models/Extensions/SortingDecorator.php (5)

17-17: LGTM - Clear alias for Collection types.

The BaseCollection alias appropriately distinguishes Illuminate\Support\Collection from Eloquent\Collection, improving type clarity in method signatures.


140-166: LGTM - Clean extraction of SQL sorting logic.

The applySqlSorting() method correctly:

  • Strips the _strict suffix before SQL ordering (line 151), as SQL columns don't carry this marker
  • Processes criteria from pivot_idx + 1 onwards, ensuring only SQL-compatible sorts are applied
  • Handles the edge case where pivot_idx === -1 by processing all criteria

168-195: LGTM - PHP sorting correctly implements post-pagination natural sort.

The applyPhpSorting() method correctly:

  • Applies sorts in reverse order (lines 180-192) for stable sorting behavior
  • Strips both photos. prefix (line 182) and _strict suffix (line 183) to match model properties
  • Uses SORT_NATURAL | SORT_FLAG_CASE for title/description columns (line 186) to enable human-friendly ordering
  • Returns the sorted collection with reset keys via values()

Based on learnings, PHP-level sorting occurs after pagination is an intentional trade-off to avoid memory issues from loading all items before pagination.


197-218: LGTM - Clean refactoring with proper delegation.

The get() method now cleanly delegates to applySqlSorting() and applyPhpSorting(), improving separation of concerns while preserving the existing behavior.


232-245: LGTM - Paginate correctly applies PHP sorting post-pagination.

The paginate() method:

  • Delegates SQL sorting (line 237), then paginates (line 240)
  • Correctly wraps $result->items() in collect() (line 242) to produce a BaseCollection for applyPhpSorting()
  • Constructs a new LengthAwarePaginator with sorted items while preserving pagination metadata (line 244)

Based on learnings, applying PHP sorting after pagination is an intentional trade-off to avoid loading all items into memory, which means sorting is per-page rather than globally correct across all pages.

resources/js/stores/AlbumState.ts (7)

14-16: LGTM - Resource type updates align with pagination refactor.

The migration to Head*Resource types is consistent with the PR's goal to separate metadata from child collections, enabling efficient paginated loading.


24-42: Well-structured pagination state with backward compatibility.

The separate pagination states for photos and albums, along with retained legacy state, provide clean separation while maintaining compatibility with existing smart album pagination.


69-141: Excellent race condition handling in loadHead().

The method properly guards against multiple concurrent loads and user navigation during fetch by:

  • Capturing the requested album ID before the async call
  • Checking consistency at multiple points (lines 96, 105, 122, 136)
  • Preventing state corruption from stale responses

143-191: LGTM - Clean pagination implementation with append/replace modes.

The append parameter elegantly handles both infinite scroll (append=true) and page navigation (append=false) scenarios. Race condition guards prevent stale data from overwriting current state.


193-241: LGTM - Proper delegation to PhotosState for timeline handling.

The method correctly delegates timeline merging logic to PhotosState.appendPhotos() and PhotosState.setPhotos(), maintaining separation of concerns while supporting both pagination modes.


243-283: LGTM - Well-guarded convenience methods for infinite scroll.

Both loadMorePhotos() and loadMoreAlbums() properly prevent:

  • Duplicate requests during ongoing loads
  • Loading beyond the last available page

These guards are essential for reliable infinite scroll UX.


390-410: LGTM - Comprehensive pagination getters.

The new getters provide all necessary pagination state:

  • hasMore*: Navigation guards for "load more" buttons
  • *RemainingCount: UI feedback for remaining items
  • has*Pagination: Feature detection for conditional rendering

The Math.max(0, ...) guard prevents negative remaining counts.

resources/js/components/gallery/albumModule/AlbumPanel.vue (2)

55-69: LGTM - Well-integrated albums pagination component.

The Pagination component properly binds to albumStore's pagination state and responds to both "load more" (infinite scroll) and "go to page" events. The conditional rendering ensures the UI only appears when both albums exist and pagination metadata is available.


83-97: LGTM - Consistent photos pagination implementation.

The photos pagination mirrors the albums implementation with appropriate state bindings and event handlers. The dual condition ensures proper rendering only when photos are loaded and pagination is initialized.

Copy link

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
lang/fa/gallery.php (1)

245-263: “Strict/Lexico” sort option labels are English in fa.
Same concern as pagination: these will show up untranslated in the Persian UI.

Proposed update (placeholder translations; please adjust to preferred terminology)
-    'photo_select_3_strict' => 'Title (Lexico)',
-    'photo_select_4_strict' => 'Description (Lexico)',
+    'photo_select_3_strict' => 'عنوان (Lexico)',
+    'photo_select_4_strict' => 'توضیحات (Lexico)',
     ...
-    'album_select_2_strict' => 'Title (Lexico)',
-    'album_select_3_strict' => 'Description (Lexico)',
+    'album_select_2_strict' => 'عنوان (Lexico)',
+    'album_select_3_strict' => 'توضیحات (Lexico)',
🧹 Nitpick comments (7)
lang/it/gallery.php (2)

70-78: Pagination strings are in English, not Italian.

The new pagination block contains English strings (e.g., "Infinite scroll", "Load more button", "Loading...") rather than Italian translations. Other parts of this file use Italian (e.g., 'pinned_albums' => 'Album fissati').

Based on learnings, you may not prioritize strict localization consistency, but flagging this for awareness in case Italian translations are intended:

  • 'infinite_scroll' => 'Scorrimento infinito'
  • 'load_more_button' => 'Pulsante carica altro'
  • 'page_navigation' => 'Navigazione pagina'
  • 'loading' => 'Caricamento...'
  • 'load_more' => 'Carica altro'
  • 'load_more_photos' => 'Carica altro (%s foto rimanenti)'
  • 'load_more_albums' => 'Carica altro (%s album rimanenti)'

248-260: Sort option labels use English abbreviations.

The sort options use English abbreviations "(Nat)" and "(Lexico)" which may not be clear to Italian users. Consider using Italian equivalents like "(Naturale)" and "(Lessicografico)" for consistency with the target locale.

That said, technical abbreviations are sometimes left in English intentionally for clarity across locales. Based on learnings regarding localization preferences, this is optional.

lang/fr/gallery.php (1)

248-260: Sort labels: consider standardizing the abbreviations (“Nat”, “Lexico”)

Not a blocker, but “Nat”/“Lexico” are a bit opaque in French; consider “Nat.”/“Lex.” or “Naturel”/“Lexicographique” if you want maximum clarity.

lang/ru/all_settings.php (1)

306-325: Consider clarifying the description for webshop_manual_fulfill_enabled.

Line 325's description "Enable auto-fulfillment of orders on manual action." could be confusing since the key name contains "manual_fulfill" but the description mentions "auto-fulfillment". The detailed description at line 642 explains this better.

Consider rewording to something like:

📝 Suggested wording
-        'webshop_manual_fulfill_enabled' => 'Enable auto-fulfillment of orders on manual action.',
+        'webshop_manual_fulfill_enabled' => 'Enable automatic content delivery when manually marking orders as delivered.',
lang/nl/all_settings.php (1)

306-326: Verify language: English content in Dutch language file path.

The file path lang/nl/all_settings.php suggests this should contain Dutch translations, but all content (both existing and newly added) is in English. While the new translation keys are well-written with clear descriptions, appropriate security warnings, and performance considerations, please confirm whether:

  1. This is intentional (English is the standard for this file despite the nl directory)
  2. These entries should be translated to Dutch
  3. The nl directory name refers to something other than "Nederlands"

The new keys follow the established pattern in the file consistently, covering pagination settings (albums/photos per page, UI modes, infinite scroll thresholds), rating features, unlock propagation, and webshop configurations. The documentation is clear and professional.

Minor note: Lines 641-642 both mention content being "automatically made available to the user when possible" - consider slightly varying the wording to distinguish between auto-fulfillment (on payment completion) and manual fulfillment (on admin action).

Also applies to: 623-643

lang/zh_TW/all_settings.php (1)

325-325: Clarify potentially confusing wording.

The description "Enable auto-fulfillment of orders on manual action" contains contradictory terms. The phrase combines "auto-fulfillment" (suggesting automatic) with "on manual action" (requiring manual trigger).

Based on Line 642's details, the feature automatically makes content available when manually marked as delivered. Consider rewording for clarity:

📝 Suggested wording improvements
-        'webshop_manual_fulfill_enabled' => 'Enable auto-fulfillment of orders on manual action.',
+        'webshop_manual_fulfill_enabled' => 'Enable fulfillment of orders on manual action.',

Or, if emphasizing the automatic content delivery aspect:

-        'webshop_manual_fulfill_enabled' => 'Enable auto-fulfillment of orders on manual action.',
+        'webshop_manual_fulfill_enabled' => 'Enable automatic content delivery when manually marking orders as delivered.',
lang/el/gallery.php (1)

70-78: Greek locale adds English pagination strings; also fix minor consistency (ellipsis/capitalization).

If lang/el is intended to be Greek, these new values should be translated (or at least kept stylistically consistent: ellipsis, sentence case). Also consider pluralization for “photo(s)/album(s)” if the i18n system supports it.

Possible consistency-only tweak (no translation)
 'pagination' => [
     'infinite_scroll' => 'Infinite scroll',
     'load_more_button' => 'Load more button',
     'page_navigation' => 'Page navigation',
-    'loading' => 'Loading...',
-    'load_more' => 'Load More',
+    'loading' => 'Loading…',
+    'load_more' => 'Load more',
     'load_more_photos' => 'Load More (%s photos remaining)',
     'load_more_albums' => 'Load More (%s albums remaining)',
 ],
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45c02df and 6b42070.

📒 Files selected for processing (43)
  • lang/ar/all_settings.php
  • lang/ar/gallery.php
  • lang/cz/all_settings.php
  • lang/cz/gallery.php
  • lang/de/all_settings.php
  • lang/de/gallery.php
  • lang/el/all_settings.php
  • lang/el/gallery.php
  • lang/en/all_settings.php
  • lang/en/gallery.php
  • lang/es/all_settings.php
  • lang/es/gallery.php
  • lang/fa/all_settings.php
  • lang/fa/gallery.php
  • lang/fr/all_settings.php
  • lang/fr/gallery.php
  • lang/hu/all_settings.php
  • lang/hu/gallery.php
  • lang/it/all_settings.php
  • lang/it/gallery.php
  • lang/ja/all_settings.php
  • lang/ja/gallery.php
  • lang/nl/all_settings.php
  • lang/nl/gallery.php
  • lang/no/all_settings.php
  • lang/no/gallery.php
  • lang/pl/all_settings.php
  • lang/pl/gallery.php
  • lang/pt/all_settings.php
  • lang/pt/gallery.php
  • lang/ru/all_settings.php
  • lang/ru/gallery.php
  • lang/sk/all_settings.php
  • lang/sk/gallery.php
  • lang/sv/all_settings.php
  • lang/sv/gallery.php
  • lang/vi/all_settings.php
  • lang/vi/gallery.php
  • lang/zh_CN/all_settings.php
  • lang/zh_CN/gallery.php
  • lang/zh_TW/all_settings.php
  • lang/zh_TW/gallery.php
  • resources/js/components/pagination/PaginationLoadMore.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • resources/js/components/pagination/PaginationLoadMore.vue
🧰 Additional context used
📓 Path-based instructions (1)
**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.php: Any new PHP file should contain the license header and have a single blank line after the opening PHP tag
Variable names should be in snake_case in PHP
Apply the PSR-4 coding standard in PHP
Use in_array() with true as the third parameter in PHP
Only use booleans in if statements, not integers or strings
Use strict comparison (===) instead of loose comparison (==)
Avoid code duplication in both if and else statements
Do not use empty() in PHP
Use the moneyphp/money library for handling monetary values in PHP
Never use floats or doubles to represent monetary values; use integers representing the smallest currency unit (e.g., cents for USD)

**/*.php: Write or extend executable specifications (unit, behaviour, or scenario tests) ahead of implementation, confirm they fail, and then drive code to green before refactoring. List the expected success, validation, and failure branches and add thin failing tests for each path.
For PHP code, adhere to conventions: license headers in new files, strict comparison (===), no empty(), in_array() with third parameter true, snake_case variables, PSR-4 standard, test base classes (AbstractTestCase for Unit, BaseApiWithDataTest for Feature_v2).
Always run phpunit tests. If a test remains red, disable it with a TODO, note the reason, and capture the follow-up in the relevant plan.
Spotless now uses Palantir Java Format 2.78.0 with a 120-character wrap; configure IDE formatters to match before pushing code changes.
Keep each increment's control flow flat by delegating validation/normalisation into tiny pure helpers that return simple enums or result records, then compose them instead of introducing inline branching that inflates the branch count per change.
When introducing new helpers/utilities or editing files prone to style violations (records, DTOs, generated adapters), run the narrowest applicable lint target (for example phpstan) before the full pipeline. Note the command in the related plan/task.
For PHP changes, ru...

Files:

  • lang/ar/all_settings.php
  • lang/en/gallery.php
  • lang/it/gallery.php
  • lang/ru/gallery.php
  • lang/nl/gallery.php
  • lang/no/all_settings.php
  • lang/no/gallery.php
  • lang/cz/all_settings.php
  • lang/el/all_settings.php
  • lang/pt/gallery.php
  • lang/cz/gallery.php
  • lang/fr/gallery.php
  • lang/es/all_settings.php
  • lang/sv/all_settings.php
  • lang/de/all_settings.php
  • lang/ru/all_settings.php
  • lang/fa/gallery.php
  • lang/zh_TW/gallery.php
  • lang/fa/all_settings.php
  • lang/zh_TW/all_settings.php
  • lang/sk/gallery.php
  • lang/ja/gallery.php
  • lang/es/gallery.php
  • lang/ar/gallery.php
  • lang/vi/gallery.php
  • lang/vi/all_settings.php
  • lang/it/all_settings.php
  • lang/de/gallery.php
  • lang/nl/all_settings.php
  • lang/pt/all_settings.php
  • lang/fr/all_settings.php
  • lang/en/all_settings.php
  • lang/sk/all_settings.php
  • lang/zh_CN/all_settings.php
  • lang/el/gallery.php
  • lang/sv/gallery.php
  • lang/pl/all_settings.php
  • lang/pl/gallery.php
  • lang/hu/all_settings.php
  • lang/ja/all_settings.php
  • lang/zh_CN/gallery.php
  • lang/hu/gallery.php
🧠 Learnings (7)
📓 Common learnings
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3718
File: app/Models/Extensions/SortingDecorator.php:191-0
Timestamp: 2025-10-06T21:24:16.072Z
Learning: In the Lychee codebase (LycheeOrg/Lychee), when implementing pagination with the SortingDecorator class, it's acceptable for PHP-level sorting (used for natural sorting of title/description columns) to occur after pagination rather than before, even though this means sorting is per-page rather than globally correct across all pages. This is an intentional trade-off to avoid memory issues from loading all items before pagination. A config disclaimer should document this limitation.
📚 Learning: 2025-12-19T21:01:32.168Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3838
File: lang/pl/webshop.php:1-2
Timestamp: 2025-12-19T21:01:32.168Z
Learning: In the Lychee repository, PHP files under the lang/ directory (and its subdirectories) do not require the standard license header. This is an exception to the general PHP license header rule. Ensure all non-lang PHP files continue to include the license header.

Applied to files:

  • lang/ar/all_settings.php
  • lang/en/gallery.php
  • lang/it/gallery.php
  • lang/ru/gallery.php
  • lang/nl/gallery.php
  • lang/no/all_settings.php
  • lang/no/gallery.php
  • lang/cz/all_settings.php
  • lang/el/all_settings.php
  • lang/pt/gallery.php
  • lang/cz/gallery.php
  • lang/fr/gallery.php
  • lang/es/all_settings.php
  • lang/sv/all_settings.php
  • lang/de/all_settings.php
  • lang/ru/all_settings.php
  • lang/fa/gallery.php
  • lang/zh_TW/gallery.php
  • lang/fa/all_settings.php
  • lang/zh_TW/all_settings.php
  • lang/sk/gallery.php
  • lang/ja/gallery.php
  • lang/es/gallery.php
  • lang/ar/gallery.php
  • lang/vi/gallery.php
  • lang/vi/all_settings.php
  • lang/it/all_settings.php
  • lang/de/gallery.php
  • lang/nl/all_settings.php
  • lang/pt/all_settings.php
  • lang/fr/all_settings.php
  • lang/en/all_settings.php
  • lang/sk/all_settings.php
  • lang/zh_CN/all_settings.php
  • lang/el/gallery.php
  • lang/sv/gallery.php
  • lang/pl/all_settings.php
  • lang/pl/gallery.php
  • lang/hu/all_settings.php
  • lang/ja/all_settings.php
  • lang/zh_CN/gallery.php
  • lang/hu/gallery.php
📚 Learning: 2025-12-19T21:01:45.910Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3838
File: lang/ar/webshop.php:1-2
Timestamp: 2025-12-19T21:01:45.910Z
Learning: Do not require license headers for language translation files under the lang/ directory (e.g., lang/ar/webshop.php). These resource files are exempt from header checks; apply header enforcement to other PHP source files in the repo.

Applied to files:

  • lang/ar/all_settings.php
  • lang/en/gallery.php
  • lang/it/gallery.php
  • lang/ru/gallery.php
  • lang/nl/gallery.php
  • lang/no/all_settings.php
  • lang/no/gallery.php
  • lang/cz/all_settings.php
  • lang/el/all_settings.php
  • lang/pt/gallery.php
  • lang/cz/gallery.php
  • lang/fr/gallery.php
  • lang/es/all_settings.php
  • lang/sv/all_settings.php
  • lang/de/all_settings.php
  • lang/ru/all_settings.php
  • lang/fa/gallery.php
  • lang/zh_TW/gallery.php
  • lang/fa/all_settings.php
  • lang/zh_TW/all_settings.php
  • lang/sk/gallery.php
  • lang/ja/gallery.php
  • lang/es/gallery.php
  • lang/ar/gallery.php
  • lang/vi/gallery.php
  • lang/vi/all_settings.php
  • lang/it/all_settings.php
  • lang/de/gallery.php
  • lang/nl/all_settings.php
  • lang/pt/all_settings.php
  • lang/fr/all_settings.php
  • lang/en/all_settings.php
  • lang/sk/all_settings.php
  • lang/zh_CN/all_settings.php
  • lang/el/gallery.php
  • lang/sv/gallery.php
  • lang/pl/all_settings.php
  • lang/pl/gallery.php
  • lang/hu/all_settings.php
  • lang/ja/all_settings.php
  • lang/zh_CN/gallery.php
  • lang/hu/gallery.php
📚 Learning: 2025-12-28T18:12:55.752Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3901
File: app/Providers/AppServiceProvider.php:0-0
Timestamp: 2025-12-28T18:12:55.752Z
Learning: When using Laravel Octane's tick API, Octane::tick(...) returns an InvokeTickCallable that only has ->seconds(int) and ->immediate() methods. There is no ->every(N) method. Use the correct usage: Octane::tick('name', fn() => ...)->seconds(N) or Octane::tick('name', fn() => ..., N). Apply this guideline to PHP files across the project (not just AppServiceProvider.php).

Applied to files:

  • lang/ar/all_settings.php
  • lang/en/gallery.php
  • lang/it/gallery.php
  • lang/ru/gallery.php
  • lang/nl/gallery.php
  • lang/no/all_settings.php
  • lang/no/gallery.php
  • lang/cz/all_settings.php
  • lang/el/all_settings.php
  • lang/pt/gallery.php
  • lang/cz/gallery.php
  • lang/fr/gallery.php
  • lang/es/all_settings.php
  • lang/sv/all_settings.php
  • lang/de/all_settings.php
  • lang/ru/all_settings.php
  • lang/fa/gallery.php
  • lang/zh_TW/gallery.php
  • lang/fa/all_settings.php
  • lang/zh_TW/all_settings.php
  • lang/sk/gallery.php
  • lang/ja/gallery.php
  • lang/es/gallery.php
  • lang/ar/gallery.php
  • lang/vi/gallery.php
  • lang/vi/all_settings.php
  • lang/it/all_settings.php
  • lang/de/gallery.php
  • lang/nl/all_settings.php
  • lang/pt/all_settings.php
  • lang/fr/all_settings.php
  • lang/en/all_settings.php
  • lang/sk/all_settings.php
  • lang/zh_CN/all_settings.php
  • lang/el/gallery.php
  • lang/sv/gallery.php
  • lang/pl/all_settings.php
  • lang/pl/gallery.php
  • lang/hu/all_settings.php
  • lang/ja/all_settings.php
  • lang/zh_CN/gallery.php
  • lang/hu/gallery.php
📚 Learning: 2025-08-22T06:11:18.329Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3641
File: lang/no/settings.php:9-9
Timestamp: 2025-08-22T06:11:18.329Z
Learning: For lang/* translation files in the Lychee project: only review PHP-related issues (syntax, structure, etc.), not translation content, grammar, or language-related nitpicks. The maintainer ildyria has explicitly requested this approach.

Applied to files:

  • lang/en/gallery.php
  • lang/no/all_settings.php
  • lang/el/all_settings.php
  • lang/cz/gallery.php
  • lang/es/all_settings.php
  • lang/fa/all_settings.php
  • lang/it/all_settings.php
  • lang/de/gallery.php
  • lang/pt/all_settings.php
  • lang/pl/gallery.php
  • lang/hu/all_settings.php
  • lang/ja/all_settings.php
📚 Learning: 2025-08-27T08:48:32.956Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3654
File: lang/pl/gallery.php:210-210
Timestamp: 2025-08-27T08:48:32.956Z
Learning: The user ildyria does not prioritize strict localization consistency for new menu items in language files, as indicated by their "Lang = don't care" response when suggested to translate 'Import from Server' to Polish in lang/pl/gallery.php.

Applied to files:

  • lang/it/gallery.php
  • lang/ru/gallery.php
  • lang/cz/gallery.php
  • lang/de/gallery.php
  • lang/pl/gallery.php
📚 Learning: 2025-10-06T21:24:16.072Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3718
File: app/Models/Extensions/SortingDecorator.php:191-0
Timestamp: 2025-10-06T21:24:16.072Z
Learning: In the Lychee codebase (LycheeOrg/Lychee), when implementing pagination with the SortingDecorator class, it's acceptable for PHP-level sorting (used for natural sorting of title/description columns) to occur after pagination rather than before, even though this means sorting is per-page rather than globally correct across all pages. This is an intentional trade-off to avoid memory issues from loading all items before pagination. A config disclaimer should document this limitation.

Applied to files:

  • lang/fr/gallery.php
  • lang/fa/gallery.php
  • lang/zh_TW/gallery.php
  • lang/ja/gallery.php
  • lang/de/gallery.php
  • lang/hu/gallery.php
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: 2️⃣ PHP dist / 8.5 - mariadb
  • GitHub Check: 2️⃣ PHP dist / 8.4 - sqlite
  • GitHub Check: 1️⃣ PHP tests / 8.4 - postgresql -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Precomputing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Precomputing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Precomputing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Webshop

@ildyria ildyria merged commit 9ac6dc3 into master Jan 11, 2026
44 checks passed
@ildyria ildyria deleted the paginate branch January 11, 2026 00:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants