Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
# Project-Specific Instructions

## Code Quality Gates

- Type coverage must be 100%. Verify with: `./vendor/bin/pest --type-coverage --min=100`
- Run `composer qa` before finalizing any PHP changes.
- Run relevant tests with `php artisan test --compact` after changes.

## PHP Type Hints

- All closure parameters must have explicit type hints — Pest type-coverage enforces this.
- Use `Illuminate\Database\Query\Builder` for query builder closures: `fn (Builder $q) => ...`
- Use `\stdClass` for raw DB query result rows (e.g., in `->map(fn (\stdClass $row) => ...)`).
- When fixing type coverage, check every `fn ($var)` closure in the file — they all need types.

## Pint Auto-Formatting (Linter Hook)

- Pint runs automatically on file save as a hook. It will remove unused imports and convert FQCNs to `use` imports.
- When adding an import and its usage in the same file, do it in a **single edit** — otherwise Pint removes the "unused" import before the usage is added.
- Workaround: use a FQCN inline (e.g., `\App\Actions\AI\GetLitterBotUrlAction::CACHE_KEY`) and Pint will convert it to a proper `use` import automatically.

## PHPStan Strictness

- No `(int)` casts or `intval()` on `mixed` — use a PHPDoc `@var` annotation to type the variable first.
- No `??` on array keys declared as always-existing in a PHPDoc `@var array{key: type}` shape — PHPStan knows they can't be null.
- When consuming JSON responses, annotate with `@var` array shapes and access keys directly without fallbacks.

## DB Facade Usage

- `DB::query()` and `DB::table()` are acceptable for complex reporting/metrics queries that don't map to a single Eloquent model (e.g., aggregate stats, cross-table counts). Prefer Eloquent for standard CRUD operations.

<laravel-boost-guidelines>
=== foundation rules ===

Expand Down
31 changes: 31 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
# Project-Specific Instructions

## Code Quality Gates

- Type coverage must be 100%. Verify with: `./vendor/bin/pest --type-coverage --min=100`
- Run `composer qa` before finalizing any PHP changes.
- Run relevant tests with `php artisan test --compact` after changes.

## PHP Type Hints

- All closure parameters must have explicit type hints — Pest type-coverage enforces this.
- Use `Illuminate\Database\Query\Builder` for query builder closures: `fn (Builder $q) => ...`
- Use `\stdClass` for raw DB query result rows (e.g., in `->map(fn (\stdClass $row) => ...)`).
- When fixing type coverage, check every `fn ($var)` closure in the file — they all need types.

## Pint Auto-Formatting (Linter Hook)

- Pint runs automatically on file save as a hook. It will remove unused imports and convert FQCNs to `use` imports.
- When adding an import and its usage in the same file, do it in a **single edit** — otherwise Pint removes the "unused" import before the usage is added.
- Workaround: use a FQCN inline (e.g., `\App\Actions\AI\GetLitterBotUrlAction::CACHE_KEY`) and Pint will convert it to a proper `use` import automatically.

## PHPStan Strictness

- No `(int)` casts or `intval()` on `mixed` — use a PHPDoc `@var` annotation to type the variable first.
- No `??` on array keys declared as always-existing in a PHPDoc `@var array{key: type}` shape — PHPStan knows they can't be null.
- When consuming JSON responses, annotate with `@var` array shapes and access keys directly without fallbacks.

## DB Facade Usage

- `DB::query()` and `DB::table()` are acceptable for complex reporting/metrics queries that don't map to a single Eloquent model (e.g., aggregate stats, cross-table counts). Prefer Eloquent for standard CRUD operations.

<laravel-boost-guidelines>
=== foundation rules ===

Expand Down
30 changes: 30 additions & 0 deletions app/Actions/AI/GetLitterBotUrlAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace App\Actions\AI;

use App\Models\AppSetting;
use Illuminate\Container\Attributes\Config;

