Skip to content

Conversation

@ildyria
Copy link
Member

@ildyria ildyria commented Dec 27, 2025

Summary by CodeRabbit

  • New Features
    • Photo star rating (0–5) with API endpoint, rating widget, keyboard shortcuts, and per-photo ratings.
  • Config
    • New settings to control rating visibility and whether averages or user ratings are shown per view.
  • Data & Migrations
    • New rating storage and added statistics fields for rating sum/count/average with migrations and factory defaults.
  • UI
    • New rating components and overlays; UI updates reflect changes immediately.
  • Docs & Translations
    • Spec, plan, docs, and translations added for rating UI.
  • Tests
    • Integration and concurrency tests covering rating behavior and edge cases.

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

@coderabbitai
Copy link

coderabbitai bot commented Dec 27, 2025

📝 Walkthrough

Walkthrough

Adds photo star-rating: DB migrations and PhotoRating model; Statistics gains rating_sum/rating_count and rating_avg; new rating API/request/action/policy/resources; model relations and eager-loads; frontend components, composables, state/types, configs, translations, docs and tests.

Changes

Cohort / File(s) Summary
Database & Migrations
database/migrations/2025_12_27_034137_create_photo_ratings_table.php, database/migrations/2025_12_27_034141_add_rating_columns_to_photo_statistics.php, database/migrations/2025_12_27_130512_add_photo_rating_config_settings.php
New photo_ratings table; add rating_sum/rating_count columns to statistics; insert rating-related config settings.
Models & ORM
app/Models/PhotoRating.php, app/Models/Photo.php, app/Models/Statistics.php, database/factories/StatisticsFactory.php
New PhotoRating model; Photo adds ratings() and per-user rating() relations; Statistics adds rating_sum, rating_count, casts and rating_avg accessor; factory defaults updated.
Backend Actions & Persistence
app/Actions/Photo/Rating.php, app/Actions/Photo/Pipes/Shared/SaveStatistics.php, app/Actions/Album/Create.php
New transactional rating action handling create/update/delete and statistics adjustments; statistics initialization includes rating fields.
Eager-loading & Queries
app/Actions/... (PositionData, Timeline, Search, Albums/PositionData, Flow), app/Factories/AlbumFactory.php, app/SmartAlbums/*, app/Http/Controllers/Gallery/FrameController.php
Added rating eager-loads to many Photo queries so per-user rating is available where resources expect it.
API, Controller, Request & Policy
routes/api_v2.php, app/Http/Controllers/Gallery/PhotoController.php, app/Http/Requests/Photo/SetPhotoRatingRequest.php, app/Policies/PhotoPolicy.php, app/Contracts/Http/Requests/RequestAttribute.php
New POST /Photo::setRatingPhotoController::rate(); SetPhotoRatingRequest validates/loads photo and rating; new CAN_READ_RATINGS policy and RATING_ATTRIBUTE constant.
Resources & DTOs
app/Http/Resources/Models/PhotoRatingResource.php, app/Http/Resources/Models/PhotoStatisticsResource.php, app/Http/Resources/Models/PhotoResource.php, app/Http/Resources/GalleryConfigs/InitConfig.php
New PhotoRatingResource; PhotoStatisticsResource adds rating_count/rating_avg; PhotoResource exposes optional rating when permitted; InitConfig reads new rating config fields and uses VisibilityType.
Enums & Types
app/Enum/VisibilityType.php, resources/js/lychee.d.ts
Enum renamed to VisibilityType; TypeScript types updated for visibility and new PhotoRating/Statistics/Photo typings.
Frontend UI & Components
resources/js/components/... (PhotoRatingWidget.vue, PhotoRatingOverlay.vue, ThumbRatingOverlay.vue, StarRow.vue, PhotoDetails.vue, PhotoPanel.vue, PhotoThumb.vue, AlbumThumb.vue)
New rating UI components and integrations into details, panel and thumbs; conditional rendering based on config/state.
Frontend State, Services & Composables
resources/js/stores/LycheeState.ts, resources/js/services/photo-service.ts, resources/js/composables/photo/useRating.ts, resources/js/composables/album/photoActions.ts, resources/js/views/.../Album.vue, Search.vue
Lychee state adds rating settings and modes; PhotoService adds setRating; new useRating composable updates stores and shows toasts; UI handlers sync in-memory state immediately.
Settings & Middleware
app/Http/Middleware/ConfigIntegrity.php, app/Http/Resources/GalleryConfigs/InitConfig.php, resources/js/components/settings/ConfirmSave.vue
ConfigIntegrity includes rating keys in SE_FIELDS; InitConfig reads rating settings; minor layout/RTL tweaks.
Docs & Specs
docs/specs/...
Added feature plan, spec, tasks, open questions, roadmap entry, and coding-conventions updates (DB transactions, TS type generation).
Translations
lang/{ar,cz,de,el,en,es,fa,fr,hu,it,ja,nl,no,pl,pt,ru,sk,sv,vi,zh_CN,zh_TW}/gallery.php
Added photo.edit.rating translation block across locales.
Tests
tests/Feature_v2/Photo/*, tests/Traits/RequiresImageHandler.php, tests/Unit/Actions/Db/*
New integration, request, resource and concurrency tests for rating; small test adjustments (cache invalidation and expected values).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hopped a star into each frame,
sums and counts tucked in neat,
transactions stitch each tiny name,
one click blooms metrics, none obsolete,
galleries hum — a rabbit's feat. ✨

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.45% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.

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.

ildyria and others added 28 commits December 27, 2025 12:40
Add PhotoRatingConcurrencyTest to verify:
- Unique constraint prevents duplicate ratings
- Same user rapid updates work correctly
- Multiple users concurrent ratings maintain statistics consistency
- Add/remove operations maintain data integrity

Update database optimization tests to account for new photo_ratings table
(table count increased from 34-35 to 35-36).

Mark tasks I10 (Error Handling & Edge Cases) and I11 (Concurrency & Data
Integrity Tests) as complete in feature 001 tasks document.

Spec impact: Updates docs/specs/4-architecture/features/001-photo-star-rating/tasks.md
to reflect completion of backend testing increments I10-I11.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add PhotoService.setRating() method to call the POST /Photo::setRating endpoint
with type-safe rating values (0-5).

Auto-generate TypeScript types from PHP resources using php artisan typescript:transform.
This adds to PhotoResource:
- current_user_rating: number | null
And to PhotoStatisticsResource:
- rating_count: number
- rating_avg: number | null

Document the typescript:transform command in coding-conventions.md to ensure
developers regenerate types after modifying PHP resource classes.

Mark task I7 (Frontend Service Layer) as complete in feature 001 tasks document.

Spec impact: Updates docs/specs/3-reference/coding-conventions.md with TypeScript
type generation workflow and docs/specs/4-architecture/features/001-photo-star-rating/tasks.md
to reflect completion of frontend service layer increment I7.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Create PhotoRatingWidget Vue component with interactive 5-star rating interface:
- Display average rating with half-star support when metrics enabled
- Show user's current rating with hover preview
- Remove rating button (0) to clear user's rating
- Loading states with spinner during API calls
- Toast notifications for success/error feedback
- Error handling for 401/403/404 responses

Integrate PhotoRatingWidget into PhotoDetails drawer, displaying below statistics section.
Component updates photoStore.photo automatically after successful rating changes.

Mark tasks I8 (PhotoRatingWidget Component) and I9 (Integrate into PhotoDetails)
as complete in feature 001 tasks document.

Spec impact: Updates docs/specs/4-architecture/features/001-photo-star-rating/tasks.md
to reflect completion of frontend components I8-I9.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Create ThumbRatingOverlay component for compact star display on photo thumbnails:
- Shows current user rating as filled stars
- Hidden by default, visible on hover (opacity-0 group-hover:opacity-100)
- Positioned top-right with backdrop blur

Create PhotoRatingOverlay for full photo view:
- Displays user's rating in top corner of photo view
- Only shows when user has rated the photo
- Integrates with existing Overlay component pattern

Integrate overlays:
- ThumbRatingOverlay added to PhotoThumb component
- PhotoRatingOverlay added to PhotoPanel component

Mark tasks I9a-I9d as complete in feature 001 tasks document.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update task tracking to reflect completion of photo rating overlay components and their integration.

Spec impact: docs/specs/4-architecture/features/001-photo-star-rating/tasks.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add 6 configuration settings to control rating feature visibility:
- ratings_enabled: master switch for rating feature
- rating_show_avg_in_details: show average in photo details drawer
- rating_show_avg_in_photo_view: show average in full photo view
- rating_photo_view_mode: when to show rating overlay (always/hover/hidden)
- rating_show_avg_in_album_view: show average on thumbnails
- rating_album_view_mode: when to show rating on thumbnails (always/hover/hidden)

Backend changes:
- Migration adds 6 config rows with sensible defaults
- PhotoController::rate() checks ratings_enabled config, throws ConfigurationException if disabled
- InitConfig resource exposes all 6 settings to frontend

Frontend changes:
- LycheeState store includes 6 rating config properties
- PhotoRatingWidget respects ratings_enabled and rating_show_avg_in_details settings
- ThumbRatingOverlay respects ratings_enabled, rating_show_avg_in_album_view, and rating_album_view_mode
- PhotoRatingOverlay respects ratings_enabled, rating_show_avg_in_photo_view, and rating_photo_view_mode
- All components use actual config instead of placeholder computed properties

All quality gates pass (PHPStan, php-cs-fixer, TypeScript, Prettier).

Spec impact: docs/specs/4-architecture/features/001-photo-star-rating/tasks.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Create new VisibilityType enum (never/always/hover) and use it for rating configuration settings instead of string literals.

Changes:
- Add VisibilityType enum with NEVER, ALWAYS, HOVER cases
- Update InitConfig to use VisibilityType for rating_photo_view_mode and rating_album_view_mode
- Update migration to use "never" instead of "hidden" for consistency with existing ThumbOverlayVisibilityType
- Update frontend LycheeState to use App.Enum.VisibilityType
- Update Vue components to check for "never" instead of "hidden"
- TypeScript types auto-generated with new VisibilityType enum

All quality gates pass (PHPStan, php-cs-fixer, TypeScript, Prettier).

Spec impact: docs/specs/4-architecture/features/001-photo-star-rating/tasks.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update rating config migration to extend BaseConfigMigration instead of implementing custom up/down methods.

Changes:
- Extend BaseConfigMigration instead of Migration
- Use self::BOOL constant instead of '0|1' string literals
- Add proper PHPDoc return type annotation for getConfigs()
- Remove custom up() and down() methods (provided by BaseConfigMigration)

All quality gates pass (PHPStan, php-cs-fixer).

Spec impact: docs/specs/4-architecture/features/001-photo-star-rating/tasks.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update task tracking to reflect completion of config settings implementation for rating visibility.

Completed deliverables:
- 6 config settings using BaseConfigMigration
- VisibilityType enum for type safety
- PhotoController checks ratings_enabled config
- Frontend components use actual config values
- All quality gates passing

Spec impact: docs/specs/4-architecture/features/001-photo-star-rating/tasks.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update database schema documentation and roadmap to reflect completion of photo rating feature.

Changes to database-schema.md:
- Add PhotoRating model documentation with fields, constraints, and relationships
- Update Photo model to include PhotoRating and PhotoStatistics relationships
- Update User model to include PhotoRating relationship

Changes to roadmap.md:
- Move Feature 001 from Active to Completed
- Record completion date as 2025-12-27
- Add notes about feature capabilities

Spec impact: docs/specs/4-architecture/features/001-photo-star-rating/tasks.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Mark final increments as complete in tasks.md. All 18 tasks delivered successfully.

I12 (Documentation):
- Updated database-schema.md with PhotoRating model
- Updated roadmap.md to move feature from Active to Completed

I13 (Final Quality Gate):
- PHP: php-cs-fixer (0 changes), PHPStan (no errors)
- Frontend: Prettier formatted, TypeScript validated
- Tests: 21/21 passing (1090 assertions)

Feature 001 Photo Star Rating is now complete and ready for production use.

Spec impact: docs/specs/4-architecture/features/001-photo-star-rating/tasks.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add new increment I14 to address N+1 query problem in PhotoResource when loading rating collections.

Problem:
- PhotoResource fetches current_user_rating individually for each photo
- Causes N+1 queries when loading albums/collections with many photos
- Performance degrades linearly with photo count

Solution approaches documented:
1. Eager load ratings relation in PhotoResource with loadMissing
2. Filter to current user in memory after loading
3. Alternative: Eager load with query constraint in controllers

Estimated: 90 minutes
Priority: High (performance issue)

Spec impact: docs/specs/4-architecture/features/001-photo-star-rating/tasks.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@ildyria ildyria marked this pull request as ready for review December 28, 2025 00:12
@ildyria ildyria requested a review from a team as a code owner December 28, 2025 00:12
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: 10

Caution

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

⚠️ Outside diff range comments (4)
tests/Unit/Actions/Db/OptimizeTablesTest.php (2)

26-30: Fix the incorrect docblock comment.

The docblock describes checking "directories" and "license and copyright info," but this test actually verifies table optimization behavior. This appears to be copy-pasted from another test.

🔎 Proposed fix
 	/**
-	 * Iterate over the directories and check if the files contain the correct license and copyright info..
+	 * Test that the OptimizeTables action returns the expected number of optimized table IDs.
 	 *
 	 * @return void
 	 */

