Zum Hauptinhalt springen

Admin & Wartung Commands

Commands für die Administration: Workspace-Management, Profil-Wartung, Pruning, Retry-Logik und Notifications.


watchers:backfill-admin-workspace

Spiegelt alle Social Profiles in den Admin-Workspace (ID: 999999999999). Der Admin-Workspace hat damit Zugriff auf alle Profile im System.

watchers:backfill-admin-workspace
{--platform= : Filter (youtube, instagram)}
{--dry-run : Nur Statistiken anzeigen}

Beispiele

# Alle Profile spiegeln
php artisan watchers:backfill-admin-workspace

# Nur YouTube-Profile
php artisan watchers:backfill-admin-workspace --platform=youtube

# Statistiken anzeigen ohne Änderungen
php artisan watchers:backfill-admin-workspace --dry-run

Optionen

OptionBeschreibung
--platform=youtube oder instagram
--dry-runZeigt Total/Existing/Missing Counts ohne Watcher zu erstellen

Interne Schritte

  1. Admin-Workspace via AdminWorkspaceManager::ensureAdminWorkspace() sicherstellen
  2. Alle Profile mit external_id laden, optional nach Plattform filtern
  3. Vorhandene Admin-Watcher zählen
  4. Fehlende Watcher via ensureProfileWatcher() erstellen (in 200er Chunks)
  5. Progress-Bar und Fehler-Logging

Schedule

Stündlich mit .withoutOverlapping().

Location: app/Console/Commands/BackfillAdminWorkspaceWatchers.php


watchers:resume-imports

Setzt pausierte Watcher-Import-Runs automatisch fort, sobald paused_until erreicht ist. Dies ist ein Schedule-Call (kein Artisan Command), definiert in routes/console.php.

watchers:resume-imports

Interne Schritte

  1. WatcherImportRun Records mit status = 'paused' laden
  2. Filter: paused_until IS NULL OR paused_until <= now()
  3. Status auf running setzen, paused_until und pause_reason leeren
  4. Bereits in der Queue "geparkte" Jobs laufen automatisch weiter

Hinweise

  • Es werden bewusst keine neuen ImportWatcherFromUrl Jobs dispatched
  • Die bestehenden Queue-Jobs werden durch den Status-Wechsel freigeschaltet
  • Verhindert Doppel-Dispatch und damit falsche Counts / doppelte API-Calls

Schedule

Alle 5 Minuten mit .withoutOverlapping().

Location: routes/console.php (Schedule::call)


imports:retry-failed

Recovery-Command für fehlgeschlagene Import-Runs. Erstellt frische ImportWatcherFromUrl-Jobs mit dem aktuellen Code ($tries = 0, retryUntil = 48h), anstatt alte Payloads via queue:retry erneut zu pushen.

imports:retry-failed
{--run= : Komma-getrennte Run-IDs (z. B. --run=747,748,749)}
{--from= : Startzeitpunkt für Run-Suche (z. B. 2026-02-07)}
{--to= : Endzeitpunkt für Run-Suche (z. B. 2026-02-10)}
{--workspace= : Filter auf Workspace-ID}
{--source= : URL-Quelle: "research" liest URLs direkt aus youtube_research_queries}
{--chunk=500 : Anzahl Jobs pro Batch}
{--delay=0 : Sekunden Verzögerung zwischen Batches}
{--queue=imports-youtube : Ziel-Queue für die neuen Jobs}
{--include-failed-jobs : Auch Laravel failed_jobs-Tabelle durchsuchen}
{--dry-run : Nur anzeigen, was passieren würde}

Beispiele

# Bestimmte Runs wiederholen (Vorschau)
php artisan imports:retry-failed --run=747,748,749 --dry-run

# Zeitraum-basierte Suche
php artisan imports:retry-failed --from=2026-02-07 --to=2026-02-10

# Mit failed_jobs-Tabelle, 200er Batches, 5s Pause
php artisan imports:retry-failed --run=750 --include-failed-jobs --chunk=200 --delay=5

# Bestimmter Workspace, andere Queue
php artisan imports:retry-failed --from=2026-02-07 --to=2026-02-10 --workspace=42 --queue=imports-youtube-priority

# Research-URLs als Quelle (Recovery nach Admin-WS-Guard-Bug)
php artisan imports:retry-failed --source=research --from=2026-02-10 --to=2026-02-14 --dry-run

# Research-URLs importieren (Admin-Workspace als Default-Ziel)
php artisan imports:retry-failed --source=research --from=2026-02-10 --to=2026-02-14

Optionen

OptionTypDefaultBeschreibung
--run=string--Komma-getrennte Run-IDs
--from=date--Start-Datum (alternativ zu --run)
--to=date--End-Datum (alternativ zu --run)
--workspace=int--Workspace-Filter (bei --source=research: Admin-WS als Default)
--source=string--URL-Quelle: research liest aus youtube_research_queries
--chunk=int500Jobs pro Dispatch-Batch
--delay=int0Sekunden Pause zwischen Batches
--queue=stringimports-youtubeZiel-Queue
--include-failed-jobsflag--Auch failed_jobs-Tabelle durchsuchen
--dry-runflag--Vorschau ohne Dispatch

URL-Quellen

Standard-Modus (ohne --source)

Der Command sammelt URLs aus drei Quellen in Prioritaetsreihenfolge:

QuelleTabelleBeschreibung
1. Failureswatcher_import_failuresURLs aus failed()-Callback (zuverlaessigste Quelle)
2. Pending Itemswatcher_import_itemsNoch nicht verarbeitete URLs
3. Failed Jobsfailed_jobsFallback: URL per Regex aus serialisiertem Payload extrahiert

Duplikate werden automatisch entfernt (URL als Key).

Research-Modus (--source=research)

Liest URLs direkt aus youtube_research_queries.result_urls (JSON-Spalte). Filtert auf Queries mit gesetztem imported_at im angegebenen Datumsbereich. Nützlich für Recovery wenn Imports lautlos fehlgeschlagen sind und weder watcher_import_failures noch watcher_import_items Eintraege enthalten (z.B. nach dem Admin-WS-Guard-Bug).

  • Zeigt eine Tabelle aller gefundenen Research-Queries (ID, Query, URL-Anzahl, Import-Datum)
  • URLs werden query-übergreifend dedupliziert
  • Standard-Ziel: Admin-Workspace (AdminWorkspaceManager::ADMIN_WORKSPACE_ID)

Interne Schritte

  1. Run-IDs ermitteln (via --run oder --from/--to Query) — oder Research-Queries laden bei --source=research
  2. Run-Statistiken als Tabelle anzeigen (bzw. Query-Übersicht bei Research)
  3. URLs aus allen Quellen sammeln und deduplizieren
  4. Bei --dry-run: Vorschau (max 20 URLs) anzeigen und beenden
  5. Interaktive Bestaetigung
  6. Neuen WatcherImportRun erstellen (Status running)
  7. WatcherImportItem-Records in 1000er-Chunks einfuegen
  8. ImportWatcherFromUrl-Jobs in konfigurierbaren Batches dispatchen (Progress-Bar)
  9. Ergebnis loggen

Warum nicht queue:retry?

queue:retry re-pushed die originale Payload mit baked-in maxTries: 3. Aenderungen am Job-Code (z.B. $tries = 0) haben keinen Effekt auf bereits serialisierte Jobs. imports:retry-failed erstellt komplett neue Jobs mit dem aktuellen Code.

Schedule

Manuell (kein automatischer Schedule).

Location: app/Console/Commands/RetryFailedImportRun.php


watchers:prune-by-metrics

Löscht Watcher global deren Latest-Metriken die angegebenen Schwellwerte/Ranges matchen. Verwaiste Social Profiles werden physisch gelöscht (Metriken + Bilder + Dateien).

watchers:prune-by-metrics {type}
{--followers=} {--following=} {--videos=} {--posts=}
{--min-followers=} {--max-followers=}
{--min-following=} {--max-following=}
{--min-videos=} {--max-videos=}
{--min-posts=} {--max-posts=}
{--confirm : Bestätigung überspringen}

Beispiele

# Instagram Watcher mit 0/0/0 Metriken löschen
php artisan watchers:prune-by-metrics instagram --followers=0 --following=0 --posts=0

# YouTube Watcher mit 0 Followern und 0 Videos
php artisan watchers:prune-by-metrics youtube --followers=0 --videos=0

# Follower-Range ohne Prompt
php artisan watchers:prune-by-metrics youtube --min-followers=1000 --max-followers=5000 --confirm

Optionen

OptionBeschreibung
typePlattform: instagram oder youtube
--followers=Exakter Follower-Wert
--min-followers= / --max-followers=Follower-Range
--following= / --min-following= / --max-following=Following-Filter
--videos= / --min-videos= / --max-videos=Video-Filter
--posts= / --min-posts= / --max-posts=Post-Filter
--confirmInteraktive Bestätigung überspringen

Interne Schritte

  1. Mindestens ein Metrik-Filter erforderlich
  2. Watcher und watcher_sources global löschen (über alle Workspaces)
  3. Verwaiste Social Profiles hard-deleten (analog zum Admin "Final Delete")
  4. Auch wenn keine Watcher matchen: Prüfung auf verwaiste Profile (mit Bestätigung)

Schedule

Manuell (kein automatischer Schedule).

Location: app/Console/Commands/PruneWatchersByMetrics.php


watchers:prune-to-max

Löscht Watcher global bis ein Maximum pro Plattform erreicht ist. Entfernt die Profile mit den niedrigsten Follower-Zahlen zuerst.

watchers:prune-to-max {type}
{--max= : Maximale Watcher-Anzahl}

Beispiele

# YouTube auf 250 Watcher begrenzen
php artisan watchers:prune-to-max youtube --max=250

# Instagram auf 500 Watcher begrenzen
php artisan watchers:prune-to-max instagram --max=500

Optionen

OptionBeschreibung
typePlattform: instagram oder youtube
--max=Maximale Watcher-Anzahl (Pflicht)

Interne Schritte

  1. Sortierung nach Latest followers_count (niedrigste zuerst)
  2. Überzählige Watcher und watcher_sources global löschen
  3. Verwaiste Social Profiles hard-deleten (Metriken + Bilder + Dateien)
  4. Interaktive Bestätigung vor dem Löschen

Schedule

Manuell.

Location: app/Console/Commands/PruneWatchersToMax.php


profiles:retry-inactive

