comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); Artisan::command('uploads:cleanup {--limit=100 : Maximum drafts to clean in one run}', function (): void { $limit = (int) $this->option('limit'); $deleted = app(CleanupService::class)->cleanupStaleDrafts($limit); $this->info("Uploads cleanup deleted {$deleted} draft(s)."); })->purpose('Delete stale draft uploads and temporary files'); // ── Scheduled tasks ──────────────────────────────────────────────────────────── // Recalculate trending scores every 30 minutes, staggered away from other hot paths. Schedule::command('skinbase:recalculate-trending --period=24h') ->cron('6,36 * * * *') ->name('trending-24h') ->withoutOverlapping(); Schedule::command('skinbase:recalculate-trending --period=7d --skip-index') ->cron('19,49 * * * *') ->name('trending-7d') ->runInBackground() ->withoutOverlapping(); // Reset windowed view/download counters so trending uses recent-activity data. // Downloads are recomputed from the artwork_downloads log (accurate). // Views are zeroed (no per-view event log) and re-accumulate from midnight. Schedule::command('skinbase:reset-windowed-stats --period=24h') ->dailyAt('03:30') ->name('reset-windowed-stats-24h') ->withoutOverlapping(); Schedule::command('skinbase:reset-windowed-stats --period=7d') ->weeklyOn(1, '03:50') // Monday 03:50 ->name('reset-windowed-stats-7d') ->withoutOverlapping(); // Daily maintenance Schedule::command('uploads:cleanup')->dailyAt('03:00'); Schedule::command('analytics:aggregate-similar-artworks')->dailyAt('03:10'); Schedule::command('analytics:aggregate-feed')->dailyAt('03:20'); Schedule::command('analytics:aggregate-discovery-feedback')->dailyAt('03:25'); Schedule::command('analytics:aggregate-tag-interactions')->dailyAt('03:35'); // Drain Redis artwork-stat delta queue so MySQL counters stay fresh. // Offset this off the :00/:10 boundaries so it does not pile onto publish jobs. Schedule::command('skinbase:flush-redis-stats') ->cron('1,11,21,31,41,51 * * * *') ->name('flush-redis-stats') ->withoutOverlapping(); // Prune artwork_view_events rows older than 90 days. // Runs Sunday at 04:00, after all other weekly maintenance. Schedule::command('skinbase:prune-view-events --days=90') ->weekly() ->sundays() ->at('04:00') ->name('prune-view-events') ->withoutOverlapping(); // ── Similar Artworks (Hybrid Recommender) ────────────────────────────────────── // Build co-occurrence pairs from favourites every 4 hours. Schedule::job(new \App\Jobs\RecBuildItemPairsFromFavouritesJob()) ->everyFourHours() ->name('rec-build-item-pairs') ->withoutOverlapping(); // Nightly: recompute tag, behavior, and hybrid similarity lists. Schedule::job(new \App\Jobs\RecComputeSimilarByTagsJob()) ->dailyAt('02:00') ->name('rec-compute-tags') ->withoutOverlapping(); Schedule::job(new \App\Jobs\RecComputeSimilarByBehaviorJob()) ->dailyAt('02:15') ->name('rec-compute-behavior') ->withoutOverlapping(); Schedule::job(new \App\Jobs\RecComputeSimilarHybridJob()) ->dailyAt('02:30') ->name('rec-compute-hybrid') ->withoutOverlapping(); // ── Feed 2.0: Scheduled Posts ───────────────────────────────────────────────── // Publish queued posts every minute. Schedule::command('posts:publish-scheduled') ->everyMinute() ->name('publish-scheduled-posts') ->withoutOverlapping(); // ── Scheduled content publishing ────────────────────────────────────────────── // These must live in routes/console.php for Laravel 11's active scheduler. Schedule::command('artworks:publish-scheduled') ->everyMinute() ->name('publish-scheduled-artworks') ->withoutOverlapping(2) ->runInBackground(); Schedule::command('news:publish-scheduled') ->everyMinute() ->name('publish-scheduled-news') ->withoutOverlapping(2) ->runInBackground(); Schedule::command('nova-cards:publish-scheduled') ->everyMinute() ->name('publish-scheduled-nova-cards') ->withoutOverlapping(2) ->runInBackground(); Schedule::command('collections:sync-lifecycle') ->cron('3,13,23,33,43,53 * * * *') ->name('sync-collection-lifecycle') ->withoutOverlapping() ->runInBackground(); Schedule::command('homepage:warm-guest-cache') ->cron('5,15,25,35,45,55 * * * *') ->name('warm-homepage-guest-cache') ->withoutOverlapping() ->runInBackground(); // Safety-net audit for Meilisearch drift on recently touched artworks. // Scans the last 65 minutes to cover the previous hour plus a small buffer. Schedule::command('artworks:search-reconcile --repair --reverse --remove-unexpected --limit=1000 --recent-minutes=65') ->hourlyAt(28) ->name('artworks-search-reconcile-recent') ->withoutOverlapping() ->runInBackground(); // ── Feed 2.0: Trending Cache Warm-up ───────────────────────────────────────── // Warm the post trending cache every 2 minutes on odd minutes to avoid :00/:10 pileups. Schedule::command('posts:warm-trending') ->cron('1-59/2 * * * *') ->name('warm-post-trending') ->withoutOverlapping(); // ── Ranking Engine V2 ────────────────────────────────────────────────────────── // Recalculate ranking_score + engagement_velocity every 30 minutes. // Also syncs V2 scores to rank_artwork_scores so list builds benefit. Schedule::command('nova:recalculate-rankings --sync-rank-scores') ->cron('7,37 * * * *') ->name('ranking-v2') ->withoutOverlapping() ->runInBackground(); // ── Rising Engine (Heat / Momentum) ─────────────────────────────────────────── // Snapshot current totals each hour, then recalculate heat every 15 minutes. Schedule::command('nova:metrics-snapshot-hourly') ->hourlyAt(2) ->name('metrics-snapshot-hourly') ->withoutOverlapping() ->runInBackground(); Schedule::command('nova:recalculate-heat') ->cron('9,24,39,54 * * * *') ->name('recalculate-heat') ->withoutOverlapping() ->runInBackground(); // Additional production schedules that must live here because Laravel 11's // active scheduler in this app is defined in routes/console.php, not Kernel. // Generate static sitemap XML files that nginx can serve directly without PHP. // The generate command writes public/sitemap.xml + public/sitemaps/{name}.xml. Schedule::command('skinbase:sitemaps:generate') ->dailyAt('22:30') ->name('sitemaps-generate') ->withoutOverlapping() ->runInBackground(); Schedule::command('skinbase:sitemaps:publish --sync') ->cron('8 */6 * * *') ->name('sitemaps-publish') ->withoutOverlapping() ->runInBackground(); Schedule::command('skinbase:sitemaps:validate') ->dailyAt('04:45') ->name('sitemaps-validate') ->withoutOverlapping() ->runInBackground(); // Keep the old release-pipeline cleanup running so stale release artifacts are pruned. Schedule::job(new \App\Jobs\Sitemaps\CleanupSitemapReleasesJob()) ->dailyAt('05:00') ->name('sitemaps-cleanup') ->withoutOverlapping(); Schedule::command('collections:dispatch-maintenance') ->hourlyAt(43) ->name('dispatch-collection-maintenance') ->withoutOverlapping() ->runInBackground(); Schedule::job(new \App\Jobs\RankComputeArtworkScoresJob()) ->hourlyAt(5) ->name('rank-compute-artwork-scores'); Schedule::job(new \App\Jobs\RankBuildListsJob()) ->hourlyAt(15) ->name('rank-build-lists') ->withoutOverlapping(); Schedule::job(new \App\Jobs\UpdateLeaderboardsJob()) ->hourlyAt(20) ->name('leaderboards-refresh') ->withoutOverlapping(); Schedule::job(new \App\Jobs\RebuildTrendingNovaCardsJob()) ->hourlyAt(25) ->name('nova-cards-trending-refresh') ->withoutOverlapping(); Schedule::job(new \App\Jobs\RecalculateRisingNovaCardsJob()) ->cron('12,27,42,57 * * * *') ->name('nova-cards-rising-cache-refresh') ->withoutOverlapping(); Schedule::command('nova:prune-metric-snapshots --keep-days=7') ->dailyAt('04:00') ->name('prune-metric-snapshots') ->withoutOverlapping(); Schedule::command('skinbase:sync-countries') ->monthlyOn(1, '03:40') ->name('sync-countries') ->withoutOverlapping() ->runInBackground(); Schedule::command('health:tick') ->everyMinute() ->name('health-scheduler-tick') ->withoutOverlapping(); Schedule::command('forum:ai-scan') ->hourlyAt(16) ->name('forum-ai-scan') ->withoutOverlapping() ->runInBackground(); Schedule::command('forum:bot-scan') ->hourlyAt(22) ->name('forum-bot-scan') ->withoutOverlapping() ->runInBackground(); Schedule::command('forum:scan-posts --limit=250') ->hourlyAt(17) ->name('forum-post-scan') ->withoutOverlapping() ->runInBackground(); Schedule::command('forum:firewall-scan') ->hourlyAt(40) ->name('forum-firewall-scan') ->withoutOverlapping() ->runInBackground(); Schedule::command('horizon:snapshot') ->hourlyAt(45) ->name('horizon-snapshot') ->withoutOverlapping();