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: 24h first, then 7d) Schedule::command('skinbase:recalculate-trending --period=24h') ->everyThirtyMinutes() ->name('trending-24h') ->withoutOverlapping(); Schedule::command('skinbase:recalculate-trending --period=7d --skip-index') ->everyThirtyMinutes() ->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:30') // Monday 03:30 ->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'); // Drain Redis artwork-stat delta queue so MySQL counters stay fresh. // Run every 5 minutes with overlap protection. Schedule::command('skinbase:flush-redis-stats') ->everyFiveMinutes() ->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(); // ── Feed 2.0: Trending Cache Warm-up ───────────────────────────────────────── // Warm the post trending cache every 2 minutes (complements the 2-min TTL). Schedule::command('posts:warm-trending') ->everyTwoMinutes() ->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') ->everyThirtyMinutes() ->name('ranking-v2') ->withoutOverlapping() ->runInBackground(); Schedule::command('forum:ai-scan') ->everyTenMinutes() ->name('forum-ai-scan') ->withoutOverlapping() ->runInBackground(); Schedule::command('forum:bot-scan') ->everyFiveMinutes() ->name('forum-bot-scan') ->withoutOverlapping() ->runInBackground(); Schedule::command('forum:scan-posts --limit=250') ->everyFifteenMinutes() ->name('forum-post-scan') ->withoutOverlapping() ->runInBackground(); Schedule::command('forum:firewall-scan') ->everyFiveMinutes() ->name('forum-firewall-scan') ->withoutOverlapping() ->runInBackground();