Zum Hauptinhalt springen

Intelligente Video-Stats-Tiers (YouTube)

5-stufiges Tier-System (full / standard / light / paused / off) fuer YouTube-Per-Video-Statistiken. Reduziert die youtube.videos.list-API-Calls deutlich bei gleichzeitig besserer Datenqualitaet fuer aktive Channels.

Quickstart — Wie anstoßen

Initialer Backfill (einmalig nach Migration):

# Alle 545K YT-Profile klassifizieren (Subprocess-Orchestrator-Mode aktiv)
php artisan youtube:backfill-stats-tier

# Mit Dry-Run vorab pruefen
php artisan youtube:backfill-stats-tier --dry-run

# Subprocess-Range (Default 10K Profile pro Subprocess) anpassen falls noetig
php artisan youtube:backfill-stats-tier --subprocess-range=5000

Manueller Reevaluate (z.B. nach Config-Aenderung):

# Alle Profile sofort reklassifizieren (ignoriert review_due)
php artisan youtube:reevaluate-stats-tier --force

# Einzelnes Profil debuggen
php artisan youtube:reevaluate-stats-tier --profile=42 --dry-run

# Subprocess deaktivieren (kleine Datenmenge → in-process schneller)
php artisan youtube:reevaluate-stats-tier --no-subprocess

Coverage-Snapshot manuell:

php artisan youtube:snapshot-coverage              # persistiert
php artisan youtube:snapshot-coverage --dry-run # nur JSON-Output

Initialer Backfill (2026-04-28)

Der erste Backfill-Run auf Produktion klassifizierte 544.571 YouTube-Profile mit folgender Verteilung:

TierAnzahlAnteilBedeutung
full00 %Per G2 nie automatisch — nur User-Override
standard14.3392,6 %PRO-Profile mit ≥ 3 Videos/30d (Default fuer aktive PRO-Channels)
light526.32596,6 %Free-Profile (free_no_sync) + PRO mit niedriger Aktivitaet
paused3.9070,7 %PRO ohne Upload-Aktivitaet (≥ 30d kein Upload)
off00 %Admin-Override-only (Hard-Failure-Marker)

Per-Video-Snapshot-Pool reduziert sich auf ~14.000 Channels (vorher: ~21.000 PRO-Profile in 1:1-Sync). Sub-Seg standard mit 2-Tage-Kadenz statt taeglich → Quota-Einsparung in Per-Video-Pool ca. 50 % gegenueber dem alten 1:1-Modell.

Wie laeuft das regelmäßig

Beide Scheduler-Jobs sind in routes/console.php registriert und laufen automatisch mit dem normalen Forge-Cron (schedule:run jede Minute):

CommandFrequenzDefault-Zeit.env-OverridewithoutOverlapping
youtube:reevaluate-stats-tierTaeglich02:00 UTCYOUTUBE_STATS_REVIEW_TIME + YOUTUBE_STATS_REVIEW_TZ60 min
youtube:snapshot-coverageWoechentlich (Sonntag)04:00 UTCYOUTUBE_STATS_COVERAGE_TIME + YOUTUBE_STATS_COVERAGE_DAY (0=Sonntag) + YOUTUBE_STATS_COVERAGE_TZ60 min

Kein manuelles Cron-Setup notwendig. Schedule-Zeiten sind .env-konfigurierbar — der Admin-UI-Text passt sich dynamisch an.

Memory-Strategie (128 MB Limit)

Beide Iterativen Commands schuetzen sich gegen Memory-Probleme bei 545K+ Profilen:

  • Subprocess-Orchestrator-Mode (Default): Top-Level-Aufruf splittet den Range in 10K-Profile-Chunks und ruft sich selbst per Process::run(PHP_BINARY ...) auf. PHP gibt freigegebene Memory-Pages zwischen Subprocess-Iterationen tatsaechlich ans OS zurueck.
  • In-Process-Chunks: Innerhalb jedes Subprocess wird in 1K-Profile-Chunks per CTE-Query geladen, mit gc_collect_cycles() zwischen Chunks.
  • DB::disableQueryLog() zwingend in allen 3 Commands (CLAUDE.md).

Mit --no-subprocess Flag laesst sich der Orchestrator deaktivieren (z.B. fuer Tests oder kleine Datenmengen).

