Wire admin studio SSR and search infrastructure
This commit is contained in:
@@ -37,8 +37,12 @@ class PostSearchController extends Controller
|
||||
->where('status', Post::STATUS_PUBLISHED)
|
||||
->paginate($perPage, 'page', $page);
|
||||
|
||||
// Load relations
|
||||
$results->load($this->feedService->publicEagerLoads());
|
||||
if ($viewerId) {
|
||||
$results->getCollection()->loadExists([
|
||||
'saves as viewer_saved' => fn ($saveQuery) => $saveQuery->where('user_id', $viewerId),
|
||||
]);
|
||||
}
|
||||
|
||||
$formatted = $results->getCollection()
|
||||
->map(fn ($post) => $this->feedService->formatPost($post, $viewerId))
|
||||
@@ -57,6 +61,9 @@ class PostSearchController extends Controller
|
||||
} catch (\Exception $e) {
|
||||
// Fallback: basic LIKE search on body
|
||||
$paginated = Post::with($this->feedService->publicEagerLoads())
|
||||
->withExists([
|
||||
'saves as viewer_saved' => fn ($saveQuery) => $saveQuery->where('user_id', $viewerId),
|
||||
])
|
||||
->where('status', Post::STATUS_PUBLISHED)
|
||||
->where('visibility', Post::VISIBILITY_PUBLIC)
|
||||
->where(function ($q) use ($query) {
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Models\User;
|
||||
use Carbon\CarbonInterface;
|
||||
use App\Services\ThumbnailPresenter;
|
||||
use App\Support\UsernamePolicy;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -48,19 +49,15 @@ final class ProfileApiController extends Controller
|
||||
},
|
||||
])
|
||||
->where('user_id', $user->id)
|
||||
->whereNull('deleted_at');
|
||||
->whereNull('artworks.deleted_at');
|
||||
|
||||
if (! $isOwner) {
|
||||
$query->where('is_public', true)->where('is_approved', true)->whereNotNull('published_at');
|
||||
$query->where('artworks.is_public', true)
|
||||
->where('artworks.is_approved', true)
|
||||
->whereNotNull('artworks.published_at');
|
||||
}
|
||||
|
||||
$query = match ($sort) {
|
||||
'trending' => $query->orderByDesc('ranking_score'),
|
||||
'rising' => $query->orderByDesc('heat_score'),
|
||||
'views' => $query->orderByDesc('view_count'),
|
||||
'favs' => $query->orderByDesc('favourite_count'),
|
||||
default => $query->orderByDesc('published_at'),
|
||||
};
|
||||
$query = $this->applyArtworkSort($query, $sort);
|
||||
|
||||
$perPage = 24;
|
||||
$paginator = $query->cursorPaginate($perPage);
|
||||
@@ -185,6 +182,30 @@ final class ProfileApiController extends Controller
|
||||
return null;
|
||||
}
|
||||
|
||||
private function applyArtworkSort(Builder $query, string $sort): Builder
|
||||
{
|
||||
$statsColumn = match ($sort) {
|
||||
'trending' => 'profile_artwork_stats.ranking_score',
|
||||
'rising' => 'profile_artwork_stats.heat_score',
|
||||
'views' => 'profile_artwork_stats.views',
|
||||
'favs' => 'profile_artwork_stats.favorites',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($statsColumn !== null) {
|
||||
return $query
|
||||
->leftJoin('artwork_stats as profile_artwork_stats', 'profile_artwork_stats.artwork_id', '=', 'artworks.id')
|
||||
->select('artworks.*')
|
||||
->orderByDesc($statsColumn)
|
||||
->orderByDesc('artworks.published_at')
|
||||
->orderByDesc('artworks.id');
|
||||
}
|
||||
|
||||
return $query
|
||||
->orderByDesc('artworks.published_at')
|
||||
->orderByDesc('artworks.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
|
||||
@@ -117,7 +117,7 @@ final class SimilarArtworksController extends Controller
|
||||
$filterParts[] = '(' . $tagFilter . ')';
|
||||
} elseif ($categorySlugs !== []) {
|
||||
$catFilter = implode(' OR ', array_map(
|
||||
fn (string $c): string => 'category = "' . addslashes($c) . '"',
|
||||
fn (string $c): string => '(category = "' . addslashes($c) . '" OR categories = "' . addslashes($c) . '")',
|
||||
$categorySlugs
|
||||
));
|
||||
$filterParts[] = '(' . $catFilter . ')';
|
||||
|
||||
@@ -11,7 +11,10 @@ use App\Http\Requests\Uploads\UploadChunkRequest;
|
||||
use App\Http\Requests\Uploads\UploadCancelRequest;
|
||||
use App\Http\Requests\Uploads\UploadStatusRequest;
|
||||
use App\Jobs\GenerateDerivativesJob;
|
||||
use App\Jobs\AnalyzeArtworkAiAssistJob;
|
||||
use App\Jobs\IndexArtworkJob;
|
||||
use App\Jobs\AutoTagArtworkJob;
|
||||
use App\Jobs\DetectArtworkMaturityJob;
|
||||
use App\Jobs\GenerateArtworkEmbeddingJob;
|
||||
use App\Repositories\Uploads\UploadSessionRepository;
|
||||
use App\Services\Uploads\UploadChunkService;
|
||||
@@ -19,6 +22,7 @@ use App\Services\Uploads\UploadCancelService;
|
||||
use App\Services\Uploads\UploadAuditService;
|
||||
use App\Services\Uploads\UploadPipelineService;
|
||||
use App\Services\Uploads\UploadQuotaService;
|
||||
use App\Services\Uploads\UploadQueueService;
|
||||
use App\Services\Uploads\UploadSessionStatus;
|
||||
use App\Services\Uploads\UploadStatusService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -82,11 +86,13 @@ final class UploadController extends Controller
|
||||
UploadFinishRequest $request,
|
||||
UploadPipelineService $pipeline,
|
||||
UploadSessionRepository $sessions,
|
||||
UploadAuditService $audit
|
||||
UploadAuditService $audit,
|
||||
UploadQueueService $queue
|
||||
) {
|
||||
$user = $request->user();
|
||||
$sessionId = (string) $request->validated('session_id');
|
||||
$artworkId = (int) $request->validated('artwork_id');
|
||||
$batchItemId = (int) $request->validated('batch_item_id', 0);
|
||||
$originalFileName = $request->validated('file_name');
|
||||
$archiveSessionId = $request->validated('archive_session_id');
|
||||
$archiveOriginalFileName = $request->validated('archive_file_name');
|
||||
@@ -97,16 +103,33 @@ final class UploadController extends Controller
|
||||
$session = $sessions->getOrFail($sessionId);
|
||||
|
||||
$request->artwork();
|
||||
$request->batchItem();
|
||||
|
||||
$failResponse = function (int $statusCode, string $message, ?string $reason = null) use ($queue, $user, $batchItemId) {
|
||||
if ($batchItemId > 0) {
|
||||
$queue->markItemFailedForUser($user, $batchItemId, $reason ?? 'upload_failed', $message);
|
||||
}
|
||||
|
||||
return response()->json(array_filter([
|
||||
'message' => $message,
|
||||
'reason' => $reason,
|
||||
], static fn (mixed $value): bool => $value !== null), $statusCode);
|
||||
};
|
||||
|
||||
$validated = $pipeline->validateAndHash($sessionId);
|
||||
if (! $validated->validation->ok || ! $validated->hash) {
|
||||
return response()->json([
|
||||
'message' => 'Upload validation failed.',
|
||||
'reason' => $validated->validation->reason,
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
return $failResponse(
|
||||
Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
'Upload validation failed.',
|
||||
$validated->validation->reason
|
||||
);
|
||||
}
|
||||
|
||||
if ($pipeline->originalHashExists($validated->hash)) {
|
||||
if ($batchItemId > 0) {
|
||||
$queue->markItemFailedForUser($user, $batchItemId, 'duplicate_hash', 'Duplicate upload is not allowed. This file already exists.');
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Duplicate upload is not allowed. This file already exists.',
|
||||
'reason' => 'duplicate_hash',
|
||||
@@ -116,28 +139,31 @@ final class UploadController extends Controller
|
||||
|
||||
$scan = $pipeline->scan($sessionId);
|
||||
if (! $scan->ok) {
|
||||
return response()->json([
|
||||
'message' => 'Upload scan failed.',
|
||||
'reason' => $scan->reason,
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
return $failResponse(
|
||||
Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
'Upload scan failed.',
|
||||
$scan->reason
|
||||
);
|
||||
}
|
||||
|
||||
$validatedArchive = null;
|
||||
if (is_string($archiveSessionId) && trim($archiveSessionId) !== '') {
|
||||
$validatedArchive = $pipeline->validateAndHashArchive($archiveSessionId);
|
||||
if (! $validatedArchive->validation->ok || ! $validatedArchive->hash) {
|
||||
return response()->json([
|
||||
'message' => 'Archive validation failed.',
|
||||
'reason' => $validatedArchive->validation->reason,
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
return $failResponse(
|
||||
Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
'Archive validation failed.',
|
||||
$validatedArchive->validation->reason
|
||||
);
|
||||
}
|
||||
|
||||
$archiveScan = $pipeline->scan($archiveSessionId);
|
||||
if (! $archiveScan->ok) {
|
||||
return response()->json([
|
||||
'message' => 'Archive scan failed.',
|
||||
'reason' => $archiveScan->reason,
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
return $failResponse(
|
||||
Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
'Archive scan failed.',
|
||||
$archiveScan->reason
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,18 +176,20 @@ final class UploadController extends Controller
|
||||
|
||||
$validatedScreenshot = $pipeline->validateAndHash($screenshotSessionId);
|
||||
if (! $validatedScreenshot->validation->ok || ! $validatedScreenshot->hash) {
|
||||
return response()->json([
|
||||
'message' => 'Screenshot validation failed.',
|
||||
'reason' => $validatedScreenshot->validation->reason,
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
return $failResponse(
|
||||
Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
'Screenshot validation failed.',
|
||||
$validatedScreenshot->validation->reason
|
||||
);
|
||||
}
|
||||
|
||||
$screenshotScan = $pipeline->scan($screenshotSessionId);
|
||||
if (! $screenshotScan->ok) {
|
||||
return response()->json([
|
||||
'message' => 'Screenshot scan failed.',
|
||||
'reason' => $screenshotScan->reason,
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
return $failResponse(
|
||||
Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
'Screenshot scan failed.',
|
||||
$screenshotScan->reason
|
||||
);
|
||||
}
|
||||
|
||||
$validatedAdditionalScreenshots[] = [
|
||||
@@ -172,7 +200,7 @@ final class UploadController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
$status = DB::transaction(function () use ($pipeline, $sessionId, $validated, $artworkId, $originalFileName, $archiveSessionId, $validatedArchive, $archiveOriginalFileName, $validatedAdditionalScreenshots) {
|
||||
$status = DB::transaction(function () use ($pipeline, $sessionId, $validated, $artworkId, $originalFileName, $archiveSessionId, $validatedArchive, $archiveOriginalFileName, $validatedAdditionalScreenshots, $queue, $batchItemId) {
|
||||
if ((bool) config('uploads.queue_derivatives', false)) {
|
||||
GenerateDerivativesJob::dispatch(
|
||||
$sessionId,
|
||||
@@ -182,8 +210,14 @@ final class UploadController extends Controller
|
||||
is_string($archiveSessionId) ? $archiveSessionId : null,
|
||||
$validatedArchive?->hash,
|
||||
is_string($archiveOriginalFileName) ? $archiveOriginalFileName : null,
|
||||
$validatedAdditionalScreenshots
|
||||
$validatedAdditionalScreenshots,
|
||||
$batchItemId > 0 ? $batchItemId : null
|
||||
)->afterCommit();
|
||||
|
||||
if ($batchItemId > 0) {
|
||||
$queue->markItemProcessingQueued($batchItemId);
|
||||
}
|
||||
|
||||
return 'queued';
|
||||
}
|
||||
|
||||
@@ -198,9 +232,15 @@ final class UploadController extends Controller
|
||||
$validatedAdditionalScreenshots
|
||||
);
|
||||
|
||||
if ($batchItemId > 0) {
|
||||
$queue->markItemMediaProcessed($batchItemId);
|
||||
}
|
||||
|
||||
// Derivatives are available now; dispatch AI auto-tagging.
|
||||
AutoTagArtworkJob::dispatch($artworkId, $validated->hash)->afterCommit();
|
||||
DetectArtworkMaturityJob::dispatch($artworkId, $validated->hash)->afterCommit();
|
||||
GenerateArtworkEmbeddingJob::dispatch($artworkId, $validated->hash)->afterCommit();
|
||||
AnalyzeArtworkAiAssistJob::dispatch($artworkId)->afterCommit();
|
||||
return UploadSessionStatus::PROCESSED;
|
||||
});
|
||||
|
||||
@@ -224,6 +264,10 @@ final class UploadController extends Controller
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
if ($batchItemId > 0) {
|
||||
$queue->markItemFailedForUser($user, $batchItemId, 'upload_finish_failed', $e->getMessage());
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Upload finish failed.',
|
||||
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
@@ -588,6 +632,7 @@ final class UploadController extends Controller
|
||||
'world_submissions' => ['nullable', 'array', 'max:12'],
|
||||
'world_submissions.*.world_id' => ['required', 'integer', 'exists:worlds,id'],
|
||||
'world_submissions.*.note' => ['nullable', 'string', 'max:1000'],
|
||||
'world_submissions.*.source_surface' => ['nullable', 'string', 'max:80'],
|
||||
]);
|
||||
|
||||
$mode = $validated['mode'] ?? 'now';
|
||||
@@ -676,14 +721,7 @@ final class UploadController extends Controller
|
||||
$artwork->published_at = null;
|
||||
$artwork->save();
|
||||
|
||||
try {
|
||||
$artwork->unsearchable();
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Failed to remove scheduled artwork from search index', [
|
||||
'artwork_id' => (int) $artwork->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
IndexArtworkJob::dispatch((int) $artwork->id);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
@@ -704,18 +742,7 @@ final class UploadController extends Controller
|
||||
$artwork->publish_at = null;
|
||||
$artwork->save();
|
||||
|
||||
try {
|
||||
if ((bool) $artwork->is_public && (bool) $artwork->is_approved && !empty($artwork->published_at)) {
|
||||
$artwork->searchable();
|
||||
} else {
|
||||
$artwork->unsearchable();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Failed to sync artwork search index after publish', [
|
||||
'artwork_id' => (int) $artwork->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
IndexArtworkJob::dispatch((int) $artwork->id);
|
||||
|
||||
// Record upload activity event
|
||||
try {
|
||||
@@ -784,6 +811,7 @@ final class UploadController extends Controller
|
||||
'world_submissions' => ['nullable', 'array', 'max:12'],
|
||||
'world_submissions.*.world_id' => ['required', 'integer', 'exists:worlds,id'],
|
||||
'world_submissions.*.note' => ['nullable', 'string', 'max:1000'],
|
||||
'world_submissions.*.source_surface' => ['nullable', 'string', 'max:80'],
|
||||
]);
|
||||
|
||||
if (! ctype_digit($id)) {
|
||||
|
||||
Reference in New Issue
Block a user