Zum Hauptinhalt springen

Dashboard

Das Dashboard zeigt aggregierte Metriken fuer alle Watcher eines Workspaces. Die Daten werden ueber eine mehrstufige Rollup-Pipeline vorberechnet und als Snapshots gespeichert.

Rollup-Pipeline

Die Pipeline besteht aus drei Stufen:

social_profile_daily_metrics
--> dashboard_daily_rollups (Command: dashboard:rollup-daily-metrics)
--> dashboard_leaderboard_snapshots / dashboard_global_snapshots
(Command: BuildDashboardLeaderboardSnapshots)

Stufe 1: Daily Metrics

Rohdaten pro Profil und Tag in social_profile_daily_metrics:

FeldBeschreibung
followers_countAktuelle Follower-Zahl
view_countGesamte Views (YouTube)
video_countAnzahl Videos (YouTube)
post_countAnzahl Beitraege (Instagram)
following_countAnzahl Following (Instagram)
comment_countKommentare (YouTube)
rawVollstaendige Roh-Payload als JSON

Location: app/Models/SocialProfileDailyMetric.php

Stufe 2: Dashboard Daily Rollups

Der Command dashboard:rollup-daily-metrics aggregiert die Rohdaten pro Workspace.

php artisan dashboard:rollup-daily-metrics

Schedule: Taeglich 00:10 UTC

Fuer jeden Workspace werden die Metriken aller zugehoerigen Watcher zusammengefasst: Summen, Durchschnitte und Deltas gegenueber Vortagen.

Location: app/Console/Commands/BuildDashboardDailyRollups.php

Stufe 3: Leaderboard Snapshots

Aus den Rollups werden Winners/Losers und Absolute-Rankings berechnet. Zwei Snapshot-Tabellen:

TabelleBeschreibung
dashboard_leaderboard_snapshotsWorkspace-spezifische Leaderboards
dashboard_global_snapshotsGlobale Leaderboards (Tops & Flops)

Location: app/Console/Commands/BuildDashboardLeaderboardSnapshots.php

Metriken

Das Dashboard unterstuetzt sechs Metriken:

KeyLabelPlattform
followersFollowerYouTube + Instagram
viewsViewsYouTube
videosVideosYouTube
postsBeitraegeInstagram
followingFollowingInstagram
scorePostbox ScoreYouTube + Instagram

Zeitraeume

Das Dashboard arbeitet mit festen 7-Tage-Perioden und unterstuetzt Date-Navigation (vor/zurueck).

  • Clamped to "gestern": Das End-Datum ist maximal der Vortag (nie heute)
  • Fallback: Wenn fuer den gewaehlten Tag keine Daten existieren, wird der naechste verfuegbare Vortag verwendet
  • Fruehestes Datum: Daten ab 2026-01-08 verfuegbar
$yesterday = now()->subDay()->startOfDay();
// End-Datum nie groesser als gestern
if ($end->greaterThan($yesterday)) {
$end = $yesterday;
}

Winners/Losers (Growth Delta)

Die Dashboard-Listen zeigen Profile mit dem groessten Wachstum (Winners) bzw. groessten Verlust (Losers) innerhalb der Periode.

  • Winners: Eintraege mit delta >= 0, sortiert nach Delta absteigend
  • Losers: Eintraege mit delta <= 0, sortiert nach Delta aufsteigend
  • Maximum: 25 Eintraege pro Liste (Performance-Limit)

Jeder Eintrag enthaelt:

  • Profil-Name, Handle, Platform
  • Aktueller Wert und Delta
  • Tracking-Status (ob der User das Profil bereits beobachtet)
  • Link zum Watcher (falls vorhanden)

Top-Listen (Absolute)

Neben den Growth-Listen gibt es Absolute-Rankings:

ListeSortierung
absoluteFollowersFollower absteigend
absoluteFollowingFollowing absteigend
absoluteVideosVideo-Anzahl absteigend
absolutePostsBeitrags-Anzahl absteigend

Favoriten-Filter

Der Favoriten-Filter arbeitet zur Laufzeit, nicht auf Snapshot-Ebene:

public string $favoritesFilter = 'all'; // 'all' | 'favorites'