Retry-Mechanismus für Profile die durch Fail-Streaks deaktiviert wurden. Archiviert Profile die zu lange inaktiv sind (> 6 Monate).

profiles:retry-inactive
{--platform= : Filter (youtube, instagram)}
{--limit=1000 : Maximale Profile pro Run}
{--dry-run : Vorschau ohne Job-Dispatch}

Beispiele

# Standard-Lauf
php artisan profiles:retry-inactive

# Nur YouTube, Vorschau
php artisan profiles:retry-inactive --platform=youtube --dry-run

# Begrenzter Lauf
php artisan profiles:retry-inactive --limit=100

Optionen

OptionBeschreibung
--platform=youtube oder instagram
--limit=1000Max Profile pro Run
--dry-runTabelle mit Profilen anzeigen, keine Jobs dispatchen

Fail-Streak-Cooldown (vor Deaktivierung)

Bevor ein Profil deaktiviert wird (Streak < Threshold), greift ein Cooldown-Mechanismus in den Daily-Scrape-Commands (social:scrape-daily-followers, social:queue-daily-instagram):

PhaseVerhaltenBedingung
Normal (Streak 0-2)Täglich gescraptscrape_fail_streak < 3
Cooldown (Streak 3-13)Nur alle 3 Tagescrape_fail_streak >= 3, basierend auf last_scrape_failed_at
DeaktivierungProfil wird deaktiviertscrape_fail_streak >= 14

Die Cooldown-Logik basiert auf last_scrape_failed_at (nicht auf dem Streak-Zähler), weil der Streak nicht inkrementiert wird wenn das Profil übersprungen wird.

Konfiguration:

# Ab welchem Fail-Streak der Cooldown greift (Default: 3)
POSTBOX_FAIL_STREAK_COOLDOWN_AFTER=3

# Tage zwischen Retry-Versuchen im Cooldown (Default: 3)
POSTBOX_FAIL_STREAK_COOLDOWN_DAYS=3

Retry-Strategie (nach Deaktivierung)

PhaseIntervallBedingung
Initial (Retry 1-3)14 Tageretry_count < 3
Extended (Retry 4+)30 Tage (monatlich)retry_count >= 3
Archive-deactivated_at > 6 Monate

Interne Schritte

  1. Phase 1: Archivieren - Profile mit deactivated_at > 6 Monate archivieren
    • Setzt archived_at, archive_reason, next_retry_at = null
  2. Phase 2: Retry - Profile mit next_retry_at <= now() laden
    • Filter: archived_at IS NULL, tracking_enabled = false
    • RetryInactiveProfileScrape Jobs auf profile-retry Queue dispatchen
    • Sortierung nach next_retry_at ASC

Konfiguration

POSTBOX_PROFILE_RETRY_FAIL_STREAK_THRESHOLD=14
POSTBOX_PROFILE_RETRY_ARCHIVE_AFTER_MONTHS=6

Schedule

Täglich um 04:00 UTC mit .withoutOverlapping().

Location: app/Console/Commands/RetryInactiveProfiles.php


profiles:sanitize

Automatische Deaktivierung von Low-Value Profilen basierend auf Metrik-Regeln. Zwei Phasen: Neue Kandidaten sanitizen + bereits sanitized Profile re-checken.

profiles:sanitize
{--platform= : Filter (youtube, instagram)}
{--dry-run : Vorschau ohne Ausführung}
{--limit=5000 : Maximale Profile pro Run}

Beispiele

# Standard-Lauf
php artisan profiles:sanitize

# Nur Instagram, Vorschau
php artisan profiles:sanitize --platform=instagram --dry-run

# Begrenzter Lauf
php artisan profiles:sanitize --limit=1000

Sanitization-Regeln

PlattformBedingungAktion
YouTubevideo_count < 1 AND followers_count < 100Deaktivieren
Instagrampost_count < 1 AND followers_count < 100Deaktivieren

Schutz-Mechanismen

SchutzBeschreibung
Min. AlterProfil muss >= min_age_days alt sein
Min. Datenpunkte>= min_data_points Metriken-Einträge erforderlich
Blocked/Archived/DeactivatedWerden übersprungen
Admin Overridesanitize_checked_at IS NOT NULL AND sanitized_at IS NULL = permanent geschützt

Interne Schritte

  1. Phase 1: Neue Kandidaten -- Aktive Profile gegen Regeln prüfen, bei Match deaktivieren
    • Setzt sanitized_at, sanitize_reason, tracking_enabled = false
    • Dispatcht ProfileSanitized Event (Broadcast an betroffene User)
  2. Phase 2: Re-Check -- Bereits sanitized Profile alle 3 Monate erneut prüfen
    • Falls Regeln nicht mehr zutreffen: Profil reaktivieren
    • Dispatcht ProfileUnsanitized Event

Admin Override Flow

  1. Admin findet sanitized Profil in /admin/social-profiles
  2. Admin klickt "Enable" -> enableTracking() setzt sanitized_at = null, behält sanitize_checked_at
  3. Sanitizer überspringt Profile mit sanitize_checked_at IS NOT NULL AND sanitized_at IS NULL

Schedule

Täglich um 04:30 UTC (nach profiles:retry-inactive).

Location: app/Console/Commands/SanitizeInactiveProfiles.php


cross-platform:queue-related

