Zum Hauptinhalt springen

Dashboard & Metriken Commands

Commands für die Berechnung von Dashboard-Rollups, Leaderboards, Scores und Explore-Metriken. Diese Commands bilden eine Kette: Rollups -> Leaderboards -> Global Leaderboards -> Scores.


dashboard:rollup-daily-metrics

Berechnet Dashboard-Rollups aus social_profile_daily_metrics für Winners/Losers und Top-Listen.

dashboard:rollup-daily-metrics {date?}
{--from= : Start-Datum (Y-m-d)}
{--to= : End-Datum (Y-m-d)}

Beispiele

# Rollup für ein bestimmtes Datum
php artisan dashboard:rollup-daily-metrics 2026-01-10

# Rollup für Datumsbereich
php artisan dashboard:rollup-daily-metrics --from=2026-01-01 --to=2026-01-10

# Standard-Schedule: letzte 7 Tage
php artisan dashboard:rollup-daily-metrics --from=2026-02-03 --to=2026-02-09

Optionen

OptionBeschreibung
date?Einzelnes Datum (Y-m-d)
--from=Start-Datum für Range
--to=End-Datum für Range

Interne Schritte

  1. Datum/Range bestimmen, auf gestern clampen (keine Future-Daten)
  2. Per-Tag INSERT...SELECT...ON CONFLICT — jeder Tag wird als separate PostgreSQL-Transaktion ausgefuehrt (~96K Rows je Tag). Vorher war es ein einzelnes Statement fuer den gesamten Datumsbereich (~672K Rows fuer 7 Tage), das lange Locks hielt und die Pipeline blockierte.
  3. Falls ein Tag fehlt, wird der Vortag als Fallback genutzt (typisch bei Instagram-Rotation alle 2 Tage)
  4. PostgreSQL statement_timeout: 10 Minuten pro Statement, verhindert endlos laufende Queries
  5. Auto-Chain: Nach jedem Rollup wird automatisch dashboard:rollup-leaderboards aufgerufen (deaktivierbar via --skip-leaderboards)

Schedule

Täglich um 00:10 UTC. Der Schedule-Call berechnet die letzten 7 Tage per --from/--to.

Location: app/Console/Commands/BuildDashboardDailyRollups.php


dashboard:rollup-leaderboards

Vorberechnung von Leaderboard-Snapshots, damit Dashboard-Filter ohne Live-Queries auskommen.

dashboard:rollup-leaderboards {date?}
{--from= : Start-Datum (Y-m-d)}
{--to= : End-Datum (Y-m-d)}
{--owner= : Nur Leaderboards fuer diesen Owner berechnen}

Beispiele

# Snapshot für ein bestimmtes Datum
php artisan dashboard:rollup-leaderboards 2026-01-21

# Snapshots für Datumsbereich
php artisan dashboard:rollup-leaderboards --from=2026-01-01 --to=2026-01-21

# Nur einen bestimmten Owner berechnen
php artisan dashboard:rollup-leaderboards 2026-01-21 --owner=42

Optionen

OptionBeschreibung
date?Einzelnes Datum
--from=Start-Datum für Range
--to=End-Datum für Range
--owner=Nur diesen Owner verarbeiten (wird intern fuer Subprocess-Isolation genutzt)

Interne Schritte

  1. Exakte Leader/Chart-Logik des Dashboards replizieren (inkl. Vortags-Fallback)
  2. Subprocess-Isolation (OOM-Schutz):
    • --from/--to → Subprocess pro Tag (PHP_BINARY)
    • Einzelnes Datum → Subprocess pro Owner (--owner=X)
    • Einzelnes Datum + --owner=X → In-Process-Verarbeitung (Blatt-Prozess)
    • Grund: PHP Zend MM gibt freigegebene Memory-Pages nicht ans OS zurueck. Nur frische Prozesse haben ein echtes Memory-Budget-Reset.
  3. Coverage-Watcher-IDs materialisieren: getCoverageWatcherIds() ermittelt Watcher mit ausreichender Datenabdeckung einmalig und cached das Ergebnis in-memory. Verwendet whereIn() statt eingebetteter Subquery, um wiederholte PostgreSQL-Evaluierung zu vermeiden.
  4. Start-Zeitraum an frühestes Rollup im Bereich clampen
  5. Falls keine Rollups im Zeitraum: neuestes Rollup vor dem End-Datum verwenden
  6. Snapshots pro Owner + Zeitraum + Metrik + End-Datum speichern in dashboard_leaderboard_snapshots

