Wire admin studio SSR and search infrastructure

This commit is contained in:
2026-05-01 11:46:06 +02:00
parent 257b0dbef6
commit 18cea8b0f0
329 changed files with 197465 additions and 2741 deletions

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
use App\Models\Artwork;
function encodeLegacyPhotoId(int $value, string $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'): string
{
if ($value === 0) {
return '0';
}
$encoded = '';
while ($value > 0) {
$encoded = $chars[$value % 62] . $encoded;
$value = intdiv($value, 62);
}
return $encoded;
}
it('redirects legacy photo thumbnail urls to the mapped CDN thumbnail size', function (): void {
config([
'cdn.files_url' => 'https://cdn.example.test',
'uploads.object_storage.prefix' => 'artworks',
]);
$artwork = Artwork::factory()->create([
'hash' => 'aabbccddeeff0011',
'thumb_ext' => 'webp',
'file_ext' => 'jpg',
'file_path' => '',
]);
$response = $this->get('/photo/' . encodeLegacyPhotoId($artwork->id) . '_6.png');
$response
->assertStatus(301)
->assertRedirect('https://cdn.example.test/artworks/md/aa/bb/aabbccddeeff0011.webp');
});
it('redirects legacy full-size photo urls to the original CDN asset', function (): void {
config([
'cdn.files_url' => 'https://cdn.example.test',
'uploads.object_storage.prefix' => 'artworks',
]);
$artwork = Artwork::factory()->create([
'hash' => '1122334455667788',
'thumb_ext' => 'webp',
'file_ext' => 'png',
'file_path' => '',
]);
$response = $this->get('/photo/' . encodeLegacyPhotoId($artwork->id) . '_7.png');
$response
->assertStatus(301)
->assertRedirect('https://cdn.example.test/artworks/original/11/22/1122334455667788.png');
});

View File

@@ -428,3 +428,38 @@ it('produces cosine-normalized weights in pair builder', function () {
// S_beh = 2 / sqrt(2 * 2) = 2 / 2 = 1.0
expect($pair->weight)->toBe(1.0);
});
it('accumulates pair weights across small user chunks and rebuilds cleanly on rerun', function () {
$userA = User::factory()->create();
$userB = User::factory()->create();
$art1 = createPublicArtwork();
$art2 = createPublicArtwork();
DB::table('artwork_favourites')->insert([
['user_id' => $userA->id, 'artwork_id' => $art1->id, 'created_at' => now()->subMinute(), 'updated_at' => now()->subMinute()],
['user_id' => $userA->id, 'artwork_id' => $art2->id, 'created_at' => now()->subMinute(), 'updated_at' => now()->subMinute()],
['user_id' => $userB->id, 'artwork_id' => $art1->id, 'created_at' => now(), 'updated_at' => now()],
['user_id' => $userB->id, 'artwork_id' => $art2->id, 'created_at' => now(), 'updated_at' => now()],
]);
$job = new RecBuildItemPairsFromFavouritesJob(1);
$job->handle();
$pair = RecItemPair::query()
->where('a_artwork_id', min($art1->id, $art2->id))
->where('b_artwork_id', max($art1->id, $art2->id))
->first();
expect($pair)->not->toBeNull();
expect($pair?->weight)->toBe(1.0);
$job->handle();
$rebuiltPair = RecItemPair::query()
->where('a_artwork_id', min($art1->id, $art2->id))
->where('b_artwork_id', max($art1->id, $art2->id))
->first();
expect($rebuiltPair)->not->toBeNull();
expect($rebuiltPair?->weight)->toBe(1.0);
});

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
it('redirects malformed public search query strings to a canonical url', function (): void {
$response = $this->get('/search?amp;id=&amp;sort=&filter=&group=all&id=&page=0&page=1&q=winstep?page=1?page=1?page=1&sort=&sort=category?page=1&txtfilter=');
$response
->assertStatus(301)
->assertRedirect('/search?q=winstep');
});

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
use App\Models\Artwork;
use Illuminate\Support\Facades\DB;
it('falls back to the latest 1000 downloads when today has no downloads', function () {
$newerArtwork = Artwork::factory()->create([
'title' => 'Latest Window Artwork',
'published_at' => now()->subDays(5),
]);
$olderArtwork = Artwork::factory()->create([
'title' => 'Older Window Artwork',
'published_at' => now()->subDays(5),
]);
DB::table('artwork_downloads')->insert([
[
'artwork_id' => $newerArtwork->id,
'user_id' => null,
'ip' => inet_pton('127.0.0.1'),
'user_agent' => 'Pest',
'created_at' => now()->subDay()->setTime(12, 0, 0),
],
[
'artwork_id' => $newerArtwork->id,
'user_id' => null,
'ip' => inet_pton('127.0.0.1'),
'user_agent' => 'Pest',
'created_at' => now()->subDays(2)->setTime(12, 0, 0),
],
[
'artwork_id' => $olderArtwork->id,
'user_id' => null,
'ip' => inet_pton('127.0.0.1'),
'user_agent' => 'Pest',
'created_at' => now()->subDays(3)->setTime(12, 0, 0),
],
]);
$this->get('/downloads/today')
->assertOk()
->assertSee('Latest Window Artwork', false)
->assertSee('Older Window Artwork', false)
->assertSee('Latest 1000 downloads', false)
->assertDontSee('No download activity is available yet.', false);
});
it('prefers real today downloads over the fallback window', function () {
$todayArtwork = Artwork::factory()->create([
'title' => 'Today Download Artwork',
'published_at' => now()->subDays(5),
]);
$fallbackArtwork = Artwork::factory()->create([
'title' => 'Fallback Only Artwork',
'published_at' => now()->subDays(5),
]);
DB::table('artwork_downloads')->insert([
[
'artwork_id' => $todayArtwork->id,
'user_id' => null,
'ip' => inet_pton('127.0.0.1'),
'user_agent' => 'Pest',
'created_at' => now()->setTime(11, 0, 0),
],
[
'artwork_id' => $fallbackArtwork->id,
'user_id' => null,
'ip' => inet_pton('127.0.0.1'),
'user_agent' => 'Pest',
'created_at' => now()->subDays(2)->setTime(12, 0, 0),
],
]);
$this->get('/downloads/today')
->assertOk()
->assertSee('Today Download Artwork', false)
->assertSee('Live today', false)
->assertDontSee('Fallback Only Artwork', false)
->assertDontSee('Latest 1000 downloads', false);
});

View File

@@ -19,6 +19,7 @@ beforeEach(function (): void {
config()->set('vision.vector_gateway.base_url', 'https://vision.klevze.net');
config()->set('vision.vector_gateway.api_key', 'test-key');
config()->set('vision.vector_gateway.search_endpoint', '/vectors/search');
config()->set('vision.vector_gateway.search_file_endpoint', '/vectors/search/file');
config()->set('cdn.files_url', 'https://files.skinbase.org');
config()->set('app.url', 'https://skinbase.test');
Storage::fake('public');
@@ -44,7 +45,8 @@ it('returns AI similar artworks for a public artwork', function (): void {
]);
Http::fake([
'https://vision.klevze.net/vectors/search' => Http::response([
'https://files.skinbase.org/*' => Http::response('image-bytes', 200, ['Content-Type' => 'image/webp']),
'https://vision.klevze.net/vectors/search/file' => Http::response([
'results' => [
['id' => $source->id, 'score' => 1.0],
['id' => $match->id, 'score' => 0.91234],
@@ -61,6 +63,43 @@ it('returns AI similar artworks for a public artwork', function (): void {
->assertJsonCount(1, 'data');
});
it('falls back to URL search when the file vector endpoint fails for similar-ai', function (): void {
$source = Artwork::factory()->create([
'title' => 'Fallback source artwork',
'hash' => 'ffeeddccbbaa',
'thumb_ext' => 'webp',
'is_public' => true,
'is_approved' => true,
'published_at' => now()->subHour(),
]);
$match = Artwork::factory()->create([
'title' => 'Fallback match',
'hash' => '998877665544',
'thumb_ext' => 'webp',
'is_public' => true,
'is_approved' => true,
'published_at' => now()->subHour(),
]);
Http::fake([
'https://files.skinbase.org/*' => Http::response('image-bytes', 200, ['Content-Type' => 'image/webp']),
'https://vision.klevze.net/vectors/search/file' => Http::response(['error' => 'missing endpoint'], 404),
'https://vision.klevze.net/vectors/search' => Http::response([
'results' => [
['id' => $source->id, 'score' => 1.0],
['id' => $match->id, 'score' => 0.90123],
],
], 200),
]);
getJson('/api/art/' . $source->id . '/similar-ai')
->assertOk()
->assertJsonPath('data.0.id', $match->id)
->assertJsonPath('meta.artwork_id', $source->id)
->assertJsonCount(1, 'data');
});
it('returns 404 for missing similar-ai source artwork', function (): void {
getJson('/api/art/999999/similar-ai')
->assertStatus(404)
@@ -79,7 +118,7 @@ it('searches by uploaded image through the vector gateway', function (): void {
]);
Http::fake([
'https://vision.klevze.net/vectors/search' => Http::response([
'https://vision.klevze.net/vectors/search/file' => Http::response([
'results' => [
['id' => $match->id, 'score' => 0.88765],
],
@@ -99,12 +138,7 @@ it('searches by uploaded image through the vector gateway', function (): void {
->assertJsonPath('meta.limit', 12);
Http::assertSent(function ($request): bool {
$payload = json_decode($request->body(), true);
return $request->url() === 'https://vision.klevze.net/vectors/search'
&& $request->hasHeader('X-API-Key', 'test-key')
&& is_array($payload)
&& str_contains((string) ($payload['url'] ?? ''), '/storage/ai-search/tmp/')
&& ($payload['limit'] ?? null) === 12;
return $request->url() === 'https://vision.klevze.net/vectors/search/file'
&& $request->hasHeader('X-API-Key', 'test-key');
});
});