Queued Cross-Platform Related Profile Berechnungen (Bulk oder Einzelprofil). Findet Instagram-Profile für YouTube-Channels und umgekehrt, nur über lokale Daten (keine API-Calls, keine Quota-Kosten).

cross-platform:queue-related
{--profile= : Einzelnes Profil per ID queuen}
{--platform= : Source-Plattform (youtube, instagram)}
{--days= : Nur Profile nicht berechnet in letzten N Tagen (Default: config, 90)}
{--limit=500 : Max Profile pro Run}
{--force : Neuberechnung erzwingen}

Optionen

OptionTypDefaultBeschreibung
--profile=intEinzelnes Profil per ID queuen (Bulk-Filter werden ignoriert)
--platform=stringbeideSource-Plattform: youtube oder instagram
--days=int90 (Config)Stale-Schwelle in Tagen (config('postbox.cross_platform.stale_days'))
--limit=int500Maximale Anzahl Profile pro Bulk-Lauf
--forceflagNeuberechnung erzwingen, auch für kürzlich berechnete Profile

Beispiele

# Einzelnes Profil manuell berechnen (sofort dispatched)
php artisan cross-platform:queue-related --profile=12345

# Standard Bulk-Lauf (alle Plattformen, 90 Tage stale, Limit 500)
php artisan cross-platform:queue-related

# Nur YouTube → Instagram (findet Instagram-Profile zu YouTube-Channels)
php artisan cross-platform:queue-related --platform=youtube

# Nur Instagram → YouTube (findet YouTube-Channels zu Instagram-Profilen)
php artisan cross-platform:queue-related --platform=instagram

# Force Recalculation für alle, Limit 100
php artisan cross-platform:queue-related --force --limit=100

# Profile die in den letzten 30 Tagen nicht berechnet wurden
php artisan cross-platform:queue-related --days=30

Matching-Score (0-100)

KriteriumPunkteBeschreibung
Keywords30AI-detected Keywords Match
Category25AI-detected Category Match
Favorites15Bonus für favorisierte Profile
Follower Tier10Ähnliche Follower-Größenordnung
Name Similarity10Handle/Titel-Ähnlichkeit
Language/Country10Sprach-/Land-Match

Interne Logik

  1. Single-Profile Mode (--profile=ID): Lädt das Profil direkt, prüft ob youtube/instagram, dispatched FindCrossPlatformRelatedProfiles Job
  2. Bulk Mode: Nutzt followers_count direkt aus social_profiles (kein teurer Metrics-Join), filtert nach tracking_enabled, notExcluded() und Stale-Schwelle
  3. Memory-Safe: Nur IDs werden geladen (pluck('id')) statt vollständiger Eloquent-Models — ermöglicht Limits von 1M+ ohne OOM. Dispatch in 1000er-Chunks.
  4. Priorität: Noch nie berechnete Profile zuerst (nach Followern absteigend), dann veraltete

Job

Dispatched FindCrossPlatformRelatedProfiles auf Queue cross-platform-related.

PropertyWert
Queuecross-platform-related
Tries3
Unique For3600 (1 Stunde)
Backoff[60, 300, 900] (1min, 5min, 15min)

Schedule

Täglich um 06:00 UTC mit --limit=200.

Location: app/Console/Commands/QueueCrossPlatformRelated.php


cross-platform:auto-fill-related

Dispatched Cross-Platform Related Jobs in kleinen Batches für graduellen Ausbau. Läuft alle 5 Minuten und füllt automatisch Lücken auf (keine API-Kosten).

cross-platform:auto-fill-related
{--batch=5 : Anzahl Profile pro Run}
{--dry-run : Vorschau ohne Jobs zu dispatchen}

Optionen

OptionTypDefaultBeschreibung
--batch=int5Anzahl Profile pro Durchlauf
--dry-runflagNur Anzeige, keine Jobs dispatchen

Beispiele

# Standard: 5 Profile pro Lauf
php artisan cross-platform:auto-fill-related

# Größerer Batch (z.B. initialer Backfill)
php artisan cross-platform:auto-fill-related --batch=20

# Vorschau: welche Profile würden berechnet?
php artisan cross-platform:auto-fill-related --dry-run

Interne Logik

  1. Queue-Guard: Überspringt, wenn Queue bereits ≥ batch * 3 offene Jobs hat
  2. Phase 1 — Noch nie berechnete Profile (nach Followern absteigend)
  3. Phase 2 — Veraltete Profile (älteste zuerst, stale_days aus Config, Default 90 Tage)
  4. Output: Für jedes Profil: Name, Plattform, Follower-Anzahl und Grund (never calculated / stale)

Schedule

Alle 5 Minuten mit ->withoutOverlapping().

Location: app/Console/Commands/AutoFillCrossPlatformRelated.php


social:recalculate-related-scores

Berechnet Relevance-Scores für bestehende Related-Beziehungen (YouTube + Instagram) basierend auf aktuellen Follower-Daten neu. Führt keine neuen API-Suchen durch — aktualisiert nur die Scores bestehender Beziehungen.

social:recalculate-related-scores
{--platform=all : Plattform (youtube, instagram, all)}
{--only-existing : Nur Profile mit bestehenden Related-Beziehungen}
{--dry-run : Vorschau ohne Jobs zu dispatchen}
{--sync : Synchron ausführen statt Queue}