Dashboard Periods

Snapshots werden für alle drei Dashboard-Perioden generiert:

PeriodBeschreibung
3d3 Tage (Daten von vorgestern bis gestern)
7d7 Tage (Default-Ansicht)
14d14 Tage

Favoriten werden zur Laufzeit gefiltert, damit Änderungen sofort sichtbar sind.

Schedule

Täglich um 00:15 UTC (direkt nach den Rollups).

Location: app/Console/Commands/BuildDashboardLeaderboardSnapshots.php


dashboard:rollup-global-leaderboards

Systemweite Top/Flop-Leaderboards (nicht pro Owner, sondern global).

dashboard:rollup-global-leaderboards {date?}
{--from= : Start-Datum (Y-m-d)}
{--to= : End-Datum (Y-m-d)}
{--metric= : Nur diese Metrik berechnen (Subprocess-Isolation)}

Beispiele

# Globale Leaderboards für heute
php artisan dashboard:rollup-global-leaderboards

# Für bestimmten Bereich
php artisan dashboard:rollup-global-leaderboards --from=2026-01-01 --to=2026-01-21

# Nur eine Metrik berechnen (intern fuer Subprozesse)
php artisan dashboard:rollup-global-leaderboards 2026-01-21 --metric=followers

Optionen

OptionBeschreibung
date?Einzelnes Datum
--from=Start-Datum
--to=End-Datum
--metric=Nur diese Metrik verarbeiten (wird intern fuer Subprocess-Isolation genutzt)

Interne Schritte

  1. Nur 7d-Period für vereinfachtes globales Dashboard
  2. Metriken: followers, views, videos, posts, following, score
  3. Subprocess-Isolation (OOM-Schutz):
    • --from/--to → Subprocess pro Tag (PHP_BINARY)
    • Einzelnes Datum → Subprocess pro Metrik (--metric=X, 6 Subprozesse)
    • Einzelnes Datum + --metric=X → In-Process-Verarbeitung (Blatt-Prozess, 6 Tiers)
    • Grund: 6 Metriken × 6 Tiers = 36 Iterationen mit grossen Coverage-Arrays uebersteigen sonst 128 MB.
  4. Tier-Filterung (all, micro, small, medium, large, mega) pro Metrik
  5. Coverage-Profile-IDs materialisieren: getCoverageProfileIds() ermittelt Profile mit ausreichender Datenabdeckung einmalig und cached das Ergebnis in-memory.
  6. Absolute Metriken in einer einzigen Query berechnen (followers_count, video_count, etc.) — danach in PHP nach Tier partitionieren
  7. Snapshots in dashboard_global_snapshots speichern

Schedule

Täglich um 00:20 UTC (nach per-Owner Leaderboards).

Location: app/Console/Commands/BuildGlobalLeaderboardSnapshots.php


scores:calculate

Berechnet tier-normalisierte Postbox-Scores (0-100) für alle getrackten Profile. Kleine und große Channels werden fair vergleichbar anhand ihrer Wachstumsleistung.

scores:calculate
{--date= : Bestimmtes Datum (Y-m-d)}
{--from= : Start-Datum für Range}
{--to= : End-Datum für Range}
{--platform= : Filter (youtube, instagram)}
{--force : Bestehende Scores überschreiben}
{--dry-run : Vorschau ohne Speichern}

Beispiele

# Letzte 3 Tage berechnen (Default — füllt automatisch Lücken)
php artisan scores:calculate

# Scores für bestimmtes Datum
php artisan scores:calculate --date=2026-02-05