Monitoring

Beide Scheduled Commands sind vollstaendig im Monitoring-Stack:

  • Heartbeat via WritesCronHeartbeat-Trait — Heartbeat-Keys: youtube_stats_tier_review + youtube_coverage_snapshot. Bei --dry-run wird der Heartbeat NICHT geschrieben (sonst maskiert ein Test einen tatsaechlich faehlligen Schedule).
  • /admin/log-queue Heartbeats-Tabelle zeigt Last-Run + Status (via CronStatusService::SCHEDULE_MAP).
  • /admin/update-status Daily-Pipeline-Status zeigt Run-History + Retry-Button (via DailyPipelineStatus::RETRY_COMMANDS).
  • config/postbox.php cron_heartbeats: max_minutes-Schwellen + sla_weight (Reevaluate=3, Snapshot=2) → Health-Score-Aggregation.

Ziele

  • Quota-Reduktion: Aggressivere Defaults — full (taegliche Per-Video-Snapshots) ist nur fuer hochaktive PRO-Channels. Default fuer alle anderen ist standard (alle 2 Tage) bis paused (kein Per-Video-Sync).
  • User-Override: User koennen ihren Tier selbst sperren — Free-User bis standard, PRO-User bis full.
  • Auto-Reaktivierung: Wenn ein pausierter Channel wieder uploadet, wird er automatisch reklassifiziert.
  • Audit + Notification: Jede Tier-Aenderung wird in extended_stats_tier_changes geloggt + scheduler/reactivation-Aenderungen werden mit 14-Tage-Throttle dem Workspace-Owner zugestellt. Inhalt der Notification: Titel Stats-Tier geaendert: {handle}, Body Tier: {prev} → {new}. Grund: {reason}., Icon chart-bar, URL fuehrt direkt zum ersten Watcher des Owners, der dieses Profil als Source fuehrt. Bei reinen User-/Admin-Triggers (user_override, admin_override, admin_disabled) feuert keine Notification, da der Auslöser selbst der User ist.

Admin-PRO-Disable (Plan 67)

Admin kann PRO + Extended-Stats für ein YouTube-Profil dauerhaft deaktivieren. Der youtube:auto-activate-pro-Cron ignoriert blockierte Profile — aber seit 2026-05-25 ist die Auto-Aktivierung sowieso global deaktiviert (YOUTUBE_PRO_AUTO_ACTIVATION_ENABLED=false). Der Lock-Flag pro_auto_activation_blocked_at bleibt trotzdem wichtig: er signalisiert anderen Code-Pfaden (Repair-Command, evtl. zukünftige Re-Aktivierungs-Logik) "Hände weg, das Profil wurde bewusst deaktiviert". Nur der User kann den Block wieder aufheben.

Admin-Aktionen (4 Stellen):

  • /admin/social-profiles → Button "PRO deaktivieren"
  • /admin/youtube-management/video-stats-tiers → Button "PRO deaktivieren"
  • Watcher-Detail-Seite (Livewire-Action) → "Erweiterte Video-Statistiken deaktivieren" (Admin-only)
  • Watcher-Detail-Form (POST-Route) → DisableYouTubeVideoAutoSyncController (seit 2026-05-25 ebenfalls Plan-67-konform — vorher fehlten dort die Lock-Felder, Userreport 2026-05-25)

Was passiert bei Admin-Disable (alle 4 Pfade jetzt identisch):

  1. pro_enabled → false
  2. pro_auto_activation_blocked_at → jetzt (Block gesetzt)
  3. extended_stats_tier → off
  4. extended_stats_reasonadmin_disabled
  5. extended_stats_locked → true
  6. extended_stats_reviewed_at → jetzt
  7. extended_stats_review_due → null
  8. auto_sync_enabled → false (auf youtube_video_syncs)
  9. VideoStatsTierChanged-Event wird dispatcht

User-Re-Enable: Wenn ein User auf der Watcher-Seite einen aktiven Tier wählt (z.B. Standard), wird der Block aufgehoben + PRO re-enabled + Tier auf standard gesetzt (Reevaluator darf den Tier später anpassen, weil extended_stats_locked zurueck auf false geht).

Admin-Filter: In /admin/social-profiles unter PRO-Filter → "Auto-Activate blockiert".