class GetLitterBotUrlAction
{
public const CACHE_KEY = 'suggest_photo_tags_action_litterbot_url';

public function __construct(
#[Config('services.litterbot.url')] protected string $defaultUrl,
) {}

public function run(): string
{
$valueFromSettings = cache()->remember(
self::CACHE_KEY,
now()->addSeconds(10),
fn () => AppSetting::query()->where('key', 'litterbot_url')->value('value')
);

if (is_string($valueFromSettings) && $valueFromSettings !== '') {
return $valueFromSettings;
}

return $this->defaultUrl;
}
}
11 changes: 0 additions & 11 deletions app/Actions/Photos/ClassifiesPhoto.php

This file was deleted.

61 changes: 0 additions & 61 deletions app/Actions/Photos/ClassifyPhotoAction.php

This file was deleted.

2 changes: 1 addition & 1 deletion app/Actions/Photos/FilterPhotosAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public function run(User $user): LengthAwarePaginator
->filter($user->settings->photo_filters)
->withExists([
'items',
'photoItemSuggestions' => fn (Builder $query) => $query->whereNull('is_accepted')->where('score', '>=', 80),
'photoSuggestions' => fn (Builder $query) => $query->whereNull('is_accepted')->where('item_score', '>=', 30),
])
->orderBy($user->settings->sort_column, $user->settings->sort_direction);

Expand Down
67 changes: 0 additions & 67 deletions app/Actions/Photos/GetItemFromPredictionAction.php

This file was deleted.

22 changes: 0 additions & 22 deletions app/Actions/Photos/GetRelevantTagShortcutAction.php

This file was deleted.

54 changes: 54 additions & 0 deletions app/Actions/Photos/SuggestPhotoTagsAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace App\Actions\Photos;

use App\Actions\AI\GetLitterBotUrlAction;
use App\DTO\PhotoSuggestionResult;
use App\Models\Photo;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class SuggestPhotoTagsAction implements SuggestsPhotoTags
{
public function __construct(
protected GetLitterBotUrlAction $getLitterBotUrl,
) {}

/**
* @throws ConnectionException
*/
public function run(Photo $photo): ?PhotoSuggestionResult
{
$response = Http::timeout(15)->post("{$this->getLitterBotUrl->run()}/predict", [
'photo_url' => $photo->full_path,
]);

if ($response->failed()) {
Log::error('Failed to get photo suggestions', [
'photo_id' => $photo->id,
'response' => $response->body(),
]);

return null;
}

Log::info('Received photo suggestions from LitterBot', [
'photo_id' => $photo->id,
'response' => $response->json(),
]);

/** @var array<int, array{id: int, name: string, confidence: float, count: int}> $items */
$items = $response->json('items', []);
/** @var array<int, array{id: int, name: string, confidence: float, count: int}> $brands */
$brands = $response->json('brands', []);
/** @var array<int, array{id: int, name: string, confidence: float, count: int}> $content */
$content = $response->json('content', []);

return new PhotoSuggestionResult(
items: $items,
brands: $brands,
content: $content,
);
}
}
11 changes: 11 additions & 0 deletions app/Actions/Photos/SuggestsPhotoTags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace App\Actions\Photos;

use App\DTO\PhotoSuggestionResult;
use App\Models\Photo;

interface SuggestsPhotoTags
{
public function run(Photo $photo): ?PhotoSuggestionResult;
}
36 changes: 36 additions & 0 deletions app/Console/Commands/MigratePhotoSuggestions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class MigratePhotoSuggestions extends Command
{
protected $signature = 'app:migrate-photo-suggestions';

protected $description = 'Migrate data from photo_item_suggestions to photo_suggestions table';

public function handle(): int
{
$count = DB::table('photo_item_suggestions')->count();

if ($count === 0) {
$this->components->info('No data to migrate.');

return Command::SUCCESS;
}

$this->components->info("Migrating {$count} records...");

DB::statement('
INSERT INTO photo_suggestions (id, photo_id, item_id, item_score, item_count, is_accepted)
SELECT id, photo_id, item_id, ROUND(score * 100), 0, is_accepted
FROM photo_item_suggestions
');

$this->components->info('Migration completed successfully.');

return Command::SUCCESS;
}
}
Loading