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: Timeout 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 koennen Memory-Spikes verursachen.
- 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
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:
- 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 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 ?+ einINSERT 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 wegencascadeOnDeleteauf demwatcher_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 LOCAList 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)aufdashboard_daily_rollupsoptimiert die DELETE-Query (Unique-Index ist(watcher_id, date)— falsche Spaltenreihenfolge fuerWHERE 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 exhaustedbei 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_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-Konfiguration (Forge):
php8.4 artisan queue:work database --queue=pipeline --memory=4096 --timeout=3600 --tries=3 --sleep=3
| Parameter | Wert | Grund |
|---|---|---|
--memory | 4096 MB | Headroom fuer Leaderboard-Jobs mit grossen Coverage-Arrays |
--timeout | 3600s | Muss groesser sein als der laengste Job-Timeout (Leaderboards: 2400s) |
--tries | 3 | Worker-Level Fallback; Jobs definieren eigene $tries (1-3) |
Job-Timeouts:
| Job | $tries | $timeout | Beschreibung |
|---|---|---|---|
CalculateProfileMetricsBatch | 3 | 300s (5 Min) | Score + Explore Daily pro Chunk |
CalculateTrendingScoresBatch | 3 | 300s (5 Min) | Trending Scores pro Chunk |
BuildDailyRollupsBatch | 2 | 900s (15 Min) | Dashboard Rollups pro Tag (Single-Statement, NTILE=1) |
RunPipelinePhase3 | 1 | 1200s (20 Min) | Trending Flags, Videos, Categories, Tags, Explorer |
BuildLeaderboardsJob | 2 | 2400s (40 Min) | Workspace-Leaderboards (Subprocess-Isolation) |
BuildGlobalLeaderboardsJob | 3 | 2400s (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