Optionen

OptionTypDefaultBeschreibung
--platform=stringallPlattform-Filter: youtube, instagram oder all
--only-existingflagNur Profile mit mindestens einer bestehenden Related-Beziehung
--dry-runflagNur Anzeige, keine Jobs dispatchen
--syncflagSynchron ausführen (statt über Queue), nützlich für Debugging

Beispiele

# Alle Plattformen, über Queue
php artisan social:recalculate-related-scores

# Nur Instagram, nur Profile mit bestehenden Beziehungen
php artisan social:recalculate-related-scores --platform=instagram --only-existing

# YouTube synchron (direkt, ohne Queue)
php artisan social:recalculate-related-scores --platform=youtube --sync

# Vorschau
php artisan social:recalculate-related-scores --dry-run

Interne Logik

  1. YouTube: Dispatched RecalculateRelatedChannelScores pro YouTube-Profil auf Queue youtube-related-channels
  2. Instagram: Dispatched RecalculateRelatedInstagramScores pro Instagram-Profil auf Queue instagram-related-profiles
  3. --only-existing: Prüft youtube_related_channels / instagram_related_profiles Tabellen für existierende Beziehungen
  4. --sync: Führt die Job-handle()-Methode direkt aus (nützlich für einzelne Profile beim Debugging)
  5. Fortschrittsbalken für beide Plattformen

Score-Neuberechnung (YouTube)

KriteriumPunkteBeschreibung
Base Score (Existenz)40Channel existiert in Postbox
Subscriber Bonus0-25Tier-basiert (1K → 3, 10K → 6, ..., 10M+ → 25)
Title Similarity0-5similar_text() > 30%
Topic Match10Wenn search_query vorhanden
Position Estimate10Durchschnittliche Position

Score-Neuberechnung (Instagram)

KriteriumPunkteBeschreibung
Base Score (Existenz)25Bestehende Beziehung
Popularity Bonus0-25Tier-basiert (1K → 3, ..., 1M+ → 25)
Follower Similarity0-15Log-Scale (gleiche Größenordnung = 15)
Name Similarity0-15similar_text() > 30%
Same Profile Type0-10Business/Creator/Private Match
Language/Country0-10+5 pro Match

Schedule

Manuell (nach größeren Datenänderungen oder Follower-Backfills).

Location: app/Console/Commands/RecalculateRelatedScores.php


queue:prune-stale

Entfernt veraltete Queue-Jobs aus Collector-Queue und/oder Laravel Jobs-Tabelle. Schützt First-Time-Imports vor versehentlichem Löschen.

queue:prune-stale
{--before= : Zeitpunkt (Datum + Uhrzeit)}
{--scope= : collector, laravel, both}
{--source= : Collector-Source Filter}
{--queue= : Laravel Queue Filter}
{--delete-duplicates : Duplicate queued Jobs entfernen}
{--duplicates-only : Nur Duplikate entfernen, kein Time-Pruning}
{--dry-run : Vorschau}

Beispiele

# Collector-Jobs vor bestimmtem Zeitpunkt löschen
php artisan queue:prune-stale --before="2026-01-24 12:00:00" --scope=collector --dry-run

# Beide Scopes, mit Duplikat-Bereinigung
php artisan queue:prune-stale --before="2026-01-24 12:00:00" --scope=both --delete-duplicates

# Nur Duplikate entfernen
php artisan queue:prune-stale --scope=both --duplicates-only

Schedule

Manuell.

Location: app/Console/Commands/PruneStaleQueueJobs.php


notifications:cleanup

Löscht abgelaufene und alte Notifications und Announcements.

notifications:cleanup
{--dry-run : Vorschau ohne Löschen}

Beispiele

# Standard Cleanup
php artisan notifications:cleanup

# Vorschau
php artisan notifications:cleanup --dry-run

Cleanup-Regeln

KategorieBedingung
Expired Notificationsexpires_at < now()
Old Notifications> 90 Tage ohne Expiry
Expired Announcementsexpires_at < now()
Old Announcements> 90 Tage ohne Expiry
Old Announcement Reads> 30 Tage

Schedule

Täglich um 03:00 UTC.

Location: app/Console/Commands/CleanupNotifications.php


vantage:prune

Memory-effizientes Pruning der vantage_jobs Tabelle. Ersetzt den Vendor-Command (houdaslassi/vantage), der bei grossen Tabellen (45+ GB) durch Eloquent Model-Hydration mit serialisierten Payloads OOM verursacht. Nutzt batched raw SQL DELETE ohne Model-Loading.

vantage:prune
{--days= : Jobs der letzten X Tage behalten (Default: Config oder 30)}
{--hours= : Jobs der letzten X Stunden behalten (ueberschreibt --days)}
{--status= : Nur Jobs mit bestimmtem Status prunen (completed, failed, processing)}
{--keep-processing : Jobs mit Status "processing" immer behalten}
{--dry-run : Vorschau ohne Loeschung}
{--force : Bestaetigungsprompt ueberspringen}

Beispiele

# Completed Jobs aelter als 24h loeschen
php artisan vantage:prune --status=completed --hours=24

# Failed Jobs aelter als 4 Tage loeschen
php artisan vantage:prune --status=failed --days=4

# Vorschau: was wuerde geloescht?
php artisan vantage:prune --status=completed --hours=24 --dry-run

Optionen

OptionTypDefaultBeschreibung
--days=intConfig / 30Aufbewahrungsfrist in Tagen
--hours=intAufbewahrungsfrist in Stunden (ueberschreibt --days)
--status=stringcompleted (alias: processed), failed oder processing
--keep-processingflagProcessing-Jobs immer ausnehmen
--dry-runflagNur Vorschau mit Status-Breakdown
--forceflagKeine interaktive Bestaetigung

Interne Schritte (3 Phasen)

  1. Phase 1: Tags loeschenvantage_job_tags fuer betroffene Jobs in Batches loeschen (Foreign Key Constraint)
  2. Phase 2: Retry-Chains aufraumenretried_from_id nullifizieren fuer Child-Jobs deren Parent geloescht wird
  3. Phase 3: Jobs loeschenvantage_jobs Rows in 10.000er-Batches loeschen

Cursor-basierte Pagination: Phase 1 und 2 nutzen WHERE id > $afterId ORDER BY id statt einfaches LIMIT, da die Jobs in diesen Phasen noch nicht geloescht werden. Ohne Cursor wuerde getJobIdBatch() immer dieselben IDs zurueckliefern (Endlosschleife).

Schedule

Zweimal taeglich:

  • 01:25 UTC: --status=completed --hours=24 --force (Completed > 24h)
  • 01:30 UTC: --status=failed --days=4 --force (Failed > 4 Tage)

Location: app/Console/Commands/VantagePruneCommand.php


db:prune-historical-data

Löscht alte Aggregate-Daten aus Dashboard-Rollups und Leaderboard-Snapshots. WICHTIG: social_profile_daily_metrics wird NIEMALS gelöscht — nur abgeleitete Aggregate.

db:prune-historical-data
{--rollups-days=400 : Aufbewahrung für dashboard_daily_rollups (Tage)}
{--snapshots-days=90 : Aufbewahrung für dashboard_leaderboard_snapshots (Tage)}
{--dry-run : Vorschau ohne Löschung}

Optionen

OptionTypDefaultMinBeschreibung
--rollups-days=int40090Aufbewahrungsfrist für dashboard_daily_rollups
--snapshots-days=int9030Aufbewahrungsfrist für dashboard_leaderboard_snapshots
--dry-runflagNur Vorschau, keine Löschung

Beispiele

# Standard: Rollups > 400 Tage, Snapshots > 90 Tage löschen
php artisan db:prune-historical-data

# Kürzere Aufbewahrung (z.B. bei Speicherproblemen)
php artisan db:prune-historical-data --rollups-days=180 --snapshots-days=60

# Vorschau: wie viele Rows würden gelöscht?
php artisan db:prune-historical-data --dry-run

Interne Logik

  1. Minimum Retention erzwungen: Rollups mindestens 90 Tage, Snapshots mindestens 30 Tage — verhindert versehentlichen Datenverlust
  2. Löschung in Chunks (10.000 Rows pro Query) für kurze Transaktionen
  3. dashboard_daily_rollups: Löscht nach date-Spalte
  4. dashboard_leaderboard_snapshots: Löscht nach generated_for_date-Spalte

Geschützte Tabellen

TabelleStatusGrund
social_profile_daily_metricsNIEMALS löschenKern-Verlaufsdaten aller Profile
dashboard_daily_rollupsWird gepruntAbgeleitete Aggregate, jederzeit neu berechenbar
dashboard_leaderboard_snapshotsWird gepruntAbgeleitete Snapshots

Schedule

Täglich um 01:00 UTC.

Location: app/Console/Commands/PruneHistoricalData.php


favorites:cleanup

Entfernt verwaiste Watchers auf Favoriten-Workspaces die kein zugehöriges Favorite-Entry mehr haben. Bidirektionaler Cleanup: Watcher ohne Favorite UND Favorites ohne Watcher.

favorites:cleanup
{--dry-run : Vorschau ohne Löschung}
{--user= : Nur für einen bestimmten User aufräumen}

Optionen

OptionTypDefaultBeschreibung
--dry-runflagNur Vorschau, keine Löschung
--user=intAuf bestimmten User einschränken

Beispiele

# Alle User: verwaiste Watchers/Favorites finden und entfernen
php artisan favorites:cleanup

# Vorschau: was würde aufgeräumt?
php artisan favorites:cleanup --dry-run

# Nur für einen bestimmten User
php artisan favorites:cleanup --user=42

Interne Logik

  1. Alle "Favoriten"-Workspaces identifizieren (FavoriteManager::FAVORITES_WORKSPACE_NAME)
  2. Orphan Watchers: Watcher auf Favoriten-Workspace deren Source-Profil nicht in favorite_social_profiles des Users existiert
  3. Orphan Favorites: FavoriteSocialProfile-Einträge ohne korrespondierenden Watcher auf dem Favoriten-Workspace
  4. Transaktionale Löschung (Watcher + Sources zusammen)

Schedule

Manuell (nach Favoriten-Refactoring oder Dateninkonsistenzen).

Location: app/Console/Commands/CleanupOrphanFavoriteWatchers.php


profiles:backfill-descriptions

Parst Profilbeschreibungen die vor dem parsed_links-Feature importiert wurden. Extrahiert URLs, E-Mail-Adressen und Social-Media-Handles aus dem description-Feld.

