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: Timeout 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 koennen Memory-Spikes verursachen.
  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

Der Pipeline-Worker laeuft mit --memory=4096 (4 GB). Trotzdem nutzt die Pipeline chunkById-Streaming als Best Practice, um Memory-Spikes bei 448K+ Profilen zu vermeiden:

  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 in einem einzigen DELETE + INSERT...SELECT ueber den gesamten Range — kein PHP-seitiger Chunk-Loop mehr. Ein Sub-Job pro Tag (NTILE=1), verschiedene Tage laufen weiterhin parallel.

  • Single-Statement-Pattern: Ein DELETE FROM dashboard_daily_rollups WHERE date=? AND watcher_id BETWEEN ? AND ? + ein INSERT INTO ... SELECT ... FROM watchers JOIN workspaces JOIN LATERAL (watcher_sources LIMIT 1) JOIN social_profile_daily_metrics WHERE watchers.id BETWEEN ? AND ?, gemeinsam in einer Transaction. PostgreSQL fuehrt beide Statements serverseitig aus — null PHP-Memory-Druck.
  • Idempotent-Overwrite: Das BETWEEN-DELETE loescht bestehende Rows im Range, bevor der INSERT eine frische Snapshot-Menge schreibt. Echte Orphan-Rollups koennen wegen cascadeOnDelete auf dem watcher_id-FK nicht existieren.
  • NTILE=1 (Single-Writer-per-Date): Jeder Tag wird von einem einzigen Sub-Job verarbeitet. Verschiedene Tage laufen weiterhin parallel (7-Tage-Fenster = 7 parallele Sub-Jobs, jeweils auf unterschiedlichen Index-Pages). Eliminiert LWLock-Contention auf den date-keyed Indizes komplett.
  • statement_timeout 600s + lock_timeout 120s (SET LOCAL): Grosses Budget fuer das einzelne Statement (~52K Rows pro Tag). SET LOCAL ist transaction-scoped und ueberlebt DB-Reconnections zuverlaessig.
  • $tries = 2, $timeout = 900s: Ein Retry fuer transiente DB-Probleme (Autovacuum, Load-Spike). Mit allowFailures() auf dem Batch ist ein einzelner fehlgeschlagener Tag 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::connection()->disableQueryLog() verhindert Query-Log-Akkumulation in long-running Queue Workers. Keine weiteren Memory-Massnahmen noetig — ohne Chunk-Loop gibt es keine Treiber-State-Akkumulation mehr.
  • Rationale (OOM-Fix 2026-04-13): Vorher wurde der Range in ~520 Chunks zu 100 Watchern pro Sub-Job verarbeitet, jeweils in eigener Transaction mit eigenem DELETE + INSERT. Production zeigte dauerhaft Allowed memory size of 536870912 bytes exhausted bei PDO $statement->execute() — der Chunk-Loop akkumulierte Treiber-State, Carbon-Objekte, Monolog- und Pulse-Query-Buffers, bis das 512 MB Worker-Limit geknackt wurde. Der Wechsel auf ein einzelnes Statement pro Sub-Job eliminiert den Loop komplett.

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-Konfiguration (Forge):

php8.4 artisan queue:work database --queue=pipeline --memory=4096 --timeout=3600 --tries=3 --sleep=3
ParameterWertGrund
--memory4096 MBHeadroom fuer Leaderboard-Jobs mit grossen Coverage-Arrays
--timeout3600sMuss groesser sein als der laengste Job-Timeout (Leaderboards: 2400s)
--tries3Worker-Level Fallback; Jobs definieren eigene $tries (1-3)

Job-Timeouts:

Job$tries$timeoutBeschreibung
CalculateProfileMetricsBatch3300s (5 Min)Score + Explore Daily pro Chunk
CalculateTrendingScoresBatch3300s (5 Min)Trending Scores pro Chunk
BuildDailyRollupsBatch2900s (15 Min)Dashboard Rollups pro Tag (Single-Statement, NTILE=1)
RunPipelinePhase311200s (20 Min)Trending Flags, Videos, Categories, Tags, Explorer
BuildLeaderboardsJob22400s (40 Min)Workspace-Leaderboards (Subprocess-Isolation)
BuildGlobalLeaderboardsJob32400s (40 Min)Globale Leaderboards (Subprocess-Isolation)

Wichtig: retry_after in config/queue.php (Default: 3600s) muss groesser als der laengste Job-Timeout (2400s) sein, sonst markiert der Queue-Driver den Job vorzeitig als "abandoned" und dispatched ihn erneut.

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