31-36: Consider refactoring this brittle test to improve maintainability.

The test checks for exact table counts [3, 35, 36] without explaining what these magic numbers represent. This design is fragile:

  • Every schema change requires updating this test
  • The test doesn't verify WHICH tables are optimized, only HOW MANY
  • The magic numbers lack context (what are tables 3, 35, and 36?)

Consider refactoring to:

  • Verify the actual table names being optimized instead of counts
  • Or at least add comments explaining what these numbers represent
  • Or simplify to self::assertGreaterThan(0, $output) if the exact count isn't critical
🔎 Example refactor verifying table names
 	public function testOptimizeTables(): void
 	{
 		$optimize = new OptimizeTables();
-		$output = count($optimize->do());
-		self::assertTrue(in_array($output, [3, 35, 36], true), 'OptimizeTables should return either 3 or 35 or 36: ' . $output);
+		$tables = $optimize->do();
+		self::assertNotEmpty($tables, 'OptimizeTables should return at least one table');
+		// Or verify specific expected table names if possible
+		// self::assertContains('photos', $tables);
 	}
docs/specs/4-architecture/open-questions.md (1)

1-1143: Remove all resolved question entries per documentation standard; retain only active open questions.

Coding guidelines specify: "remove each row as soon as it is resolved, ensuring the answer is captured first in the governing spec's normative sections." The instructions section explicitly states: "Resolve and remove: When answered, update the relevant spec sections (and create ADR if high-impact), then delete both the table row and Question Details entry."

