From a2457f4e4917f436708dc250154be336241b02aa Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Thu, 9 Apr 2026 08:50:36 +0200 Subject: [PATCH] minor fixes --- .env.example | 6 + .../Commands/ConfigureMeilisearchIndex.php | 1 + app/Console/Commands/HealthCheckCommand.php | 419 +++++++++++++ app/Console/Commands/ImportLegacyArtworks.php | 23 +- .../Commands/RecalculateHeatCommand.php | 90 ++- .../RepairLegacyWallzUsersCommand.php | 2 +- .../Dashboard/DashboardGalleryController.php | 31 +- .../RSS/DiscoverFeedController.php | 102 +++- .../Web/BrowseGalleryController.php | 30 +- .../Controllers/Web/DiscoverController.php | 230 ++++++- .../Controllers/Web/ExploreController.php | 30 +- .../Web/SimilarArtworksPageController.php | 30 +- app/Http/Controllers/Web/TagController.php | 25 +- .../Requests/Uploads/UploadChunkRequest.php | 71 +++ app/Http/Resources/ArtworkListResource.php | 28 +- app/Models/Artwork.php | 4 +- app/Providers/AppServiceProvider.php | 4 + app/Services/ArtworkSearchService.php | 62 +- app/Services/ArtworkService.php | 27 +- .../EarlyGrowth/AdaptiveTimeWindow.php | 4 +- app/Services/HomepageService.php | 117 +++- .../Studio/CreatorStudioContentService.php | 19 +- .../Providers/ArtworkStudioProvider.php | 28 +- .../Studio/StudioBulkActionService.php | 2 + config/scout.php | 1 + config/uploads.php | 5 + deploy/supervisor/skinbase-queue.conf | 2 +- deploy/systemd/skinbase-queue.service | 2 +- docs/Discover/README.md | 93 +++ docs/Discover/for-you.md | 174 ++++++ docs/Discover/fresh.md | 88 +++ docs/Discover/most-downloaded.md | 59 ++ docs/Discover/on-this-day.md | 52 ++ docs/Discover/rising.md | 115 ++++ docs/Discover/today-downloads.md | 63 ++ docs/Discover/top-rated.md | 58 ++ docs/Discover/trending.md | 125 ++++ public/gfx/skinbase_logo.webp | Bin 0 -> 84220 bytes public/gfx/skinbase_logo_128.webp | Bin 0 -> 5338 bytes public/gfx/skinbase_logo_256.webp | Bin 0 -> 5338 bytes public/gfx/skinbase_logo_32.webp | Bin 0 -> 866 bytes public/gfx/skinbase_logo_512.webp | Bin 0 -> 52124 bytes public/gfx/skinbase_logo_64.webp | Bin 0 -> 1904 bytes public/gfx/skinbase_logo_96.webp | Bin 0 -> 3594 bytes .../js/Pages/Studio/StudioArtworkEdit.jsx | 101 +++- .../js/Pages/Studio/StudioGroupCreate.jsx | 55 +- .../js/Pages/Studio/StudioGroupSettings.jsx | 21 +- resources/js/Pages/Upload/Index.jsx | 48 +- .../Studio/StudioContentBrowser.jsx | 571 +++++++++++++++++- .../js/components/artwork/ArtworkCard.jsx | 49 +- .../js/components/artwork/ArtworkHero.jsx | 96 ++- .../js/components/gallery/MasonryGallery.jsx | 6 +- resources/js/components/tags/TagInput.jsx | 282 +++++++-- .../js/components/tags/TagInput.test.jsx | 29 +- resources/js/components/tags/TagPicker.jsx | 213 ++++++- .../js/components/tags/TagPicker.test.jsx | 33 + .../js/components/upload/UploadWizard.jsx | 2 + resources/js/hooks/upload/useUploadMachine.js | 37 +- resources/js/lib/uploadNotices.js | 10 +- .../views/components/artwork-card.blade.php | 36 +- .../staff_application_received.blade.php | 2 +- resources/views/gallery/index.blade.php | 5 +- resources/views/layouts/nova/footer.blade.php | 2 +- resources/views/search/index.blade.php | 5 +- .../views/web/discover/for-you.blade.php | 3 + resources/views/web/discover/index.blade.php | 5 +- resources/views/web/explore/index.blade.php | 5 +- .../views/web/featured-artworks.blade.php | 5 +- routes/api.php | 2 +- routes/console.php | 40 ++ routes/web.php | 2 + scripts/deploy-production.sh | 173 +++++- tests/Feature/DiscoverRisingFeedTest.php | 65 ++ tests/Feature/DiscoverRisingTest.php | 23 + tests/Feature/RisingEngineTest.php | 87 ++- 75 files changed, 3848 insertions(+), 387 deletions(-) create mode 100644 app/Console/Commands/HealthCheckCommand.php create mode 100644 docs/Discover/README.md create mode 100644 docs/Discover/for-you.md create mode 100644 docs/Discover/fresh.md create mode 100644 docs/Discover/most-downloaded.md create mode 100644 docs/Discover/on-this-day.md create mode 100644 docs/Discover/rising.md create mode 100644 docs/Discover/today-downloads.md create mode 100644 docs/Discover/top-rated.md create mode 100644 docs/Discover/trending.md create mode 100644 public/gfx/skinbase_logo.webp create mode 100644 public/gfx/skinbase_logo_128.webp create mode 100644 public/gfx/skinbase_logo_256.webp create mode 100644 public/gfx/skinbase_logo_32.webp create mode 100644 public/gfx/skinbase_logo_512.webp create mode 100644 public/gfx/skinbase_logo_64.webp create mode 100644 public/gfx/skinbase_logo_96.webp create mode 100644 tests/Feature/DiscoverRisingFeedTest.php diff --git a/.env.example b/.env.example index 967bfff2..fea5f577 100644 --- a/.env.example +++ b/.env.example @@ -78,6 +78,12 @@ VITE_REVERB_SCHEME="${REVERB_SCHEME}" # Upload UI feature flag (legacy upload remains default unless explicitly enabled) SKINBASE_UPLOADS_V2=false +# Upload transport tuning +UPLOAD_CHUNK_MAX_BYTES=5242880 +UPLOAD_CHUNK_REQUEST_TIMEOUT_MS=45000 +UPLOAD_RATE_CHUNK_USER=180 +UPLOAD_RATE_CHUNK_IP=360 + # Draft abuse prevention controls SKINBASE_MAX_DRAFTS=10 SKINBASE_MAX_DRAFT_STORAGE_MB=1024 diff --git a/app/Console/Commands/ConfigureMeilisearchIndex.php b/app/Console/Commands/ConfigureMeilisearchIndex.php index ab3c7e5a..b56fbb59 100644 --- a/app/Console/Commands/ConfigureMeilisearchIndex.php +++ b/app/Console/Commands/ConfigureMeilisearchIndex.php @@ -26,6 +26,7 @@ class ConfigureMeilisearchIndex extends Command */ private const SORTABLE_ATTRIBUTES = [ 'created_at', + 'published_at_ts', 'trending_score_24h', 'trending_score_7d', 'favorites_count', diff --git a/app/Console/Commands/HealthCheckCommand.php b/app/Console/Commands/HealthCheckCommand.php new file mode 100644 index 00000000..28c22710 --- /dev/null +++ b/app/Console/Commands/HealthCheckCommand.php @@ -0,0 +1,419 @@ + [status, message, details]] */ + private array $results = []; + + public function handle(): int + { + $only = $this->option('only') ? strtolower((string) $this->option('only')) : null; + + $checks = [ + 'mysql' => fn () => $this->checkMysql(), + 'redis' => fn () => $this->checkRedis(), + 'cache' => fn () => $this->checkCache(), + 'meilisearch' => fn () => $this->checkMeilisearch(), + 'reverb' => fn () => $this->checkReverb(), + 'vision' => fn () => $this->checkVision(), + 'horizon' => fn () => $this->checkHorizon(), + 'app' => fn () => $this->checkApp(), + ]; + + if ($only !== null) { + if (! array_key_exists($only, $checks)) { + $this->error("Unknown check '{$only}'. Available: " . implode(', ', array_keys($checks))); + return self::FAILURE; + } + $checks = [$only => $checks[$only]]; + } + + foreach ($checks as $name => $check) { + $check(); + } + + if ($this->option('json')) { + $this->line(json_encode($this->results, JSON_PRETTY_PRINT)); + return $this->hasFailures() ? self::FAILURE : self::SUCCESS; + } + + $this->renderTable(); + + $failed = $this->countByStatus('fail'); + $warned = $this->countByStatus('warn'); + + $this->newLine(); + if ($failed > 0) { + $this->error("❌ {$failed} check(s) FAILED" . ($warned > 0 ? ", {$warned} warning(s)" : '') . '.'); + return self::FAILURE; + } + if ($warned > 0) { + $this->warn("⚠️ All checks passed with {$warned} warning(s)."); + return self::SUCCESS; + } + $this->info('✅ All checks passed.'); + return self::SUCCESS; + } + + // ── Individual checks ────────────────────────────────────────────────────── + + private function checkMysql(): void + { + try { + DB::select('SELECT 1'); + $db = config('database.connections.' . config('database.default') . '.database'); + $artworkCount = DB::table('artworks')->whereNull('deleted_at')->count(); + $this->pass('mysql', "Connected to `{$db}`. Artworks in DB: {$artworkCount}.", ['artwork_count' => $artworkCount]); + } catch (Throwable $e) { + $this->failCheck('mysql', 'Connection failed: ' . $e->getMessage()); + } + } + + private function checkRedis(): void + { + try { + $pong = Redis::ping(); + // ping returns "+PONG\r\n" string or true depending on driver + $ok = $pong === true || str_contains((string) $pong, 'PONG'); + if ($ok) { + $info = Redis::info('server'); + $version = $info['redis_version'] ?? ($info['Server']['redis_version'] ?? 'unknown'); + $this->pass('redis', "Reachable. Redis version: {$version}.", ['version' => $version]); + } else { + $this->failCheck('redis', 'Unexpected ping response: ' . var_export($pong, true)); + } + } catch (Throwable $e) { + $this->failCheck('redis', 'Connection failed: ' . $e->getMessage()); + } + } + + private function checkCache(): void + { + try { + $key = '_healthcheck_' . uniqid('', true); + $value = 'ok_' . time(); + Cache::put($key, $value, 10); + $got = Cache::get($key); + Cache::forget($key); + + $driver = config('cache.default'); + if ($got === $value) { + $this->pass('cache', "Driver `{$driver}` read/write OK.", ['driver' => $driver]); + } else { + $this->failCheck('cache', "Driver `{$driver}`: wrote '{$value}' but read back " . var_export($got, true)); + } + } catch (Throwable $e) { + $this->failCheck('cache', 'Cache test failed: ' . $e->getMessage()); + } + } + + private function checkMeilisearch(): void + { + try { + /** @var MeilisearchClient $client */ + $client = app(MeilisearchClient::class); + $health = $client->health(); + + if (($health['status'] ?? '') !== 'available') { + $this->failCheck('meilisearch', 'Meilisearch reports unhealthy status: ' . json_encode($health)); + return; + } + + $version = $client->version()['pkgVersion'] ?? 'unknown'; + $indexName = (new Artwork())->searchableAs(); + $index = $client->index($indexName); + $stats = $index->stats(); + $docCount = (int) ($stats['numberOfDocuments'] ?? 0); + $isIndexing = (bool) ($stats['isIndexing'] ?? false); + + // Expected: ≥ 50% of DB artworks should be indexed + $dbCount = DB::table('artworks') + ->whereNull('deleted_at') + ->where('is_public', 1) + ->where('is_approved', 1) + ->count(); + + $status = 'pass'; + $message = "v{$version}. Index `{$indexName}`: {$docCount} docs (DB public+approved: {$dbCount})."; + + if ($isIndexing) { + $message .= ' [currently re-indexing]'; + } + + if ($docCount === 0) { + $status = 'fail'; + $message = "Index `{$indexName}` is EMPTY (DB has {$dbCount} public+approved artworks). Run: php artisan scout:import \"App\\\\Models\\\\Artwork\""; + } elseif ($dbCount > 0 && $docCount < (int) ($dbCount * 0.5)) { + $status = 'warn'; + $message .= " — indexed count is < 50% of DB count. Index may be stale. Run: php artisan artworks:search-rebuild"; + } + + // Check pending Meilisearch tasks + try { + $tasks = $client->getTasks(['statuses' => 'enqueued,processing']); + $pendingCount = $tasks->getTotal(); + if ($pendingCount > 0) { + $message .= " ({$pendingCount} tasks still pending)"; + } + } catch (Throwable) { + // non-fatal + } + + $this->result('meilisearch', $status, $message, [ + 'index' => $indexName, + 'indexed_docs' => $docCount, + 'db_eligible' => $dbCount, + 'is_indexing' => $isIndexing, + 'meili_version' => $version, + ]); + } catch (Throwable $e) { + $this->failCheck('meilisearch', 'Unreachable or error: ' . $e->getMessage()); + } + } + + private function checkReverb(): void + { + $host = config('reverb.servers.reverb.options.host') ?? env('REVERB_HOST', ''); + $port = (int) (config('reverb.servers.reverb.options.port') ?? env('REVERB_PORT', 443)); + $scheme = config('reverb.servers.reverb.options.scheme') ?? env('REVERB_SCHEME', 'https'); + + if (empty($host)) { + $this->warn_check('reverb', 'REVERB_HOST not configured — skipping.'); + return; + } + + // Reverb exposes an HTTP health endpoint at /apps/{appId} + // We do a plain TCP connect as the minimal check; a refused connection means down. + $timeout = 5; + try { + $errno = 0; + $errstr = ''; + $proto = $scheme === 'https' ? 'ssl' : 'tcp'; + $fp = @fsockopen("{$proto}://{$host}", $port, $errno, $errstr, $timeout); + + if ($fp === false) { + $this->failCheck('reverb', "Cannot connect to {$host}:{$port} ({$scheme}) — {$errstr} [{$errno}]"); + return; + } + + fclose($fp); + + // Try the HTTP health probe (Reverb answers 200 on /) + $url = "{$scheme}://{$host}:{$port}/"; + $response = $this->httpGet($url, 3); + $code = $response['code'] ?? 0; + + // Reverb returns 200 or 400 on the root — both mean it's alive + if ($code >= 200 && $code < 500) { + $this->pass('reverb', "Reachable at {$host}:{$port} (HTTP {$code}).", ['host' => $host, 'port' => $port]); + } else { + $this->warn_check('reverb', "TCP open but HTTP returned {$code}.", ['host' => $host, 'port' => $port]); + } + } catch (Throwable $e) { + $this->failCheck('reverb', 'Check failed: ' . $e->getMessage()); + } + } + + private function checkVision(): void + { + $services = [ + 'CLIP / Gateway' => rtrim((string) config('vision.gateway.base_url', ''), '/') . '/health', + 'Vector Gateway' => rtrim((string) config('vision.vector_gateway.base_url', ''), '/') . '/health', + ]; + + $allPassed = true; + $messages = []; + + foreach ($services as $label => $url) { + if ($url === '/health' || $url === '') { + $messages[] = "{$label}: not configured"; + continue; + } + + $response = $this->httpGet($url, 5); + $code = $response['code'] ?? 0; + + if ($code >= 200 && $code < 300) { + $messages[] = "{$label}: OK (HTTP {$code})"; + } elseif ($code === 0) { + $allPassed = false; + $messages[] = "{$label}: UNREACHABLE ({$url})"; + } else { + $allPassed = false; + $messages[] = "{$label}: HTTP {$code} ({$url})"; + } + } + + $summary = implode(' | ', $messages); + + if ($allPassed) { + $this->pass('vision', $summary); + } else { + $this->warn_check('vision', $summary); + } + } + + private function checkHorizon(): void + { + try { + // Horizon stores its status in Redis under the horizon:master-supervisor key prefix. + // A simpler cross-version check: look for any horizon-related Redis key. + $status = Cache::store('redis')->get('horizon:status'); + + if ($status === null) { + // Try reading directly from Redis + $status = Redis::get('horizon:status'); + } + + if ($status === null) { + $this->warn_check('horizon', 'No Horizon status key in Redis — Horizon may not be running or has never started.'); + return; + } + + $status = is_string($status) ? strtolower(trim($status)) : strtolower((string) $status); + + if ($status === 'running') { + $this->pass('horizon', "Horizon status: running."); + } elseif ($status === 'paused') { + $this->warn_check('horizon', "Horizon is PAUSED. Resume with: php artisan horizon:continue"); + } else { + $this->failCheck('horizon', "Horizon status: {$status}. Start with: php artisan horizon"); + } + } catch (Throwable $e) { + $this->warn_check('horizon', 'Could not read Horizon status: ' . $e->getMessage()); + } + } + + private function checkApp(): void + { + $appUrl = rtrim((string) config('app.url', ''), '/'); + + if (empty($appUrl) || str_contains($appUrl, '.test') || str_contains($appUrl, 'localhost')) { + $this->warn_check('app', "APP_URL is `{$appUrl}` — looks like a local/dev URL, skipping HTTP probe."); + return; + } + + // Probe the app homepage + $response = $this->httpGet($appUrl . '/', 10); + $code = $response['code'] ?? 0; + + if ($code === 200) { + $ttfb = $response['ttfb'] ?? 0; + $this->pass('app', "Homepage responded HTTP 200. TTFB: {$ttfb}ms.", ['url' => $appUrl, 'ttfb_ms' => $ttfb]); + } elseif ($code > 0) { + $this->warn_check('app', "Homepage returned HTTP {$code}. URL: {$appUrl}", ['url' => $appUrl, 'http_code' => $code]); + } else { + $this->failCheck('app', "Homepage unreachable. URL: {$appUrl}"); + } + } + + // ── Helpers ──────────────────────────────────────────────────────────────── + + private function httpGet(string $url, int $timeout = 5): array + { + $start = microtime(true); + try { + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => $timeout, + CURLOPT_CONNECTTIMEOUT => 3, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 3, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_USERAGENT => 'SkinbaseHealthCheck/1.0', + CURLOPT_HTTPHEADER => ['Accept: application/json, text/html'], + ]); + $body = curl_exec($ch); + $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + $ttfb = (int) round((microtime(true) - $start) * 1000); + return ['code' => $code, 'body' => $body ?: '', 'ttfb' => $ttfb]; + } catch (Throwable) { + return ['code' => 0, 'body' => '', 'ttfb' => 0]; + } + } + + private function pass(string $name, string $message, array $details = []): void + { + $this->result($name, 'pass', $message, $details); + } + + private function failCheck(string $name, string $message, array $details = []): void + { + $this->result($name, 'fail', $message, $details); + } + + private function warn_check(string $name, string $message, array $details = []): void + { + $this->result($name, 'warn', $message, $details); + } + + private function result(string $name, string $status, string $message, array $details = []): void + { + $this->results[$name] = [ + 'status' => $status, + 'message' => $message, + 'details' => $details, + ]; + } + + private function renderTable(): void + { + $this->newLine(); + $this->line(' SERVICE STATUS MESSAGE'); + $this->line(' ' . str_repeat('─', 90)); + + foreach ($this->results as $name => $r) { + [$icon, $color] = match ($r['status']) { + 'pass' => ['✅', 'green'], + 'warn' => ['⚠️ ', 'yellow'], + default => ['❌', 'red'], + }; + + $label = str_pad(strtoupper($name), 15); + $status = str_pad(strtoupper($r['status']), 7); + $message = $r['message']; + + $this->line(" {$icon} {$label} {$status} {$message}"); + } + + $this->line(' ' . str_repeat('─', 90)); + } + + private function hasFailures(): bool + { + return $this->countByStatus('fail') > 0; + } + + private function countByStatus(string $status): int + { + return count(array_filter($this->results, fn ($r) => $r['status'] === $status)); + } +} diff --git a/app/Console/Commands/ImportLegacyArtworks.php b/app/Console/Commands/ImportLegacyArtworks.php index 56177bff..2b7c9e7f 100644 --- a/app/Console/Commands/ImportLegacyArtworks.php +++ b/app/Console/Commands/ImportLegacyArtworks.php @@ -13,12 +13,14 @@ use Throwable; * * Usage: * php artisan skinbase:import-legacy-artworks --chunk=500 --dry-run + * php artisan skinbase:import-legacy-artworks --artwork-id=69527 */ class ImportLegacyArtworks extends Command { protected $signature = 'skinbase:import-legacy-artworks {--chunk=500 : chunk size for processing} {--limit= : maximum number of legacy rows to import} + {--artwork-id= : import only one legacy wallz row by id} {--dry-run : do not persist any changes} {--legacy-connection=legacy : name of legacy DB connection} {--legacy-table=wallz : legacy artworks table name} @@ -73,15 +75,28 @@ class ImportLegacyArtworks extends Command { $chunk = (int) $this->option('chunk'); $limit = $this->option('limit') ? (int) $this->option('limit') : null; + $artworkId = $this->option('artwork-id') ? (int) $this->option('artwork-id') : null; $dryRun = (bool) $this->option('dry-run'); $legacyConn = $this->option('legacy-connection'); $legacyTable = $this->option('legacy-table'); $connectedTable = $this->option('connected-table'); + if ($artworkId !== null && $artworkId <= 0) { + $this->error('The --artwork-id option must be a positive integer.'); + + return self::FAILURE; + } + $this->info("Starting import from {$legacyConn}.{$legacyTable} (chunk={$chunk})"); $query = DB::connection($legacyConn)->table($legacyTable)->orderBy('id'); + if ($artworkId !== null) { + $this->info("Scoping import to legacy artwork id={$artworkId}"); + $query->where('id', $artworkId); + $limit = 1; + } + $processed = 0; $query->chunkById($chunk, function ($rows) use (&$processed, $limit, $dryRun, $legacyConn, $connectedTable) { @@ -277,8 +292,14 @@ class ImportLegacyArtworks extends Command return null; }, 'id'); + if ($artworkId !== null && $processed === 0) { + $this->warn("Legacy artwork id={$artworkId} was not found in {$legacyConn}.{$legacyTable}."); + + return self::FAILURE; + } + $this->info('Import complete. Processed: ' . $processed); - return 0; + return self::SUCCESS; } } diff --git a/app/Console/Commands/RecalculateHeatCommand.php b/app/Console/Commands/RecalculateHeatCommand.php index 5e34342f..e7ba9382 100644 --- a/app/Console/Commands/RecalculateHeatCommand.php +++ b/app/Console/Commands/RecalculateHeatCommand.php @@ -12,21 +12,22 @@ use Illuminate\Support\Facades\Log; * Runs every 10–15 minutes via scheduler. * * Formula: - * raw_heat = views_delta*1 + downloads_delta*3 + favourites_delta*6 - * + comments_delta*8 + shares_delta*12 + * raw_heat = ((views_delta*1 + downloads_delta*3 + favourites_delta*6 + * + comments_delta*8 + shares_delta*12) / window_hours) * * age_factor = 1 / (1 + hours_since_upload / 24) * * heat_score = raw_heat * age_factor * * Usage: php artisan nova:recalculate-heat - * php artisan nova:recalculate-heat --days=60 --chunk=1000 --dry-run + * php artisan nova:recalculate-heat --days=60 --chunk=1000 --lookback-hours=24 --dry-run */ class RecalculateHeatCommand extends Command { protected $signature = 'nova:recalculate-heat {--days=60 : Only process artworks created within this many days} {--chunk=1000 : Chunk size for DB queries} + {--lookback-hours=24 : Smooth heat deltas over this many trailing hours} {--dry-run : Compute scores without writing to DB}'; protected $description = 'Recalculate heat/momentum scores for the Rising engine'; @@ -44,31 +45,34 @@ class RecalculateHeatCommand extends Command { $days = (int) $this->option('days'); $chunk = (int) $this->option('chunk'); + $lookbackHours = max(1, (int) $this->option('lookback-hours')); $dryRun = (bool) $this->option('dry-run'); $now = now(); $currentHour = $now->copy()->startOfHour(); $prevHour = $currentHour->copy()->subHour(); + $lookbackStart = $currentHour->copy()->subHours($lookbackHours); - $this->info("[nova:recalculate-heat] current_hour={$currentHour->toDateTimeString()} prev_hour={$prevHour->toDateTimeString()} days={$days}" . ($dryRun ? ' (dry-run)' : '')); + $this->info("[nova:recalculate-heat] current_hour={$currentHour->toDateTimeString()} prev_hour={$prevHour->toDateTimeString()} lookback_start={$lookbackStart->toDateTimeString()} lookback_hours={$lookbackHours} days={$days}" . ($dryRun ? ' (dry-run)' : '')); $updatedCount = 0; $skippedCount = 0; - // Process in chunks using artwork IDs that have at least one snapshot in the two hours + // Process in chunks using artwork IDs that have at least one snapshot in the smoothing window $artworkIds = DB::table('artwork_metric_snapshots_hourly') - ->whereIn('bucket_hour', [$currentHour, $prevHour]) + ->whereBetween('bucket_hour', [$lookbackStart, $currentHour]) ->distinct() ->pluck('artwork_id'); if ($artworkIds->isEmpty()) { - $this->warn('No snapshots found for the current or previous hour. Run nova:metrics-snapshot-hourly first.'); + $this->warn('No snapshots found inside the requested lookback window. Run nova:metrics-snapshot-hourly first.'); return self::SUCCESS; } - // Load all snapshots for the two hours in bulk + // Load all snapshots for the lookback window in bulk $snapshots = DB::table('artwork_metric_snapshots_hourly') - ->whereIn('bucket_hour', [$currentHour, $prevHour]) + ->whereBetween('bucket_hour', [$lookbackStart, $currentHour]) ->whereIn('artwork_id', $artworkIds) + ->orderBy('bucket_hour') ->get() ->groupBy('artwork_id'); @@ -101,27 +105,57 @@ class RecalculateHeatCommand extends Command } $currentSnapshot = $artworkSnapshots->firstWhere('bucket_hour', $currentHour->toDateTimeString()); - $prevSnapshot = $artworkSnapshots->firstWhere('bucket_hour', $prevHour->toDateTimeString()); + if (! $currentSnapshot) { + $currentSnapshot = $artworkSnapshots->last(); + } - // If we only have one snapshot, use it as current with zero deltas - if (!$currentSnapshot && !$prevSnapshot) { + $prevSnapshot = $artworkSnapshots->firstWhere('bucket_hour', $prevHour->toDateTimeString()); + $baselineSnapshot = $artworkSnapshots + ->filter(fn ($snapshot) => (string) $snapshot->bucket_hour < (string) ($currentSnapshot->bucket_hour ?? '')) + ->first(); + + if (! $currentSnapshot) { $skippedCount++; continue; } - // Calculate deltas - $viewsDelta = max(0, (int) ($currentSnapshot?->views_count ?? 0) - (int) ($prevSnapshot?->views_count ?? 0)); - $downloadsDelta = max(0, (int) ($currentSnapshot?->downloads_count ?? 0) - (int) ($prevSnapshot?->downloads_count ?? 0)); - $favouritesDelta = max(0, (int) ($currentSnapshot?->favourites_count ?? 0) - (int) ($prevSnapshot?->favourites_count ?? 0)); - $commentsDelta = max(0, (int) ($currentSnapshot?->comments_count ?? 0) - (int) ($prevSnapshot?->comments_count ?? 0)); - $sharesDelta = max(0, (int) ($currentSnapshot?->shares_count ?? 0) - (int) ($prevSnapshot?->shares_count ?? 0)); + // One-hour counters remain explicit fields for dashboards and debugging. + $viewsDelta1h = max(0, (int) ($currentSnapshot?->views_count ?? 0) - (int) ($prevSnapshot?->views_count ?? 0)); + $downloadsDelta1h = max(0, (int) ($currentSnapshot?->downloads_count ?? 0) - (int) ($prevSnapshot?->downloads_count ?? 0)); + $favouritesDelta1h = max(0, (int) ($currentSnapshot?->favourites_count ?? 0) - (int) ($prevSnapshot?->favourites_count ?? 0)); + $commentsDelta1h = max(0, (int) ($currentSnapshot?->comments_count ?? 0) - (int) ($prevSnapshot?->comments_count ?? 0)); + $sharesDelta1h = max(0, (int) ($currentSnapshot?->shares_count ?? 0) - (int) ($prevSnapshot?->shares_count ?? 0)); + + // Smooth the heat signal over a trailing window so low-traffic periods do not flatten Rising. + // A single snapshot without an earlier baseline should not count as new momentum. + if ($baselineSnapshot) { + $viewsDelta = max(0, (int) ($currentSnapshot?->views_count ?? 0) - (int) ($baselineSnapshot->views_count ?? 0)); + $downloadsDelta = max(0, (int) ($currentSnapshot?->downloads_count ?? 0) - (int) ($baselineSnapshot->downloads_count ?? 0)); + $favouritesDelta = max(0, (int) ($currentSnapshot?->favourites_count ?? 0) - (int) ($baselineSnapshot->favourites_count ?? 0)); + $commentsDelta = max(0, (int) ($currentSnapshot?->comments_count ?? 0) - (int) ($baselineSnapshot->comments_count ?? 0)); + $sharesDelta = max(0, (int) ($currentSnapshot?->shares_count ?? 0) - (int) ($baselineSnapshot->shares_count ?? 0)); + + $windowHours = max( + 1.0, + abs($currentHour->copy()->parse($currentSnapshot->bucket_hour)->floatDiffInHours($currentHour->copy()->parse($baselineSnapshot->bucket_hour))) + ); + } else { + $viewsDelta = 0; + $downloadsDelta = 0; + $favouritesDelta = 0; + $commentsDelta = 0; + $sharesDelta = 0; + $windowHours = 1.0; + } // Raw heat - $rawHeat = ($viewsDelta * self::WEIGHTS['views']) - + ($downloadsDelta * self::WEIGHTS['downloads']) - + ($favouritesDelta * self::WEIGHTS['favourites']) - + ($commentsDelta * self::WEIGHTS['comments']) - + ($sharesDelta * self::WEIGHTS['shares']); + $rawHeat = ( + ($viewsDelta * self::WEIGHTS['views']) + + ($downloadsDelta * self::WEIGHTS['downloads']) + + ($favouritesDelta * self::WEIGHTS['favourites']) + + ($commentsDelta * self::WEIGHTS['comments']) + + ($sharesDelta * self::WEIGHTS['shares']) + ) / $windowHours; // Age factor: favors newer works $hoursSinceUpload = abs($now->floatDiffInHours($createdAt)); @@ -134,11 +168,11 @@ class RecalculateHeatCommand extends Command 'artwork_id' => $artworkId, 'heat_score' => round($heatScore, 4), 'heat_score_updated_at' => $now, - 'views_1h' => $viewsDelta, - 'downloads_1h' => $downloadsDelta, - 'favourites_1h' => $favouritesDelta, - 'comments_1h' => $commentsDelta, - 'shares_1h' => $sharesDelta, + 'views_1h' => $viewsDelta1h, + 'downloads_1h' => $downloadsDelta1h, + 'favourites_1h' => $favouritesDelta1h, + 'comments_1h' => $commentsDelta1h, + 'shares_1h' => $sharesDelta1h, ]; $updatedCount++; diff --git a/app/Console/Commands/RepairLegacyWallzUsersCommand.php b/app/Console/Commands/RepairLegacyWallzUsersCommand.php index 06ace14d..fb25328c 100644 --- a/app/Console/Commands/RepairLegacyWallzUsersCommand.php +++ b/app/Console/Commands/RepairLegacyWallzUsersCommand.php @@ -13,7 +13,7 @@ use Carbon\Carbon; class RepairLegacyWallzUsersCommand extends Command { - protected $signature = 'skinbase:repair-legacy-wallz-users + protected $signature = 'legacySB:repair-legacy-wallz-users {--chunk=500 : Number of legacy wallz rows to scan per batch} {--legacy-connection=legacy : Legacy database connection name} {--legacy-table=wallz : Legacy table to update} diff --git a/app/Http/Controllers/Dashboard/DashboardGalleryController.php b/app/Http/Controllers/Dashboard/DashboardGalleryController.php index ead9c6e4..8d209192 100644 --- a/app/Http/Controllers/Dashboard/DashboardGalleryController.php +++ b/app/Http/Controllers/Dashboard/DashboardGalleryController.php @@ -64,6 +64,18 @@ class DashboardGalleryController extends Controller { $primary = $artwork->categories->sortBy('sort_order')->first(); $present = ThumbnailPresenter::present($artwork, 'md'); + $group = $artwork->group; + $isGroupPublisher = $group !== null; + $displayName = $isGroupPublisher ? ($group->name ?? 'Skinbase') : ($artwork->user?->name ?? 'Skinbase'); + $username = $isGroupPublisher ? '' : ($artwork->user?->username ?? ''); + $avatarUrl = $isGroupPublisher + ? $group->avatarUrl() + : AvatarUrl::forUser( + (int) ($artwork->user_id ?? 0), + $artwork->user?->profile?->avatar_hash ?? null, + 64 + ); + $profileUrl = $isGroupPublisher ? $group->publicUrl() : ($username !== '' ? '/@' . $username : null); return (object) [ 'id' => $artwork->id, @@ -74,13 +86,18 @@ class DashboardGalleryController extends Controller 'category_slug' => $primary?->slug ?? '', 'thumb_url' => $present['url'], 'thumb_srcset' => $present['srcset'] ?? $present['url'], - 'uname' => $artwork->user?->name ?? 'Skinbase', - 'username' => $artwork->user?->username ?? '', - 'avatar_url' => AvatarUrl::forUser( - (int) ($artwork->user_id ?? 0), - $artwork->user?->profile?->avatar_hash ?? null, - 64 - ), + 'uname' => $displayName, + 'username' => $username, + 'avatar_url' => $avatarUrl, + 'profile_url' => $profileUrl, + 'published_as_type' => $isGroupPublisher ? 'group' : 'user', + 'publisher' => [ + 'type' => $isGroupPublisher ? 'group' : 'user', + 'name' => $displayName, + 'username' => $username, + 'avatar_url' => $avatarUrl, + 'profile_url' => $profileUrl, + ], 'published_at' => $artwork->published_at, 'slug' => $artwork->slug ?? '', 'width' => $artwork->width ?? null, diff --git a/app/Http/Controllers/RSS/DiscoverFeedController.php b/app/Http/Controllers/RSS/DiscoverFeedController.php index 73394ff8..1b798ae9 100644 --- a/app/Http/Controllers/RSS/DiscoverFeedController.php +++ b/app/Http/Controllers/RSS/DiscoverFeedController.php @@ -6,9 +6,12 @@ namespace App\Http\Controllers\RSS; use App\Http\Controllers\Controller; use App\Models\Artwork; +use App\Services\EarlyGrowth\AdaptiveTimeWindow; use App\Services\RSS\RSSFeedBuilder; use Illuminate\Http\Response; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\DB; /** * DiscoverFeedController @@ -22,7 +25,10 @@ use Illuminate\Support\Facades\Cache; */ final class DiscoverFeedController extends Controller { - public function __construct(private readonly RSSFeedBuilder $builder) {} + public function __construct( + private readonly RSSFeedBuilder $builder, + private readonly AdaptiveTimeWindow $timeWindow, + ) {} /** /rss/discover → redirect to fresh */ public function index(): Response @@ -77,15 +83,19 @@ final class DiscoverFeedController extends Controller public function rising(): Response { $feedUrl = url('/rss/discover/rising'); - $artworks = Cache::remember('rss:discover:rising', 600, fn () => - Artwork::public()->published() - ->with(['user:id,username', 'categories:id,name,slug,content_type_id']) - ->leftJoin('artwork_stats', 'artwork_stats.artwork_id', '=', 'artworks.id') - ->orderByDesc('artwork_stats.heat_score') - ->orderByDesc('artworks.published_at') - ->select('artworks.*') - ->limit(RSSFeedBuilder::FEED_LIMIT) - ->get() + $windowDays = $this->timeWindow->getTrendingWindowDays(30); + $artworks = Cache::remember( + "rss:discover:rising.{$windowDays}d", + 600, + function () use ($windowDays) { + $artworks = $this->risingArtworks($windowDays); + + if ($this->collectionHasNoRisingMomentum($artworks)) { + return $this->risingLowSignalArtworks($windowDays); + } + + return $artworks; + } ); return $this->builder->buildFromArtworks( @@ -95,4 +105,76 @@ final class DiscoverFeedController extends Controller $artworks, ); } + + private function risingArtworks(int $windowDays): Collection + { + $cutoff = now()->subDays($windowDays)->startOfDay(); + + return Artwork::public() + ->published() + ->with(['user:id,username', 'categories:id,name,slug,content_type_id']) + ->leftJoin('artwork_stats', 'artwork_stats.artwork_id', '=', 'artworks.id') + ->select('artworks.*') + ->selectRaw('COALESCE(artwork_stats.heat_score, 0) as heat_score') + ->selectRaw('COALESCE(artwork_stats.engagement_velocity, 0) as engagement_velocity') + ->where('artworks.published_at', '>=', $cutoff) + ->orderByDesc('artwork_stats.heat_score') + ->orderByDesc('artwork_stats.engagement_velocity') + ->orderByDesc('artworks.published_at') + ->orderByDesc('artworks.id') + ->limit(RSSFeedBuilder::FEED_LIMIT) + ->get(); + } + + private function risingLowSignalArtworks(int $windowDays): Collection + { + $cutoff = now()->subDays($windowDays)->startOfDay(); + + return Artwork::public() + ->published() + ->with(['user:id,username', 'categories:id,name,slug,content_type_id']) + ->leftJoin('artwork_stats', 'artwork_stats.artwork_id', '=', 'artworks.id') + ->leftJoinSub($this->risingRecentActivitySubquery(), 'recent_rising_activity', function ($join): void { + $join->on('recent_rising_activity.artwork_id', '=', 'artworks.id'); + }) + ->select('artworks.*') + ->selectRaw('COALESCE(artwork_stats.heat_score, 0) as heat_score') + ->selectRaw('COALESCE(artwork_stats.engagement_velocity, 0) as engagement_velocity') + ->selectRaw('COALESCE(recent_rising_activity.recent_signal_24h, 0) as recent_signal_24h') + ->where('artworks.published_at', '>=', $cutoff) + ->orderByDesc('recent_signal_24h') + ->orderByDesc('artworks.published_at') + ->orderByDesc('artworks.id') + ->limit(RSSFeedBuilder::FEED_LIMIT) + ->get(); + } + + private function collectionHasNoRisingMomentum(Collection $artworks): bool + { + if ($artworks->isEmpty()) { + return false; + } + + return $artworks->every(function (Artwork $artwork): bool { + return (float) ($artwork->heat_score ?? 0) <= 0 + && (float) ($artwork->engagement_velocity ?? 0) <= 0; + }); + } + + private function risingRecentActivitySubquery() + { + $since = now()->startOfHour()->subHours(24); + + return DB::table('artwork_metric_snapshots_hourly as rising_snapshots') + ->selectRaw('rising_snapshots.artwork_id') + ->selectRaw('( + COALESCE(MAX(rising_snapshots.views_count) - MIN(rising_snapshots.views_count), 0) + + (COALESCE(MAX(rising_snapshots.downloads_count) - MIN(rising_snapshots.downloads_count), 0) * 3) + + (COALESCE(MAX(rising_snapshots.favourites_count) - MIN(rising_snapshots.favourites_count), 0) * 4) + + (COALESCE(MAX(rising_snapshots.comments_count) - MIN(rising_snapshots.comments_count), 0) * 5) + + (COALESCE(MAX(rising_snapshots.shares_count) - MIN(rising_snapshots.shares_count), 0) * 6) + ) as recent_signal_24h') + ->where('rising_snapshots.bucket_hour', '>=', $since) + ->groupBy('rising_snapshots.artwork_id'); + } } diff --git a/app/Http/Controllers/Web/BrowseGalleryController.php b/app/Http/Controllers/Web/BrowseGalleryController.php index a74861c7..a706b24d 100644 --- a/app/Http/Controllers/Web/BrowseGalleryController.php +++ b/app/Http/Controllers/Web/BrowseGalleryController.php @@ -280,11 +280,18 @@ class BrowseGalleryController extends \App\Http\Controllers\Controller { $primaryCategory = $artwork->categories->sortBy('sort_order')->first(); $present = ThumbnailPresenter::present($artwork, 'md'); - $avatarUrl = \App\Support\AvatarUrl::forUser( - (int) ($artwork->user_id ?? 0), - $artwork->user?->profile?->avatar_hash ?? null, - 64 - ); + $group = $artwork->group; + $isGroupPublisher = $group !== null; + $avatarUrl = $isGroupPublisher + ? $group->avatarUrl() + : \App\Support\AvatarUrl::forUser( + (int) ($artwork->user_id ?? 0), + $artwork->user?->profile?->avatar_hash ?? null, + 64 + ); + $displayName = $isGroupPublisher ? ($group->name ?? 'Skinbase') : ($artwork->user?->name ?? 'Skinbase'); + $username = $isGroupPublisher ? '' : ($artwork->user?->username ?? ''); + $profileUrl = $isGroupPublisher ? $group->publicUrl() : ($username !== '' ? '/@' . $username : null); return (object) [ 'id' => $artwork->id, @@ -295,9 +302,18 @@ class BrowseGalleryController extends \App\Http\Controllers\Controller 'category_slug' => $primaryCategory->slug ?? '', 'thumb_url' => $present['url'], 'thumb_srcset' => $present['srcset'] ?? $present['url'], - 'uname' => $artwork->user?->name ?? 'Skinbase', - 'username' => $artwork->user?->username ?? '', + 'uname' => $displayName, + 'username' => $username, 'avatar_url' => $avatarUrl, + 'profile_url' => $profileUrl, + 'published_as_type' => $isGroupPublisher ? 'group' : 'user', + 'publisher' => [ + 'type' => $isGroupPublisher ? 'group' : 'user', + 'name' => $displayName, + 'username' => $username, + 'avatar_url' => $avatarUrl, + 'profile_url' => $profileUrl, + ], 'published_at' => $artwork->published_at, 'width' => $artwork->width ?? null, 'height' => $artwork->height ?? null, diff --git a/app/Http/Controllers/Web/DiscoverController.php b/app/Http/Controllers/Web/DiscoverController.php index 54427b74..1bd9e75c 100644 --- a/app/Http/Controllers/Web/DiscoverController.php +++ b/app/Http/Controllers/Web/DiscoverController.php @@ -7,7 +7,7 @@ use App\Models\Artwork; use App\Services\CommunityActivityService; use App\Services\ArtworkSearchService; use App\Services\ArtworkService; -use App\Services\EarlyGrowth\FeedBlender; +use App\Services\EarlyGrowth\AdaptiveTimeWindow; use App\Services\EarlyGrowth\GridFiller; use App\Services\Recommendations\RecommendationFeedResolver; use App\Services\UserSuggestionService; @@ -33,8 +33,8 @@ final class DiscoverController extends Controller public function __construct( private readonly ArtworkService $artworkService, private readonly ArtworkSearchService $searchService, + private readonly AdaptiveTimeWindow $timeWindow, private readonly RecommendationFeedResolver $feedResolver, - private readonly FeedBlender $feedBlender, private readonly GridFiller $gridFiller, private readonly CommunityActivityService $communityActivity, private readonly UserSuggestionService $userSuggestions, @@ -45,9 +45,18 @@ final class DiscoverController extends Controller public function trending(Request $request) { $perPage = 24; - $page = max(1, (int) $request->query('page', 1)); - $results = $this->searchService->discoverTrending($perPage); - $results = $this->gridFiller->fill($results, 0, $page); + $windowDays = $this->timeWindow->getTrendingWindowDays(30); + + try { + $results = $this->searchService->discoverTrending($perPage); + } catch (\Throwable) { + $results = $this->fallbackTrendingFromDatabase($perPage, $windowDays); + } + + if ($this->paginatorIsEmpty($results)) { + $results = $this->fallbackTrendingFromDatabase($perPage, $windowDays); + } + $this->hydrateDiscoverSearchResults($results); return view('web.discover.index', [ @@ -64,9 +73,22 @@ final class DiscoverController extends Controller public function rising(Request $request) { $perPage = 24; - $page = max(1, (int) $request->query('page', 1)); - $results = $this->searchService->discoverRising($perPage); - $results = $this->gridFiller->fill($results, 0, $page); + $windowDays = $this->timeWindow->getTrendingWindowDays(30); + + try { + $results = $this->searchService->discoverRising($perPage); + } catch (\Throwable) { + $results = $this->fallbackRisingFromDatabase($perPage, $windowDays); + } + + if ($this->paginatorIsEmpty($results)) { + $results = $this->fallbackRisingFromDatabase($perPage, $windowDays); + } + + if ($this->paginatorHasNoRisingMomentum($results)) { + $results = $this->fallbackRisingLowSignalFromDatabase($perPage, $windowDays); + } + $this->hydrateDiscoverSearchResults($results); return view('web.discover.index', [ @@ -83,11 +105,12 @@ final class DiscoverController extends Controller public function fresh(Request $request) { $perPage = 24; - $page = max(1, (int) $request->query('page', 1)); $results = $this->searchService->discoverFresh($perPage); - // EGS: blend fresh feed with curated + spotlight on page 1 - $results = $this->feedBlender->blend($results, $perPage, $page); - $results = $this->gridFiller->fill($results, 0, $page); + + if ($this->paginatorIsEmpty($results)) { + $results = $this->fallbackFreshFromDatabase($perPage); + } + $this->hydrateDiscoverSearchResults($results); return view('web.discover.index', [ @@ -351,6 +374,152 @@ final class DiscoverController extends Controller // ─── Helpers ───────────────────────────────────────────────────────────── + private function paginatorIsEmpty($paginator): bool + { + if (! is_object($paginator) || ! method_exists($paginator, 'getCollection')) { + return true; + } + + $items = $paginator->getCollection(); + + return ! $items || $items->isEmpty(); + } + + private function paginatorHasNoRisingMomentum($paginator): bool + { + if (! is_object($paginator) || ! method_exists($paginator, 'getCollection')) { + return true; + } + + $items = $paginator->getCollection(); + + if (! $items || $items->isEmpty()) { + return true; + } + + return $items->every(function ($item): bool { + $heat = (float) ($item->heat_score ?? $item->stats?->heat_score ?? 0); + $velocity = (float) ($item->engagement_velocity ?? $item->stats?->engagement_velocity ?? 0); + + return $heat <= 0.0 && $velocity <= 0.0; + }); + } + + private function fallbackFreshFromDatabase(int $perPage) + { + return Artwork::query() + ->public() + ->published() + ->with([ + 'user:id,name,username', + 'user.profile:user_id,avatar_hash', + 'categories:id,name,slug,content_type_id,parent_id,sort_order', + 'categories.contentType:id,slug,name', + ]) + ->orderByDesc('published_at') + ->orderByDesc('id') + ->paginate($perPage) + ->withQueryString(); + } + + private function fallbackTrendingFromDatabase(int $perPage, int $windowDays) + { + $cutoff = now()->subDays($windowDays)->startOfDay(); + + return Artwork::query() + ->public() + ->published() + ->with([ + 'user:id,name,username', + 'user.profile:user_id,avatar_hash', + 'categories:id,name,slug,content_type_id,parent_id,sort_order', + 'categories.contentType:id,slug,name', + ]) + ->leftJoin('artwork_stats as discover_stats', 'discover_stats.artwork_id', '=', 'artworks.id') + ->select('artworks.*') + ->where('artworks.published_at', '>=', $cutoff) + ->orderByDesc('discover_stats.ranking_score') + ->orderByDesc('discover_stats.engagement_velocity') + ->orderByDesc('discover_stats.views') + ->orderByDesc('artworks.published_at') + ->orderByDesc('artworks.id') + ->paginate($perPage) + ->withQueryString(); + } + + private function fallbackRisingFromDatabase(int $perPage, int $windowDays) + { + $cutoff = now()->subDays($windowDays)->startOfDay(); + + return Artwork::query() + ->public() + ->published() + ->with([ + 'user:id,name,username', + 'user.profile:user_id,avatar_hash', + 'categories:id,name,slug,content_type_id,parent_id,sort_order', + 'categories.contentType:id,slug,name', + ]) + ->leftJoin('artwork_stats as discover_stats', 'discover_stats.artwork_id', '=', 'artworks.id') + ->select('artworks.*') + ->selectRaw('COALESCE(discover_stats.heat_score, 0) as heat_score') + ->selectRaw('COALESCE(discover_stats.engagement_velocity, 0) as engagement_velocity') + ->where('artworks.published_at', '>=', $cutoff) + ->orderByDesc('discover_stats.heat_score') + ->orderByDesc('discover_stats.engagement_velocity') + ->orderByDesc('artworks.published_at') + ->orderByDesc('artworks.id') + ->paginate($perPage) + ->withQueryString(); + } + + private function fallbackRisingLowSignalFromDatabase(int $perPage, int $windowDays) + { + $cutoff = now()->subDays($windowDays)->startOfDay(); + $recentActivity = $this->risingRecentActivitySubquery(); + + return Artwork::query() + ->public() + ->published() + ->with([ + 'user:id,name,username', + 'user.profile:user_id,avatar_hash', + 'categories:id,name,slug,content_type_id,parent_id,sort_order', + 'categories.contentType:id,slug,name', + ]) + ->leftJoin('artwork_stats as discover_stats', 'discover_stats.artwork_id', '=', 'artworks.id') + ->leftJoinSub($recentActivity, 'recent_rising_activity', function ($join): void { + $join->on('recent_rising_activity.artwork_id', '=', 'artworks.id'); + }) + ->select('artworks.*') + ->selectRaw('COALESCE(discover_stats.heat_score, 0) as heat_score') + ->selectRaw('COALESCE(discover_stats.engagement_velocity, 0) as engagement_velocity') + ->selectRaw('COALESCE(recent_rising_activity.recent_signal_24h, 0) as recent_signal_24h') + ->where('artworks.published_at', '>=', $cutoff) + ->orderByDesc('recent_signal_24h') + ->orderByDesc('artworks.published_at') + ->orderByDesc('artworks.id') + ->paginate($perPage) + ->withQueryString(); + } + + private function risingRecentActivitySubquery() + { + $since = now()->startOfHour()->subHours(24); + + return DB::table('artwork_metric_snapshots_hourly as rising_snapshots') + ->selectRaw('rising_snapshots.artwork_id') + ->selectRaw('( + COALESCE(MAX(rising_snapshots.views_count) - MIN(rising_snapshots.views_count), 0) + + (COALESCE(MAX(rising_snapshots.downloads_count) - MIN(rising_snapshots.downloads_count), 0) * 3) + + (COALESCE(MAX(rising_snapshots.favourites_count) - MIN(rising_snapshots.favourites_count), 0) * 4) + + (COALESCE(MAX(rising_snapshots.comments_count) - MIN(rising_snapshots.comments_count), 0) * 5) + + (COALESCE(MAX(rising_snapshots.shares_count) - MIN(rising_snapshots.shares_count), 0) * 6) + ) as recent_signal_24h') + ->where('rising_snapshots.bucket_hour', '>=', $since) + ->groupBy('rising_snapshots.artwork_id'); + } + private function hydrateDiscoverSearchResults($paginator): void { if (!is_object($paginator) || !method_exists($paginator, 'getCollection') || !method_exists($paginator, 'setCollection')) { @@ -377,6 +546,7 @@ final class DiscoverController extends Controller ->with([ 'user:id,name,username', 'user.profile:user_id,avatar_hash', + 'group:id,name,slug,avatar_path', 'categories:id,name,slug,content_type_id,parent_id,sort_order', ]) ->get() @@ -398,9 +568,12 @@ final class DiscoverController extends Controller 'category_slug' => $item->category_slug ?? '', 'thumb_url' => $item->thumbnail_url ?? $item->thumb_url ?? $item->thumb ?? null, 'thumb_srcset' => $item->thumb_srcset ?? null, - 'uname' => $item->author ?? $item->uname ?? 'Skinbase', - 'username' => $item->username ?? '', + 'uname' => $item->author_name ?? $item->author ?? $item->uname ?? 'Skinbase', + 'username' => (($item->published_as_type ?? null) === 'group') ? '' : ($item->username ?? ''), 'avatar_url' => \App\Support\AvatarUrl::forUser((int) ($item->user_id ?? $item->author_id ?? 0), null, 64), + 'profile_url' => $item->profile_url ?? null, + 'published_as_type' => $item->published_as_type ?? null, + 'publisher' => $item->publisher ?? null, 'published_at' => $item->published_at ?? null, 'width' => isset($item->width) && $item->width ? (int) $item->width : null, 'height' => isset($item->height) && $item->height ? (int) $item->height : null, @@ -413,11 +586,18 @@ final class DiscoverController extends Controller { $primaryCategory = $artwork->categories->sortBy('sort_order')->first(); $present = ThumbnailPresenter::present($artwork, 'md'); - $avatarUrl = \App\Support\AvatarUrl::forUser( - (int) ($artwork->user_id ?? 0), - $artwork->user?->profile?->avatar_hash ?? null, - 64 - ); + $group = $artwork->group; + $isGroupPublisher = $group !== null; + $avatarUrl = $isGroupPublisher + ? $group->avatarUrl() + : \App\Support\AvatarUrl::forUser( + (int) ($artwork->user_id ?? 0), + $artwork->user?->profile?->avatar_hash ?? null, + 64 + ); + $displayName = $isGroupPublisher ? ($group->name ?? 'Skinbase') : ($artwork->user?->name ?? 'Skinbase'); + $username = $isGroupPublisher ? '' : ($artwork->user?->username ?? ''); + $profileUrl = $isGroupPublisher ? $group->publicUrl() : ($username !== '' ? '/@' . $username : null); return (object) [ 'id' => $artwork->id, @@ -429,8 +609,18 @@ final class DiscoverController extends Controller 'gid_num' => $primaryCategory ? ((int) $primaryCategory->id % 5) * 5 : 0, 'thumb_url' => $present['url'], 'thumb_srcset' => $present['srcset'] ?? $present['url'], - 'uname' => $artwork->user->name ?? 'Skinbase', + 'uname' => $displayName, + 'username' => $username, 'avatar_url' => $avatarUrl, + 'profile_url' => $profileUrl, + 'published_as_type' => $isGroupPublisher ? 'group' : 'user', + 'publisher' => [ + 'type' => $isGroupPublisher ? 'group' : 'user', + 'name' => $displayName, + 'username' => $username, + 'avatar_url' => $avatarUrl, + 'profile_url' => $profileUrl, + ], 'published_at' => $artwork->published_at, 'width' => $artwork->width ?? null, 'height' => $artwork->height ?? null, diff --git a/app/Http/Controllers/Web/ExploreController.php b/app/Http/Controllers/Web/ExploreController.php index a80261c8..75c61c60 100644 --- a/app/Http/Controllers/Web/ExploreController.php +++ b/app/Http/Controllers/Web/ExploreController.php @@ -276,11 +276,18 @@ final class ExploreController extends Controller { $primary = $artwork->categories->sortBy('sort_order')->first(); $present = ThumbnailPresenter::present($artwork, 'md'); - $avatarUrl = \App\Support\AvatarUrl::forUser( - (int) ($artwork->user_id ?? 0), - $artwork->user?->profile?->avatar_hash ?? null, - 64 - ); + $group = $artwork->group; + $isGroupPublisher = $group !== null; + $avatarUrl = $isGroupPublisher + ? $group->avatarUrl() + : \App\Support\AvatarUrl::forUser( + (int) ($artwork->user_id ?? 0), + $artwork->user?->profile?->avatar_hash ?? null, + 64 + ); + $displayName = $isGroupPublisher ? ($group->name ?? 'Skinbase') : ($artwork->user?->name ?? 'Skinbase'); + $username = $isGroupPublisher ? '' : ($artwork->user?->username ?? ''); + $profileUrl = $isGroupPublisher ? $group->publicUrl() : ($username !== '' ? '/@' . $username : null); return (object) [ 'id' => $artwork->id, @@ -291,9 +298,18 @@ final class ExploreController extends Controller 'category_slug' => $primary->slug ?? '', 'thumb_url' => $present['url'], 'thumb_srcset' => $present['srcset'] ?? $present['url'], - 'uname' => $artwork->user?->name ?? 'Skinbase', - 'username' => $artwork->user?->username ?? '', + 'uname' => $displayName, + 'username' => $username, 'avatar_url' => $avatarUrl, + 'profile_url' => $profileUrl, + 'published_as_type' => $isGroupPublisher ? 'group' : 'user', + 'publisher' => [ + 'type' => $isGroupPublisher ? 'group' : 'user', + 'name' => $displayName, + 'username' => $username, + 'avatar_url' => $avatarUrl, + 'profile_url' => $profileUrl, + ], 'published_at' => $artwork->published_at, 'slug' => $artwork->slug ?? '', 'width' => $artwork->width ?? null, diff --git a/app/Http/Controllers/Web/SimilarArtworksPageController.php b/app/Http/Controllers/Web/SimilarArtworksPageController.php index 3ed1b663..4a72496f 100644 --- a/app/Http/Controllers/Web/SimilarArtworksPageController.php +++ b/app/Http/Controllers/Web/SimilarArtworksPageController.php @@ -290,11 +290,18 @@ final class SimilarArtworksPageController extends Controller { $primary = $artwork->categories->sortBy('sort_order')->first(); $present = ThumbnailPresenter::present($artwork, 'md'); - $avatarUrl = AvatarUrl::forUser( - (int) ($artwork->user_id ?? 0), - $artwork->user?->profile?->avatar_hash ?? null, - 64 - ); + $group = $artwork->group; + $isGroupPublisher = $group !== null; + $avatarUrl = $isGroupPublisher + ? $group->avatarUrl() + : AvatarUrl::forUser( + (int) ($artwork->user_id ?? 0), + $artwork->user?->profile?->avatar_hash ?? null, + 64 + ); + $displayName = $isGroupPublisher ? ($group->name ?? 'Skinbase') : ($artwork->user?->name ?? 'Skinbase'); + $username = $isGroupPublisher ? '' : ($artwork->user?->username ?? ''); + $profileUrl = $isGroupPublisher ? $group->publicUrl() : ($username !== '' ? '/@' . $username : null); return (object) [ 'id' => $artwork->id, @@ -305,9 +312,18 @@ final class SimilarArtworksPageController extends Controller 'category_slug' => $primary?->slug ?? '', 'thumb_url' => $present['url'], 'thumb_srcset' => $present['srcset'] ?? $present['url'], - 'uname' => $artwork->user?->name ?? 'Skinbase', - 'username' => $artwork->user?->username ?? '', + 'uname' => $displayName, + 'username' => $username, 'avatar_url' => $avatarUrl, + 'profile_url' => $profileUrl, + 'published_as_type' => $isGroupPublisher ? 'group' : 'user', + 'publisher' => [ + 'type' => $isGroupPublisher ? 'group' : 'user', + 'name' => $displayName, + 'username' => $username, + 'avatar_url' => $avatarUrl, + 'profile_url' => $profileUrl, + ], 'published_at' => $artwork->published_at, 'slug' => $artwork->slug ?? '', 'width' => $artwork->width ?? null, diff --git a/app/Http/Controllers/Web/TagController.php b/app/Http/Controllers/Web/TagController.php index df00402a..20fd0f02 100644 --- a/app/Http/Controllers/Web/TagController.php +++ b/app/Http/Controllers/Web/TagController.php @@ -8,7 +8,6 @@ use App\Http\Controllers\Controller; use App\Models\ContentType; use App\Models\Tag; use App\Services\ArtworkSearchService; -use App\Services\EarlyGrowth\GridFiller; use App\Services\Tags\TagDiscoveryService; use App\Services\ThumbnailPresenter; use Illuminate\Http\Request; @@ -18,7 +17,6 @@ final class TagController extends Controller { public function __construct( private readonly ArtworkSearchService $search, - private readonly GridFiller $gridFiller, private readonly TagDiscoveryService $tagDiscovery, ) {} @@ -45,29 +43,10 @@ final class TagController extends Controller public function show(Tag $tag, Request $request): View { - $sort = $request->query('sort', 'popular'); // popular | latest | downloads + $sort = $request->query('sort', 'popular'); // popular | likes | latest | downloads $perPage = min((int) $request->query('per_page', 24), 100); - // Convert sort param to Meili sort expression - $sortMap = [ - 'popular' => 'views:desc', - 'likes' => 'likes:desc', - 'latest' => 'created_at:desc', - 'downloads' => 'downloads:desc', - ]; - $meiliSort = $sortMap[$sort] ?? 'views:desc'; - - $artworks = \App\Models\Artwork::search('') - ->options([ - 'filter' => 'is_public = true AND is_approved = true AND tags = "' . addslashes($tag->slug) . '"', - 'sort' => [$meiliSort], - ]) - ->paginate($perPage) - ->appends(['sort' => $sort]); - - // EGS: ensure tag pages never show a half-empty grid on page 1 - $page = max(1, (int) $request->query('page', 1)); - $artworks = $this->gridFiller->fill($artworks, 0, $page); + $artworks = $this->search->byTag($tag->slug, $perPage, $sort); // Eager-load relations used by the gallery presenter and thumbnails. $artworks->getCollection()->each(fn($m) => $m->loadMissing(['user.profile', 'categories'])); diff --git a/app/Http/Requests/Uploads/UploadChunkRequest.php b/app/Http/Requests/Uploads/UploadChunkRequest.php index 4da808fd..e3d2da0b 100644 --- a/app/Http/Requests/Uploads/UploadChunkRequest.php +++ b/app/Http/Requests/Uploads/UploadChunkRequest.php @@ -7,10 +7,24 @@ namespace App\Http\Requests\Uploads; use App\Repositories\Uploads\UploadSessionRepository; use App\Services\Uploads\UploadTokenService; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\ValidationException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; final class UploadChunkRequest extends FormRequest { + protected function prepareForValidation(): void + { + $uploadError = $this->detectChunkUploadError(); + + if ($uploadError !== null && $uploadError !== UPLOAD_ERR_OK) { + $this->logChunkUploadFailure($uploadError); + + throw ValidationException::withMessages([ + 'chunk' => [$this->messageForUploadError($uploadError)], + ]); + } + } + public function authorize(): bool { $user = $this->user(); @@ -79,6 +93,63 @@ final class UploadChunkRequest extends FormRequest throw new NotFoundHttpException(); } + private function detectChunkUploadError(): ?int + { + $uploadedFile = $this->file('chunk'); + if ($uploadedFile !== null) { + return (int) $uploadedFile->getError(); + } + + $rawError = data_get($_FILES, 'chunk.error'); + if ($rawError === null || $rawError === '') { + return null; + } + + return (int) $rawError; + } + + private function messageForUploadError(int $error): string + { + return match ($error) { + UPLOAD_ERR_INI_SIZE => 'The upload chunk exceeded PHP upload_max_filesize. Lower UPLOAD_CHUNK_MAX_BYTES or raise upload_max_filesize/post_max_size.', + UPLOAD_ERR_FORM_SIZE => 'The upload chunk exceeded the allowed form upload size.', + UPLOAD_ERR_PARTIAL => 'The upload chunk was only partially received. Check Nginx/PHP-FPM request handling and network stability.', + UPLOAD_ERR_NO_FILE => 'No upload chunk file was received by PHP.', + UPLOAD_ERR_NO_TMP_DIR => 'PHP upload_tmp_dir is missing or unavailable. Check the configured temporary upload directory on the server.', + UPLOAD_ERR_CANT_WRITE => 'PHP could not write the upload chunk to the temporary directory. Check upload_tmp_dir permissions and free disk space.', + UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the upload chunk before Laravel could process it.', + default => 'The upload chunk failed before Laravel could read it. Check PHP temporary upload storage and request size limits.', + }; + } + + private function logChunkUploadFailure(int $error): void + { + $uploadTmpDir = (string) (ini_get('upload_tmp_dir') ?: sys_get_temp_dir() ?: ''); + $tmpExists = $uploadTmpDir !== '' ? is_dir($uploadTmpDir) : false; + $tmpWritable = $tmpExists ? is_writable($uploadTmpDir) : false; + + logger()->warning('Upload chunk failed before validation completed', [ + 'session_id' => (string) $this->input('session_id'), + 'user_id' => $this->user()?->id, + 'ip' => $this->ip(), + 'upload_error' => $error, + 'upload_error_message' => $this->messageForUploadError($error), + 'content_length' => $this->server('CONTENT_LENGTH'), + 'post_max_size' => ini_get('post_max_size'), + 'upload_max_filesize' => ini_get('upload_max_filesize'), + 'upload_tmp_dir' => $uploadTmpDir, + 'tmp_exists' => $tmpExists, + 'tmp_writable' => $tmpWritable, + 'raw_files' => isset($_FILES['chunk']) ? [ + 'name' => $_FILES['chunk']['name'] ?? null, + 'type' => $_FILES['chunk']['type'] ?? null, + 'size' => $_FILES['chunk']['size'] ?? null, + 'tmp_name' => $_FILES['chunk']['tmp_name'] ?? null, + 'error' => $_FILES['chunk']['error'] ?? null, + ] : null, + ]); + } + private function logUnauthorized(string $reason): void { logger()->warning('Upload chunk unauthorized access', [ diff --git a/app/Http/Resources/ArtworkListResource.php b/app/Http/Resources/ArtworkListResource.php index a88aac29..1aee5026 100644 --- a/app/Http/Resources/ArtworkListResource.php +++ b/app/Http/Resources/ArtworkListResource.php @@ -61,6 +61,22 @@ class ArtworkListResource extends JsonResource $decode = static fn (?string $v): string => html_entity_decode((string) ($v ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $group = $this->relationLoaded('group') ? $this->group : null; + $user = $this->relationLoaded('user') ? $this->user : null; + $isGroupPublisher = $group !== null; + $publisher = ($group || $user) + ? [ + 'type' => $isGroupPublisher ? 'group' : 'user', + 'id' => (int) ($isGroupPublisher ? $group?->id : $user?->id), + 'name' => $decode($isGroupPublisher ? $group?->name : $user?->name), + 'username' => $isGroupPublisher ? '' : (string) ($user?->username ?? ''), + 'avatar_url' => $isGroupPublisher ? $group?->avatarUrl() : $user?->profile?->avatar_url, + 'profile_url' => $isGroupPublisher + ? $group?->publicUrl() + : (! empty($user?->username) ? '/@' . $user->username : null), + ] + : null; + return [ 'id' => $artId, 'slug' => $slugVal, @@ -71,12 +87,12 @@ class ArtworkListResource extends JsonResource 'height' => $get('height'), ], 'thumbnail_url' => $this->when(! empty($hash) && ! empty($thumbExt), fn() => ThumbnailService::fromHash($hash, $thumbExt, 'md')), - 'author' => $this->whenLoaded('user', function () use ($decode) { - return [ - 'name' => $decode($this->user->name ?? null), - 'avatar_url' => $this->user?->profile?->avatar_url, - ]; - }), + 'author' => $publisher, + 'publisher' => $publisher, + 'author_name' => $publisher['name'] ?? '', + 'avatar_url' => $publisher['avatar_url'] ?? null, + 'profile_url' => $publisher['profile_url'] ?? null, + 'published_as_type' => $publisher['type'] ?? null, 'category' => $primaryCategory ? [ 'slug' => $primaryCategory->slug ?? null, 'name' => $decode($primaryCategory->name ?? null), diff --git a/app/Models/Artwork.php b/app/Models/Artwork.php index f090dc28..91033f30 100644 --- a/app/Models/Artwork.php +++ b/app/Models/Artwork.php @@ -329,6 +329,7 @@ class Artwork extends Model $stat = $this->stats; $awardStat = $this->awardStat; + $publishedSortAt = $this->published_at ?? $this->created_at; // Orientation derived from pixel dimensions $orientation = 'square'; @@ -380,7 +381,8 @@ class Artwork extends Model 'downloads' => (int) ($stat?->downloads ?? 0), 'likes' => (int) ($stat?->favorites ?? 0), 'views' => (int) ($stat?->views ?? 0), - 'created_at' => $this->published_at?->toDateString() ?? $this->created_at?->toDateString() ?? '', + 'created_at' => $publishedSortAt?->toDateString() ?? '', + 'published_at_ts' => $publishedSortAt?->getTimestamp() ?? 0, 'is_public' => (bool) $this->is_public, 'is_approved' => (bool) $this->is_approved, // ── Trending / discovery fields ──────────────────────────────────── diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f3e182c6..9e35e1c6 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -255,6 +255,10 @@ class AppServiceProvider extends ServiceProvider return $this->buildUploadLimits($request, 'init'); }); + RateLimiter::for('uploads-chunk', function (Request $request): array { + return $this->buildUploadLimits($request, 'chunk'); + }); + RateLimiter::for('uploads-finish', function (Request $request): array { return $this->buildUploadLimits($request, 'finish'); }); diff --git a/app/Services/ArtworkSearchService.php b/app/Services/ArtworkSearchService.php index 0b995502..b8730b14 100644 --- a/app/Services/ArtworkSearchService.php +++ b/app/Services/ArtworkSearchService.php @@ -9,6 +9,7 @@ use App\Models\Tag; use App\Services\EarlyGrowth\AdaptiveTimeWindow; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\DB; /** * High-level search API powered by Meilisearch via Laravel Scout. @@ -19,6 +20,7 @@ final class ArtworkSearchService { private const BASE_FILTER = 'is_public = true AND is_approved = true'; private const CACHE_TTL = 300; // 5 minutes + private const TAG_SORTS = ['popular', 'likes', 'latest', 'downloads']; public function __construct( private readonly AdaptiveTimeWindow $timeWindow, @@ -82,22 +84,46 @@ final class ArtworkSearchService /** * Load artworks for a tag page, sorted by views + likes descending. */ - public function byTag(string $slug, int $perPage = 24): LengthAwarePaginator + public function byTag(string $slug, int $perPage = 24, string $sort = 'popular'): LengthAwarePaginator { $tag = Tag::where('slug', $slug)->first(); if (! $tag) { return $this->emptyPaginator($perPage); } - $cacheKey = "search.tag.{$slug}.page." . request()->get('page', 1); + $sort = in_array($sort, self::TAG_SORTS, true) ? $sort : 'popular'; + $cacheKey = "search.tag.{$slug}.{$sort}.{$perPage}.page." . request()->get('page', 1); - return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($slug, $perPage) { - return Artwork::search('') - ->options([ - 'filter' => self::BASE_FILTER . ' AND tags = "' . addslashes($slug) . '"', - 'sort' => ['views:desc', 'likes:desc'], - ]) - ->paginate($perPage); + return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($tag, $perPage, $sort) { + $query = Artwork::query() + ->public() + ->published() + ->whereHas('tags', fn ($tagQuery) => $tagQuery->where('tags.id', $tag->id)) + ->leftJoin('artwork_stats', 'artwork_stats.artwork_id', '=', 'artworks.id') + ->select('artworks.*') + ->with(['user.profile', 'categories.contentType']); + + match ($sort) { + 'likes' => $query + ->orderByRaw('COALESCE(artwork_stats.favorites, 0) DESC') + ->orderByRaw('COALESCE(artwork_stats.views, 0) DESC') + ->orderByDesc('artworks.published_at'), + 'latest' => $query + ->orderByDesc('artworks.published_at') + ->orderByDesc('artworks.id'), + 'downloads' => $query + ->orderByRaw('COALESCE(artwork_stats.downloads, 0) DESC') + ->orderByRaw('COALESCE(artwork_stats.views, 0) DESC') + ->orderByDesc('artworks.published_at'), + default => $query + ->orderByRaw('COALESCE(artwork_stats.views, 0) DESC') + ->orderByRaw('COALESCE(artwork_stats.favorites, 0) DESC') + ->orderByDesc('artworks.published_at'), + }; + + return $query + ->paginate($perPage) + ->withQueryString(); }); } @@ -125,12 +151,12 @@ final class ArtworkSearchService * Used by categoryPageSort() and contentTypePageSort(). */ private const CATEGORY_SORT_FIELDS = [ - 'trending' => ['trending_score_24h:desc', 'created_at:desc'], - 'fresh' => ['created_at:desc'], + 'trending' => ['trending_score_24h:desc', 'published_at_ts:desc'], + 'fresh' => ['published_at_ts:desc'], 'top-rated' => ['awards_received_count:desc', 'favorites_count:desc'], 'favorited' => ['favorites_count:desc', 'trending_score_24h:desc'], 'downloaded' => ['downloads_count:desc', 'trending_score_24h:desc'], - 'oldest' => ['created_at:asc'], + 'oldest' => ['published_at_ts:asc'], ]; /** Cache TTL (seconds) per sort alias for category pages. */ @@ -237,7 +263,7 @@ final class ArtworkSearchService } /** - * Most recent artworks by created_at. + * Most recent artworks by publish timestamp. */ public function recent(int $perPage = 24): LengthAwarePaginator { @@ -245,7 +271,7 @@ final class ArtworkSearchService return Artwork::search('') ->options([ 'filter' => self::BASE_FILTER, - 'sort' => ['created_at:desc'], + 'sort' => ['published_at_ts:desc'], ]) ->paginate($perPage); }); @@ -294,7 +320,7 @@ final class ArtworkSearchService return Artwork::search('') ->options([ 'filter' => self::BASE_FILTER . ' AND created_at >= "' . $cutoff . '"', - 'sort' => ['heat_score:desc', 'engagement_velocity:desc', 'created_at:desc'], + 'sort' => ['heat_score:desc', 'engagement_velocity:desc', 'published_at_ts:desc'], ]) ->paginate($perPage); }); @@ -310,7 +336,7 @@ final class ArtworkSearchService return Artwork::search('') ->options([ 'filter' => self::BASE_FILTER, - 'sort' => ['created_at:desc'], + 'sort' => ['published_at_ts:desc'], ]) ->paginate($perPage); }); @@ -378,7 +404,7 @@ final class ArtworkSearchService } /** - * Fresh artworks in given categories, sorted by created_at desc. + * Fresh artworks in given categories, sorted by publish timestamp desc. * Used for personalized "Fresh in your favourite categories" section. * * @param string[] $categorySlugs @@ -400,7 +426,7 @@ final class ArtworkSearchService return Artwork::search('') ->options([ 'filter' => self::BASE_FILTER . ' AND (' . $catFilter . ')', - 'sort' => ['created_at:desc'], + 'sort' => ['published_at_ts:desc'], ]) ->paginate($limit); }); diff --git a/app/Services/ArtworkService.php b/app/Services/ArtworkService.php index 21232e98..c7afa5fa 100644 --- a/app/Services/ArtworkService.php +++ b/app/Services/ArtworkService.php @@ -23,6 +23,24 @@ class ArtworkService { protected int $cacheTtl = 3600; // seconds + /** + * Lightweight relations needed to render browse/list cards. + * + * @return array + */ + private function browseRelations(): array + { + return [ + 'user:id,name,username', + 'user.profile:user_id,avatar_url', + 'group:id,name,slug,avatar_path', + 'categories' => function ($q) { + $q->select('categories.id', 'categories.content_type_id', 'categories.parent_id', 'categories.name', 'categories.slug', 'categories.sort_order') + ->with(['parent:id,parent_id,content_type_id,name,slug', 'contentType:id,slug,name']); + }, + ]; + } + /** * Shared browse query used by /browse, content-type pages, and category pages. */ @@ -30,13 +48,7 @@ class ArtworkService { $query = Artwork::public() ->published() - ->with([ - 'user:id,name', - 'categories' => function ($q) { - $q->select('categories.id', 'categories.content_type_id', 'categories.parent_id', 'categories.name', 'categories.slug', 'categories.sort_order') - ->with(['parent:id,parent_id,content_type_id,name,slug', 'contentType:id,slug,name']); - }, - ]); + ->with($this->browseRelations()); $normalizedSort = strtolower(trim($sort)); if ($normalizedSort === 'oldest') { @@ -110,6 +122,7 @@ class ArtworkService public function getCategoryArtworks(Category $category, int $perPage = 24): CursorPaginator { $query = Artwork::public()->published() + ->with($this->browseRelations()) ->whereHas('categories', function ($q) use ($category) { $q->where('categories.id', $category->id); }) diff --git a/app/Services/EarlyGrowth/AdaptiveTimeWindow.php b/app/Services/EarlyGrowth/AdaptiveTimeWindow.php index 829c02fb..3b26760e 100644 --- a/app/Services/EarlyGrowth/AdaptiveTimeWindow.php +++ b/app/Services/EarlyGrowth/AdaptiveTimeWindow.php @@ -63,7 +63,7 @@ final class AdaptiveTimeWindow { $ttl = (int) config('early_growth.cache_ttl.time_window', 600); - return Cache::remember('egs.uploads_per_day', $ttl, function (): float { + return (float) Cache::remember('egs.uploads_per_day', $ttl, function (): float { $count = Artwork::query() ->where('is_public', true) ->where('is_approved', true) @@ -72,7 +72,7 @@ final class AdaptiveTimeWindow ->where('published_at', '>=', now()->subDays(7)) ->count(); - return round($count / 7, 2); + return (float) round($count / 7, 2); }); } } diff --git a/app/Services/HomepageService.php b/app/Services/HomepageService.php index 57fc923e..519f4fe4 100644 --- a/app/Services/HomepageService.php +++ b/app/Services/HomepageService.php @@ -244,6 +244,15 @@ final class HomepageService } public function getHomepageGroups(?\App\Models\User $viewer = null): array + { + if (! $viewer) { + return Cache::remember('homepage.groups', self::CACHE_TTL, fn (): array => $this->buildHomepageGroups()); + } + + return $this->buildHomepageGroups($viewer); + } + + private function buildHomepageGroups(?\App\Models\User $viewer = null): array { $featured = $this->groupDiscovery->surfaceCards($viewer, 'featured', 4); $spotlight = $featured[0] ?? null; @@ -314,6 +323,10 @@ final class HomepageService return $this->getRisingFromDb($limit); } + if ($this->collectionHasNoRisingMomentum($this->searchResultCollection($results))) { + return $this->getRisingLowSignalFromDb($limit); + } + return $items ->map(fn ($a) => $this->serializeArtwork($a)) ->values() @@ -348,6 +361,26 @@ final class HomepageService ->all(); } + private function getRisingLowSignalFromDb(int $limit): array + { + return Artwork::public() + ->published() + ->with(self::ARTWORK_SERIALIZATION_RELATIONS) + ->leftJoinSub($this->risingRecentActivitySubquery(), 'recent_rising_activity', function ($join): void { + $join->on('recent_rising_activity.artwork_id', '=', 'artworks.id'); + }) + ->select('artworks.*') + ->where('artworks.published_at', '>=', now()->subDays(30)) + ->orderByRaw('COALESCE(recent_rising_activity.recent_signal_24h, 0) DESC') + ->orderByDesc('artworks.published_at') + ->orderByDesc('artworks.id') + ->limit($limit) + ->get() + ->map(fn ($a) => $this->serializeArtwork($a)) + ->values() + ->all(); + } + /** * Trending: up to 12 artworks sorted by Ranking V2 `ranking_score`. * @@ -466,26 +499,38 @@ final class HomepageService try { $since = now()->subWeek(); - $rows = DB::table('artworks') - ->join('users as u', 'u.id', '=', 'artworks.user_id') + $weeklyUploads = Artwork::query() + ->selectRaw('user_id, COUNT(*) as weekly_uploads') + ->where('is_public', true) + ->where('is_approved', true) + ->whereNull('deleted_at') + ->whereNotNull('published_at') + ->where('published_at', '<=', now()) + ->where('published_at', '>=', $since) + ->groupBy('user_id'); + + $rows = DB::table('users as u') ->leftJoin('user_profiles as up', 'up.user_id', '=', 'u.id') - ->leftJoin('artwork_awards as aw', 'aw.artwork_id', '=', 'artworks.id') - ->leftJoin('artwork_stats as s', 's.artwork_id', '=', 'artworks.id') + ->leftJoin('user_statistics as us', 'us.user_id', '=', 'u.id') + ->leftJoinSub($weeklyUploads, 'weekly_uploads', function ($join): void { + $join->on('weekly_uploads.user_id', '=', 'u.id'); + }) ->select( 'u.id', 'u.name', 'u.username', 'up.avatar_hash', - DB::raw('COUNT(DISTINCT artworks.id) as upload_count'), - DB::raw('SUM(CASE WHEN artworks.published_at >= \'' . $since->toDateTimeString() . '\' THEN 1 ELSE 0 END) as weekly_uploads'), - DB::raw('COALESCE(SUM(s.views), 0) as total_views'), - DB::raw('COUNT(DISTINCT aw.id) as total_awards') + DB::raw('COALESCE(us.uploads_count, 0) as upload_count'), + DB::raw('COALESCE(weekly_uploads.weekly_uploads, 0) as weekly_uploads'), + DB::raw('COALESCE(us.artwork_views_received_count, 0) as total_views'), + DB::raw('COALESCE(us.awards_received_count, 0) as total_awards') ) - ->where('artworks.is_public', true) - ->where('artworks.is_approved', true) - ->whereNull('artworks.deleted_at') - ->whereNotNull('artworks.published_at') - ->groupBy('u.id', 'u.name', 'u.username', 'up.avatar_hash') + ->whereNull('u.deleted_at') + ->where('u.is_active', true) + ->where(function ($query): void { + $query->where('us.uploads_count', '>', 0) + ->orWhere('weekly_uploads.weekly_uploads', '>', 0); + }) ->orderByDesc('weekly_uploads') ->orderByDesc('total_awards') ->orderByDesc('total_views') @@ -494,18 +539,23 @@ final class HomepageService $userIds = $rows->pluck('id')->all(); - // Pick one random artwork thumbnail per creator for the card background. - $thumbsByUser = Artwork::public() + $latestArtworkIds = Artwork::public() ->published() ->whereIn('user_id', $userIds) ->whereNotNull('hash') ->whereNotNull('thumb_ext') - ->inRandomOrder() + ->selectRaw('MAX(id) as id') + ->groupBy('user_id') + ->pluck('id') + ->all(); + + $thumbsByUser = Artwork::query() + ->whereIn('id', $latestArtworkIds) ->get(['id', 'user_id', 'hash', 'thumb_ext']) - ->groupBy('user_id'); + ->keyBy('user_id'); return $rows->map(function ($u) use ($thumbsByUser) { - $artworkForBg = $thumbsByUser->get($u->id)?->first(); + $artworkForBg = $thumbsByUser->get($u->id); $bgThumb = $artworkForBg ? $artworkForBg->thumbUrl('md') : null; return [ @@ -792,6 +842,37 @@ final class HomepageService return $artworks; } + private function collectionHasNoRisingMomentum(Collection $items): bool + { + if ($items->isEmpty()) { + return true; + } + + return $items->every(function ($item): bool { + $heat = (float) ($item->heat_score ?? $item->stats?->heat_score ?? 0); + $velocity = (float) ($item->engagement_velocity ?? $item->stats?->engagement_velocity ?? 0); + + return $heat <= 0.0 && $velocity <= 0.0; + }); + } + + private function risingRecentActivitySubquery() + { + $since = now()->startOfHour()->subHours(24); + + return DB::table('artwork_metric_snapshots_hourly as rising_snapshots') + ->selectRaw('rising_snapshots.artwork_id') + ->selectRaw('( + COALESCE(MAX(rising_snapshots.views_count) - MIN(rising_snapshots.views_count), 0) + + (COALESCE(MAX(rising_snapshots.downloads_count) - MIN(rising_snapshots.downloads_count), 0) * 3) + + (COALESCE(MAX(rising_snapshots.favourites_count) - MIN(rising_snapshots.favourites_count), 0) * 4) + + (COALESCE(MAX(rising_snapshots.comments_count) - MIN(rising_snapshots.comments_count), 0) * 5) + + (COALESCE(MAX(rising_snapshots.shares_count) - MIN(rising_snapshots.shares_count), 0) * 6) + ) as recent_signal_24h') + ->where('rising_snapshots.bucket_hour', '>=', $since) + ->groupBy('rising_snapshots.artwork_id'); + } + private function serializeArtwork(Artwork $artwork, string $preferSize = 'md'): array { $thumbMd = $artwork->thumbUrl('md'); diff --git a/app/Services/Studio/CreatorStudioContentService.php b/app/Services/Studio/CreatorStudioContentService.php index d16313e9..fa595629 100644 --- a/app/Services/Studio/CreatorStudioContentService.php +++ b/app/Services/Studio/CreatorStudioContentService.php @@ -407,6 +407,7 @@ final class CreatorStudioContentService { $now = Carbon::now(); $updatedAt = Carbon::parse((string) ($item['updated_at'] ?? $item['created_at'] ?? $now->toIso8601String())); + $status = (string) ($item['status'] ?? ''); $isDraft = ($item['status'] ?? null) === 'draft'; $missing = []; $score = 0; @@ -441,6 +442,16 @@ final class CreatorStudioContentService default => 'Needs more work', }; + $readiness = $status === 'published' + ? null + : [ + 'score' => $score, + 'max' => 4, + 'label' => $label, + 'can_publish' => $score >= 3, + 'missing' => $missing, + ]; + $workflowActions = match ((string) ($item['module'] ?? '')) { 'artworks' => [ ['label' => 'Create card', 'href' => route('studio.cards.create'), 'icon' => 'fa-solid fa-id-card'], @@ -466,13 +477,7 @@ final class CreatorStudioContentService 'is_stale_draft' => $isDraft && $updatedAt->lte($now->copy()->subDays(3)), 'last_touched_days' => max(0, $updatedAt->diffInDays($now)), 'resume_label' => $isDraft ? 'Resume draft' : 'Open item', - 'readiness' => [ - 'score' => $score, - 'max' => 4, - 'label' => $label, - 'can_publish' => $score >= 3, - 'missing' => $missing, - ], + 'readiness' => $readiness, 'cross_module_actions' => $workflowActions, ]; diff --git a/app/Services/Studio/Providers/ArtworkStudioProvider.php b/app/Services/Studio/Providers/ArtworkStudioProvider.php index 8cf10a77..e1fb8717 100644 --- a/app/Services/Studio/Providers/ArtworkStudioProvider.php +++ b/app/Services/Studio/Providers/ArtworkStudioProvider.php @@ -50,9 +50,10 @@ final class ArtworkStudioProvider implements CreatorStudioProvider $draftCount = (clone $baseQuery) ->whereNull('deleted_at') ->where(function (Builder $query): void { - $query->where('is_public', false) - ->orWhere('artwork_status', 'draft'); + $query->whereNull('artwork_status') + ->orWhere('artwork_status', '!=', 'scheduled'); }) + ->where('is_public', false) ->count(); $publishedCount = (clone $baseQuery) @@ -92,16 +93,29 @@ final class ArtworkStudioProvider implements CreatorStudioProvider $query = Artwork::query() ->withTrashed() ->where('user_id', $user->id) - ->with(['stats', 'categories', 'tags']) + ->with([ + 'stats', + 'categories', + 'tags', + 'features' => function ($query): void { + $query->where('is_active', true) + ->whereNull('deleted_at') + ->where(function (Builder $builder): void { + $builder->whereNull('expires_at') + ->orWhere('expires_at', '>', now()); + }); + }, + ]) ->orderByDesc('updated_at') ->limit($limit); if ($bucket === 'drafts') { $query->whereNull('deleted_at') ->where(function (Builder $builder): void { - $builder->where('is_public', false) - ->orWhere('artwork_status', 'draft'); - }); + $builder->whereNull('artwork_status') + ->orWhere('artwork_status', '!=', 'scheduled'); + }) + ->where('is_public', false); } elseif ($bucket === 'scheduled') { $query->whereNull('deleted_at') ->where('artwork_status', 'scheduled'); @@ -199,7 +213,7 @@ final class ArtworkStudioProvider implements CreatorStudioProvider 'published_at' => $artwork->published_at?->toIso8601String(), 'scheduled_at' => $artwork->publish_at?->toIso8601String(), 'schedule_timezone' => $artwork->artwork_timezone, - 'featured' => false, + 'featured' => $artwork->features->isNotEmpty(), 'metrics' => [ 'views' => (int) ($stats?->views ?? 0), 'appreciation' => (int) ($stats?->favorites ?? 0), diff --git a/app/Services/Studio/StudioBulkActionService.php b/app/Services/Studio/StudioBulkActionService.php index 7a3ec619..9bf1f293 100644 --- a/app/Services/Studio/StudioBulkActionService.php +++ b/app/Services/Studio/StudioBulkActionService.php @@ -31,6 +31,8 @@ final class StudioBulkActionService $query = Artwork::where('user_id', $userId); if ($action === 'unarchive') { $query->onlyTrashed(); + } elseif ($action === 'delete') { + $query->withTrashed(); } $artworks = $query->whereIn('id', $artworkIds)->get(); diff --git a/config/scout.php b/config/scout.php index 23103f06..668d8dae 100644 --- a/config/scout.php +++ b/config/scout.php @@ -105,6 +105,7 @@ return [ ], 'sortableAttributes' => [ 'created_at', + 'published_at_ts', 'downloads', 'likes', 'views', diff --git a/config/uploads.php b/config/uploads.php index 31693654..f3af29a7 100644 --- a/config/uploads.php +++ b/config/uploads.php @@ -97,6 +97,10 @@ return [ 'per_user' => env('UPLOAD_RATE_INIT_USER', 10), 'per_ip' => env('UPLOAD_RATE_INIT_IP', 30), ], + 'chunk' => [ + 'per_user' => env('UPLOAD_RATE_CHUNK_USER', 180), + 'per_ip' => env('UPLOAD_RATE_CHUNK_IP', 360), + ], 'finish' => [ 'per_user' => env('UPLOAD_RATE_FINISH_USER', 6), 'per_ip' => env('UPLOAD_RATE_FINISH_IP', 12), @@ -126,6 +130,7 @@ return [ 'max_bytes' => env('UPLOAD_CHUNK_MAX_BYTES', 5242880), 'lock_seconds' => env('UPLOAD_CHUNK_LOCK_SECONDS', 10), 'lock_wait_seconds' => env('UPLOAD_CHUNK_LOCK_WAIT_SECONDS', 5), + 'request_timeout_ms' => env('UPLOAD_CHUNK_REQUEST_TIMEOUT_MS', 45000), ], 'scan' => [ diff --git a/deploy/supervisor/skinbase-queue.conf b/deploy/supervisor/skinbase-queue.conf index 5e28cc40..3210dec5 100644 --- a/deploy/supervisor/skinbase-queue.conf +++ b/deploy/supervisor/skinbase-queue.conf @@ -1,5 +1,5 @@ [program:skinbase-queue] -command=/usr/bin/php /var/www/skinbase/artisan queue:work --sleep=3 --tries=3 --timeout=90 --queue=forum-security,forum-moderation,vision,recommendations,discovery,mail,default +command=/usr/bin/php /var/www/skinbase/artisan queue:work --sleep=3 --tries=3 --timeout=90 --queue=search,forum-security,forum-moderation,vision,recommendations,discovery,mail,default process_name=%(program_name)s_%(process_num)02d numprocs=1 autostart=true diff --git a/deploy/systemd/skinbase-queue.service b/deploy/systemd/skinbase-queue.service index a5814ff7..8eb92827 100644 --- a/deploy/systemd/skinbase-queue.service +++ b/deploy/systemd/skinbase-queue.service @@ -8,7 +8,7 @@ Group=www-data Restart=always RestartSec=3 WorkingDirectory=/var/www/skinbase -ExecStart=/usr/bin/php /var/www/skinbase/artisan queue:work --sleep=3 --tries=3 --timeout=90 --queue=forum-security,forum-moderation,vision,recommendations,discovery,mail,default +ExecStart=/usr/bin/php /var/www/skinbase/artisan queue:work --sleep=3 --tries=3 --timeout=90 --queue=search,forum-security,forum-moderation,vision,recommendations,discovery,mail,default StandardOutput=syslog StandardError=syslog SyslogIdentifier=skinbase-queue diff --git a/docs/Discover/README.md b/docs/Discover/README.md new file mode 100644 index 00000000..d6100942 --- /dev/null +++ b/docs/Discover/README.md @@ -0,0 +1,93 @@ +# Discover Pages + +This folder documents how each public discovery surface is assembled today. + +It is intentionally page-oriented rather than architecture-oriented. +For the broader discovery engine, signal collection, and personalization background, also see `docs/discovery-personalization-engine.md`. + +## Pages Covered + +- `Trending` → `GET /discover/trending` +- `Rising` → `GET /discover/rising` +- `Fresh` → `GET /discover/fresh` +- `Top Rated` → `GET /discover/top-rated` +- `Most Downloaded` → `GET /discover/most-downloaded` +- `Today Downloads` → `GET /downloads/today` +- `On This Day` → `GET /discover/on-this-day` +- `For You` → `GET /discover/for-you` (auth only) + +## Shared Request Pipeline + +Most Discover pages follow this pattern: + +1. Route enters `App\Http\Controllers\Web\DiscoverController`. +2. The controller calls `App\Services\ArtworkSearchService`. +3. `ArtworkSearchService` queries the `artworks` Meilisearch index through Laravel Scout. +4. The search result usually contains only search/index fields. +5. `DiscoverController::hydrateDiscoverSearchResults()` then reloads full `Artwork` rows from MySQL with relations (`user`, `profile`, `categories`) and converts them into the view model used by Blade. +6. Some pages can still pass through additional presentation layers such as `GridFiller`, depending on the controller action. + +## Shared Visibility Rules + +All search-backed pages use the same base visibility filter in `ArtworkSearchService`: + +```text +is_public = true AND is_approved = true +``` + +That means an artwork must be: + +- public +- approved +- present in the Meilisearch index + +If the database row is correct but search is stale, the page can still miss the artwork until indexing catches up. + +## Shared Cache Behavior + +`ArtworkSearchService` uses application cache in front of Meilisearch. + +- Default TTL: 300 seconds +- `Rising`: 120 seconds +- Category/content-type sort pages use per-sort TTLs, but those are outside this folder's scope + +The page can therefore lag behind a real publish or stat change even when the underlying data is already correct. + +## Shared Supporting Jobs + +These jobs are active in the current Laravel 11 runtime scheduler (`routes/console.php`): + +- `skinbase:flush-redis-stats` every 5 minutes +- `skinbase:recalculate-trending --period=24h` every 30 minutes +- `skinbase:recalculate-trending --period=7d --skip-index` every 30 minutes +- `skinbase:reset-windowed-stats --period=24h` daily at 03:30 +- `skinbase:reset-windowed-stats --period=7d` weekly (Monday) at 03:30 +- `nova:recalculate-rankings --sync-rank-scores` every 30 minutes +- `artworks:publish-scheduled` every minute +- `analytics:aggregate-discovery-feedback` daily at 03:25 +- `RecBuildItemPairsFromFavouritesJob` every 4 hours +- `RecComputeSimilarByTagsJob` daily at 02:00 +- `RecComputeSimilarByBehaviorJob` daily at 02:15 +- `RecComputeSimilarHybridJob` daily at 02:30 + +## Important Scheduler Caveat + +The codebase still contains some discovery-related schedules inside `app/Console/Kernel.php`, but the active Laravel 11 runtime schedule comes from `routes/console.php`. + +The Rising pipeline depends on these active runtime jobs: + +- `nova:metrics-snapshot-hourly` hourly +- `nova:recalculate-heat` every 15 minutes + +If Rising stops moving while Trending changes, check `php artisan schedule:list` first and confirm both jobs are still active. + +## File Map + +- `trending.md` +- `rising.md` +- `fresh.md` +- `top-rated.md` +- `most-downloaded.md` +- `today-downloads.md` +- `on-this-day.md` +- `for-you.md` \ No newline at end of file diff --git a/docs/Discover/for-you.md b/docs/Discover/for-you.md new file mode 100644 index 00000000..cdd80335 --- /dev/null +++ b/docs/Discover/for-you.md @@ -0,0 +1,174 @@ +# For You + +## Route + +- URL: `GET /discover/for-you` +- Auth required: yes +- Controller: `App\Http\Controllers\Web\DiscoverController::forYou()` +- Entry service: `App\Services\Recommendations\RecommendationFeedResolver` + +## High-level flow + +`For You` is not a simple search sort. +It is a recommendation pipeline with engine selection, caching, layered candidate generation, reranking, and cursor pagination. + +The controller: + +1. reads `limit` and `cursor` +2. calls `RecommendationFeedResolver::getFeed()` +3. converts feed items into the artwork card view model +4. returns HTML or JSON depending on request type + +## Engine selection + +`RecommendationFeedResolver` chooses between two implementations: + +- V2: `App\Services\Recommendations\RecommendationServiceV2` +- V1: `App\Services\Recommendations\PersonalizedFeedService` + +Selection is based on: + +- `config('discovery.v2.enabled')` +- rollout percentage bucket for the current user +- explicit `algo_version` override + +## Cache model + +Both engines use `user_recommendation_cache`. + +At request time: + +1. Load cache row for `(user_id, algo_version)` +2. Check cache version and `expires_at` +3. If missing or stale, dispatch `RegenerateUserRecommendationCacheJob` +4. If the row is empty, build fallback recommendations inline for the current request + +This means the page is usually cache-backed, but it does not hard-fail if the cache is cold. + +## V2 pipeline + +V2 is the richer layered engine. + +### Candidate layers + +The candidate pool is blended from: + +- personalized layer +- social layer +- trending layer +- exploration layer +- vector layer (only when V3/vector support is enabled and configured) + +Default target ratios from `config/discovery.php`: + +- personalized: 50% +- social: 20% +- trending: 20% +- exploration: 10% + +### Main V2 score + +For each candidate row: + +```text +score + = (base_score * weight_base) + + session_boost + + social_boost + + trending_boost + + exploration_boost + + creator_boost + + vector_boost + - negative_penalty + - repetition_penalty +``` + +Where: + +- `session_boost` comes from merged session/profile signals +- `social_boost` comes from followed creators and artworks liked by followed creators +- `trending_boost` is built from `trending_score_1h`, `trending_score_24h`, `trending_score_7d` +- `exploration_boost` rewards fresh uploads, new creators, and unseen tags +- `creator_boost` uses creator follower count plus artwork momentum metrics +- `vector_boost` comes from visual similarity when vector mode is enabled +- `negative_penalty` reflects hidden artworks and disliked tags +- `repetition_penalty` suppresses creator and tag repetition inside one result page + +### Trending contribution inside V2 + +V2 combines the artwork's stored trending columns like this: + +```text +trendingBoost + = (trending_score_1h * weight_1h) + + (trending_score_24h * weight_24h) + + (trending_score_7d * weight_7d) +``` + +Then divides by 100 before merging into the final score. + +### Negative signals + +V2 reads `user_negative_signals` and applies: + +- hidden artwork exclusion +- disliked tag penalty + +### Supporting data sources + +V2 reads from: + +- `user_recommendation_cache` +- session/profile signal builders +- `artworks` +- `artwork_stats` +- `artwork_similarities` +- `artwork_embeddings` and vector service, when enabled +- `user_followers` +- `artwork_favourites` +- `user_negative_signals` + +## V1 pipeline + +V1 is simpler and category-affinity driven. + +It reads `user_interest_profiles`, then scores candidates with a weighted blend: + +```text +score = (w1 * affinity) + + (w2 * recency) + + (w3 * popularity) + + (w4 * novelty) +``` + +Cold start falls back to a blend of popular artworks and `artwork_similarities` seeds. + +## Background jobs and schedules + +### Directly relevant + +- `RegenerateUserRecommendationCacheJob` + - dispatched on demand when cache is missing or stale + +### Support jobs for candidate quality + +- `RecBuildItemPairsFromFavouritesJob` every 4 hours +- `RecComputeSimilarByTagsJob` daily at 02:00 +- `RecComputeSimilarByBehaviorJob` daily at 02:15 +- `RecComputeSimilarHybridJob` daily at 02:30 +- `analytics:aggregate-discovery-feedback` daily at 03:25 + +These jobs do not directly render the page, but they improve the offline inputs and behavioral data used by the recommender. + +## Cache behavior + +- V1 cache TTL default: 60 minutes +- V2 cache TTL default: 15 minutes + +Cursor pagination is offset-based under the hood. + +## Notes + +- `For You` is the most configuration-sensitive page in this set. +- What a given user sees can differ by rollout bucket, `algo_version`, cache state, and whether V2/V3 features are enabled. +- If you are debugging a single user's page, inspect `RecommendationFeedResolver::inspectDecision()` first. \ No newline at end of file diff --git a/docs/Discover/fresh.md b/docs/Discover/fresh.md new file mode 100644 index 00000000..7daed0dc --- /dev/null +++ b/docs/Discover/fresh.md @@ -0,0 +1,88 @@ +# Fresh + +## Route + +- URL: `GET /discover/fresh` +- Controller: `App\Http\Controllers\Web\DiscoverController::fresh()` +- Service: `App\Services\ArtworkSearchService::discoverFresh()` + +## What the page reads + +Fresh is Meilisearch-backed, then hydrated from MySQL. + +The page does not directly query `artworks` for ranking order. +It relies on the search index being up to date. + +If the search-backed result comes back empty, the controller falls back to a direct MySQL query ordered by `published_at DESC, id DESC` so the page does not render blank while search is catching up. + +## Search query + +`discoverFresh()` uses: + +- filter: `is_public = true AND is_approved = true` +- sort: + - `published_at_ts:desc` + +`published_at_ts` is a numeric timestamp field stored in the search document specifically so same-day uploads can be ordered correctly by hour and minute. + +## Why the dedicated timestamp field exists + +Historically, the index sorted by a date-only `created_at` string, which meant all uploads on the same calendar day could collapse to the same sort value. + +The current implementation uses `published_at_ts` to preserve intra-day ordering and avoid newer uploads being buried behind older uploads from the same date. + +## Page behavior + +Fresh now uses the raw newest-first search result without any curated blending or grid filler injection. + +That means: + +- page 1 is not mixed with spotlight content +- page 1 is not padded with older trending artworks +- deeper pages follow the same ordering model as page 1 + +If older artworks appear near the top, the likely causes are stale search documents or stale cache, not intentional feed mixing. + +If the page renders completely empty even though recent public artworks exist, the DB fallback should populate it. A blank page after that points to a real data visibility problem, not just search freshness. + +## Data sources + +Ranking eligibility depends on: + +- `is_public` +- `is_approved` +- presence in Meilisearch +- `published_at_ts` in the indexed document + +Hydration reads full rows from MySQL after the search query returns IDs. + +When the fallback path is used, the page is served directly from MySQL and does not require Meilisearch for that request. + +## Relevant jobs and schedules + +Fresh does not have a dedicated score calculation job. +It depends on publication and indexing freshness. + +Relevant active schedules: + +- `artworks:publish-scheduled` every minute +- `skinbase:flush-redis-stats` every 5 minutes (not for ordering, but for displayed stats freshness) + +Index freshness depends on: + +- normal Scout indexing from artwork updates +- scheduled-publication indexing after an artwork transitions from scheduled to published +- manual/full imports after search-document schema changes + +## Cache behavior + +- Cache key: `discover.fresh.{page}` +- TTL: 300 seconds + +## Notes + +- Fresh is the page most sensitive to stale indexing because it is supposed to surface the latest publish action immediately. +- If an artwork is public in MySQL but absent from Fresh, the usual causes are: + - it has not been indexed yet + - app cache has not expired yet + - the artwork is not actually public and approved at the same time \ No newline at end of file diff --git a/docs/Discover/most-downloaded.md b/docs/Discover/most-downloaded.md new file mode 100644 index 00000000..b4aaf2bc --- /dev/null +++ b/docs/Discover/most-downloaded.md @@ -0,0 +1,59 @@ +# Most Downloaded + +## Route + +- URL: `GET /discover/most-downloaded` +- Controller: `App\Http\Controllers\Web\DiscoverController::mostDownloaded()` +- Service: `App\Services\ArtworkSearchService::discoverMostDownloaded()` + +## What the page reads + +This page is Meilisearch-ranked and then hydrated from MySQL. + +## Search query + +`discoverMostDownloaded()` uses: + +- filter: `is_public = true AND is_approved = true` +- sort: + - `downloads:desc` + - `views:desc` + +In the indexed document: + +- `downloads` is sourced from `artwork_stats.downloads` +- `views` is sourced from `artwork_stats.views` + +This is therefore an all-time leaderboard, not a rolling-window leaderboard. + +## Where the download counts come from + +Downloads are recorded in two places: + +1. `artwork_downloads` + - full event log of each tracked download +2. `artwork_stats.downloads` + - all-time aggregate counter used by this page + +The page sorts on the aggregate counter, not by counting the event log live. + +## Background jobs and schedules + +No dedicated page-specific cron exists. + +Relevant active maintenance: + +- `skinbase:flush-redis-stats` every 5 minutes + - pushes deferred Redis counters into MySQL + +Window reset commands exist too, but they maintain `downloads_24h` and `downloads_7d` rather than the all-time `downloads` column used by this page. + +## Cache behavior + +- Cache key: `discover.most-downloaded.{page}` +- TTL: 300 seconds + +## Notes + +- This page is separate from `Today Downloads`. +- If you need "what is hot today by download activity," use the dedicated `/downloads/today` page instead. \ No newline at end of file diff --git a/docs/Discover/on-this-day.md b/docs/Discover/on-this-day.md new file mode 100644 index 00000000..cbb022c5 --- /dev/null +++ b/docs/Discover/on-this-day.md @@ -0,0 +1,52 @@ +# On This Day + +## Route + +- URL: `GET /discover/on-this-day` +- Controller: `App\Http\Controllers\Web\DiscoverController::onThisDay()` + +## What the page reads + +This page is a direct MySQL query over `artworks`. +It does not use Meilisearch. + +## Query logic + +The controller selects artworks that are: + +- public +- published +- approved +- published on the same month/day as today +- from a previous year only + +It then sorts them by `published_at DESC` and paginates 24 per page. + +Equivalent logic: + +```text +MONTH(published_at) = today.month +AND DAY(published_at) = today.day +AND YEAR(published_at) < today.year +``` + +## Data sources + +- primary source: `artworks` +- supporting eager loads: `user`, `user.profile`, `categories` + +## Cache behavior + +- no explicit controller cache + +## Background jobs and schedules + +No dedicated cron drives this page. + +It only depends on correct `published_at` values and the usual public/published scopes. + +## Notes + +- This is a calendar-history page, not a popularity or momentum page. +- The current implementation simply orders by newest qualifying `published_at`, not by views, downloads, or favourites. +- There are also legacy `TodayInHistoryController` variants elsewhere in the codebase, but `/discover/on-this-day` currently uses `DiscoverController::onThisDay()`. \ No newline at end of file diff --git a/docs/Discover/rising.md b/docs/Discover/rising.md new file mode 100644 index 00000000..0192dc09 --- /dev/null +++ b/docs/Discover/rising.md @@ -0,0 +1,115 @@ +# Rising + +## Route + +- URL: `GET /discover/rising` +- Controller: `App\Http\Controllers\Web\DiscoverController::rising()` +- Service: `App\Services\ArtworkSearchService::discoverRising()` +- RSS feed: `GET /rss/discover/rising` via `App\Http\Controllers\RSS\DiscoverFeedController::rising()` + +## What the page reads + +Like most Discover surfaces, this page ranks via Meilisearch and then hydrates the result IDs from MySQL for presentation. + +If the search-backed query throws or returns no items, the controller falls back to a direct MySQL query against `artworks` + `artwork_stats`. + +If the page receives a non-empty result set but every item has zero `heat_score` and zero `engagement_velocity`, it switches to a low-signal fallback policy instead of pretending that the zero-heat order is meaningful. + +The RSS Rising feed now follows the same low-signal policy and the same adaptive lookback window, so it does not drift to a stale zero-heat ordering when recent engagement is sparse. + +Primary ranking fields: + +- `heat_score` +- `engagement_velocity` +- `published_at_ts` as the final recency tie-breaker + +## Search query + +`discoverRising()` uses: + +- filter: `is_public = true AND is_approved = true AND created_at >= cutoff` +- sort: + - `heat_score:desc` + - `engagement_velocity:desc` + - `published_at_ts:desc` + +The cutoff comes from the same adaptive time-window service used by Trending. + +## Rising formula + +`heat_score` is produced by `App\Console\Commands\RecalculateHeatCommand`. + +Current formula: + +```text +raw_heat + = ((views_delta * 1) + + (downloads_delta * 3) + + (favourites_delta * 6) + + (comments_delta * 8) + + (shares_delta * 12)) / window_hours + +age_factor + = 1 / (1 + hours_since_upload / 24) + +heat_score + = raw_heat * age_factor +``` + +The heat command smooths deltas over a trailing lookback window, rather than relying only on the last single hour. + +That matters on low-traffic periods, because a pure 1-hour delta often collapses to zero for almost every artwork. + +An artwork still needs at least two snapshots inside that window for the smoothed heat delta to count. A single snapshot without an earlier baseline does not count as momentum. + +The `views_1h`, `downloads_1h`, `favourites_1h`, `comments_1h`, and `shares_1h` columns are still stored from the previous-hour comparison for diagnostics and dashboards. + +## Data sources + +The page depends on: + +- `artwork_metric_snapshots_hourly` +- `artwork_stats.heat_score` +- `artwork_stats.engagement_velocity` +- artwork publish timestamps + +In zero-signal periods, the fallback policy also uses a 24-hour snapshot delta rollup from `artwork_metric_snapshots_hourly` and then falls back to `published_at DESC`. + +`engagement_velocity` is not part of the heat command. It comes from the ranking engine and acts as a secondary momentum signal. + +## Intended background jobs + +The intended pipeline is: + +1. `nova:metrics-snapshot-hourly` + - captures hourly totals into `artwork_metric_snapshots_hourly` +2. `nova:recalculate-heat` + - computes `heat_score` from snapshot deltas +3. Meilisearch picks up the updated score after indexing + +## Runtime schedule + +Rising depends on two active Laravel 11 runtime jobs in `routes/console.php`: + +- `nova:metrics-snapshot-hourly` +- `nova:recalculate-heat` + +If either one disappears from `php artisan schedule:list`, Rising will quickly drift toward stale or low-signal ordering. + +## Active jobs that still affect Rising + +- `nova:recalculate-rankings --sync-rank-scores` every 30 minutes updates `engagement_velocity` +- `skinbase:flush-redis-stats` every 5 minutes keeps all-time stats fresher + +## Cache behavior + +- Cache key: `discover.rising.{windowDays}d.{page}` +- TTL: 120 seconds + +If Meilisearch sort settings are missing or the search result is empty, the controller falls back to the DB query instead of returning an empty page or a 500. + +## Notes + +- If Rising looks frozen while Trending moves, the first place to check is whether `nova:metrics-snapshot-hourly` and `nova:recalculate-heat` are actually being executed in production. +- The page no longer uses `GridFiller`, so it should not pull in unrelated older artworks when the real result set is thin. +- If all heat and velocity values are zero, Rising intentionally behaves like a low-signal discovery feed: recent activity in the last 24 hours first, then newest published artworks. \ No newline at end of file diff --git a/docs/Discover/today-downloads.md b/docs/Discover/today-downloads.md new file mode 100644 index 00000000..55587e4b --- /dev/null +++ b/docs/Discover/today-downloads.md @@ -0,0 +1,63 @@ +# Today Downloads + +## Route + +- URL: `GET /downloads/today` +- Controller: `App\Http\Controllers\User\TodayDownloadsController::index()` + +## Important scope note + +This page is not inside `/discover/*`, but it is included here because it is discovery-adjacent and was requested together with the Discover surfaces. + +## What the page reads + +This page does not use Meilisearch. +It is a direct MySQL query over the download event log. + +## Query logic + +The controller: + +1. Takes today's date +2. Reads `artwork_downloads` +3. Filters rows to `whereDate(created_at, today)` +4. Groups by `artwork_id` +5. Orders by `COUNT(*) DESC` +6. Eager-loads each artwork and related user/category data + +Effectively: + +```text +SELECT artwork_id, COUNT(*) AS num_downloads +FROM artwork_downloads +WHERE DATE(created_at) = today +GROUP BY artwork_id +ORDER BY num_downloads DESC +``` + +Only artworks that are currently public and published are allowed through `whereHas('artwork', ...)`. + +## Data sources + +- primary source: `artwork_downloads` +- supporting source: `artworks`, `users`, `user_profiles`, `categories` + +Unlike `Most Downloaded`, this page does not trust the aggregate `artwork_stats.downloads` counter for ranking. +It re-counts today's actual events. + +## Cache behavior + +- no dedicated application cache in the controller + +The page is as fresh as the underlying event log. + +## Background jobs and schedules + +No page-specific cron is needed because it reads the raw event log directly. + +The only prerequisite is that downloads are being recorded correctly by the download endpoint. + +## Notes + +- This page is often the more operationally trustworthy answer to "what is being downloaded right now?" +- Because it counts raw rows, it is less sensitive to delayed aggregate-counter flushes than `Most Downloaded`. \ No newline at end of file diff --git a/docs/Discover/top-rated.md b/docs/Discover/top-rated.md new file mode 100644 index 00000000..c175777b --- /dev/null +++ b/docs/Discover/top-rated.md @@ -0,0 +1,58 @@ +# Top Rated + +## Route + +- URL: `GET /discover/top-rated` +- Controller: `App\Http\Controllers\Web\DiscoverController::topRated()` +- Service: `App\Services\ArtworkSearchService::discoverTopRated()` + +## What the page reads + +This is a Meilisearch-ranked page with MySQL hydration after the fact. + +## Search query + +`discoverTopRated()` uses: + +- filter: `is_public = true AND is_approved = true` +- sort: + - `likes:desc` + - `views:desc` + +In the indexed document: + +- `likes` is sourced from `artwork_stats.favorites` +- `views` is sourced from `artwork_stats.views` + +So "Top Rated" really means highest favourite count, with views as a tie-breaker. + +## Data sources + +The page depends on: + +- `artwork_stats.favorites` +- `artwork_stats.views` +- Scout/Meilisearch document freshness + +It does not run a custom score formula beyond the sort order above. + +## Background jobs and schedules + +There is no dedicated top-rated cron. +The page depends on the freshness of the underlying stats. + +Relevant active maintenance: + +- `skinbase:flush-redis-stats` every 5 minutes for deferred stat deltas + +Favorites themselves are typically updated in the normal request path rather than by a dedicated scheduled command. + +## Cache behavior + +- Cache key: `discover.top-rated.{page}` +- TTL: 300 seconds + +## Notes + +- Awards are not part of this page's ranking. +- If the business meaning should be "best overall" rather than "most favourited," this page would need a different sort field. \ No newline at end of file diff --git a/docs/Discover/trending.md b/docs/Discover/trending.md new file mode 100644 index 00000000..15e1e811 --- /dev/null +++ b/docs/Discover/trending.md @@ -0,0 +1,125 @@ +# Trending + +## Route + +- URL: `GET /discover/trending` +- Controller: `App\Http\Controllers\Web\DiscoverController::trending()` +- Service: `App\Services\ArtworkSearchService::discoverTrending()` + +## What the page actually reads + +Primary ranking comes from Meilisearch, not directly from MySQL. + +The controller flow is: + +1. Query Meilisearch through `ArtworkSearchService` +2. If the search-backed query throws or returns no items, fall back to a direct MySQL query against `artworks` + `artwork_stats` +3. Hydrate the returned IDs back into full `Artwork` Eloquent models when needed +4. Render `resources/views/web/discover/index.blade.php` + +So the page is effectively: + +- ranking source: Meilisearch +- fallback ranking source: MySQL + `artwork_stats` +- display hydration source: MySQL + +## Search query + +`discoverTrending()` uses: + +- filter: `is_public = true AND is_approved = true AND created_at >= cutoff` +- sort: + - `ranking_score:desc` + - `engagement_velocity:desc` + - `views:desc` + +`created_at` here is the search index field, not necessarily the raw DB column semantics. + +## Time window + +The cutoff is not hardcoded to 30 days in all cases. + +`App\Services\EarlyGrowth\AdaptiveTimeWindow` chooses the effective look-back window: + +- 7 days when uploads/day is healthy +- 30 days in moderate activity +- 90 days in low activity + +That widening only changes which artworks are eligible for the query. +It does not rewrite timestamps. + +## Ranking formula + +The page does not sort by `trending_score_7d` anymore. +It sorts by `ranking_score`, which is produced by `App\Services\Ranking\ArtworkRankingService`. + +Current V2 ranking formula: + +```text +base_score + = (views_all * 0.2) + + (downloads_all * 1.5) + + (favourites_all * 2.5) + + (comments_count * 3.0) + + (shares_count * 4.0) + +authority_multiplier + = 1 + ((log10(1 + author_followers_count) + + (author_favourites_received / 1000)) * 0.05) + +decay_factor + = 1 / (1 + age_hours / 48) + +velocity_boost + = ((views_24h * 1.0) + + (favourites_24h * 3.0) + + (comments_24h * 4.0) + + (shares_24h * 5.0)) * 0.5 + +ranking_score + = (base_score * authority_multiplier * decay_factor) + velocity_boost +``` + +`engagement_velocity` is the raw `velocity_boost` term stored separately in `artwork_stats`. + +## Where the numbers come from + +The page depends mainly on: + +- `artwork_stats.ranking_score` +- `artwork_stats.engagement_velocity` +- `artwork_stats.views` +- `artwork_stats.downloads` +- `artwork_stats.favorites` +- `artwork_stats.comments_count` +- `artwork_stats.shares_count` +- author-level follower and favourites-received signals + +Those values are copied into the search document by `Artwork::toSearchableArray()`. + +## Active jobs and schedules + +Relevant active schedules: + +- `nova:recalculate-rankings --sync-rank-scores` every 30 minutes +- `skinbase:flush-redis-stats` every 5 minutes + +Indirectly relevant: + +- `artworks:publish-scheduled` every minute +- Meilisearch indexing jobs dispatched after score recalculation + +## Cache behavior + +- Cache key: `discover.trending.{windowDays}d.{page}` +- TTL: 300 seconds + +That means a ranking update can be correct in Meilisearch but still hidden by app cache for up to 5 minutes. + +If Meilisearch sort settings are missing or the index returns no results, the controller falls back to a DB query instead of rendering a 500 or an empty page. + +## Notes + +- The view text still says "most-viewed artworks on Skinbase over the past 7 days," but the current implementation is really a `ranking_score` page with a dynamic eligibility window. +- The page is only as fresh as both the index and the app cache. +- The page no longer uses `GridFiller`, so it should not inject older out-of-window artworks just to pad page 1. \ No newline at end of file diff --git a/public/gfx/skinbase_logo.webp b/public/gfx/skinbase_logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..6ab17a6f0bd6ed81879d67988b24cc7737966217 GIT binary patch literal 84220 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�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 IPyhe`03pe7g8%>k literal 0 HcmV?d00001 diff --git a/public/gfx/skinbase_logo_128.webp b/public/gfx/skinbase_logo_128.webp new file mode 100644 index 0000000000000000000000000000000000000000..ce254c1d690c42bb9bee77e70b88839a227cb84b GIT binary patch literal 5338 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 literal 0 HcmV?d00001 diff --git a/public/gfx/skinbase_logo_512.webp b/public/gfx/skinbase_logo_512.webp new file mode 100644 index 0000000000000000000000000000000000000000..ce515e1aba8fd608c8d34b32c2fe137ce7df66bd GIT binary patch literal 52124 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 literal 0 HcmV?d00001 diff --git a/public/gfx/skinbase_logo_64.webp b/public/gfx/skinbase_logo_64.webp new file mode 100644 index 0000000000000000000000000000000000000000..7900b15955b0133264a40be426a881996cbfeda5 GIT binary patch literal 1904 zcmV-$2aottNk&F!2LJ$9MM6+kP&il$0000G0000#002J#06|PpNcRE&00E#yZJQxU ziniUewr$(CZQHhO+cvIj+qP}*`(8vu_Ga|iy^e?p2>;CkgQIPg?MK`3AC9(Hop5w* ztXqz%>HpT_N5^zs(Hy_{k7tdBxnDUngwS}5f~S=>`@UCw;5{MqANZ{T?pD;yXMU=B zdRXvIRX}GdXr8A{{&x90rop>Ix#yS!lc4C_p{EwArnC=lj3Pn!%Pr?|~4-Q{!s)bMeTHUaWlT^up z-+lat$wNbtZC^_ztL_`hT*^2BDsi+*DD+@aj1lwK4~0^%iP9*E&rY5wl>2l7QtB7` zPZdh-So)}kJ~fBQ{X(&`T=26hprqAxLaCG7V@#|H#utTBk2hPvyn8ekULmR6)^ei+Dy6^j~qE0?smF!V1Rt*Tl6gsR#)aL=Y6wTWWh$ z5D`$~O6Y1eEny;^lQ@Bo*L0D90;*)vDC+#RG=33*SPdjX@q}9Dv>H`F5X9$e*;El6 zk)lTE5AAgvq<{%DG7(3rlh<*ATLNsLdlq+%B8i08zLbCNEBz*uV(|TfQlp0@4D(~`CLGw?1)Ye zHSeiFgorIrDr=ueRAvYp#02Q{QC1dgO!!C!S}n=_VNJ zZfmR~G&Huh_YO}m#mw_Az2+7>ZL!rpC!c@ybr+tz*SZTYwCG&Zk2BQO-qINAZ)j?6 z?dTmGdxD9ln0&%91AQHxZC#zMjp2VT09H^qAoK$O0MHZwodGI906+jfZ7!2Wq#~iA zEEQY;fDMUk0L+m=eu4M_$|*GZui^*cU&tjj`!>$e&aVfk56BzCe9HOA`Uw9~z8(7= z>;dJ^?2G;f1Z(-u+;j+%l(wkU6$CV2ARkt63}_;_6OPuoWBHTQJ!DBdxWC5Ue+;`GSm$9f5{#=CEMiR{7C2FNu*LhC`N*Sx>^s&>NM@fM^xP1zJU?iH`@KM z0RH|IqEBbY@ia>eTO;|4ByFU+?UV|%6hl|tNO z@Y{FYCbY6xfO|JQf+r~V*0NDHB>+r5hM;)hMq zxi?nvh<@y9;y655)R%}Utfy5*i(p=-GZq94SWv*W%)a7z8-hId10En)vFE*~QiGz9 zIlj})e-+)lm|}zOt0#V1H`a3V{2x0I$gzbyOwCQxz(*V@{`I~_qNoSENNb}`#v@vwvejng_ zeU17cIuB)Me>cLL(^0d1Kapny#h&y}#GAV!8UMwoOX{4+cDIK%Ht|l*0OTI!C++sp zRJ4_et$k>3PKK9gLH~g!`U~_%81nwV31_JwQ5Wx*OIN^qCMn=g?)3jtt0LT*j|Pwa zA%WpkMnwh35ooq#Qs!G|MQ_3bncEJ5EabPq%JuGkV7%)yrgKG3{&OuBsJaY~Uc5?Y zz)n~tTGu0FddvE%D00j7F}#L6vf>|z{9rskVqy4o{*KC@tA%v zZ^Z62JFnLUf^;`x0&PmIgVOcwP>e!5nV)PW315B)w^=tMmmx?DTSb8&rE$o^#q%e2 z?TF_tawKSl{ZS~4#sF?!PE-#WwDoZk0F9k7L{pGB_+5x6Ndi_7;xea5#eN#)j zu{&gVrK1WJMoaA7$D{Ve=9`Wv^FPYus0bJjnx4Y;8)PyEvI!}R$ujdjMXRQnX(s|p zHkzOy!hxS=)fiG1dYB0AZNyeor(neI&E03tLE?YC4o@aG?$GtsR<(EAx=`?z6PL$d z$#IVq5w2?*NZ)j3Qh{#R88Za)37nK@H}S5=gRf;a;H*+v9&X>aFl0`)>;P zIXDmi08mpwNlpL&060QPO#|%!000005CBL3000310su$=000310ysiRO#~01fbkZQGe= z+qTVX+FrPA+qP}nwr$(CZQHhoY}@vI_sBYswD&Y6eJ~gi6X5?HPv~a{+y&0^yaVoJ zmpkY#;k@rY&`x|>SAF||b}ugQnS<B^syv-a(V@^#K*f{M`X_`eS^0 z?qnY}JB$a)(Vl*+Gn>8Ly=cPdz&z~qXSvmx+`#lw&!hr>Iv~zpw+nOnJ6+SXkv3o1UESfgxsPeZy$^rvjiJ)e#D}c?UiX6re*cKan2kO2 z&*!Y{oNqB34aYbZ6(uwAMA8*K=xS87NlJwZQsQGDap7Nmdp$2e2S_ytV!nfK-oufu z`W&l$4mv=ZISq}b8GPY(Jpb)qT+Oo}MIb5&8sUK-;sI`Atv5OHq=QBWbU+H>Z_j%I zJ1_K?kFDi|529%*P0%O?V)L=5eU$C{+>e)8>r`&- z)>7~zmmW!U$?0v2EZko^pQYgmb-t>wmHrw!6Ydy=U&38u7 zNV09(Xn6R)R&#m(Wu-}DwnP*6vfgWI(9F>gMI&n9de(d5ss+i zOdB2A!F)p_roX=1FRl6PGbtVH9kJ)9p7}BBK9hzfMKcpL_{_^bZ1ty-LQ~PCG{iTa z^HM86(iP5j>T}v>Du~%%-}Fb;ebPH!E-nquTj%`|Lkcx^%BcHpk zRp0vI=9@GPq>u`6GiyHYYoXCZL#2U`HoV%JpL=#1F>UOf(h%!9#Rq88RA|zfRAaW` z2kW`?^Pn-?*t2P%0>}Q%TCRS36==*?OrshEmw)d{E_DZ@(WEnJqNEzBwBZ_WS;rZl zO2vGd6dH|b6DFs<@{`uE{T2-kDIMB$P^6KXO~Vu4{jn9i*)%E;q!kE_AZWbuHvd@M zt!bp9(a?}eQK6#JG$*>xU6ywA`)KSl)uGa%&_s!8id%i}qYJwyDa{-m8spG3q!lV= zE028evW|TtB}k<~Lu${c*@`AEc)~X<>ZXL+cT}Y5M1wR#o3wJ@$1myz8q=UdiD_y! zL`j9hIZp7d1)XJ!hNeOzG-+i9jcKC^&6SpPbq!4#6*Y}CooQ03Ju6)6-Tzq5X`n-C zXsBqQiP_4eB5fS=xL;q+*^$~a_HLk=*)vL-<|3b1%qb2Nvk%%rR#&Mk<;R>Cj3uNrjkgOppAR#caf^M|9`JJKNL)5d%{ z(CC1Me=g_O$C)&y4Y6m&q?wyG5ly4{!*afUloK^(E1IU!q#e+Rh8i@Q2EMkOe&M|7 z94W+}4I0t`4MaQm!g74;v`7g}L?c8AA!gHwCdRKV=p&a%HK^FLQK5p$q?t7Mk_CVG zL4`nrDB?xS^2B?mqKQHUA!6E?Zv^kL@OK<`8EM8e8bp&y2|Cb0L!H_>Z#^zC+t6t2^SO_; z_(%p4t#SPH)$jNa)6{%w?~mL9qD#sULxy31Tp`^cYeHwpQ(TS`|f75yJ^Sev`M?w zVUR<%?btcR8Q=BnpPckGCp>Zp{QJ*e`SR~ibGhsP{R@AH%{aCe+_5zpJC53pwrV`? z3BUijAN@7QKH3qtcK`avpZwWz&UE3^fAa@9EVj0Wt*v26blgqTKVKU>E<$x)AX?0^30-$y)r*xA`0k|7pd+R?b%akD#acK_bZzsCH1 zceA^Fltcc#IpmPdZpN*^Xtq|`YCCq^j5aN`&-%YN09H^qAl?Q501zDjodGIf0AK(< zZ7h>UBqE`qE0!$SfDMUk0L{H|egXIavC-524u2cIODq4~e;slGl?w)Xjr|q96U^uB zcdZAZFYx}LAL2Tby##!;zjFQ%KfHg_^ta42`YYhi$MAcOfbHeCq*7-dLSK96#^6iW zj1>UT${O`tJKuBL)gJ0rvS$KK2hCb>u^*_X!;eLleP_E{w}lA&@NBj0M9Tl5{;R4J z&Gw>rh7)z#$JxQAaBKK7tgC&?qvjt z%LG|9D(+1O^!dDWw!v7)ati$6n*ad*{#DQhoQUPYiozrqfJZ6@i!XF7cNHpQ{jmB^ z-2csx+Ur|;Nw`R*co1!)-eeX(+c+LbZV&0B3ERY&^1ot zbo(yePqgEghkynlo8+YYEPh}6Q^FFG)*yG$S8=5e>|6%mp!DZ%pqs%~jiB^(O?9iN zQv>A`#AFAB=md2LEaa^Y<-Fb|ez@-~X{P-SNCA{S-YkD7NM_lHpY~U6${hkCxDa#Q5+qR*XNumQ*H?<&Liyk>&YGMBLU#(yR7b~wx@ zVE}ncdq%*B8b78W-_ZZhy2W!iONMxgLNzO6)YBj$Jgfgs3VTksgP zbT`pAR%DQCKaiF5H=XH*5u->++9A3CZ|!`SB#cB1jGR!zBwVA1pN!PhBcLHvC4qOy zArPX1JA>;|C(!3|6NW&p0dbiuPL$Q936Xi#uPvJQLNsC)Eb!f>w+L|R53H5nf^3;?CA5fi~AY$ zs9W_1C&?>DfiM3=8MrG$bGmUY&dlwp#&k+BK}11fN|YwZpA-|Rh4g*TjYmVUgDu+K z7%wUIh2-c#2-5EWKYAlRDph zuekCqS~y(rK}O`l`YMk?R!7cF8$TSd1Q{+eQ9l4~* zpnbqSMq5Bphkl>=m1=j6Qn2eVh1~?&Pd9Jjy3U~m-DS8P3FZIx<49hkAi8*fzbTo^ z#KT|u9#tsH;Txy9MjsO+8=}}XzAEGpE@ZkKw(Q(n{a#bk%QFdkeIVovDkyo-8l<}1 zQh4qm$P6>AQOyM>x2jC&ihx&@fTKM{KEFr6S_ZxAr|OnozMi;C~q2;oUzW?<`F_}@$%jwsJ8;i_XsviNBUFsi;{`2wLF@w zf<{#zY0AZ`-@r>V>}Yj(GngmZ#m-aj?`or0Kl`il^M0=k{V`*8IgN3G?~MSC>>TpM zu30`Nc^T0wx>|LT% z@-kaQhPo33f|mM6V}!tzJBzK@O3f{wTk+Qeg&9b=%Y;yn(T`W}CZ6{u0EgP`2{K`c z$oStao{{8XG<)v;rnNKUB0iSOC?zCyx=Xt^_d zBvi`uBN{JMU##KwNf_Oq;D^7e6Lv_agKy#LdMLM~mHU?Kh-`mZ@3_^wSmC?!Tr0Qk z{fh*ipvbksUC6#4CWExRyLRX0=-)lpE{hM5W)>_h53%|>%J7{efnwFoPnNO+h<2lV z86zsGUFASUl(Ao2m;DM!1rx_i`g^r|eo^&bU~&L!%~K31?27{W3f67(#!MO$OmBM} zgpO4wl=vg${%9pSck)?aX0>AnjyxPhjkz&-T|yulC8ldUOf69_o)s<}7&i%jsM`%b z$!w{e84Vj*Zq?fs8A8usSGi!YNocm&rZ>5FHVm-4t_{N+_EN=BQ}3miLTyFvsPs!J zn?EnA(000mG zNB{r;0RRF3NB{r;0RRFxLP<>oC;$Ke000aC0006%@Bjb+0000uLP<>oLjV8(000h9 QVr5qW5C8@MPyhe`01%z~+yDRo literal 0 HcmV?d00001 diff --git a/resources/js/Pages/Studio/StudioArtworkEdit.jsx b/resources/js/Pages/Studio/StudioArtworkEdit.jsx index edd6a082..5b80c295 100644 --- a/resources/js/Pages/Studio/StudioArtworkEdit.jsx +++ b/resources/js/Pages/Studio/StudioArtworkEdit.jsx @@ -7,6 +7,7 @@ import Button from '../../components/ui/Button' import Modal from '../../components/ui/Modal' import FormField from '../../components/ui/FormField' import Toggle from '../../components/ui/Toggle' +import NovaSelect from '../../components/ui/NovaSelect' import TagPicker from '../../components/tags/TagPicker' import SchedulePublishPicker from '../../components/upload/SchedulePublishPicker' @@ -286,6 +287,29 @@ export default function StudioArtworkEdit() { selectedRoot?.name || 'No root category', subCategoryId ? subCategories.find((item) => item.id === subCategoryId)?.name : null, ].filter(Boolean) + const publishingIdentityOptions = useMemo(() => { + const personalOption = { + value: '', + label: 'Personal profile', + icon: