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:
| Feld | Beschreibung |
|---|---|
followers_count | Aktuelle Follower-Zahl |
view_count | Gesamte Views (YouTube) |
video_count | Anzahl Videos (YouTube) |
post_count | Anzahl Beitraege (Instagram) |
following_count | Anzahl Following (Instagram) |
comment_count | Kommentare (YouTube) |
raw | Vollstaendige 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:
| Tabelle | Beschreibung |
|---|---|
dashboard_leaderboard_snapshots | Workspace-spezifische Leaderboards |
dashboard_global_snapshots | Globale Leaderboards (Tops & Flops) |
Location: app/Console/Commands/BuildDashboardLeaderboardSnapshots.php
Metriken
Das Dashboard unterstuetzt sechs Metriken:
| Key | Label | Plattform |
|---|---|---|
followers | Follower | YouTube + Instagram |
views | Views | YouTube |
videos | Videos | YouTube |
posts | Beitraege | |
following | Following | |
score | Postbox Score | YouTube + 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:
| Liste | Sortierung |
|---|---|
absoluteFollowers | Follower absteigend |
absoluteFollowing | Following absteigend |
absoluteVideos | Video-Anzahl absteigend |
absolutePosts | Beitrags-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:
- Pre-Fetch: Rollup-Daten werden ab
startDate - fallbackDaysgeladen (nicht erst ab Chartstart) - Pre-Seed: Fuer jedes Profil wird der letzte bekannte Wert aus der Vorperiode als Startwert uebernommen
- 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:
| Methode | Beschreibung |
|---|---|
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
Trending Slider
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):
- Top 1.000 Profile nach Postbox Score (≥50) aus
social_profile_scores - JOIN auf
explore_profile_metrics(Kategorie, Mindest-Follower) — ohne "other" - Filter:
notExcluded(),tracking_enabled - Kategorie-Metadata vorberechnet (Icon, deutsches Label aus
explore_categories) - 1 schwere Query alle 6 Stunden, geteilt fuer alle User
Stufe 2 — Per-User Personalisierung (Cache-Key: recommended_profiles:{user_id}, 6h TTL):
- User's getrackte Profile-IDs laden (1 Query, Hash-Map fuer O(1)-Lookup)
- User's Kategorie-Praeferenzen ermitteln (1 Query, GROUP BY Frequenz)
- PHP: getrackte Profile aus Pool entfernen
- PHP: nach User-Kategorien sortieren (haeufigste zuerst)
- PHP: max. 5 pro Kategorie, 25 total
- 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