Files
klevze ce0f278bac Add a read-only Google AdSense analytics module for admins.
Connect AdSense over OAuth, sync entities and daily reports locally, and show placement performance in the admin panel without calling the Management API from public pages.
2026-09-20 14:49:48 +02:00

343 lines
12 KiB
PHP

<?php
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
use App\Uploads\Services\CleanupService;
Artisan::command('inspire', function () {
$this->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')
->name('uploads-cleanup')
->withoutOverlapping();
Schedule::command('enhance:cleanup --force')
->dailyAt('03:30')
->name('enhance-cleanup')
->withoutOverlapping()
->runInBackground();
Schedule::command('analytics:aggregate-similar-artworks')
->dailyAt('03:10')
->name('analytics-aggregate-similar-artworks')
->withoutOverlapping();
Schedule::command('analytics:aggregate-feed')
->dailyAt('03:20')
->name('analytics-aggregate-feed')
->withoutOverlapping();
Schedule::command('analytics:aggregate-discovery-feedback')
->dailyAt('03:25')
->name('analytics-aggregate-discovery-feedback')
->withoutOverlapping();
Schedule::command('analytics:aggregate-tag-interactions')
->dailyAt('03:35')
->name('analytics-aggregate-tag-interactions')
->withoutOverlapping();
Schedule::command('academy:analytics-rollup')
->hourlyAt(12)
->name('academy-analytics-rollup')
->withoutOverlapping()
->runInBackground();
Schedule::command('academy:analytics-recalculate-popularity --days=30')
->dailyAt('03:45')
->name('academy-analytics-popularity')
->withoutOverlapping()
->runInBackground();
Schedule::command('academy:analytics-prune-events --days=180')
->dailyAt('04:20')
->name('academy-analytics-prune')
->withoutOverlapping()
->runInBackground();
// 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.
// :18 not :21 so this does not start with leaderboards:refresh (observed >2 min).
Schedule::command('skinbase:flush-redis-stats')
->cron('1,11,18,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/sitemaps/sitemap.xml + public/sitemaps/{name}.xml.
Schedule::command('skinbase:sitemaps:generate')
->cron('30 10,22 * * *')
->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();
if ((bool) config('security-report.enabled', true)) {
Schedule::command('security:scan --notify')
->weeklyOn(1, '05:15')
->name('security-report-weekly-scan')
->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();
if ((bool) config('collections.v5.queue.dispatch_enabled', false)) {
Schedule::command('collections:dispatch-maintenance')
->hourlyAt(43)
->name('dispatch-collection-maintenance')
->withoutOverlapping()
->runInBackground();
}
Schedule::job(new \App\Jobs\RankBuildListsJob())
->hourlyAt(15)
->name('rank-build-lists')
->withoutOverlapping();
Schedule::command('leaderboards:refresh')
->hourlyAt(21)
->name('leaderboards-refresh')
->withoutOverlapping()
->runInBackground();
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();
// After hourly snapshot at :02 so prune does not share the 04:02 upsert window.
Schedule::command('nova:prune-metric-snapshots')
->dailyAt('04:25')
->name('prune-metric-snapshots')
->withoutOverlapping(120);
Schedule::command('skinbase:sync-countries')
->monthlyOn(1, '03:40')
->name('sync-countries')
->withoutOverlapping()
->runInBackground();
Schedule::command('adsense:sync --days=3')
->hourlyAt(8)
->name('adsense-sync-hourly')
->withoutOverlapping()
->runInBackground();
Schedule::command('adsense:sync --days=30')
->dailyAt('04:35')
->name('adsense-sync-nightly')
->withoutOverlapping()
->runInBackground();
Schedule::command('health:tick')
->everyMinute()
->name('health-scheduler-tick')
->withoutOverlapping();
if ((bool) config('skinbase_ai_moderation.queue.dispatch_enabled', false)) {
Schedule::command('forum:ai-scan')
->hourlyAt(16)
->name('forum-ai-scan')
->withoutOverlapping()
->runInBackground();
}
Schedule::command('forum:scan-posts --limit=250')
->hourlyAt(17)
->name('forum-post-scan')
->withoutOverlapping()
->runInBackground();
if ((bool) config('forum_security.queues.dispatch_enabled', false)) {
Schedule::command('forum:bot-scan')
->hourlyAt(22)
->name('forum-bot-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();
if ((bool) config('traffic.online_visitors.prune_enabled', false)) {
Schedule::command('skinbase:prune-online-visitor-index --execute')
->hourlyAt(33)
->name('prune-online-visitor-index')
->withoutOverlapping(25)
->runInBackground();
}