Cleanup historischer Video-Metriken (seit 2026-05-27)

Nach Plan-67-Disable bleiben die historischen youtube_video_daily_metrics-Rows der deaktivierten Profile in der DB liegen — bei ~13.000 disablten Profilen mit jeweils ~10-50 Videos × hunderten Tagen Metriken summiert sich das auf ~20 GB.

Cleanup via php artisan youtube:prune-inactive-video-metrics --dry-run (siehe Commands-Doku). Triple-Lock-Schutz garantiert dass aktive PRO-Profile NICHT angefasst werden: ein Profil bleibt nur verschont wenn platform=youtube UND tracking_enabled=true UND pro_enabled=true UND youtube_video_syncs.auto_sync_enabled=true.

Cleanup zugehöriger Videos + Thumbnails (seit 2026-05-28)

Folge-Cleanup zu prune-inactive-video-metrics: der erste Lauf löschte nur Daily-Metric-Rows, aber youtube_videos-Rows und ihre Thumbnails (R2/local Storage-Disk) blieben bestehen. Bei ~13k disablten Profilen × ~50 Videos × ~4 Thumbnail-Varianten = bis zu 2,6M nutzlose Storage-Files.

Cleanup via php artisan youtube:prune-inactive-video-records --dry-run (siehe Commands-Doku). Identische Triple-Lock-Logik. Per Batch: erst Thumbnail-Paths aus 4 Spalten sammeln und via Storage::disk('public')->delete([...]) löschen (best-effort), dann DB-DELETE — youtube_video_scores und Reste der youtube_video_daily_metrics werden via cascadeOnDelete() mitgelöscht.

Tier-Stufen

TierBedeutungPer-Video-SnapshotTop-Video-Limit
fullHochaktiver PRO-Channel mit ≥10 Videos in 30 TagenTaeglich50
standardModerate Aktivitaet (3-9 Videos in 30 Tagen)Alle 2 Tage20
lightNiedrige Cadence (1-2 Videos in 30 Tagen)Alle 7 Tage5
pausedKein Upload in den letzten 30+ TagenKein Per-Video-Sync0
offHart deaktiviert (Admin-Override)Kein Sync ueberhaupt0

Source of Truth: Werte stammen aus config/postbox.phpyoutube_stats_tier.*. Defaults sind oben dokumentiert; alle sind via ENV-Variablen override-bar (YOUTUBE_STATS_INTERVAL_FULL/STANDARD/LIGHT, YOUTUBE_STATS_TOP_FULL/STANDARD/LIGHT). Aenderungen in der ENV greifen sofort, da VideoStatsTier::snapshotIntervalDays() per Aufruf liest. Der Skip-Check in SyncYouTubeVideoStats::handle() (Job-Level) prueft last_sync_at > now()->subDays($intervalDays) — Cycle-Length entspricht dem Intervall (interval=2 → Sync alle 2 Tage, interval=3 → alle 3 Tage, etc.).

paused und off blockieren Snapshot-Inserts in youtube_video_daily_metrics ueber VideoStatsTier::allowsVideoSnapshots().

Heuristik-Grundsaetze

CodeGarantie
G1Profil-Daily-Metriken (social_profile_daily_metrics) werden NIE getroffen — Plan 56 betrifft nur Per-Video-Stats.
G2Heuristik liefert NIEMALS full automatisch — full ist ausschliesslich User-Override (PRO).
G3Heuristik liefert NIEMALS offoff ist ausschliesslich Admin-Override.
G4extended_stats_locked = true → Heuristik laesst den Tier unberuehrt.
G5Free-User-Cap: Free-User koennen bis free_max_lock_tier (Default standard) sperren.
G6Audit-Insert in extended_stats_tier_changes ist die einzige Source-of-Truth fuer Tier-Verlauf.
G7Notifications gehen nur bei triggered_by ∈ {scheduler, reactivation} raus, mit 14-Tage-Throttle.
G8IntelligentVideoStatsTier::classify() ist eine reine Funktion (keine DB-Side-Effects).

Konfiguration

config/postbox.phpyoutube_stats_tier:

