Watcher Sub-Components
Untergeordnete Livewire-Komponenten, die auf der Watcher-Detailseite (Watchers\Show) eingebettet werden.
Lazy Loading
Folgende Sub-Components nutzen #[Lazy] fuer asynchrones Laden per XHR (Viewport-basiert):
| Component | Placeholder |
|---|---|
ContactLinks | Inline-Skeleton (Link-Liste mit 4 Icon-Zeilen) |
RelatedChannels | Shared Slider: livewire.partials.slider-placeholder |
RelatedProfiles | Shared Slider: livewire.partials.slider-placeholder |
CrossPlatformRelated | Shared Slider: livewire.partials.slider-placeholder |
VideoStatsChart | Inline-Skeleton (Stats-Grid + Chart) |
AggregatedVideoChart | Inline-Skeleton in <flux:card> |
Die drei Related-Slider teilen sich denselben Flux UI Placeholder (<flux:skeleton.group animate="shimmer">, 7 Karten im w-48-Format) wie die Dashboard-Slider (TrendingSlider, RecommendedProfiles).
Nicht-Lazy: ApexCharts-Komponenten
ScoreChart und VideoScoreChart werden nicht lazy geladen. Beide nutzen wire:ignore fuer ApexCharts mit dynamischem CDN-Script-Loading. Livewires Lazy-Morph (Placeholder → Content) initialisiert wire:ignore-Elemente nicht korrekt — Alpine's init() feuert nicht und der Chart wird nicht gerendert. Diese Components werden daher eager geladen.
Testen von Lazy Components: Livewire::test() rendert initial den Placeholder. Fuer Assertions auf den echten Inhalt muss ->call('$refresh') aufgerufen werden, um einen normalen Render-Zyklus auszuloesen.
Watchers\ContactLinks
Kontakt-Links eines Profils anzeigen und verwalten. Admin-Workflow fuer Approve/Reject, User-Vorschlaege, Re-Parse.
View: resources/views/livewire/watchers/contact-links.blade.php
Location: app/Livewire/Watchers/ContactLinks.php
Public Properties
| Property | Typ | Default | Beschreibung |
|---|---|---|---|
$profileId | int | -- | ID des SocialProfile |
$watcherId | ?int | null | ID des Eltern-Watchers (optional) |
$newLinkInput | string | '' | Neuer Link: URL/Email/Handle Eingabe |
$newLinkType | string | 'social' | Neuer Link: Typ (social, email, url) |
$newLinkPlatform | string | '' | Neuer Link: Auto-detected Plattform |
$showAddForm | bool | false | Eingabeformular sichtbar |
$detectedPlatform | ?string | null | Auto-Erkennung: Plattform aus URL |
$detectedHandle | ?string | null | Auto-Erkennung: Handle aus URL |
Actions
| Method | Beschreibung | Berechtigung |
|---|---|---|
addLink() | Link hinzufuegen (Admin: direkt, User: Vorschlag) | Alle authentifizierten User |
toggleApproval(int $id) | Link genehmigen/ablehnen + Auto-Import triggern | Admin |
deleteLink(int $id) | Link loeschen | Admin |
reparseLinks() | Profil-Beschreibung erneut parsen | Admin |
retryImport(int $id) | Fehlgeschlagenen Import erneut versuchen | Admin |
editLink(int $id) | Plattform-Zuordnung aendern (Inline-Edit) | Admin |
updateLink() | Plattform-Aenderung speichern | Admin |
cancelEdit() | Inline-Edit abbrechen | Admin |
validateLink(int $id) | Link-URL auf Erreichbarkeit pruefen | Admin |
Computed Properties
| Property | Beschreibung |
|---|---|
links | Alle Links des Profils (Non-Admins: nur genehmigte) |
linkedWatcherUrls | Map link_id → Watcher-URL für verknuepfte Profile im selben Workspace |
Auto-Import bei Freigabe
Wenn ein Admin einen YouTube/Instagram-Kontaktlink freigibt (toggleApproval), wird maybeImportProfile() aufgerufen:
- Profil existiert: Handle wird direkt mit
linked_social_profile_idverknuepft,import_status = completed(Auto-Approval) - Profil nicht gefunden:
ImportContactLinkProfile-Job wird dispatcht,import_status = pending
UI-Elemente
- Interne Links: Verknuepfte Profile zeigen indigo-farbene Links mit
wire:navigatezum Watcher - Import-Status Badges: "Import..." (blau, pending) oder "Fehler" (rot, failed) mit Tooltip
- Retry-Button: Pfeil-Icon bei fehlgeschlagenen Imports (nur Admin)
User-Vorschlaege
Nicht-Admins koennen Links vorschlagen (source='user_suggested', suggested_by gesetzt). Admins werden via NotificationService::announceToAll() benachrichtigt. Vorgeschlagene Links erscheinen erst nach Admin-Genehmigung oeffentlich.
Links werden aus dem SocialProfileLink-Model geladen. Geparste Links (aus Profil-Beschreibungen via ProfileDescriptionParser) durchlaufen einen Approval-Workflow. Genehmigte Links (approved_at IS NOT NULL) werden in Explore-Profil-Cards angezeigt und fliessen ins Related-Scoring ein (approved=30pt, auto-parsed=25pt).
Watchers\CrossPlatformRelated
Cross-Platform Related Profiles: zeigt die Verknuepfung zwischen YouTube- und Instagram-Profilen desselben Creators.
View: resources/views/livewire/watchers/cross-platform-related.blade.php
Location: app/Livewire/Watchers/CrossPlatformRelated.php
Public Properties
| Property | Typ | Default | Beschreibung |
|---|---|---|---|
$watcher | Watcher | -- | Eltern-Watcher |
$relatedProfiles | Collection | -- | Cross-Platform Profile |
Nutzt das CrossPlatformRelatedProfile-Model. Ausschluss-Filter (blocked_at, sanitized_at, archived_at) werden auf die Related-Profile angewendet.
Watchers\RelatedChannels
Aehnliche YouTube-Kanaele zu einem Watcher. Berechnung ueber die YouTube Data API (Related Topics, Keywords).
View: resources/views/livewire/watchers/related-channels.blade.php
Location: app/Livewire/Watchers/RelatedChannels.php
Public Properties
| Property | Typ | Default | Beschreibung |
|---|---|---|---|
$watcher | Watcher | -- | Eltern-Watcher |
$relatedChannels | Collection | -- | Berechnete Related Channels |
Actions
| Method | Beschreibung |
|---|---|
calculate() | FindRelatedYouTubeChannels-Job dispatchen |
Der Button loest einen Background-Job aus (youtube-related-channels-Queue). Ergebnisse werden via RelatedProfilesCalculated-Event ueber Reverb zurueckgemeldet.
Ausschluss-Filter Pattern:
->whereHas('relatedProfile', fn ($q) => $q
->whereNull('blocked_at')
->whereNull('sanitized_at')
->whereNull('archived_at')
)
Location: app/Livewire/Watchers/RelatedChannels.php
Watchers\RelatedProfiles
Aehnliche Instagram-Profile zu einem Watcher. Matching basiert auf Keywords und Bio-Analyse.
View: resources/views/livewire/watchers/related-profiles.blade.php
Location: app/Livewire/Watchers/RelatedProfiles.php
Public Properties
| Property | Typ | Default | Beschreibung |
|---|---|---|---|
$watcher | Watcher | -- | Eltern-Watcher |
$relatedProfiles | Collection | -- | Berechnete Related Profiles |
Actions
| Method | Beschreibung |
|---|---|
calculate() | FindRelatedInstagramProfiles-Job dispatchen |
Gleiche Architektur wie RelatedChannels: Job-Dispatch, Reverb-Event, Ausschluss-Filter.
Watchers\ScoreChart
Postbox Score Verlauf mit Komponenten-Breakdown (Growth, Momentum, Consistency, Engagement).
View: resources/views/livewire/watchers/score-chart.blade.php
Location: app/Livewire/Watchers/ScoreChart.php
Public Properties
| Property | Typ | Default | Beschreibung |
|---|---|---|---|
$watcher | Watcher | -- | Eltern-Watcher |
$scoreData | array | [] | Score-Verlauf (date, score) |
$componentScores | array | [] | Komponenten-Breakdown |
Datenquelle: SocialProfileScore-Model. Der Chart zeigt den Gesamtscore (0-100) als Linie und die vier Komponenten als gestapelte Bereiche:
| Komponente | Gewichtung |
|---|---|
| Growth | 40% |
| Momentum | 30% |
| Consistency | 20% |
| Engagement | 10% |
Score-Status wird farblich kodiert: pending (grau), preliminary (gelb), stable (gruen), no_data (rot).
Watchers\VideoScoreChart
Video Performance Score Chart pro YouTube-Video auf der Watcher-Detailseite. Zeigt den Score-Verlauf der letzten 30 Tage mit Gap-Interpolation.
View: resources/views/livewire/watchers/video-score-chart.blade.php
Location: app/Livewire/Watchers/VideoScoreChart.php
Pattern
Adaptiert vom ScoreChart-Component (1:1 Pattern). Wesentliche Unterschiede:
| Aspekt | ScoreChart | VideoScoreChart |
|---|---|---|
| Datenquelle | SocialProfileScore | YouTubeVideoScore |
| Parameter | $socialProfileId (int) | $videoId (string) |
| "Aehnliche anzeigen"-Button | Ja | Nein |
| Followers-Check | Ja (Mindest-Follower) | Nein |
| Score-Erklaerung | Growth 40%, Momentum 30%, Consistency 20%, Engagement 10% | View Performance 40%, Engagement 25%, Growth 20%, Freshness 15% |
Public Properties
| Property | Typ | Default | Beschreibung |
|---|---|---|---|
$videoId | string | -- | YouTube Video ID |
$days | int | 30 | Zeitraum in Tagen |
Computed Properties
| Property | Beschreibung |
|---|---|
currentScore | Neuester YouTubeVideoScore-Eintrag mit Score |
scoreDelta | Differenz zwischen den letzten zwei Score-Werten |
chartEligible | true wenn >= 2 Score-Datenpunkte existieren |
chartData | Zwei Serien: solid (echte Datenpunkte) + dashed (interpolierte Luecken) |
Chart-Rendering
ApexCharts-Liniendiagramm (CDN) mit:
- Solid-Serie: Echte Score-Datenpunkte mit Delta-Berechnung
- Dashed-Serie: Linear interpolierte Werte fuer Tage ohne Score. Boundary-Points ueberlappen mit Solid-Serie fuer nahtlose Verbindung.
- Y-Achse: Fest 0-100
- Tooltip: Datum, Score, Delta zum Vortag
- Erklaerungsmodal: Video-spezifische Score-Gewichtung (40/25/20/15)
Integration
Eingebettet unterhalb jedes Video-Cards auf der Watcher-Detailseite:
<livewire:watchers.video-score-chart
:video-id="$video->video_id"
:key="'video-score-chart-' . $video->video_id"
/>
Wird nur gerendert wenn chartEligible (>= 2 Datenpunkte) UND currentScore existiert.
Tests
Location: tests/Feature/Livewire/Watchers/VideoScoreChartTest.php
| Test | Beschreibung |
|---|---|
| renders without errors (no scores) | Leerer State ohne Fehler |
| not eligible with < 2 data points | chartEligible = false bei 1 Datenpunkt |
| eligible with >= 2 data points | chartEligible = true, currentScore, scoreDelta korrekt |
| calculates score delta correctly | Delta = neuester Score - vorheriger Score |
| provides solid and dashed series | Gap-Interpolation erzeugt beide Serien |