Current file contains ~26 resolved questions (lines 13–179, strikethrough format) and ~22 archived entries (lines 213–296) that violate this removal requirement. Verification confirms that all decisions are properly captured in the feature spec (docs/specs/4-architecture/features/001-photo-star-rating/spec.md):

  • Q001-05, Q001-06 encoded in FR-001-01, FR-001-02, NFR-001-04
  • Q001-13 encoded in NFR-001-02
  • Q001-01, Q001-02, Q001-03, Q001-04 encoded in FR-001-09, FR-001-10
  • Q001-11 encoded in FR-001-16
  • Q001-19 encoded across telemetry sections

Since all decisions are durably recorded in the governing specs' normative sections (Requirements and NFRs), delete all strikethrough and archived entries. This file should contain only questions that remain unresolved.

docs/specs/3-reference/database-schema.md (1)

260-261: Update the "Last updated" date.

The documentation has been modified to include the PhotoRating model, but the "Last updated" date still shows "December 22, 2025". Per coding guidelines, this should be updated to reflect the date of this update.

Update the date
 ---
 
-*Last updated: December 22, 2025*
+*Last updated: December 27, 2025*
🧹 Nitpick comments (12)
docs/specs/4-architecture/open-questions.md (1)

441-441: Address three minor style issues flagged by static analysis.

Three informal/weak word choices degrade writing quality:

  • Line 441: "retry" is weak in formal spec context
  • Line 656: "hard to read" could use stronger phrasing
  • Line 922: "gonna" is too informal for technical documentation; use "going to"
🔎 Proposed fixes
- - May retry without fixing underlying issue
+ - May attempt retries without fixing underlying issue

- - May be hard to read at small sizes
+ - May be difficult to read at small sizes