'pause_days_no_upload' => env('YOUTUBE_STATS_PAUSE_DAYS', 30),       // ab welchem Upload-Stand ist `paused`
'high_activity_per_month' => env('YOUTUBE_STATS_HIGH_ACTIVITY', 10), // ab wieviel Videos/30d ist `full` moeglich
'moderate_activity_min' => env('YOUTUBE_STATS_MODERATE_MIN', 3), // ab wieviel Videos/30d ist `standard`
'dormant_days' => env('YOUTUBE_STATS_DORMANT_DAYS', 180), // ab wann ist Channel dormant
'review_default_days' => env('YOUTUBE_STATS_REVIEW_DEFAULT', 7), // Reklassifizierungs-Intervall (active)
'review_paused_days' => env('YOUTUBE_STATS_REVIEW_PAUSED', 14), // Reklassifizierungs-Intervall (paused)
'reevaluate_chunk_size' => env('YOUTUBE_STATS_REEVAL_CHUNK', 1000), // Chunk-Size im Reevaluate-Command
'free_max_lock_tier' => env('YOUTUBE_STATS_FREE_MAX_LOCK', 'standard'), // Free-User-Cap (kein Full)

Commands

php artisan youtube:reevaluate-stats-tier

Klassifiziert alle Profile mit tracking_enabled neu. Standardmaessig nur Profile mit extended_stats_review_due <= now().

FlagWirkung
--forceIgnoriert review_due — alle Profile reklassifizieren.
--dry-runSchreibt nichts; Ausgabe zeigt geplante Aenderungen.
--profile=IDNur einzelnes Profil klassifizieren.
--limit=NMaximal N Profile pro Lauf.

Der Command:

  1. Laedt Activity-Signals via CTE-Query (ActivitySignalsLoader).
  2. Klassifiziert via IntelligentVideoStatsTier::classify() (G8 — pure).
  3. Persistiert nur, wenn decision->changed === true.
  4. Dispatcht VideoStatsTierChanged → Listener schreibt Audit + Notification.
  5. Setzt extended_stats_review_due auf review_default_days bzw. review_paused_days.

php artisan youtube:backfill-stats-tier

Initialer Backfill — laeuft einmalig nach Migration. Setzt fuer alle Profile ohne Tier den Erst-Tier.

php artisan youtube:snapshot-coverage

Wochentlicher Snapshot der Coverage-Metriken in youtube_coverage_weekly_snapshots. Aggregiert:

  • Profile-Counts (total, pro, free, blocked, sanitized)
  • Tier-Verteilung (full / standard / light / paused / off) — nur Profile mit pro_enabled=true (siehe Hinweis unten)
  • Per-Video-Snapshot-Counts der letzten 30 Tage
  • Frische-Verteilung (≤7d / 7-30d / >30d)
  • YouTube-Quota-Verbrauch als 7-Tage-Mittel aus google_api_quota_usages (gemessen, mit Heuristik-Fallback)
  • Storage-Size der youtube_video_daily_metrics-Tabelle

Datengrundlage fuer das Coverage-Trends-Dashboard.

Quota-Spalte estimated_daily_quota_units (seit 2026-05-27):

  • Liest primär AVG(used_value) aus google_api_quota_usages für service='youtube.googleapis.com' der letzten 7 Tage (CURRENT_DATE-Range). Das ist der GEMESSENE Quota-Verbrauch.
  • Fallback (wenn keine Messdaten verfügbar): Heuristik tier_full × 50 + tier_standard × 50 × 0.43 + tier_light × 50 × 0.14. Die Heuristik nutzt die pro_enabled=true-gefilterten Tier-Counts (vorher zählte sie alle ~469k Tracking-Profile, davon ~467k Default-tier=light × 14% Sync-Frequenz = 3,3M Units-Beitrag — komplett unrealistisch, da nur ~1.200 PRO-Profile tatsächlich syncen).
  • Dashboard-Label im Coverage-Trends-Widget: Quota /Tag (7d-Avg).

Schedule

routes/console.php:

// Plan 56 v2: taegliche Reklassifizierung (Default 02:00 UTC, .env-konfigurierbar)
Schedule::command('youtube:reevaluate-stats-tier')
->dailyAt(config('postbox.youtube_stats_tier.review_schedule_time'))
->timezone(config('postbox.youtube_stats_tier.review_schedule_timezone'))
->withoutOverlapping();
// Wochentlicher Coverage-Snapshot (Default Sonntag 04:00 UTC, .env-konfigurierbar)
Schedule::command('youtube:snapshot-coverage')
->weeklyOn(config('postbox.youtube_stats_tier.coverage_schedule_day'), config('postbox.youtube_stats_tier.coverage_schedule_time'))
->timezone(config('postbox.youtube_stats_tier.coverage_schedule_timezone'))
->withoutOverlapping();

Beide Commands schreiben WritesCronHeartbeat-Eintraege und sind in config/postbox.phpcron_heartbeats registriert.

Auswertung

Admin-UIs

  • /admin/youtube/video-stats-tiers — Tabelle aller Profile mit Tier-Filter, Set-Tier-Action, Lock-Toggle, Force-Review-Button.
  • /admin/youtube/video-coverage-trends — ApexCharts-Trends auf Basis der wochentlichen Snapshots (4w / 13w / 26w / 52w / all).

User-UI

/watchers/{watcher} zeigt fuer YouTube-Quellen eine Tier-Card mit:

  • Aktuellem Tier + Reason
  • Lock-Status + naechster Pruefungstermin
  • Tier-Override-Buttons (Free-Cap auf standard)
  • Tier-Verlauf (kollabierbar, letzte 10 Eintraege aus extended_stats_tier_changes)

Direkte SQL-Auswertung

-- Tier-Verteilung aktuell
SELECT extended_stats_tier, COUNT(*) AS profiles
FROM social_profiles
WHERE platform = 'youtube' AND tracking_enabled = TRUE
GROUP BY extended_stats_tier
ORDER BY profiles DESC;

-- Tier-Aenderungen der letzten 7 Tage
SELECT triggered_by, reason, COUNT(*) AS changes
FROM extended_stats_tier_changes
WHERE changed_at >= NOW() - INTERVAL '7 days'
GROUP BY triggered_by, reason
ORDER BY changes DESC;

-- Geschaetzte Quota-Reduktion (Snapshot-Counts)
SELECT
snapshot_date,
estimated_daily_quota_units,
snapshots_total_30d,
avg_snapshots_per_video
FROM youtube_coverage_weekly_snapshots
ORDER BY snapshot_date DESC
LIMIT 13;

Datenbank

social_profiles (neue Spalten)

SpalteTypDefaultBedeutung
extended_stats_tiervarchar (Enum-Cast)nullAktiver Tier.
extended_stats_reasonvarcharnullReason aus letzter Klassifizierung (moderate_activity, no_recent_upload, user_override, …).
extended_stats_reviewed_attimestampnullLetzte Klassifizierung.
extended_stats_review_duetimestampnullNaechste Klassifizierung. Wird im Reevaluate-Command als WHERE-Filter genutzt.
extended_stats_lockedbooleanfalseUser/Admin hat Tier gesperrt — Heuristik haelt Abstand.

Indexe:

  • (extended_stats_tier) — Tier-Filter
  • (extended_stats_review_due) — Reevaluate-Command-Filter

extended_stats_tier_changes (Audit-Tabelle)

Vollstaendige Tier-Verlaufs-History. Wird vom Listener HandleVideoStatsTierChange gefuellt.

SpalteTypBedeutung
social_profile_idbigintFK
previous_tiervarcharLetzter Tier (kann null sein bei Erst-Klassifizierung)
new_tiervarcharNeuer Tier
reasonvarcharReason-Code
triggered_byvarcharscheduler / reactivation / user / admin / backfill
changed_attimestampAenderungszeitpunkt

youtube_coverage_weekly_snapshots

~30 Spalten Coverage-Metriken pro Snapshot-Datum. Datengrundlage fuer das Coverage-Trends-Dashboard.

.env-Block