# Nur YouTube, Range
php artisan scores:calculate --from=2026-02-01 --to=2026-02-05 --platform=youtube

# Erzwungene Neuberechnung
php artisan scores:calculate --force --dry-run

Optionen

OptionBeschreibung
--date=Einzelnes Datum (Y-m-d)
--from= / --to=Datumsbereich
--platform=youtube oder instagram
--forceVorhandene Scores überschreiben
--dry-runKeine Scores in DB schreiben

Default-Verhalten (ohne Parameter)

Ohne --date oder --from/--to werden automatisch die letzten 3 Tage berechnet (vorgestern, gestern, heute-3). Bestehende Scores werden übersprungen (es sei denn --force), sodass nur fehlende Tage nachgeholt werden. Dies füllt Lücken automatisch, falls ein vorheriger Lauf ausgefallen ist.

Score-Formel

KomponenteGewichtungBeschreibung
Growth40%Tägliche Wachstumsrate vs. Tier-Erwartung
Momentum30%Wachstumsbeschleunigung (letzte 7d vs. vorherige 7d)
Consistency20%Datenabdeckung in den letzten 14 Tagen
Engagement10%Plattformspezifische Aktivitätsmetriken

Tier-Definitionen

TierFollower-Range
micro0 - 10K
small10K - 100K
medium100K - 500K
large500K - 1M
mega1M+

Score-Status

StatusDatenpunkteBeschreibung
pending< 7Kein Score berechnet
preliminary7-9Vorläufiger Score
stable10+Stabiler Score
no_data0Keine Metriken vorhanden

Schedule

Täglich um 05:00 UTC (nach allen Scrape-Jobs).

Location: app/Console/Commands/CalculateProfileScores.php


explore:calculate

Berechnet Explore-Metriken für Profile und Videos: Daily Growth, Trending Scores, Kategorien.

explore:calculate
{--type=daily : Berechnungstyp (daily, trending, videos, categories, all)}
{--force : Neuberechnung erzwingen}

Beispiele

# Alle Explore-Metriken berechnen
php artisan explore:calculate --type=all

# Nur tägliche Growth-Metriken
php artisan explore:calculate --type=daily

# Nur Trending-Videos
php artisan explore:calculate --type=videos

Optionen

OptionBeschreibung
--type=daily, trending, videos, categories, all
--forceNeuberechnung auch wenn kürzlich berechnet

Berechnungstypen

TypServiceBeschreibung
dailyExploreMetricsCalculator::calculateDailyMetrics()Tägliche Growth-Metriken für Profile
trendingExploreMetricsCalculator::calculateTrendingScores()Trending-Scores und Flags (Top 25%)
videosExploreTrendingVideosCalculator::calculateTrendingVideos()Trending-Videos nach View-Growth
categoriesExploreCategoryDetector::detectCategories()Kategorie-Erkennung aus Keywords
allAlle vier TypenKombination aller Berechnungen

Interne Schritte (all)

Bei --type=all werden alle vier Sub-Berechnungen in Isolation ausgefuehrt. Jeder Schritt hat einen eigenen try/catch, sodass ein Fehler in einem Schritt die restlichen nicht blockiert:

  1. Daily Growth Metrics für alle getrackten Profile berechnen
  2. Trending Scores berechnen und is_rising_star Flags setzen (via chunkById(1000))
  3. Trending Videos nach View-Velocity ranken
  4. Kategorien aus AI-Keywords und YouTube-Kategorien ableiten

Falls einzelne Schritte fehlschlagen, werden alle Fehler gesammelt und als zusammengefasste RuntimeException geworfen. Die erfolgreichen Schritte bleiben bestehen.

Schedule

Standalone: Nicht mehr eigenstaendig gescheduled — die Berechnung laeuft jetzt als Teil von pipeline:run (Phase 1A + Phase 3).

Manuell: Kann weiterhin direkt aufgerufen werden fuer Nachberechnungen:

php artisan explore:calculate --type=all    # Alle Typen
php artisan explore:calculate --type=videos # Nur Trending Videos

