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
| Option | Beschreibung |
|---|---|
date? | Einzelnes Datum (Y-m-d) |
--from= | Start-Datum für Range |
--to= | End-Datum für Range |
Interne Schritte
- Datum/Range bestimmen, auf gestern clampen (keine Future-Daten)
- 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.
- Falls ein Tag fehlt, wird der Vortag als Fallback genutzt (typisch bei Instagram-Rotation alle 2 Tage)
- PostgreSQL statement_timeout: 10 Minuten pro Statement, verhindert endlos laufende Queries
- Auto-Chain: Nach jedem Rollup wird automatisch
dashboard:rollup-leaderboardsaufgerufen (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
| Option | Beschreibung |
|---|---|
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
- Exakte Leader/Chart-Logik des Dashboards replizieren (inkl. Vortags-Fallback)
- 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.
- Coverage-Watcher-IDs materialisieren:
getCoverageWatcherIds()ermittelt Watcher mit ausreichender Datenabdeckung einmalig und cached das Ergebnis in-memory. VerwendetwhereIn()statt eingebetteter Subquery, um wiederholte PostgreSQL-Evaluierung zu vermeiden. - Start-Zeitraum an frühestes Rollup im Bereich clampen
- Falls keine Rollups im Zeitraum: neuestes Rollup vor dem End-Datum verwenden
- Snapshots pro Owner + Zeitraum + Metrik + End-Datum speichern in
dashboard_leaderboard_snapshots
Dashboard Periods
Snapshots werden für alle drei Dashboard-Perioden generiert:
| Period | Beschreibung |
|---|---|
3d | 3 Tage (Daten von vorgestern bis gestern) |
7d | 7 Tage (Default-Ansicht) |
14d | 14 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
| Option | Beschreibung |
|---|---|
date? | Einzelnes Datum |
--from= | Start-Datum |
--to= | End-Datum |
--metric= | Nur diese Metrik verarbeiten (wird intern fuer Subprocess-Isolation genutzt) |
Interne Schritte
- Nur 7d-Period für vereinfachtes globales Dashboard
- Metriken: followers, views, videos, posts, following, score
- 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.
- Tier-Filterung (all, micro, small, medium, large, mega) pro Metrik
- Coverage-Profile-IDs materialisieren:
getCoverageProfileIds()ermittelt Profile mit ausreichender Datenabdeckung einmalig und cached das Ergebnis in-memory. - Absolute Metriken in einer einzigen Query berechnen (followers_count, video_count, etc.) — danach in PHP nach Tier partitionieren
- Snapshots in
dashboard_global_snapshotsspeichern
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
| Option | Beschreibung |
|---|---|
--date= | Einzelnes Datum (Y-m-d) |
--from= / --to= | Datumsbereich |
--platform= | youtube oder instagram |
--force | Vorhandene Scores überschreiben |
--dry-run | Keine 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
| Komponente | Gewichtung | Beschreibung |
|---|---|---|
| Growth | 40% | Tägliche Wachstumsrate vs. Tier-Erwartung |
| Momentum | 30% | Wachstumsbeschleunigung (letzte 7d vs. vorherige 7d) |
| Consistency | 20% | Datenabdeckung in den letzten 14 Tagen |
| Engagement | 10% | Plattformspezifische Aktivitätsmetriken |
Tier-Definitionen
| Tier | Follower-Range |
|---|---|
| micro | 0 - 10K |
| small | 10K - 100K |
| medium | 100K - 500K |
| large | 500K - 1M |
| mega | 1M+ |
Score-Status
| Status | Datenpunkte | Beschreibung |
|---|---|---|
pending | < 7 | Kein Score berechnet |
preliminary | 7-9 | Vorläufiger Score |
stable | 10+ | Stabiler Score |
no_data | 0 | Keine 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
| Option | Beschreibung |
|---|---|
--type= | daily, trending, videos, categories, all |
--force | Neuberechnung auch wenn kürzlich berechnet |
Berechnungstypen
| Typ | Service | Beschreibung |
|---|---|---|
daily | ExploreMetricsCalculator::calculateDailyMetrics() | Tägliche Growth-Metriken für Profile |
trending | ExploreMetricsCalculator::calculateTrendingScores() | Trending-Scores und Flags (Top 25%) |
videos | ExploreTrendingVideosCalculator::calculateTrendingVideos() | Trending-Videos nach View-Growth |
categories | ExploreCategoryDetector::detectCategories() | Kategorie-Erkennung aus Keywords |
all | Alle vier Typen | Kombination 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:
- Daily Growth Metrics für alle getrackten Profile berechnen
- Trending Scores berechnen und is_rising_star Flags setzen (via
chunkById(1000)) - Trending Videos nach View-Velocity ranken
- 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:
- Zwei-Level-Chunking: Profile werden in DB-Chunks von 5000 gestreamt, dann in Job-Chunks von 1000 (Scores + Explore Daily) bzw. 1000 (Trending) aufgeteilt
- Static Closures: Alle Batch-Callbacks sind
static— verhindert Serialisierung des Console-Command-Objekts ($this) mit non-serializable Properties - Early Return: Phasen ohne Profil-Abhaengigkeit (
phase3,videos,categories,rollups,leaderboards) umgehen die Profil-Abfrage komplett - Memory Cleanup:
unset($profileJobs)nach Dispatch gibt Speicher fuer Batch 1B frei - Dry-Run: Nutzt
COUNT(*)statt Profil-IDs zu laden — zero Memory-Overhead - Rollup-Jobs:
DB::disableQueryLog()verhindert Query-Log-Akkumulation ueber 100+ Chunks,gc_collect_cycles()nach jedem Chunk gibt zyklische Referenzen frei
Phasen-Uebersicht
| Phase | Profil-IDs noetig | Beschreibung |
|---|---|---|
scores | Ja (chunkById) | Score + Explore Daily fuer alle Profile |
rollups | Nein | Dashboard-Rollups der letzten 7 Tage |
leaderboards | Nein | Workspace + Globale Leaderboards |
trending | Ja (chunkById) | Trending Scores berechnen |
videos | Nein | Trending Videos nach View-Growth |
categories | Nein | Kategorien aus AI-Keywords |
phase3 | Nein | Trending Flags, Videos, Categories, Tag Cache, Explorer Refresh |
all | Ja (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 ChunkDELETE+INSERTin einer Transaction ausgefuehrt. Atomisch durchDB::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-LevelSETging 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)aufdashboard_daily_rollupsoptimiert die DELETE-Query (Unique-Index ist(watcher_id, date)— falsche Spaltenreihenfolge fuerWHERE 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 MBmemory_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_metricsEintrag 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-statusangezeigt - Ueberspringen:
--skip-completeness-checkfuer 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