profiles:backfill-descriptions
{--platform= : Nur bestimmte Plattform (youtube, instagram)}
{--force : Auch Profile mit vorhandenen parsed_links re-parsen}
{--dry-run : Nur Zählung ohne Parsing}

Optionen

OptionTypDefaultBeschreibung
--platform=stringalleyoutube oder instagram
--forceflagAuch Profile mit existierenden parsed_links erneut parsen
--dry-runflagNur Anzahl betroffener Profile anzeigen

Beispiele

# Alle Profile ohne parsed_links nachparsen
php artisan profiles:backfill-descriptions

# Nur Instagram-Profile
php artisan profiles:backfill-descriptions --platform=instagram

# Vorschau
php artisan profiles:backfill-descriptions --dry-run

Interne Logik

  1. Profile mit description IS NOT NULL und parsed_links IS NULL finden
  2. Chunks von 500 Profilen verarbeiten
  3. ProfileDescriptionParser Service extrahiert URLs, Emails, @handles
  4. Ergebnis in parsed_links JSONB-Spalte speichern

Schedule

Manuell (einmaliger Backfill).

Location: app/Console/Commands/BackfillProfileDescriptions.php


profiles:backfill-followers-count

Füllt die followers_count Spalte auf social_profiles aus der letzten täglichen Metrik. Einmaliger Backfill für Profile die vor Einführung der denormalisierten Spalte importiert wurden.

profiles:backfill-followers-count
{--dry-run : Nur Zählung ohne Update}

Beispiele

# Fehlende followers_count auffüllen
php artisan profiles:backfill-followers-count

# Vorschau: wie viele Profile betroffen?
php artisan profiles:backfill-followers-count --dry-run

Interne Logik

  1. Single Bulk-Query: PostgreSQL UPDATE...FROM mit DISTINCT ON Subquery
  2. Füllt followers_count aus der neuesten social_profile_daily_metrics Row
  3. Kein N+1 — ein einziger SQL-Statement für alle Profile

Hinweise

  • PostgreSQL-spezifisch: Nutzt DISTINCT ON + UPDATE...FROM (kein SQLite-Support)
  • Nur Profile mit followers_count IS NULL werden aktualisiert

Schedule

Manuell (einmaliger Backfill).

Location: app/Console/Commands/BackfillFollowersCount.php


profiles:backfill-instagram-descriptions

Kopiert die Instagram-Bio aus dem JSON data-Feld in die description-Spalte. Benötigt für den AI Enhancer, der Beschreibungen aus description liest.

profiles:backfill-instagram-descriptions
{--dry-run : Nur Zählung ohne Update}

Beispiele

# Bios kopieren
php artisan profiles:backfill-instagram-descriptions

# Vorschau
php artisan profiles:backfill-instagram-descriptions --dry-run

Interne Logik

  1. Instagram-Profile mit description IS NULL und data IS NOT NULL finden
  2. Bio aus data->bio oder data->biography (Fallback) extrahieren
  3. "mehr"-Normalisierung anwenden (analog zu sanitizeProfileData)
  4. Chunks von 500 Profilen verarbeiten

Schedule

Manuell (einmaliger Backfill).

Location: app/Console/Commands/BackfillInstagramDescriptions.php


profiles:sync-parsed-links

Migriert parsed_links JSONB-Daten in die relationale social_profile_links Tabelle. Einmaliger Backfill für die Link-Normalisierung.

profiles:sync-parsed-links
{--platform= : Nur bestimmte Plattform (youtube, instagram)}
{--dry-run : Nur Zählung ohne Sync}

Optionen

OptionTypDefaultBeschreibung
--platform=stringalleyoutube oder instagram
--dry-runflagNur Anzahl betroffener Profile anzeigen

Beispiele

# Alle Profile synchronisieren
php artisan profiles:sync-parsed-links

# Nur YouTube-Profile
php artisan profiles:sync-parsed-links --platform=youtube

# Vorschau
php artisan profiles:sync-parsed-links --dry-run

Interne Logik

  1. Profile mit parsed_links IS NOT NULL finden die noch keine social_profile_links Einträge haben
  2. SocialProfileLinkSyncer Service erstellt individuelle Link-Rows aus dem JSON
  3. Chunks von 500 Profilen verarbeiten

Schedule

Manuell (einmaliger Backfill).

Location: app/Console/Commands/SyncParsedLinks.php


images:generate-variants

Backfill-Command: Generiert optimierte Bild-Varianten (WebP/AVIF Thumbnail + Medium) für bestehende Profilbilder und Video-Thumbnails. Nutzt ProfileImageProcessor für die Verarbeitung.

images:generate-variants
{--dry-run : Nur zählen, nichts verarbeiten}
{--chunk=500 : Chunk-Größe für Batch-Verarbeitung}
{--profile-id= : Nur ein bestimmtes Profil verarbeiten}
{--videos : Video-Thumbnails statt Profilbilder verarbeiten}
{--force : Auch Bilder mit bestehenden Varianten neu generieren}
{--from= : Nur Bilder ab diesem Datum (YYYY-MM-DD)}
{--to= : Nur Bilder bis zu diesem Datum (YYYY-MM-DD)}

Beispiele

# Dry-Run: Wie viele Profilbilder fehlen Varianten?
php artisan images:generate-variants --dry-run