Location: app/Console/Commands/CalculateExploreMetrics.php


pipeline:run

Zentraler Orchestrator fuer die naechtliche Datenpipeline. Ersetzt 8 einzeln geschedulte Commands durch einen einzigen koordinierten Ablauf mit Bus::batch.

pipeline:run
{--dry-run : Zeigt was dispatched wuerde, ohne auszufuehren}
{--phase=all : Bestimmte Phase ausfuehren (all, scores, rollups, leaderboards, trending, videos, categories, phase3)}
{--skip-completeness-check : Metrics-Coverage-Pruefung ueberspringen}

Beispiele

# Volle Pipeline (normal, laeuft taeglich um 03:00 UTC)
php artisan pipeline:run

# Dry-Run: zeigt Job-Anzahl ohne Ausfuehrung
php artisan pipeline:run --dry-run

# Einzelne Phase manuell ausfuehren
php artisan pipeline:run --phase=phase3
php artisan pipeline:run --phase=scores
php artisan pipeline:run --phase=trending

Architektur (Hybrid-Ansatz)

Die Pipeline nutzt verschachtelte Bus::batch-Chains mit Abhaengigkeiten:

Batch 1A: CalculateProfileMetricsBatch (Score + Explore Daily) → parallel
Batch 1B: BuildDailyRollupsBatch (Dashboard Rollups) → parallel

After 1A → Batch 2C: CalculateTrendingScoresBatch → then Phase 3
After 1B → Batch 2AB: BuildLeaderboardsJob + BuildGlobalLeaderboardsJob

Phase 3 (eigenstaendiger Job):
→ Trending Flags
→ Trending Videos
→ Categories
→ Tag Cache
→ Explorer Refresh

Batch 1A und 1B laufen parallel. Batch 2C startet erst nach Abschluss aller Score-Jobs (Trending braucht aktuelle Scores). Phase 3 wird als eigenstaendiger RunPipelinePhase3-Job dispatched, damit er einen frischen Worker-Prozess mit vollem Memory erhaelt.

Memory-Safety (128 MB)

Der Server laeuft mit nur 128 MB memory_limit. Bei 448K+ Profilen wuerde ein ->pluck('id')->all() den Speicher sprengen. Deshalb nutzt die Pipeline chunkById-Streaming:

  1. Zwei-Level-Chunking: Profile werden in DB-Chunks von 5000 gestreamt, dann in Job-Chunks von 1000 (Scores + Explore Daily) bzw. 1000 (Trending) aufgeteilt
  2. Static Closures: Alle Batch-Callbacks sind static — verhindert Serialisierung des Console-Command-Objekts ($this) mit non-serializable Properties
  3. Early Return: Phasen ohne Profil-Abhaengigkeit (phase3, videos, categories, rollups, leaderboards) umgehen die Profil-Abfrage komplett
  4. Memory Cleanup: unset($profileJobs) nach Dispatch gibt Speicher fuer Batch 1B frei
  5. Dry-Run: Nutzt COUNT(*) statt Profil-IDs zu laden — zero Memory-Overhead
  6. Rollup-Jobs: DB::disableQueryLog() verhindert Query-Log-Akkumulation ueber 100+ Chunks, gc_collect_cycles() nach jedem Chunk gibt zyklische Referenzen frei

Phasen-Uebersicht

PhaseProfil-IDs noetigBeschreibung
scoresJa (chunkById)Score + Explore Daily fuer alle Profile
rollupsNeinDashboard-Rollups der letzten 7 Tage
leaderboardsNeinWorkspace + Globale Leaderboards
trendingJa (chunkById)Trending Scores berechnen
videosNeinTrending Videos nach View-Growth
categoriesNeinKategorien aus AI-Keywords
phase3NeinTrending Flags, Videos, Categories, Tag Cache, Explorer Refresh
allJa (chunkById)Volle Pipeline mit allen Phasen

Rollup-Jobs (Batch 1B)