Wenn favorites aktiv, werden nur Profile angezeigt, die der User als Favorit markiert hat. Die Filterung passiert auf den bereits geladenen Snapshot-Daten.

Location: app/Livewire/Dashboard/Index.php

Chart-Daten (Entwicklung)

Der Chart zeigt den aggregierten Verlauf der gewaehlten Metrik ueber die Periode. Datenpunkte enthalten:

[
'date' => '2026-02-01',
'value' => 125000,
'delta' => 1500, // Optional, nur wenn != 0
]

Y-Achsen-Ticks werden automatisch berechnet mit "nice numbers" (1, 2, 5, 10, ...) und angepasster Formatierung:

  • Unter 1M: Gruppierte Ganzzahlen
  • Ab 1M: Kompakte Notation (1,2M)

Forward-Fill und Fallback-Fenster

Da Instagram- und YouTube-Profile nicht taeglich aktualisiert werden (Rotation), nutzt die Chart-Berechnung ein Forward-Fill-Verfahren:

  1. Pre-Fetch: Rollup-Daten werden ab startDate - fallbackDays geladen (nicht erst ab Chartstart)
  2. Pre-Seed: Fuer jedes Profil wird der letzte bekannte Wert aus der Vorperiode als Startwert uebernommen
  3. Forward-Fill: An Tagen ohne frische Daten wird der letzte bekannte Wert weitergetragen, solange die Luecke innerhalb des Fallback-Fensters liegt

Das Fallback-Fenster (fallbackDays) berechnet sich als Maximum aller Rotationszyklen + 1 Tag Marge:

$maxRotation = max(
instagram_rotation_days, // Default: 3
youtube_rotation_days, // Default: 3
low_priority_rotation_days, // Default: 7
);
return $maxRotation + 1; // = 8 Tage

Warum: Low-Priority-Profile (< 100 Follower) werden nur alle 7 Tage aktualisiert. Ohne ausreichendes Fallback-Fenster wuerden diese Profile an Tagen ohne Daten 0 zum Chart-Gesamtwert beitragen, was kuenstlich niedrige Werte am Chartanfang erzeugt (Hockey-Stick-Artefakt).

Newcomer-Filter

Profile deren first_metric_date nach oder am Chartstart liegt, werden aus der Chart-Summe ausgeschlossen. Ohne diesen Filter wuerden grosse Kanaele, die mitten in der Periode erstmals erscheinen, die Gesamtsumme schlagartig erhoehen.

Performance-Architektur

HasSnapshotData Trait

Gemeinsame Snapshot-Logik fuer Dashboard und TopsFlops ist in app/Concerns/HasSnapshotData.php extrahiert (~300 Zeilen). Beide Komponenten nutzen diesen Trait.

Zentrale Methoden:

MethodeBeschreibung
decodeSnapshotList()JSON-Spalte in Array decodieren
filterByPlatform()In-Memory Plattform-Filter (youtube/instagram/all)
filterFavorites()In-Memory Favoriten-Filter mit O(1) isset()-Lookup
filterWinnersByDelta()Nur Eintraege mit delta >= 0
filterLosersByDelta()Nur Eintraege mit delta <= 0
resolveProfileImageUrls()Batch-Query fuer Profilbilder (lokale Varianten → externe URL → Placeholder)
loadSnapshotWithFallback()3-stufige Snapshot-Kaskade (exakt → naechst-kleiner → neuester)

In-Memory-Filter-Pattern

Plattform- und Favoriten-Filter arbeiten auf dem bereits geladenen Snapshot im Speicher. Nur ein Metrik- oder Tier-Wechsel loedt einen neuen Snapshot aus der Datenbank.

updatedPlatform() / updatedFavoritesFilter()
--> applyFiltersToSnapshot() [In-Memory, 0 DB-Queries]

updatedMetric() / updatedTier()
--> loadDashboard() / loadData() [Neuer Snapshot aus DB]

O(1) Lookup-Maps

Favoriten- und Tracking-Pruefungen nutzen array_flip() + isset() statt in_array():

$favoriteIdMap = array_flip($favoriteIds); // [42 => 0, 99 => 1, ...]
isset($favoriteIdMap[$profileId]); // O(1) statt O(n)