# Plan 56 v2 — Intelligente Video-Stats-Tiers
# Pause-Schwelle: kein Upload in N Tagen → Tier 'paused'
YOUTUBE_STATS_PAUSE_DAYS=30
# Ab wieviel Videos/30d ist 'full' theoretisch moeglich (User-Lock-Voraussetzung)
YOUTUBE_STATS_HIGH_ACTIVITY=10
# Ab wieviel Videos/30d ist 'standard'
YOUTUBE_STATS_MODERATE_MIN=3
# Ab wann gilt Channel als dormant
YOUTUBE_STATS_DORMANT_DAYS=180
# Reklassifizierungs-Intervall fuer aktive Profile (Tage)
YOUTUBE_STATS_REVIEW_DEFAULT=7
# Reklassifizierungs-Intervall fuer 'paused' Profile (Tage)
YOUTUBE_STATS_REVIEW_PAUSED=14
# Chunk-Size im Reevaluate-Command
YOUTUBE_STATS_REEVAL_CHUNK=1000
# Snapshot-Intervalle pro Tier (Tage zwischen Per-Video-Snapshots)
YOUTUBE_STATS_INTERVAL_FULL=1
YOUTUBE_STATS_INTERVAL_STANDARD=2
YOUTUBE_STATS_INTERVAL_LIGHT=7
# Top-Limit pro Tier (Anzahl Videos pro Run)
YOUTUBE_STATS_TOP_FULL=50
YOUTUBE_STATS_TOP_STANDARD=20
YOUTUBE_STATS_TOP_LIGHT=5
# Optional: Admin-Mail bei Tier-Aenderungen (default OFF)
YOUTUBE_STATS_NOTIFY_EMAIL=false
# Default-Tier fuer neue PRO-Profile (Backfill)
YOUTUBE_STATS_INITIAL_PRO=standard
# Free-User-Cap: bis zu welchem Tier duerfen Free-User selbst sperren? (full|standard|light|paused)
YOUTUBE_STATS_FREE_MAX_LOCK=standard
# Schedule-Zeit fuer den taeglichen Reevaluate-Run (HH:MM, Default 02:00 UTC)
YOUTUBE_STATS_REVIEW_TIME=02:00
YOUTUBE_STATS_REVIEW_TZ=UTC
# Schedule fuer den woechentlichen Coverage-Snapshot (Default Sonntag 04:00 UTC)
# Day: 0=Sonntag, 1=Montag, ..., 6=Samstag
YOUTUBE_STATS_COVERAGE_TIME=04:00
YOUTUBE_STATS_COVERAGE_DAY=0
YOUTUBE_STATS_COVERAGE_TZ=UTC

Tests

  • tests/Unit/Services/YouTube/IntelligentVideoStatsTierTest.php (13 Tests)
    • G1-G8 Garantien
    • User-Lock, Free-Cap
    • Klassifizierungs-Pfade (full / standard / light / paused / off)
  • tests/Feature/Commands/Youtube/ReevaluateVideoStatsTierTest.php (6 Tests)
    • Persistierung + Audit-Insert + Event-Dispatch
    • Dry-Run schreibt nichts
    • Locked-Profile werden uebersprungen
    • review_due in der Zukunft → Skip
    • Snapshot-Coverage-Command
  • tests/Feature/Listeners/HandleVideoStatsTierChangeTest.php (6 Tests)
    • Audit-Insert bei jedem Trigger-Typ
    • Notification nur bei scheduler / reactivation
    • 14-Tage-Throttle pro Profil
    • Throttle laeuft nach 15+ Tagen ab

Garantien & Edge-Cases

  • Manuelle Trigger umgehen den Tier-Filter: SyncYouTubeVideoStats-Job mit triggeredManually=true (Watcher "Video-Statistiken abrufen", CLI --profile-id) bypasst Tier-Pruefung und Snapshot-Interval-Skip. Admin-/User-Force-Aktionen laufen immer.
  • Tier off ist exklusiv Admin: Regulaere User koennen off nicht via Settings setzen — nur Admin via /admin/youtube/video-stats-tiers.
  • Admin-Override-Lock ist staerker als User-Lock: Wenn extended_stats_reason === 'admin_override', kann der Lock nur von Admins aufgehoben werden — verhindert dass Watcher-Owner Hard-Failure-Marker wegklicken.
  • Notification-Throttle prueft PRE-Insert: Damit der eigene Audit-Insert den Throttle nicht selbst triggert. 14 Tage pro Profil, nur fuer scheduler und reactivation Triggers.
  • Heartbeat skipt Dry-Run: --dry-run bei youtube:reevaluate-stats-tier schreibt keinen Cron-Heartbeat — verhindert dass manuelle Tests einen tatsaechlich faehlligen Schedule-Status maskieren.