System Jobs
Plattformübergreifende und systemweite Jobs: AI-Detection, Cross-Platform Matching, Profile Retry, Dashboard-Berechnung, Tag-Consolidation, Notifications und Tracking.
DetectProfileLanguage
Erkennt Sprache, Land, Kategorie, Keywords und Beschreibung eines Profils via Google Gemini AI. Rate-Limited über Laravel Queue Middleware.
Location: app/Jobs/DetectProfileLanguage.php
| Eigenschaft | Wert |
|---|---|
| Queue | ai-detection |
| Tries | 3 |
| Backoff | 1min, 5min, 15min |
| retryUntil | 24 Stunden |
| Unique | Ja (24h, detect-language-{profileId}) |
| Trigger | Scheduler (social:detect-languages --queue, alle 15 min) |
Rate Limiting
public function middleware(): array
{
// Rate limit konfigurierbar via AI_ENHANCER_RATE_PER_MINUTE (default: 15)
return [new RateLimited('ai-detection')];
}
Der Scheduler teilt das stündliche Limit in Chunks auf:
Schedule::command('social:detect-languages --queue --queue-limit='
.max(1, intdiv((int) config('services.gemini.hourly_limit', 100), 4)))
Ablauf
- Profil laden, Guards prüfen:
- Bereits vollständige YouTube-Daten (country + language)? Skip
- Kürzlich erkannt (innerhalb
AI_ENHANCER_COOLDOWN_DAYS, default 365)? Skip
ChannelLanguageDetector::detect()aufrufen (Gemini API)- Ergebnisse speichern:
detected_language/detected_country(nur wenn YouTube nichts liefert)ai_category+ai_category_confidenceai_keywords+ai_keywords_confidence(mit Tag-Alias-Auflösung)ai_description+ai_description_confidenceai_social_links(nur high-confidence Einträge)
detected_atTimestamp setzen
Tag-Alias-Auflösung
Keywords werden automatisch über TagAlias::resolve() aufgelöst, sodass Gemini-generierte Tags unter ihren konsolidierten Zielnamen gespeichert werden:
$updates['ai_keywords'] = array_values(array_unique(
array_map(fn ($kw) => TagAlias::resolve($kw), $result['keywords'])
));
FindCrossPlatformRelatedProfiles
Findet plattformübergreifende Related Profiles: YouTube-Kanäle zu Instagram-Profilen und umgekehrt. Nutzt ausschließlich lokale Daten (keine API-Calls).
Location: app/Jobs/CrossPlatform/FindCrossPlatformRelatedProfiles.php
| Eigenschaft | Wert |
|---|---|
| Queue | cross-platform-related |
| Tries | 3 |
| Backoff | 1min, 5min, 15min |
| Unique | Ja (1h, find-cross-platform-related-{profileId}) |
| Trigger | Scheduler (cross-platform:queue-related) / User-Klick / Auto-Fill |
Ablauf
- Profil mit Relations laden (
latestMetric,exploreMetrics,instagramKeywords) - Status auf
runningsetzen - Plattformspezifische Berechnung delegieren:
- YouTube ->
CrossPlatformRelatedCalculator::findInstagramForYouTube() - Instagram ->
CrossPlatformRelatedCalculator::findYouTubeForInstagram()
- YouTube ->
- Status auf
completedsetzen mitcross_platform_related_calculated_at RelatedProfilesCalculatedEvent broadcasten (mitcross_platform_youtube/cross_platform_instagramals Platform)
RetryInactiveProfileScrape
Versucht ein durch Fail-Streak deaktiviertes Profil erneut zu scrapen. Bei Erfolg wird das Profil reaktiviert, bei Misserfolg wird der nächste Retry-Termin berechnet oder das Profil archiviert.
Location: app/Jobs/RetryInactiveProfileScrape.php
| Eigenschaft | Wert |
|---|---|
| Queue | profile-retry |
| Tries | 1 |
| Timeout | 60s |
| Unique | Nein |
| Trigger | profiles:retry-inactive Command / Admin-Button |
Ablauf
- Profil laden, Guards prüfen (bereits reaktiviert? archiviert? leere URL?)
EnsureSocialProfileFromUrl::execute()aufrufen (Scrape-Versuch)- Erfolg: Profil vollständig reaktivieren +
ProfileReactivatedEvent dispatchen - Fehler: Retry-Schedule aktualisieren oder archivieren
Retry-Intervalle
// Konfigurierbar via config('postbox.profile_retry')
$initialRetryCount = 3; // Erste 3 Retries
$initialRetryDays = 14; // Alle 14 Tage
$monthlyRetryDays = 30; // Danach monatlich
$archiveAfterMonths = 6; // Nach 6 Monaten archivieren
| Retry # | Intervall | Beschreibung |
|---|---|---|
| 1-3 | 14 Tage | Initiale Phase |
| 4+ | 30 Tage | Monatliche Checks |
| Nach 6 Monaten | -- | Permanent archiviert (archived_at) |
Reaktivierung bei Erfolg
$profile->forceFill([
'tracking_enabled' => true,
'deactivated_at' => null,
'scrape_fail_streak' => 0,
'next_retry_at' => null,
'retry_count' => 0,
'api_status' => 'active',
]);
BuildOwnerDashboardSnapshots
Berechnet Dashboard-Rollups für einen einzelnen Owner. Wird von der Dashboard-View getriggert wenn Rollups veraltet sind.
Location: app/Jobs/BuildOwnerDashboardSnapshots.php
| Eigenschaft | Wert |
|---|---|
| Queue | default (database connection) |
| Tries | 1 |
| Timeout | 120s (2 Min) |
| Unique | Nein |
| Trigger | Dashboard Livewire Component (DashboardSnapshotRefresher) |
Hinweis: Dieser Job wurde von sync Connection auf die database Queue umgestellt. Auf der sync Connection gab es kein Timeout-Enforcement — der Job blockierte FPM-Worker fuer 2+ Stunden ohne Abbruch.
Ablauf
- Metriken-Range für Owner ermitteln (
DashboardSnapshotRefresher::metricsRangeForOwner) - Range auf max 12 Monate und bis gestern limitieren
dashboard:rollup-daily-metrics --skip-leaderboardsaufrufen (schnelles INSERT...SELECT)dashboard:rollup-leaderboardsnur für den letzten Tag aufrufen (statt volle Range)- Build-Marker aus Cache löschen
Optimierung (2026-03-12): Vorher rief der Job dashboard:rollup-daily-metrics ohne --skip-leaderboards auf, was automatisch dashboard:rollup-leaderboards für die gesamte 365-Tage-Range triggerte (~8.000 Queries, 600s+ Timeout). Jetzt wird das Leaderboard separat nur für den letzten Tag berechnet — reduziert die Laufzeit von 600s+ auf unter 30s.
ConsolidateTagChunk
Verarbeitet einen Chunk von AI-Tags durch Gemini für die Tag-Konsolidierung. High-Confidence Merges werden automatisch ausgeführt, Low-Confidence Vorschläge gehen an Admin-Review.
Location: app/Jobs/ConsolidateTagChunk.php
| Eigenschaft | Wert |
|---|---|
| Queue | default |
| Tries | 3 |
| Backoff | 2min, 5min, 15min |
| retryUntil | 12 Stunden |
| Rate Limit | RateLimited('tag-consolidation') |
| Trigger | tags:consolidate Command (wöchentlich, Sonntag 18:00) |
Ablauf
TagConsolidator::consolidateChunk()aufrufen (Gemini API)- High-Confidence Merges automatisch ausführen (
TagMerger::executeMerge) - Suppress-Aktionen immer ausführen (
TagMerger::executeSuppress) - Bei letztem Chunk: Admin-Notification über pending Low-Confidence Merges
SendNotificationEmail
Sendet Benachrichtigungs-E-Mails unter Berücksichtigung von Flood Guard, Quiet Hours und User-Preferences.
Location: app/Jobs/SendNotificationEmail.php
| Eigenschaft | Wert |
|---|---|
| Queue | emails |
| Tries | 5 |
| Backoff | 30s, 1min, 2min, 5min, 10min |
| Trigger | NotificationService |
Guards
- Flood Guard: Bei aktiver Pause ->
release(300)(5 min Delay) - User Preferences: E-Mail-Kanal für diesen Typ deaktiviert? -> Discard
- Quiet Hours: Innerhalb der Ruhezeiten? ->
release()bisnextActiveTime
RefreshSocialProfile
Aktualisiert Profil-Daten via Scraper. Nur für YouTube-Profile aktiv (Instagram läuft über Collector).
Location: app/Jobs/RefreshSocialProfile.php
| Eigenschaft | Wert |
|---|---|
| Queue | default |
| Trigger | Diverse (Rescrape-Button, etc.) |
Guards
- Profil existiert nicht? Skip
tracking_enabledistfalse? Skip- Platform ist Instagram? Skip (Collector-basiert)
- Leere
canonical_url? Skip
TrackMatomoPageView / TrackMatomoEvent
Server-seitiges Matomo-Tracking für Page Views und Custom Events.
Location: app/Jobs/TrackMatomoPageView.php, app/Jobs/TrackMatomoEvent.php
| Eigenschaft | Wert |
|---|---|
| Queue | matomo |
| Tries | 5 |
| Timeout | 15s |
| Backoff | 30s, 2min, 5min, 15min |
| Trigger | TrackPageView Middleware / diverse Services |
Fehlerbehandlung
Non-critical: Bei letztem Retry-Versuch wird nur eine Warning geloggt statt eines Error-Reports, um Noise bei Matomo-Downtime zu vermeiden:
if ($this->attempts() < $this->tries) {
throw $e; // Retry
}
Log::warning('Matomo page view tracking failed after all retries');
ImportContactLinkProfile
Importiert ein SocialProfile aus einem freigegebenen Kontaktlink. Erstellt bei Bedarf ein neues Profil (YouTube: Scrape, Instagram: minimal record) und verknüpft es mit einem Watcher im Workspace.
Location: app/Jobs/ImportContactLinkProfile.php
| Eigenschaft | Wert |
|---|---|
| Queue | imports-youtube |
| Tries | 0 (unbegrenzt) |
| maxExceptions | 3 |
| retryUntil | 24 Stunden |
| Backoff | 30s, 2min, 10min |
| Unique | Ja (contact-link-{linkId}) |
| Trigger | Admin-Approval in ContactLinks Component |
Ablauf
- Link laden, Guards prüfen (existiert? bereits verknüpft? importierbar?)
- Profil suchen/erstellen:
- YouTube:
EnsureSocialProfileFromUrl(vollständiger Scrape) - Instagram: minimaler Record (Collector füllt später)
- YouTube:
- Watcher prüfen/erstellen im Ziel-Workspace
- Link aktualisieren (
linked_social_profile_id,import_status = completed) - Auto-Approval: andere Links mit gleichem Handle+Platform werden auch verknüpft
- Cross-References: bei bidirektionalen Links →
CrossPlatformRelatedProfile-Einträge - Notification an Workspace + Broadcast Event
Fehlerbehandlung
- Quota-Errors (
quotaExceeded,rateLimitExceeded, HTTP 429): Release mit 1h Delay - Andere Fehler:
import_status = failed,import_errorgespeichert, Exception re-thrown failed()Method: Setztimport_status = failedals Fallback
ProcessYouTubeResearchBatch
Verarbeitet eine Batch von YouTube-Research-Keywords sequentiell. Fuer jedes Keyword wird die YouTube Search API abgefragt, optional mit Qualitaetsfilter (Subscriber/Video-Count).
Location: app/Jobs/ProcessYouTubeResearchBatch.php
| Eigenschaft | Wert |
|---|---|
| Queue | imports-youtube-priority |
| Tries | 3 |
| Timeout | 600s (10 min) |
| Unique | Ja (youtube-research-batch-{batchId}) |
| Trigger | Admin/YouTubeResearch/Index::startBatchSearch() |
Ablauf
- Alle
YouTubeResearchQuery-Eintraege der Batch laden (Statuspending) - Fuer jedes Keyword:
a. Status auf
runningsetzen b. YouTube Search API abfragen (max 1000 Ergebnisse, Paginierung uebernextPageToken) c. Optional:channels.listAPI fuer Qualitaetsfilter (50 Channels pro Chunk) d. Ergebnisse inresult_urls,filtered_countspeichern e.YouTubeResearchBatchProgress-Event broadcasten - Batch-Status auf
completedsetzen
Qualitaetsfilter
Wenn min_subscribers oder min_videos gesetzt sind, wird fuer die gefundenen Channel-IDs ein channels.list-Call gemacht (1 Quota-Unit pro Call, 50 Channels pro Chunk). Channels unterhalb der Schwellwerte werden herausgefiltert. filtered_count zeigt die Anzahl nach Filterung.
Fehlerbehandlung
- Per-Keyword-Fehler stoppen nicht die Batch (werden in
error_message/error_payloadgespeichert) - Quota-Fehler: Batch wird abgebrochen, Status
failed YouTubeResearchBatchProgress-Event wird auch bei Fehlern gesendet