# Alle fehlenden Varianten generieren
php artisan images:generate-variants

# Einzelnes Profil
php artisan images:generate-variants --profile-id=42

# Video-Thumbnails verarbeiten
php artisan images:generate-variants --videos

# Alle Varianten erzwingen (auch bestehende)
php artisan images:generate-variants --force

# Nur Bilder aus einem Zeitraum
php artisan images:generate-variants --from=2026-01-01 --to=2026-02-01

Schedule

Manuell (einmaliger Backfill oder bei Format-Wechsel).

Location: app/Console/Commands/GenerateImageVariants.php


images:cleanup-orphaned

Findet und entfernt verwaiste Varianten-Dateien auf Disk (_md.webp, _sm.webp, _md.avif, _sm.avif), die keinen zugehörigen DB-Eintrag haben.

images:cleanup-orphaned
{--dry-run : Nur auflisten, nichts löschen}
{--videos : Video-Thumbnails statt Profilbilder prüfen}

Beispiele

# Dry-Run: Verwaiste Dateien auflisten
php artisan images:cleanup-orphaned --dry-run

# Verwaiste Varianten löschen
php artisan images:cleanup-orphaned

# Video-Thumbnails prüfen
php artisan images:cleanup-orphaned --videos

Schedule

Manuell.

Location: app/Console/Commands/CleanupOrphanedImageVariants.php


social:discover-from-links

Backfill-Command zur automatischen Profil-Discovery aus Contact-Link-Handles. Dispatcht EnsureProfileFromContactLink Jobs für Social-Links, die noch nicht entdeckt wurden.

social:discover-from-links
{--platform= : Filter by platform (youtube, instagram)}
{--only-existing : Only discover links where a matching profile already exists}
{--limit=0 : Max number of links to process (0 = unlimited)}
{--batch-size=100 : Chunk size for processing}
{--priority : Process high-follower source profiles first (E7)}
{--retry-failed : Retry previously failed discoveries}
{--dry-run : Show what would be processed without dispatching}

Beispiele

# Alle unentdeckten Links verarbeiten
php artisan social:discover-from-links

# Nur Instagram-Links
php artisan social:discover-from-links --platform=instagram

# Nur existierende Profile verknüpfen (0 API-Quota)
php artisan social:discover-from-links --only-existing

# Max 100 Links, High-Follower zuerst
php artisan social:discover-from-links --limit=100 --priority

# Fehlgeschlagene Discoveries erneut versuchen
php artisan social:discover-from-links --retry-failed --limit=200

# Vorschau ohne Dispatch
php artisan social:discover-from-links --dry-run
OptionTypDefaultBeschreibung
--platformstringalleFilter auf youtube oder instagram
--only-existingflagfalseNur Links verarbeiten, deren Profil schon existiert
--limitint0Max. Links (0 = unbegrenzt)
--batch-sizeint100Chunk-Größe für chunkById()
--priorityflagfalseHigh-Follower-Profile zuerst (E7)
--retry-failedflagfalseNur discovery_status=failed erneut versuchen
--dry-runflagfalseVorschau-Tabelle ohne Dispatch

Interne Logik:

  1. Query auf social_profile_links mit type=social, importable Plattform, kein linked_social_profile_id
  2. Filtert nach discovery_status IS NULL (oder failed bei --retry-failed)
  3. Bei --priority: JOIN auf social_profiles für Follower-Sortierung
  4. Dispatcht EnsureProfileFromContactLink auf profile-discovery Queue

Schedule: Wöchentlich Sonntag 09:00 UTC (--retry-failed --limit=200) Location: app/Console/Commands/DiscoverProfilesFromLinks.php


Stößt die Generierung aller drei Related-Slider (Instagram Related Profiles, YouTube Related Channels, Cross-Platform Related) für alle PRO-Accounts und Accounts mit erweiterten Video-Statistiken an. Jobs werden auf Standard-Queues dispatched und nutzen ShouldBeUnique (1h Fenster) zur Deduplizierung.

php8.4 artisan tinker --execute="\$ids = \App\Models\SocialProfile::query()->where(function(\$q) { \$q->where('platform', 'youtube')->whereHas('youtubeVideoSync', fn(\$s) => \$s->whereNotNull('video_tracking_start_date')->orWhere('auto_sync_enabled', true)); })->orWhere(function(\$q) { \$q->where('platform', 'instagram')->where('pro_enabled', true); })->pluck('id'); \$c = 0; foreach (\$ids as \$id) { \App\Jobs\GenerateRelatedProfilesSlider::dispatch(\$id); \App\Jobs\GenerateRelatedChannelsSlider::dispatch(\$id); \App\Jobs\GenerateCrossPlatformRelatedSlider::dispatch(\$id); \$c++; } echo \"Dispatched 3 slider jobs each for {\$c} profiles (\" . \$ids->count() . \" total).\\n\";"

Hinweise:

  • Kann jederzeit ausgeführt werden — bestehende Slider-Daten werden überschrieben
  • Jobs sind ShouldBeUnique mit 1h Lock, doppelte Ausführung ist sicher
  • PRO-Kriterien: YouTube via youtube_video_syncs.video_tracking_start_date IS NOT NULL oder auto_sync_enabled = true, Instagram via social_profiles.pro_enabled = true