- - YAGNI (You Aren't Gonna Need It) principle violation
+ - YAGNI (You Aren't Going to Need It) principle violation

Also applies to: 656-656, 922-922

resources/js/components/settings/ConfirmSave.vue (1)

13-13: Simplify redundant directional classes.

Both ltr:lg:w-3xs and rtl:lg:w-3xs apply the same width value. Since there's no directional difference, you can simplify this to just lg:w-3xs.

🔎 Proposed simplification
-				'w-0 ltr:lg:w-3xs rtl:lg:w-3xs': !props.areAllSettingsEnabled,
+				'w-0 lg:w-3xs': !props.areAllSettingsEnabled,
database/migrations/2025_12_27_034137_create_photo_ratings_table.php (1)

25-25: Consider adding a CHECK constraint for the rating value.

While application-level validation exists (in SetPhotoRatingRequest), a database CHECK constraint provides defense-in-depth to ensure rating values are always within the valid 1-5 range, preventing invalid data from direct database access or bugs.

🔎 Proposed enhancement
-			$table->unsignedTinyInteger('rating')->comment('Rating value 1-5');
+			$table->unsignedTinyInteger('rating')->comment('Rating value 1-5');
+			
+			// Add CHECK constraint to enforce valid rating range
+			DB::statement('ALTER TABLE photo_ratings ADD CONSTRAINT check_rating_range CHECK (rating >= 1 AND rating <= 5)');

Note: You'll also need to add the constraint drop in the down() method:

public function down(): void
{
	Schema::table('photo_ratings', function (Blueprint $table) {
		$table->dropConstraintIfExists('check_rating_range');
	});
	Schema::dropIfExists('photo_ratings');
}

Alternatively, use Laravel's native support if available in your version:

-			$table->unsignedTinyInteger('rating')->comment('Rating value 1-5');
+			$table->unsignedTinyInteger('rating')->comment('Rating value 1-5')->check('rating >= 1 AND rating <= 5');
app/Actions/Photo/Timeline.php (1)

55-55: LGTM on rating addition. Consider removing duplicate 'statistics' entry.

The 'rating' relationship is correctly added to the eager-loading list. However, 'statistics' appears twice in the array, which is redundant (though harmless).

Remove duplicate 'statistics' entry
-		query: Photo::query()->with(['statistics', 'size_variants', 'statistics', 'palette', 'tags', 'rating']),
+		query: Photo::query()->with(['statistics', 'size_variants', 'palette', 'tags', 'rating']),
lang/ru/gallery.php (1)

184-196: Consider localizing the rating strings to Russian.

The rating translation block contains English strings instead of Russian translations. While functional, localizing these would improve the user experience for Russian-speaking users.

lang/zh_CN/gallery.php (1)

185-197: Consider localizing the rating strings to Chinese.

The rating translation block contains English strings instead of Chinese translations. While functional, localizing these would improve the user experience for Chinese-speaking users.

app/Models/Photo.php (1)

246-257: Docstring mismatch: method returns HasOne, not "all ratings".

The docstring on Line 247 says "Get all ratings for this photo" but this method returns a HasOne relationship filtered by the current user. Consider updating to clarify, e.g., "Get the rating for this photo by the current user (or anonymous rating if unauthenticated)."

The implementation itself is correct and properly scopes to the authenticated user or anonymous ratings.

🔎 Suggested docstring fix
	/**
-	 * Get all ratings for this photo.
+	 * Get the current user's rating for this photo, or the anonymous rating if unauthenticated.
	 *
	 * @return HasOne<PhotoRating,$this>
	 */
	public function rating(): HasOne
resources/js/composables/album/photoActions.ts (1)

25-29: Consider logging when photo sync is skipped.

The synchronization logic silently skips updating the photo in photosStore.photos if the photo is not found (when photoIndex === -1). While this may be intentional for cases where the photo list hasn't been loaded yet, it could lead to subtle inconsistencies if the photo should be present but isn't found due to other issues.

Optional: Add debug logging for skipped updates
 // Update the photo in the album list (photosStore) to keep it in sync
 const photoIndex = photosStore.photos.findIndex((p) => p.id === photoStore.photo!.id);
 if (photoIndex !== -1) {
 	photosStore.photos[photoIndex].is_starred = newStarValue;
+} else {
+	// Photo not in current list - this is expected when viewing individual photos
+	console.debug('Photo not in current album list, skipping photosStore sync');
 }
app/Actions/Photo/Rating.php (1)

42-53: Consider documenting the race condition behavior for statistics creation.

The firstOrCreate call at lines 42-53 could theoretically encounter a race condition if two concurrent requests attempt to create statistics for a photo without any. The DB unique constraint will handle this, causing one transaction to fail and retry via the outer try-catch.

While the current implementation handles this correctly, consider adding a comment explaining this behavior:

 // Ensure statistics record exists atomically (Q001-07)
+// Note: firstOrCreate may encounter race conditions on initial creation,
+// but the DB constraint ensures only one record is created.
+// Failed transaction will be caught and retried via outer exception handler.
 $statistics = Statistics::firstOrCreate(

This clarifies the expected behavior for future maintainers.

tests/Feature_v2/Photo/PhotoResourceRatingTest.php (1)

26-47: Test logic appears redundant.

Line 29-33 creates a rating record, then Line 36-39 calls Photo::setRating with the same rating value (4). This seems redundant - if you're testing that the resource includes the current user rating, you could either:

  1. Just call Photo::setRating without pre-creating the rating, OR
  2. Fetch the photo resource directly after creating the rating (if there's a GET endpoint)

The current approach tests rating creation + update in one test, which might be better split or clarified.

app/Models/Statistics.php (1)

80-89: Consider returning null instead of 0 when there are no ratings.

The accessor returns 0 when rating_count is zero, but PhotoStatisticsResource declares ?float $rating_avg = null, suggesting the intent may be to distinguish "no ratings" from "zero average". Returning 0 could cause the UI to display "0 stars" instead of "not rated".

🔎 Proposed fix to return null for no ratings
 protected function getRatingAvgAttribute(): float
 {
-    return $this->rating_count === 0 ? 0 : round($this->rating_sum / $this->rating_count, 2);
+    return $this->rating_count === 0 ? null : round($this->rating_sum / $this->rating_count, 2);
 }

Note: If you apply this change, also update the PHPDoc on line 28 and the return type to ?float.

tests/Feature_v2/Photo/SetPhotoRatingRequestTest.php (1)

44-53: The non-existent photo ID generation approach is acceptable but consider using a fixed test ID.

The random_bytes approach works but introduces non-determinism. A fixed placeholder ID matching the expected format might make debugging easier if the test ever fails unexpectedly.

🔎 Alternative with fixed test ID
 public function testSetRatingWithNonExistentPhoto(): void
 {
-    // Generate a valid random ID format that doesn't exist in the database
-    $nonExistentId = strtr(base64_encode(random_bytes(18)), '+/', '-_');
+    // Use a fixed valid-format ID that doesn't exist in the database
+    $nonExistentId = 'AAAAAAAAAAAAAAAAAAAAAAA_';
     $response = $this->actingAs($this->userMayUpload1)->postJson('Photo::setRating', [
         'photo_id' => $nonExistentId,
         'rating' => 5,
     ]);
     $this->assertNotFound($response);
 }

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: 4

Caution

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

⚠️ Outside diff range comments (3)
resources/js/views/gallery-panels/Search.vue (2)

276-287: Add error handling for the service call to prevent inconsistent UI state.

The optimistic update to albumStore.modelAlbum.cover_id occurs without waiting for or handling the result of PhotoService.setAsCover. If the API call fails, the UI will display an incorrect cover state with no user feedback or rollback mechanism.

Per coding guidelines, use .then() to handle the async result and implement rollback on failure.

🔎 Proposed fix with error handling and rollback
 setAsCover: () => {
 	if (albumId.value === undefined) {
 		return;
 	}
-	PhotoService.setAsCover(selectedPhoto.value!.id, albumId.value);
+	const previousCoverId = albumStore.modelAlbum?.cover_id;
+	const newCoverId = previousCoverId === selectedPhoto.value!.id ? null : selectedPhoto.value!.id;
+	
 	// Update the album's cover_id immediately to reflect the change (toggle behavior)
 	if (albumStore.modelAlbum !== undefined) {
-		albumStore.modelAlbum.cover_id = albumStore.modelAlbum.cover_id === selectedPhoto.value!.id ? null : selectedPhoto.value!.id;
+		albumStore.modelAlbum.cover_id = newCoverId;
 	}
-	AlbumService.clearCache(albumId.value);
-	// refresh();
+	
+	PhotoService.setAsCover(selectedPhoto.value!.id, albumId.value).then(
+		() => {
+			AlbumService.clearCache(albumId.value);
+		},
+		(error) => {
+			// Rollback on failure
+			if (albumStore.modelAlbum !== undefined) {
+				albumStore.modelAlbum.cover_id = previousCoverId;
+			}
+			toast.add({ severity: 'error', summary: trans('Error'), detail: trans('Failed to set cover'), life: 3000 });
+		}
+	);
 },

288-310: Add error handling for the service call to prevent inconsistent UI state.

The optimistic updates to albumStore.modelAlbum.header_id and albumStore.album.preFormattedData.url occur without waiting for or handling the result of PhotoService.setAsHeader. If the API call fails, the UI will display an incorrect header state with no user feedback or rollback mechanism.

Per coding guidelines, use .then() to handle the async result and implement rollback on failure.

🔎 Proposed fix with error handling and rollback
 setAsHeader: () => {
 	if (albumId.value === undefined) {
 		return;
 	}
-	PhotoService.setAsHeader(selectedPhoto.value!.id, albumId.value, false);
+	const previousHeaderId = albumStore.modelAlbum?.header_id;
+	const previousHeaderUrl = albumStore.album?.preFormattedData?.url;
+	
 	// Update the album's header_id immediately to reflect the change (toggle behavior)
 	const isToggleOff = albumStore.modelAlbum?.header_id === selectedPhoto.value!.id;
 	if (albumStore.modelAlbum !== undefined) {
 		albumStore.modelAlbum.header_id = isToggleOff ? null : selectedPhoto.value!.id;
 	}
 	// Update the header image URL in the album's preFormattedData
 	if (albumStore.album?.preFormattedData) {
 		if (isToggleOff) {
 			albumStore.album.preFormattedData.url = null;
 		} else {
 			// Use medium or small variant for the header image
 			const headerUrl = selectedPhoto.value!.size_variants.medium?.url ?? selectedPhoto.value!.size_variants.small?.url ?? null;
 			albumStore.album.preFormattedData.url = headerUrl;
 		}
 	}
-	AlbumService.clearCache(albumId.value);
-	// refresh();
+	
+	PhotoService.setAsHeader(selectedPhoto.value!.id, albumId.value, false).then(
+		() => {
+			AlbumService.clearCache(albumId.value);
+		},
+		(error) => {
+			// Rollback on failure
+			if (albumStore.modelAlbum !== undefined) {
+				albumStore.modelAlbum.header_id = previousHeaderId;
+			}
+			if (albumStore.album?.preFormattedData) {
+				albumStore.album.preFormattedData.url = previousHeaderUrl;
+			}
+			toast.add({ severity: 'error', summary: trans('Error'), detail: trans('Failed to set header'), life: 3000 });
+		}
+	);
 },
resources/js/components/gallery/albumModule/AlbumPanel.vue (1)

310-322: Critical: Async operation not properly handled.

The PhotoService.setAsCover call is asynchronous but the code immediately updates albumStore.modelAlbum.cover_id without waiting for the server response. If the service call fails, the UI will display incorrect state. Additionally, the refresh emit should only occur after the operation succeeds.

🔎 Proposed fix using .then() chaining
 setAsCover: () => {
 	if (albumStore.album === undefined) return;
 	if (selectedAlbum.value?.thumb?.id === undefined) return;
-	PhotoService.setAsCover(selectedAlbum.value!.thumb?.id, albumStore.album.id);
-	// Update the album's cover_id immediately to reflect the change (toggle behavior)
-	if (albumStore.modelAlbum !== undefined) {
-		albumStore.modelAlbum.cover_id =
-			albumStore.modelAlbum.cover_id === selectedAlbum.value!.thumb?.id ? null : selectedAlbum.value!.thumb?.id;
-	}
-	AlbumService.clearCache(albumStore.album.id);
-	emits("refresh");
+	PhotoService.setAsCover(selectedAlbum.value!.thumb?.id, albumStore.album.id).then(() => {
+		// Update the album's cover_id after server confirms (toggle behavior)
+		if (albumStore.modelAlbum !== undefined) {
+			albumStore.modelAlbum.cover_id =
+				albumStore.modelAlbum.cover_id === selectedAlbum.value!.thumb?.id ? null : selectedAlbum.value!.thumb?.id;
+		}
+		AlbumService.clearCache(albumStore.album.id);
+		emits("refresh");
+	});
 },

Based on coding guidelines: "Do not use await async calls in Vue3; use .then() instead"

♻️ Duplicate comments (2)
resources/js/components/gallery/albumModule/AlbumPanel.vue (2)

257-265: Critical: Async operation not properly handled.

The PhotoService.setAsCover call is asynchronous but the code immediately updates albumStore.modelAlbum.cover_id without waiting for the server response. If the service call fails, the UI will display incorrect state. The new toggle behavior (setting to null when already set) doesn't address the underlying async handling issue.

🔎 Proposed fix using .then() chaining
 setAsCover: () => {
 	if (albumStore.album === undefined) return;
-	PhotoService.setAsCover(selectedPhoto.value!.id, albumStore.album.id);
-	// Update the album's cover_id immediately to reflect the change (toggle behavior)
-	if (albumStore.modelAlbum !== undefined) {
-		albumStore.modelAlbum.cover_id = albumStore.modelAlbum.cover_id === selectedPhoto.value!.id ? null : selectedPhoto.value!.id;
-	}
-	AlbumService.clearCache(albumStore.album.id);
+	PhotoService.setAsCover(selectedPhoto.value!.id, albumStore.album.id).then(() => {
+		// Update the album's cover_id after server confirms (toggle behavior)
+		if (albumStore.modelAlbum !== undefined) {
+			albumStore.modelAlbum.cover_id = albumStore.modelAlbum.cover_id === selectedPhoto.value!.id ? null : selectedPhoto.value!.id;
+		}
+		AlbumService.clearCache(albumStore.album.id);
+	});
 },

Based on coding guidelines: "Do not use await async calls in Vue3; use .then() instead"


266-285: Critical: Async operation not properly handled.

The PhotoService.setAsHeader call is asynchronous but the code immediately updates albumStore.modelAlbum.header_id and albumStore.album.preFormattedData.url without waiting for the server response. If the service call fails, the UI will display incorrect state for both the header ID and URL. The new toggle behavior and URL updates don't address the underlying async handling issue.

🔎 Proposed fix using .then() chaining
 setAsHeader: () => {
 	if (albumStore.album === undefined) return;
-	PhotoService.setAsHeader(selectedPhoto.value!.id, albumStore.album.id, false);
-	// Update the album's header_id immediately to reflect the change (toggle behavior)
-	const isToggleOff = albumStore.modelAlbum?.header_id === selectedPhoto.value!.id;
-	if (albumStore.modelAlbum !== undefined) {
-		albumStore.modelAlbum.header_id = isToggleOff ? null : selectedPhoto.value!.id;
-	}
-	// Update the header image URL in the album's preFormattedData
-	if (albumStore.album.preFormattedData) {
-		if (isToggleOff) {
-			albumStore.album.preFormattedData.url = null;
-		} else {
-			// Use medium or small variant for the header image
-			const headerUrl = selectedPhoto.value!.size_variants.medium?.url ?? selectedPhoto.value!.size_variants.small?.url ?? null;
-			albumStore.album.preFormattedData.url = headerUrl;
-		}
-	}
-	AlbumService.clearCache(albumStore.album.id);
+	const isToggleOff = albumStore.modelAlbum?.header_id === selectedPhoto.value!.id;
+	PhotoService.setAsHeader(selectedPhoto.value!.id, albumStore.album.id, false).then(() => {
+		// Update the album's header_id after server confirms (toggle behavior)
+		if (albumStore.modelAlbum !== undefined) {
+			albumStore.modelAlbum.header_id = isToggleOff ? null : selectedPhoto.value!.id;
+		}
+		// Update the header image URL in the album's preFormattedData
+		if (albumStore.album.preFormattedData) {
+			if (isToggleOff) {
+				albumStore.album.preFormattedData.url = null;
+			} else {
+				// Use medium or small variant for the header image
+				const headerUrl = selectedPhoto.value!.size_variants.medium?.url ?? selectedPhoto.value!.size_variants.small?.url ?? null;
+				albumStore.album.preFormattedData.url = headerUrl;
+			}
+		}
+		AlbumService.clearCache(albumStore.album.id);
+	});
 },

Based on coding guidelines: "Do not use await async calls in Vue3; use .then() instead"

🧹 Nitpick comments (1)
resources/js/views/gallery-panels/Search.vue (1)

281-284: Consider moving state updates to store actions.

The direct mutation of albumStore.modelAlbum and albumStore.album properties from the component violates separation of concerns. Store state updates should be encapsulated in store actions to improve maintainability, testability, and make state changes easier to track.

Consider creating actions like albumStore.updateCoverIdOptimistically(photoId) and albumStore.updateHeaderOptimistically(photoId, headerUrl) to centralize this logic.

Also applies to: 293-307

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f05b74 and 823d0d0.

📒 Files selected for processing (3)
  • resources/js/components/gallery/albumModule/AlbumPanel.vue
  • resources/js/composables/album/photoActions.ts
  • resources/js/views/gallery-panels/Search.vue
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{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/composables/album/photoActions.ts
  • resources/js/components/gallery/albumModule/AlbumPanel.vue
  • resources/js/views/gallery-panels/Search.vue
**/*.{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/composables/album/photoActions.ts
  • resources/js/components/gallery/albumModule/AlbumPanel.vue
  • resources/js/views/gallery-panels/Search.vue
**/*.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
  • resources/js/views/gallery-panels/Search.vue
🧠 Learnings (5)
📚 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/composables/album/photoActions.ts
  • resources/js/components/gallery/albumModule/AlbumPanel.vue
  • resources/js/views/gallery-panels/Search.vue
📚 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:

  • resources/js/composables/album/photoActions.ts
  • resources/js/components/gallery/albumModule/AlbumPanel.vue
📚 Learning: 2025-09-28T08:27:31.767Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3721
File: resources/js/composables/album/photoActions.ts:27-31
Timestamp: 2025-09-28T08:27:31.767Z
Learning: The maintainer ildyria has indicated that error handling for rotate functions in photoActions.ts should be ignored and not flagged as an issue.

Applied to files:

  • resources/js/composables/album/photoActions.ts
  • resources/js/components/gallery/albumModule/AlbumPanel.vue
📚 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 **/*.vue : Do not use await async calls in Vue3; use .then() instead

Applied to files:

  • resources/js/components/gallery/albumModule/AlbumPanel.vue
📚 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
🧬 Code graph analysis (1)
resources/js/composables/album/photoActions.ts (3)
resources/js/stores/PhotoState.ts (1)
  • PhotoStore (14-14)
resources/js/stores/PhotosState.ts (1)
  • usePhotosStore (7-33)
resources/js/stores/AlbumState.ts (1)
  • useAlbumStore (10-150)
⏰ 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: 3️⃣ Dockerfile Lint
  • GitHub Check: 2️⃣ PHP dist / 8.4 - mariadb
  • GitHub Check: 2️⃣ PHP dist / 8.5 - postgresql
  • GitHub Check: 2️⃣ PHP dist / 8.4 - postgresql
  • GitHub Check: 2️⃣ PHP dist / 8.4 - sqlite
  • GitHub Check: 2️⃣ PHP dist / 8.5 - mariadb
  • GitHub Check: 2️⃣ PHP dist / 8.5 - sqlite
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.4 - mariadb -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Unit
  • 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 -- Webshop
  • 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 - sqlite -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Feature_v2
🔇 Additional comments (1)
resources/js/composables/album/photoActions.ts (1)

5-6: LGTM: Store imports and initialization.

The addition of photosStore and albumStore enables state synchronization across multiple stores, which aligns with the rating-per-user feature requirements.

Also applies to: 12-13

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 (1)
resources/js/components/icons/StarRow.vue (1)

44-47: Consider adding prop validation for robustness.

While the component handles fractional ratings correctly, adding runtime validation would prevent rendering issues if invalid data (negative values, NaN, values > 5) is passed to the component.

🔎 Optional: Add prop validation
 const props = defineProps<{
 	rating: number;
 	size: "small" | "medium" | "large";
 }>();
+
+// Defensive validation
+const validatedRating = computed(() => {
+	if (isNaN(props.rating) || props.rating < 0) return 0;
+	if (props.rating > 5) return 5;
+	return props.rating;
+});

Then use validatedRating.value instead of props.rating in the template. However, this may be unnecessary if the component only receives data from trusted sources within the application.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 823d0d0 and 9a23d4d.

📒 Files selected for processing (3)
  • resources/js/components/gallery/photoModule/PhotoRatingOverlay.vue
  • resources/js/components/icons/StarRow.vue
  • tests/Feature_v2/Photo/PhotoResourceRatingTest.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/Feature_v2/Photo/PhotoResourceRatingTest.php
🧰 Additional context used
📓 Path-based instructions (3)
**/*.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/icons/StarRow.vue
  • resources/js/components/gallery/photoModule/PhotoRatingOverlay.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/icons/StarRow.vue
  • resources/js/components/gallery/photoModule/PhotoRatingOverlay.vue
**/*.{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/icons/StarRow.vue
  • resources/js/components/gallery/photoModule/PhotoRatingOverlay.vue
⏰ 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). (19)
  • GitHub Check: 3️⃣ Dockerfile Lint
  • GitHub Check: 2️⃣ PHP dist / 8.5 - postgresql
  • GitHub Check: 2️⃣ PHP dist / 8.4 - postgresql
  • GitHub Check: 2️⃣ PHP dist / 8.5 - mariadb
  • GitHub Check: 2️⃣ PHP dist / 8.4 - sqlite
  • GitHub Check: 2️⃣ PHP dist / 8.4 - mariadb
  • GitHub Check: 2️⃣ PHP dist / 8.5 - sqlite
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Feature_v2
  • 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 - postgresql -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Unit
  • 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.5 - mariadb -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Feature_v2
🔇 Additional comments (3)
resources/js/components/icons/StarRow.vue (1)

20-39: The edge case bug has been resolved correctly.

The fractional rating logic now handles ratings with fractional parts ≥ 0.75 correctly by treating them as full stars. The fix matches the previous suggestion and the star rendering logic now works correctly across all rating ranges (0.0 to 5.0 with fractional support).

resources/js/components/gallery/photoModule/PhotoRatingOverlay.vue (2)

13-13: The CSS typo has been corrected.

The transform class has been fixed from -translate-1/2 to the correct -translate-x-1/2, which properly centers the element horizontally.


13-13: The text-shadow class is already properly defined in resources/sass/app.css as a custom CSS utility (not a Tailwind utility), so no action is needed.

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: 3

🧹 Nitpick comments (1)
tests/Feature_v2/Photo/PhotoResourceRatingTest.php (1)

64-65: Use Configs facade without leading backslash.

Lines 64-65 use \Configs::set() with a leading backslash. For consistency with Laravel conventions and the rest of the codebase, import the facade at the top and use it without the backslash.

🔎 Proposed fix

At the top of the file, add the import:

+use App\Models\Configs;

Then update the method:

 	public function testPhotoResourceIncludesRatingStatisticsWhenMetricsEnabled(): void
 	{
 		// Enable metrics and set access to owner (userMayUpload1 owns photo1)
-		\Configs::set('metrics_enabled', '1');
-		\Configs::set('metrics_access', 'owner');
+		Configs::set('metrics_enabled', '1');
+		Configs::set('metrics_access', 'owner');

As per coding guidelines, avoid leading backslashes for class references when possible.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a23d4d and 58c6367.

📒 Files selected for processing (3)
  • app/Http/Controllers/Gallery/PhotoController.php
  • app/Http/Requests/Photo/SetPhotoRatingRequest.php
  • tests/Feature_v2/Photo/PhotoResourceRatingTest.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/Http/Controllers/Gallery/PhotoController.php
  • app/Http/Requests/Photo/SetPhotoRatingRequest.php
🧰 Additional context used
📓 Path-based instructions (3)
**/*.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/Feature_v2/Photo/PhotoResourceRatingTest.php
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/Photo/PhotoResourceRatingTest.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/Photo/PhotoResourceRatingTest.php
🧠 Learnings (4)
📚 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/Photo/PhotoResourceRatingTest.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:

  • tests/Feature_v2/Photo/PhotoResourceRatingTest.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, config values modified with Configs::set() are stored in the database and are automatically restored between tests when using the DatabaseTransactions trait, eliminating the need for manual restoration in tearDown methods.

Applied to files:

  • tests/Feature_v2/Photo/PhotoResourceRatingTest.php
📚 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:

  • tests/Feature_v2/Photo/PhotoResourceRatingTest.php
🧬 Code graph analysis (1)
tests/Feature_v2/Photo/PhotoResourceRatingTest.php (5)
app/Models/Photo.php (1)
  • Photo (134-508)
app/Models/PhotoRating.php (1)
  • PhotoRating (35-72)
tests/Feature_v2/Base/BaseApiWithDataTest.php (1)
  • BaseApiWithDataTest (92-254)
app/Services/Auth/SessionOrTokenGuard.php (1)
  • id (227-230)
tests/Feature_v2/Base/BaseApiTest.php (1)
  • postJson (76-79)
⏰ 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: 3️⃣ Dockerfile Lint
  • GitHub Check: 2️⃣ PHP dist / 8.4 - mariadb
  • GitHub Check: 2️⃣ PHP dist / 8.4 - postgresql
  • GitHub Check: 2️⃣ PHP dist / 8.5 - sqlite
  • GitHub Check: 2️⃣ PHP dist / 8.4 - sqlite
  • GitHub Check: 2️⃣ PHP dist / 8.5 - postgresql
  • GitHub Check: 2️⃣ PHP dist / 8.5 - mariadb
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Install
  • 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 - sqlite -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- ImageProcessing
🔇 Additional comments (2)
tests/Feature_v2/Photo/PhotoResourceRatingTest.php (2)

49-59: LGTM!

This test correctly verifies that posting a rating to a non-rated photo includes the new rating in the response.


112-133: The test expects 201 Created for an update operation, which violates REST conventions.

When updating an existing rating (line 122-125), the expected status code should be 200 OK or 204 No Content, not 201 Created. The status code 201 Created should only be returned when creating new resources.

Either the endpoint should be modified to return the appropriate status code for updates, or if this design is intentional, it should be clearly documented with an explanatory comment.

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: 1

🧹 Nitpick comments (3)
tests/Feature_v2/Photo/PhotoRatingConcurrencyTest.php (3)

61-91: Use strict assertions for integer comparisons.

Lines 78, 84, 89, and 90 use assertEquals(), which performs loose comparison (==). Per coding guidelines requiring strict comparison (===), use assertSame() for integer values to prevent type coercion issues.

🔎 Proposed fix
-		$this->assertEquals(1, $ratingCount);
+		$this->assertSame(1, $ratingCount);
 
 		// Verify final rating is the last one submitted
 		$finalRating = PhotoRating::where('photo_id', $this->photo1->id)
 			->where('user_id', $this->userMayUpload1->id)
 			->value('rating');
-		$this->assertEquals(1, $finalRating);
+		$this->assertSame(1, $finalRating);
 
 		// Verify statistics are consistent (sum = 1, count = 1)
 		$statistics = $this->photo1->statistics()->first();
 		$this->assertNotNull($statistics);
-		$this->assertEquals(1, $statistics->rating_sum);
-		$this->assertEquals(1, $statistics->rating_count);
+		$this->assertSame(1, $statistics->rating_sum);
+		$this->assertSame(1, $statistics->rating_count);

As per coding guidelines, strict comparison is required.


97-130: Use strict assertions and remove redundant checks.

Lines 126-127 use assertEquals() instead of assertSame() for integer comparisons. Additionally, lines 128-129 duplicate the assertions already made on lines 126-127 since $expectedSum is 12 and $expectedCount is 3.

🔎 Proposed fix
 		$statistics = $this->photo1->statistics()->first();
 		$this->assertNotNull($statistics);
-		$this->assertEquals($expectedSum, $statistics->rating_sum);
-		$this->assertEquals($expectedCount, $statistics->rating_count);
-		$this->assertEquals(12, $statistics->rating_sum); // 5 + 4 + 3
-		$this->assertEquals(3, $statistics->rating_count);
+		$this->assertSame($expectedSum, $statistics->rating_sum);
+		$this->assertSame($expectedCount, $statistics->rating_count);

As per coding guidelines, use strict comparison and avoid code duplication.


135-160: Use strict assertions for integer comparisons.

Lines 158-159 use assertEquals() for integer comparisons. Use assertSame() to enforce strict comparison (===) as required by the coding guidelines.

🔎 Proposed fix
 		$statistics = $this->photo1->statistics()->first();
 		$this->assertNotNull($statistics);
-		$this->assertEquals(4, $statistics->rating_sum);
-		$this->assertEquals(1, $statistics->rating_count);
+		$this->assertSame(4, $statistics->rating_sum);
+		$this->assertSame(1, $statistics->rating_count);

As per coding guidelines, strict comparison is required.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b7fb5a and 74b1e5e.

📒 Files selected for processing (1)
  • tests/Feature_v2/Photo/PhotoRatingConcurrencyTest.php
🧰 Additional context used
📓 Path-based instructions (3)
**/*.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/Feature_v2/Photo/PhotoRatingConcurrencyTest.php
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/Photo/PhotoRatingConcurrencyTest.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/Photo/PhotoRatingConcurrencyTest.php
🧠 Learnings (1)
📚 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/Photo/PhotoRatingConcurrencyTest.php
🧬 Code graph analysis (1)
tests/Feature_v2/Photo/PhotoRatingConcurrencyTest.php (5)
app/Exceptions/ModelDBException.php (1)
  • ModelDBException (33-56)
app/Models/PhotoRating.php (1)
  • PhotoRating (35-72)
tests/Feature_v2/Base/BaseApiWithDataTest.php (1)
  • BaseApiWithDataTest (92-254)
app/Models/Photo.php (1)
  • ratings (241-244)
tests/Traits/CatchFailures.php (1)
  • assertCreated (123-126)
⏰ 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 - postgresql
  • GitHub Check: 2️⃣ PHP dist / 8.5 - sqlite
  • GitHub Check: 2️⃣ PHP dist / 8.4 - sqlite
  • GitHub Check: 2️⃣ PHP dist / 8.5 - mariadb
  • GitHub Check: 2️⃣ PHP dist / 8.5 - postgresql
  • GitHub Check: 2️⃣ PHP dist / 8.4 - mariadb
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Install
  • 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 - sqlite -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Feature_v2

@codecov
Copy link

codecov bot commented Dec 28, 2025

Codecov Report

❌ Patch coverage is 88.46154% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.44%. Comparing base (15d66ca) to head (74b1e5e).
⚠️ Report is 1 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 ildyria merged commit cd0895e into master Dec 28, 2025
69 of 73 checks passed
@ildyria ildyria deleted the rating-per-user branch December 28, 2025 03:30
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