Watchers
Watchers sind die zentrale Entitaet in Postbox. Ein Watcher repraesentiert ein beobachtetes Social-Media-Profil innerhalb eines Workspaces. Die Verknuepfung zwischen Profil und Workspace erfolgt ueber WatcherSource.
Datenmodell
| Model | Tabelle | Beschreibung |
|---|---|---|
Watcher | watchers | Container im Workspace (Name, erstellt von) |
WatcherSource | watcher_sources | Verknuepfung Watcher zu SocialProfile (Platform, Input-URL) |
WatcherImportRun | watcher_import_runs | Batch-Import-Tracking (Status, Zaehler) |
WatcherImportItem | watcher_import_items | Einzelne URLs eines Import-Runs |
WatcherImportFailure | watcher_import_failures | Fehlgeschlagene Import-Versuche mit Fehlerdetails |
Location: app/Models/Watcher.php, app/Models/WatcherSource.php, app/Models/WatcherImportRun.php
Watcher-Erstellung
Einzelner Watcher
Ueber die Watcher-Detailseite oder direkte URL-Eingabe. Der Flow:
- User gibt eine YouTube- oder Instagram-URL ein
YouTubeUrlParser::parse()bzw.InstagramUrlParser::parse()extrahiert den Key (Channel-ID, Handle)ImportWatcherFromUrlJob wird dispatcht- Job ruft
EnsureSocialProfileFromUrlauf, um das Profil global anzulegen oder wiederzuverwenden Watcher+WatcherSourcewerden im Workspace erstellt
Location: app/Jobs/ImportWatcherFromUrl.php, app/Services/Social/Actions/EnsureSocialProfileFromUrl.php
Multi-Import (Textarea)
User kann mehrere URLs gleichzeitig importieren, eine pro Zeile. YouTube- und Instagram-URLs koennen gemischt werden.
// Limit: 10.000 Zeilen (konfigurierbar via config('watchers.multi_import_limit'))
private const MULTI_IMPORT_LIMIT = 10000;
Der Import-Flow:
- User oeffnet das Multi-Import-Modal und fuegt URLs ein
- Jede Zeile wird durch
YouTubeUrlParserundInstagramUrlParsergeparsed (Fallback-Kette) - Duplikate innerhalb des Inputs werden erkannt und uebersprungen
- Ein
WatcherImportRunwird erstellt mit Statusrunning - Pro URL wird ein
WatcherImportItemgespeichert - YouTube-URLs werden als
ImportWatcherFromUrlJobs aufimports-youtube-prioritydispatcht - Instagram-URLs werden als Collector-Jobs via
CollectorJobDispatcherdispatcht
Location: app/Livewire/Watchers/Index.php (Methode submitMultiImport())
CSV-Upload
Zusaetzlich zum Textarea-Import koennen CSV-Dateien hochgeladen werden. URLs werden aus allen Zellen extrahiert, auch mehrere pro Zelle (getrennt durch Whitespace oder Komma).
// CSV-Verarbeitung via Spatie SimpleExcel
$rows = SimpleExcelReader::create($path)->getRows();
CSV- und Textarea-Input werden zusammengefuehrt und gemeinsam verarbeitet.
Location: app/Livewire/Watchers/Index.php (Methode collectCsvLines())
URL-Erkennung
Die Parser erkennen verschiedene URL-Formate:
| Plattform | Unterstuetzte Formate |
|---|---|
| YouTube | youtube.com/@handle, youtube.com/channel/UC..., youtube.com/c/name, youtu.be/... |
instagram.com/handle, instagram.com/p/... |
Jeder Parser liefert:
key: Deduplizierungs-Schluessel (Channel-ID oder Handle)normalizedUrl: Kanonische URL fuer stabiles Scraping
Location: app/Services/Social/YouTubeUrlParser.php, app/Services/Social/InstagramUrlParser.php
Import Runs: Status-Lifecycle
Ein WatcherImportRun durchlaeuft diese Status:
| Status | Beschreibung |
|---|---|
running | Import laeuft, Jobs werden abgearbeitet |
paused | Import pausiert (z.B. wegen API-Quota, pause_reason = 'quota') |
completed | Alle Items verarbeitet |
failed | Import fehlgeschlagen |
Zaehler auf dem Import Run:
| Feld | Beschreibung |
|---|---|
total_count | Gesamtzahl erkannter URLs |
processed_count | Bisher verarbeitete URLs |
added_count | Erfolgreich hinzugefuegt |
duplicates_in_input_count | Duplikate im Input |
already_in_workspace_count | Bereits im Workspace vorhanden |
global_reused_count | Global existierendes Profil wiederverwendet |
failed_count | Fehlgeschlagene Imports |
Import Failures
Fehlgeschlagene Import-Versuche werden in watcher_import_failures protokolliert. Die UI zeigt maximal 50 Failures pro Run an, um die Payload-Groesse zu begrenzen. Bei Quota-Pause wird die Failure-Liste nicht geladen.
Location: app/Models/WatcherImportFailure.php
Rescrape-Feature
Admins koennen einen Priority-Rescrape fuer einzelne Watcher ausloesen. Der Flow:
- Admin klickt "Rescrape" auf der Watcher-Detailseite
- Pro Source wird ein neuer
WatcherImportRunerstellt - YouTube-Sources werden auf
imports-youtube-prioritydispatcht - Instagram-Sources werden mit erhoehter Prioritaet als Collector-Jobs dispatcht (
force: true)
// Instagram: Priority ueber aktuellem Maximum
$maxPriority = (int) CollectorJob::query()
->where('status', 'queued')
->max('priority');
Location: app/Livewire/Watchers/Show.php (Methode queuePriorityRescrape())
Move-Flow
Watcher koennen zwischen Workspaces verschoben werden (Einzel- und Bulk-Move):
- User waehlt Watcher aus und waehlt Ziel-Workspace
WatcherMover::moveWatchers()prueft auf Duplikate im Ziel- Bei Duplikat: Watcher werden zusammengefuehrt (merged)
- Favoriten- und Admin-Workspace sind als Ziel gesperrt
Location: app/Services/Watchers/WatcherMover.php, app/Livewire/Watchers/Index.php (Methode moveSelectedWatchers())
Delete-Flow
Beim Loeschen eines Watchers:
WatcherSource-Eintraege werden entferntWatcherwird geloescht- Das globale
SocialProfilebleibt erhalten (wird weiter getrackt) - Sonderfall Favoriten-Workspace: Beim Loeschen wird auch der Favorit-Marker entfernt
// Admin-Hinweis nach dem Loeschen
if (Gate::allows('admin')) {
$this->dispatch('show-toast', [
'message' => 'Admin: Das Profil bleibt intern weiter getrackt.',
'type' => 'info',
]);
}
Location: app/Livewire/Watchers/Index.php (Methode delete()), app/Livewire/Watchers/Show.php (Methode deleteWatcher())
Blocked-Watcher-Sichtbarkeit
Watchers, deren verknuepfte Profile von einem Admin gesperrt wurden (blocked_at), sind fuer regulaere User nicht sichtbar. Admins sehen gesperrte Watcher weiterhin mit einem roten Banner.
Verhalten
| Kontext | Regulaere User | Admins |
|---|---|---|
| Watcher-Liste | Gesperrte Watcher ausgeblendet | Sichtbar mit rotem Banner |
| Dashboard-Uebersicht | Gesperrte Watcher ausgeblendet | Sichtbar |
| Workspace-Statistiken | Gesperrte Watcher nicht gezaehlt | Gezaehlt |
| Watcher-Detailseite (Direktzugriff) | Redirect zur Watcher-Liste + Toast | Sichtbar mit rotem Banner |
Technische Umsetzung
- Scope:
Watcher::withoutBlockedProfiles()— filtert Watchers mit gesperrten Profilen perwhereDoesntHave('sources.profile', blocked_at) - Methode:
Watcher::hasBlockedProfile()— prueft ob ein Watcher gesperrte Profile hat (nutzt vorgeladene Relations wenn verfuegbar) - Show-Seite: Nicht-Admins werden beim Mount redirected, Admins sehen
$isProfileBlocked = trueBanner
Location: app/Models/Watcher.php, app/Livewire/Watchers/Index.php, app/Livewire/Watchers/Show.php
UI-Komponenten
| Komponente | Route | Beschreibung |
|---|---|---|
Watchers\Index | /watchers, /workspace/{workspace} | Watcher-Liste mit Multi-Import, Filter, Sortierung, Bulk-Aktionen |
Watchers\Show | /watcher/{watcher} | Detailseite mit Metriken-Charts, Profil-Info, Related, Score |
Watchers\ContactLinks | (Sub-Component) | Kontakt-Links verwalten |
Watchers\RelatedChannels | (Sub-Component) | Aehnliche YouTube-Kanaele |
Watchers\RelatedProfiles | (Sub-Component) | Aehnliche Instagram-Profile |
Watchers\CrossPlatformRelated | (Sub-Component) | Cross-Platform Verknuepfungen |
Watchers\ScoreChart | (Sub-Component) | Postbox Score Verlauf |
Watchers\AggregatedVideoChart | (Sub-Component) | Channel-weite aggregierte Video-Performance (Summe der taeglichen View/Like-Deltas ueber alle Videos) |
Watchers\VideoStatsChart | (Sub-Component) | Einzelvideo-Statistiken mit Views/Likes-Chart, Engagement-Rate und Rotations-Status (active/archived/new/removed) |
Watchers\AddYouTubeVideoByUrl | (Sub-Component) | Admin: Video per YouTube-URL manuell hinzufuegen |
Location: app/Livewire/Watchers/, resources/views/livewire/watchers/
Admin: Video manuell hinzufuegen
Admins koennen auf der Watcher-Detailseite manuell YouTube-Videos per URL hinzufuegen. Voraussetzung: YouTube-Profil mit PRO + Auto-Sync aktiviert.
Flow:
- Admin klickt „Video hinzufuegen" im Aktionen-Dropdown
- Flux Named-Modal (
add-video-modal) oeffnet sich - Admin gibt YouTube-URL ein (watch, youtu.be, shorts)
YouTubeVideoUrlResolver::resolve()validiert, fetcht und speichert das Video- Ergebnis wird im Modal angezeigt (erstellt/existiert/Fehler)
Technische Details:
- Trigger:
flux:modal.trigger name="add-video-modal"im Aktionen-Dropdown - Modal:
AddYouTubeVideoByUrlLivewire-Component mit Named Flux Modal - Formular-Reset beim Schliessen via
x-on:close="$wire.resetForm()"
Location: app/Livewire/Watchers/AddYouTubeVideoByUrl.php, app/Services/Social/YouTube/YouTubeVideoUrlResolver.php