Location: app/Concerns/HasSnapshotData.php, app/Livewire/Dashboard/Index.php, app/Livewire/TopsFlops/Index.php

Datenfluss

UI (Dashboard/Index)
--> Livewire Component laedt Snapshot aus DB
--> dashboard_leaderboard_snapshots (Workspace-spezifisch)
--> dashboard_global_snapshots (Global, fuer Tops & Flops)
--> Vorberechnet durch scheduled Commands
--> Basierend auf social_profile_daily_metrics

Relevante Commands

# Rollups berechnen (taeglich 00:10 UTC)
php artisan dashboard:rollup-daily-metrics

# Leaderboard-Snapshots erstellen
php artisan dashboard:build-leaderboard-snapshots

Location: app/Console/Commands/BuildDashboardDailyRollups.php, app/Console/Commands/BuildDashboardLeaderboardSnapshots.php, app/Livewire/Dashboard/Index.php


Horizontaler Slider unterhalb des Dashboard-Charts mit aktuell trendigen Profilen, unabhaengig vom User-Tracking.

Datenquelle

  • Profile mit Postbox Score 80–100 aus der aktuellsten Score-Berechnung
  • Zufaellige Auswahl von 25 Profilen, sortiert nach Score
  • Ausschluss-Filter: notExcluded() (blocked, sanitized, archived)
  • User mit ≤1.000 Watchern sehen eigene Profile nicht im Slider

Cache

1h TTL pro User (trending_slider:{user_id}).

UI

Alpine.js-basierter horizontaler Scroll mit Chevron-Buttons. Jede Karte zeigt Profilbild, Platform-Badge, Score-Badge, Verified/PRO-Badge, Handle und Follower-Count. "Hinzufuegen"-Button mit Workspace-Kapazitaetspruefung.

Location: app/Livewire/Dashboard/TrendingSlider.php, resources/views/livewire/dashboard/trending-slider.blade.php


Personalisierte Empfehlungen ("Koennte dich interessieren")

Kategorie-basierter Empfehlungs-Slider der Profile vorschlaegt, die zu den Interessen des Users passen.

Architektur: Global Pool + Per-User Personalisierung

Zweistufige Architektur fuer maximale Performance — ein globaler Pool (geteilt fuer alle User) mit per-User-Filterung:

graph LR
A["Globaler Pool\n(Top 1.000, 6h Cache)"] --> B["Per-User Filter"]
B --> C["Getrackte raus"]
C --> D["Kategorien priorisieren"]
D --> E["5 pro Kat., 25 total"]
E --> F["Score-Sortierung"]

Stufe 1 — Globaler Pool (Cache-Key: recommended_profiles_global_pool, 6h TTL):

  1. Top 1.000 Profile nach Postbox Score (≥50) aus social_profile_scores
  2. JOIN auf explore_profile_metrics (Kategorie, Mindest-Follower) — ohne "other"
  3. Filter: notExcluded(), tracking_enabled
  4. Kategorie-Metadata vorberechnet (Icon, deutsches Label aus explore_categories)
  5. 1 schwere Query alle 6 Stunden, geteilt fuer alle User

Stufe 2 — Per-User Personalisierung (Cache-Key: recommended_profiles:{user_id}, 6h TTL):

  1. User's getrackte Profile-IDs laden (1 Query, Hash-Map fuer O(1)-Lookup)
  2. User's Kategorie-Praeferenzen ermitteln (1 Query, GROUP BY Frequenz)
  3. PHP: getrackte Profile aus Pool entfernen
  4. PHP: nach User-Kategorien sortieren (haeufigste zuerst)
  5. PHP: max. 5 pro Kategorie, 25 total
  6. PHP: finale Sortierung nach Postbox Score absteigend

Performance: ~5ms pro User statt ~300ms+ (normal) / TIMEOUT (Admin mit 469k Watchern).

UI

Gleiche Alpine.js-Slider-Mechanik wie TrendingSlider. Zusaetzlich: Kategorie-Icon auf jeder Karte, Kategorie-Label im Info-Bereich. Lazy Loading (#[Lazy]) mit Flux UI Skeleton-Platzhaltern (Shimmer-Animation).

Location: app/Livewire/Dashboard/RecommendedProfiles.php, resources/views/livewire/dashboard/recommended-profiles.blade.php