BuildDailyRollupsBatch verarbeitet Dashboard-Rollups pro Tag und Watcher-ID-Range:

  • NTILE(32)-Splitting: PostgreSQL teilt Watchers in 32 gleich grosse Gruppen nach tatsaechlicher Zeilenanzahl (nicht ID-Range). 6 Tage × 32 Gruppen = 192 Sub-Jobs. Kleinere Ranges (~3K Watchers, ~30 Chunks) verhindern Timeouts zuverlaessig.
  • Chunk-Size 100: Innerhalb jedes Sub-Jobs werden Watchers in 100er-Chunks verarbeitet. Jedes Statement bleibt unter 30s auch unter Last.
  • DELETE + INSERT: Statt INSERT...ON CONFLICT DO UPDATE (Lock-Contention bei parallelen Jobs) wird pro Chunk DELETE + INSERT in einer Transaction ausgefuehrt. Atomisch durch DB::transaction(), sicher durch PostgreSQL MVCC.
  • Statement-Timeout 30s (SET LOCAL): SET LOCAL statement_timeout = '30s' innerhalb jeder Transaction (transaction-scoped). Ueberlebt DB-Reconnections zuverlaessig — Session-Level SET ging bei long-running Queue Workers verloren. Kein Chunk-Level-Retry — der aeussere Loop ueberspringt fehlgeschlagene Chunks.
  • Elapsed-Time-Guard (600s): Bricht den Loop nach 10 Minuten ab, auch wenn jeder Chunk erfolgreich war. Verhindert, dass viele langsame-aber-erfolgreiche Chunks (z.B. 60 × 15s = 900s) den 900s Job-Timeout treffen. Laesst 300s Puffer vor dem SIGALRM.
  • Early Abort: Nach 3 konsekutiven Chunk-Failures wird der Job abgebrochen.
  • $tries = 1, $timeout = 900s: Kein Job-Level-Retry — bei Timeout bringt ein erneuter Versuch nichts. Mit allowFailures() auf dem Batch ist ein einzelner fehlgeschlagener Range akzeptabel, die Leaderboard-Chain laeuft weiter.
  • DB-Index: Composite Index (date, watcher_id) auf dashboard_daily_rollups optimiert die DELETE-Query (Unique-Index ist (watcher_id, date) — falsche Spaltenreihenfolge fuer WHERE date = ? AND watcher_id BETWEEN).
  • Memory-Schutz: DB::disableQueryLog() verhindert Query-Log-Akkumulation, gc_collect_cycles() nach jedem Chunk fuer zyklische Referenzen. Wichtig bei 128 MB memory_limit.

Location: app/Jobs/Pipeline/BuildDailyRollupsBatch.php

Completeness-Check

Vor dem Start der vollen Pipeline (--phase=all) wird automatisch die Metrics-Coverage fuer gestern geprueft:

  • Vergleich: Trackbare Profile (tracking_enabled, nicht archived/blocked/sanitized) vs. Profile mit social_profile_daily_metrics Eintrag fuer gestern
  • Soft-Check: Bei Coverage unter Schwellwert wird eine Warnung geloggt, die Pipeline laeuft trotzdem weiter
  • Schwellwert: Konfigurierbar via PIPELINE_MIN_METRICS_COVERAGE (Default: 80%)
  • Admin-Sichtbarkeit: Coverage wird als "Metrics Coverage" Step in der Datenqualitaet-Gruppe auf /admin/update-status angezeigt
  • Ueberspringen: --skip-completeness-check fuer manuelle Runs ohne Coverage-Pruefung

Schedule

Taeglich um 03:00 UTC (verschoben von 01:00, damit Instagram-Collector-Jobs des Vortags vollstaendig abgeschlossen sind). Queue: pipeline (konfigurierbar via POSTBOX_PIPELINE_QUEUE). Worker-Timeout muss mindestens 1200s betragen (Phase 3 Timeout: 20 Min).

php artisan queue:work database --queue=pipeline --memory=128 --timeout=1200

Location: app/Console/Commands/RunNightlyPipeline.php, app/Jobs/Pipeline/RunPipelinePhase3.php