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=int–Einzelnes 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
--forceflag–Neuberechnung 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-runflag–Nur 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-existingflag–Nur Profile mit mindestens einer bestehenden Related-Beziehung
--dry-runflag–Nur Anzeige, keine Jobs dispatchen
--syncflag–Synchron 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=int–Aufbewahrungsfrist in Stunden (ueberschreibt --days)
--status=string–completed (alias: processed), failed oder processing
--keep-processingflag–Processing-Jobs immer ausnehmen
--dry-runflag–Nur Vorschau mit Status-Breakdown
--forceflag–Keine interaktive Bestaetigung

Interne Schritte (3 Phasen)​

  1. Phase 1: Tags loeschen — vantage_job_tags fuer betroffene Jobs in Batches loeschen (Foreign Key Constraint)
  2. Phase 2: Retry-Chains aufraumen — retried_from_id nullifizieren fuer Child-Jobs deren Parent geloescht wird
  3. Phase 3: Jobs loeschen — vantage_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-runflag––Nur 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-runflag–Nur Vorschau, keine Löschung
--user=int–Auf 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
--forceflag–Auch Profile mit existierenden parsed_links erneut parsen
--dry-runflag–Nur 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-runflag–Nur 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}
{--discard-originals-only : Nur die Quelldateien des Bestands wegwerfen — ohne neu zu encodieren}
{--missing : Nur Bilder, deren Variantendateien auf der Platte fehlen (geprüft, nicht geraten)}
{--limit=2000 : Obergrenze der im --missing-Modus geprüften Zeilen}
{--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

--missing — verlorene Varianten wiederherstellen, ohne den Bestand neu zu encodieren​

Der gewöhnliche Lauf erkennt „diese Zeile braucht Arbeit" an einem leeren blurhash. Das übersieht genau den Fall, der in Produktion auftritt: eine Zeile, die einmal erfolgreich verarbeitet wurde und deren Variantendateien später verschwanden. Für den regulären Lauf ist sie unsichtbar; erreichbar wäre sie nur über --force, und der arbeitet über eine Million Zeilen neu durch, um eine Handvoll zu reparieren.

Dieser Modus fragt stattdessen die Platte — die einzige Instanz, die es wirklich weiss — und begrenzt sich über --limit selbst. Seit v2.11.0 gilt er für Profilbilder und (mit --videos) für Video-Thumbnails; vorher nur für Letztere.

# Nachsehen, nichts anfassen
php artisan images:generate-variants --missing --dry-run

# Reparieren, Profilbilder
php artisan images:generate-variants --missing

# Profilbilder und Video-Thumbnails
php artisan images:generate-variants --missing --videos

⚠️ Zwei Fälle sehen identisch aus und sind es nicht — der Lauf zählt sie deshalb getrennt:

Zeile in der AusgabeBedeutung
Wiederhergestelltdie Quelldatei lag noch da, die Varianten sind daraus neu entstanden
davon ohne Quelldatei (nur per Neu-Abruf reparierbar)auch die Quelle fehlt. Die Datenbankzeile behauptet original_discarded_at IS NULL, auf der Platte liegt aber nichts mehr. Aus solchen Zeilen lässt sich nichts erzeugen — das Bild muss neu von der Plattform geholt werden

Die zweite Zahl ist das eigentliche Ergebnis eines solchen Laufs. Sie unter „übersprungen" zu verbuchen würde einen Bestand, der stillschweigend Bilder verloren hat, wie einen gesunden aussehen lassen.

--discard-originals-only — den Bestand abräumen, ohne zu encodieren​

Seit dem 3.9.2026 wird die heruntergeladene Quelldatei weggeworfen, sobald die drei Varianten vorliegen. Das gilt für neue und geänderte Bilder; der Bestand von vorher bleibt liegen. Er ist auch keine Waise — die Datenbankzeilen zeigen korrekt auf ihn, also greift kein Purge.

Dieser Modus räumt genau diesen Bestand ab: Er prüft, ob alle drei Varianten wirklich auf der Platte liegen, löscht dann die Quelle und setzt den Zeitstempel. Kein Encoding.

⚠️ Nicht --force dafür benutzen. Der erreicht denselben Endzustand, lädt dabei aber jedes Bild und encodiert drei Varianten neu — bei 1.132.070 Profilbildern und 416.727 Thumbnails Tage an Rechenzeit auf einem Netz-Speicher, für eine Aufgabe, die „prüfen, löschen, Spalte setzen" lautet.

# Erst zählen, nichts anfassen
php artisan images:generate-variants --discard-originals-only --dry-run

# Ein einzelnes Profil, zum Hinsehen
php artisan images:generate-variants --discard-originals-only --profile-id=42

# Der ganze Bestand, Bilder und Video-Thumbnails
php artisan images:generate-variants --discard-originals-only --videos

Abbrechbar und wiederholbar. Die Auswahl fragt nach Zeilen, deren Quelle noch nicht weggeworfen wurde — Erledigtes fällt von selbst aus der Menge. Ein zweiter Lauf macht dort weiter, wo der erste aufgehört hat. Es gibt bewusst keinen Fortschritts-Zähler, der falsch stehen könnte.

Zwei Dinge sagt der Befehl ausdrücklich:

SituationWas passiert
POSTBOX_IMAGE_DISCARD_ORIGINALS=falseAbbruch mit Fehler, nicht „0 weggeworfen". Mit ausgeschaltetem Schalter liefert die Prüfung für jede Zeile false, und das Ergebnis sähe aus wie ein sauberer Bestand.
Eine Variante fehltDie Quelle bleibt liegen. Die Zahl der übersprungenen Zeilen wird genannt, mit Beispielpfaden und der gesuchten Endung.

Ein hoher Wert bei „Variante fehlt" hat meist eine Ursache: POSTBOX_IMAGE_FORMAT wurde gewechselt. Die alten Varianten liegen dann unter der anderen Endung und gelten als fehlend. Das ist richtig so — die App fragt die konfigurierte Endung an, diese Bilder wären ohne ihre Quelle kaputt. Erst die Varianten neu erzeugen, dann abräumen.

Schedule​

Manuell (einmaliger Backfill, bei Format-Wechsel, oder zum Abräumen des Quell-Bestands). Der --discard-originals-only-Modus schreibt keinen Heartbeat: Der Herzschlag image_variants steht für den geplanten Varianten-Lauf, und ihn hier zu setzen hiesse, einen ausgefallenen Cron mit einem manuellen Aufräum-Lauf zu übertünchen.

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