Zum Hauptinhalt springen

Collector API

REST-API fuer die Browser Extension, die Instagram-Profile scraped und Ergebnisse zurueckliefert.

Authentifizierung

Die API nutzt Laravel Sanctum Token-Authentifizierung mit erweitertem Token-Lifecycle (Soft-Revoke, IP-Whitelist, Usage-Tracking).

Token erstellen

Tokens werden ueber die Admin-Seite /admin/api-management oder per Artisan-Command erstellt:

php artisan collector:token "Chrome Extension 1"
# Gibt den Plaintext-Token aus: 1|abc123def456...

Der Token wird im Authorization-Header mitgesendet:

Authorization: Bearer 1|abc123def456...

Middleware-Chain

auth:sanctum → RejectRevokedTokens → throttle:collector → TrackApiTokenUsage
MiddlewarePruefung
auth:sanctumToken existiert und ist nicht abgelaufen
RejectRevokedTokensToken nicht revoziert (revoked_at IS NULL) + IP-Whitelist
throttle:collector600 Requests pro Minute pro Client
TrackApiTokenUsageStuendliche Nutzungs-Aggregation in api_token_usage_logs

Token-Lifecycle

stateDiagram-v2
[*] --> Active: Token erstellt
Active --> Revoked: Admin deaktiviert
Revoked --> Deleted: Admin loescht
Active --> Expired: expires_at erreicht
note right of Active: API-Zugriff erlaubt
note right of Revoked: API-Zugriff blockiert (401)
note right of Expired: API-Zugriff blockiert (Sanctum)

IP-Whitelist

Tokens koennen optional auf bestimmte IPs beschraenkt werden (allowed_ips JSON-Array). Leere/null Whitelist erlaubt alle IPs. Requests von nicht-gelisteten IPs erhalten 403 Forbidden.

Token-Management

Admin-Dashboard unter /admin/api-management:

  • Token erstellen (Name, Abilities, Ablaufdatum, IP-Whitelist)
  • Token deaktivieren (Soft-Revoke mit revoked_at + revoked_by)
  • Token loeschen (nur revozierte, inkl. verwaister CollectorClients)
  • IP-Whitelist bearbeiten
  • Nutzungs-Charts (24h, 7d, 30d) pro Token und gesamt
  • Collector-Client Health-Status

Deaktivierte Clients: Collector-Clients ohne aktive Tokens (alle revoziert) werden automatisch aus der API-Liste, dem Update-Status-Dashboard und dem Health-Check (/up_system) gefiltert. Der withActiveTokens-Scope auf CollectorClient verwendet einen expliziten uuid::text Cast für PostgreSQL-Kompatibilität (SQLite toleriert den Typ-Unterschied zwischen UUID und varchar).

Location: routes/api.php, app/Livewire/Admin/ApiManagement/Index.php, app/Services/ApiTokenService.php

Endpoints

Übersicht aller Endpoints des Collector↔API-Vertrags (Stand v1.5.0):

EndpointMethodeAuthZweck
/api/collector/pingGETNeinConnectivity-Check (keine DB, schnell)
/api/collector/jobs/leasePOSTBearerJob leasen
/api/collector/jobs/statsGETBearerQueue-Statistiken
/api/collector/jobs/{job}/completePOSTBearerJob-Ergebnis melden
/api/collector/jobs/{job}/failPOSTBearerJob-Fehler melden
/api/collectors/heartbeatPOSTBearerHeartbeat + Meta + should_resume
/api/collectors/errorPOSTBearerCollector-Stop-Bericht

Timeouts: Der Collector setzt clientseitig kurze Timeouts (Job-Calls 15s, Heartbeat/Error 10s, Ping 5s). Die API muss deutlich darunter antworten, sonst klassifiziert der Collector den Request als request_timeout (transient) und versucht es erneut. Alle Zeitstempel sind ISO-8601 UTC (…Z).

POST /api/collector/jobs/lease

Least den naechsten verfuegbaren Job. Jobs mit hoeherer Prioritaet werden bevorzugt, danach FIFO. Parallele Collectors blockieren sich nicht gegenseitig dank FOR UPDATE SKIP LOCKED (PostgreSQL) — jeder Collector ueberspringt bereits gelockte Rows und bekommt sofort den naechsten verfuegbaren Job. Fallback auf normales FOR UPDATE fuer SQLite (Tests).

Request:

{
"source": "instagram"
}

Response (200 - Job verfuegbar):

{
"job": {
"id": "9c3f4a2b-...",
"source": "instagram",
"status": "leased",
"priority": 30,
"payload": {
"job_type": "watcher_import",
"run_id": 22,
"workspace_id": 1,
"created_by": 1,
"input_url": "https://www.instagram.com/example/",
"dedupe_key": "handle:example"
},
"lease_expires_at": "2026-02-09T14:30:00Z"
}
}

Response (200 - kein Job verfuegbar):

{
"job": null
}

Expired Leases werden automatisch beim naechsten lease-Call recycled: Jobs mit Status leased deren lease_expires_at in der Vergangenheit liegt, werden erneut vergeben.

Wichtig — leere Queue ist kein Fehler: Wenn kein Job verfügbar ist, antwortet die API mit HTTP 200 und { "job": null }, niemals mit 4xx/5xx. Der Collector wartet dann 60 s und pollt erneut. Ein Non-2xx-Status wird vom Collector als Lease-Fehler gewertet (Direct-Retry → Ping-Recovery). Einzige Ausnahme: ein wegen hoher Fehlerrate suspendierter Token erhält HTTP 429 mit Retry-After: 900 (Token-Health-Monitoring, siehe unten).

Tolerante Feld-Auflösung: Der Collector löst die Profil-URL aus dem erstbesten verfügbaren Feld auf (payload.input_urlpayload.handlepayload.username → Top-Level-Varianten). Am stabilsten ist payload.input_url mit voller URL — so liefert die API es immer. Reine Handles (cruzcampo, @cruzcampo) werden collectorseitig zu https://www.instagram.com/{handle}/ normalisiert; Nicht-Instagram-URLs lehnt der Collector mit missing_profile_url ab.

Location: app/Http/Controllers/Api/CollectorJobController.php (Methode lease)

GET /api/collector/jobs/stats

Queue-Statistiken gruppiert nach Plattform.

Response:

{
"stats": {
"instagram": {
"queued": 142,
"leased": 3,
"total": 145
},
"youtube": {
"queued": 0,
"leased": 0,
"total": 0
}
}
}

Location: app/Http/Controllers/Api/CollectorJobController.php (Methode stats)

POST /api/collector/jobs/{job}/complete

Markiert einen geleasten Job als abgeschlossen und uebergibt das Scrape-Ergebnis.

Request (vollständiges result-Schema, v1.4.0):

{
"result": {
// ── Pflichtfelder (immer vorhanden, nie null/leer) ──
"handle": "cruzcampo",
"canonical_url": "https://www.instagram.com/cruzcampo/",
"followers_count": 69193, // integer (ggf. gerundet, s.u.)
"title": "Cruzcampo", // Display-Name (Unicode/Emoji/CJK möglich)
"is_private": false,
"is_verified": true,
"profile_type": "business", // personal | business | private

// ── Bedingte Pflicht: bei is_private=false müssen beide integer sein ──
"following_count": 877, // integer | null (null bei privat)
"post_count": 515, // integer | null (null bei privat)

// ── Optional ──
"thumbnail_url": "https://.../avatar.jpg", // string | null

"profile_data": {
"bio": "#ConMuchoAcento …", // string | null
"category": "Nahrungsmittel und Getränke", // string | null
"pronouns": "she/her", // string | null
"link": "https://example.com", // string | null (externer Bio-Link)
"is_private": false, // boolean (Spiegel von oben)
"has_active_story": true, // boolean
"highlights": ["Reisen", "Food"], // string[] (kann [] sein)
"recent_posts": [ // array (kann [] sein)
{
"shortcode": "DXa2SydgHcq", // string (Pflicht im Item, Join-Key)
"type": "reel", // post | reel | carousel
"url": "https://www.instagram.com/cruzcampo/reel/DXa2SydgHcq/", // bei Collab/Repost ggf. Original-Autor-Handle
"thumbnail_url": "https://…",
"alt_text": "…", // nur Metadaten — NIE als Zahl interpretieren
"likes_count": 217, // integer | null (seit v1.5.0 immer vorhanden)
"comments_count": 14, // integer | null (seit v1.5.0 immer vorhanden)
"views_count": 1289 // integer | null (Reels/Videos; bei Fotos meist null)
}
]
},

"daily_raw": { "post_count": 515 }, // object; post_count: integer | null

// ── Vom Collector angereichert (immer vorhanden) ──
"scrape_quality": 0.85, // number 0.0–1.0
"anomalies": [ // array (kann [] sein)
{ "type": "extreme_counts", "severity": "info", "detail": "…" }
]
}
}

Pflichtfelder (immer): handle, canonical_url, followers_count (integer), title, is_private (bool), is_verified (bool), profile_type.

Bedingte Pflicht (nur bei is_private=false): following_count und post_count (integer). Bei privaten Profilen dürfen sie null sein.

profile_type ist seit v1.4.0 einer von personal | business | private. Ältere Werte (creator) werden weiterhin tolerant gespeichert — die API schreibt den Wert unverändert, ohne harte Enum-Ablehnung (lieber speichern als einen Scrape verwerfen).

Engagement-Felder in recent_posts[] (PFLICHT-Semantik)

Seit v1.5.0 kommen likes_count, comments_count und views_count aus Instagrams Inline-JSON (GraphQL/Bloks), gemappt per Shortcode — kein Text-/Caption-Parsing mehr (das erzeugte vorher Garbage wie eine aus der Caption geparste Jahreszahl). Jedes Item trägt seitdem immer alle drei Schlüssel, jeweils integer | null:

  • null = der Wert ist nicht verfügbar (Owner-Setting, IG-A/B-Test, oder von Instagram nicht server-seitig prefetcht). Wird als NULL gespeichert — niemals als geratene Zahl oder 0.
  • 0 = echter Nullwert (brandneuer Post ohne Likes). Wird als 0 gespeichert.
  • views_count ist für Reels/Videos relevant; bei Fotos i.d.R. null.
  • Anfangs niedrige Quote ist normal: Liefert Instagram die Counts nicht vorab (nur per XHR), kommen korrekt nulls. Das ist gewollt (ehrliche Nulls statt Garbage), kein Fehler.

Join immer per shortcode, nie per URL-Handle: Bei Collab-/Repost-Items kann der Handle in url der Original-Autor sein statt des gescrapten Profils. Die API nutzt den Unique-Key (social_profile_id, shortcode) für instagram_posts und mappt die Metriken per Shortcode.

Die API persistiert jeden Post mit mindestens einem vorhandenen Engagement-Wert als instagram_posts + instagram_post_metrics-Zeitreihe (append-only: jeder Scrape erzeugt eine neue Zeile, bestehende Werte werden nie mit null überschrieben). Posts ganz ohne Engagement-Daten (alle drei null) werden übersprungen — sie haben keinen Zeitreihen-Wert; ihre Metadaten bleiben im social_profiles.recent_posts-Snapshot. Location: app/Services/Instagram/InstagramPostMetricService.php.

scrape_quality & anomalies

  • scrape_quality (0.0–1.0): gewichteter Vollständigkeits-Score. Wird auf [0,1] geklemmt (2 Nachkommastellen) und in social_profile_daily_metrics.scrape_quality gespeichert. < 0.8 = unvollständiger Scrape.
  • anomalies[]: { type, severity, detail } mit severity ∈ {info, warning, critical}. Werden idempotent in social_profile_anomalies gespeichert; Einträge mit ungültiger severity oder leerem type werden verworfen (geloggt). Nur warning/critical sollten Alerting auslösen.

Beide Felder werden additiv nach der kritischen Profil-Transaktion verarbeitet (Enrichment darf den Scrape nie blockieren). Location: app/Services/Collector/CollectorResultEnrichment.php.

Präzision der Counts

followers_count ist i.d.R. exakt (DOM-title-Attribut), kann im Fallback aber gerundet sein (z.B. 69000 statt 69193). Geringe Schwankungen zwischen aufeinanderfolgenden Scrapes sind keine Anomalie — Counts als „beste verfügbare Schätzung" behandeln.

Private Profile

Private Profile liefern eingeschränkte Daten — following_count/post_count/recent_posts dürfen null/leer sein, profile_type:"private". Das ist kein unvollständiger Scrape und wird nicht verworfen.

Location: app/Services/Collector/InstagramDailyScrapeProcessor.php + InstagramWatcherImportProcessor.php (Methode validateResult)

Payload-Unterschiede: watcher_import vs. daily_scrape

Feldwatcher_importdaily_scrape
job_type"watcher_import""daily_scrape"
run_idImport-Run-ID--
workspace_idZiel-Workspace--
created_byUser-ID--
social_profile_id--Profil-ID
input_urlInstagram-URLInstagram-URL
dedupe_key"handle:xxx"--

Response (200): Es werden nur Status-Felder zurückgegeben (nicht das volle result, um Speicher/Bandbreite zu sparen):

{ "job": { "id": "9c3f4a2b-…", "source": "instagram", "status": "completed", "updated_at": "2026-06-01T12:00:00Z" } }

Error Responses:

CodeBeschreibung
409Job nicht im Status leased, Lease abgelaufen, anderer Client — oder doppelter Complete-Call auf einen bereits abgeschlossenen Job
422Validierungsfehler im Result-Payload (fehlende Pflichtfelder im Processor)

Idempotenz: Ein nach einem transienten Fehler erneut geleaster Job kann normal abgeschlossen werden (neuer Lease). Ein zweiter Complete-Call auf einen bereits completed-Job liefert 409 — kein Daten-Schaden, der Collector behandelt das als „schon erledigt".

POST /api/collector/jobs/{job}/fail

Markiert einen geleasten Job als fehlgeschlagen. Der error_code steuert, ob der Job erneut eingereiht (Re-Queue) oder endgültig geparkt wird.

Request:

{
"error_code": "not_logged_in",
"error_message": "Redirected to /accounts/login."
}

Fehler-Kategorien (v1.4.0): Jeder Code fällt in genau eine Kategorie. Unbekannte/nicht klassifizierte Codes landen im Default-Bucket Kumulativ.

KategorieCodesAPI-Verhalten
Session (Collector-Problem)account_suspended, challenge_required, action_blocked, checkpoint_required, account_disabled, automated_behavior, scraping_restricted, not_logged_in, login_redirect, consent_requiredRe-Queue ohne Strike — nicht das Profil ist schuld, sondern der ausgeloggte/geblockte Collector. Profil-Status bleibt unberührt; ein gesunder Collector übernimmt.
Benignprofile_not_foundPark (permanent fehlgeschlagen) + Profil wird auf api_status='inactive' gesetzt (existiert nicht mehr).
tab_closedRe-Queue ohne Strike (der Scrape fand schlicht nicht statt).
Transientrequest_timeout, request_aborted, profile_redirectedRe-Queue ohne Strike (mit Backoff).
Kumulativinvalid_result, parse_error, rate_limited, scrape_timeout, scrape_error, missing_profile_url, tab_create_failed + jeder unbekannte CodeRe-Queue mit Backoff bis max_fail_count (Default 3), danach Park. Schützt vor Endlos-Retry-Schleifen bei dauerhaft kaputten Profilen.

Re-Queue-Mechanik: Re-Queued Jobs gehen mit priority=0 ans Ende der Queue, created_at wird per Backoff (collector.requeue_backoff, Default [30, 120] s) in die Zukunft verschoben, und excluded_collector_id wird gesetzt, damit ein anderer Collector den Retry übernimmt (nach dem 2. Retry aufgehoben, gegen Starvation bei nur 1 Collector).

„Ohne Strike" bedeutet: Session-/Transient-/tab_closed-Fehler zählen nicht gegen max_fail_count — sie werden so lange re-queued, bis ein gesunder Collector den Job schafft. Die Absicherung gegen einen komplett kaputten Collector-Fuhrpark übernehmen Stale-Job-Pruning (Jobs > 2 Tage werden gelöscht) und das Token-Health-Monitoring (siehe unten), nicht das Verbrennen eines validen Jobs.

Profil-Seite: Bei Session-/Transient-/tab_closed-Fehlern bleibt das Profil unberührt (kein api_status='error', kein Fail-Streak, kein Daily-Progress-Decrement — der spätere Retry wird gezählt). profile_not_found markiert das Profil inactive. Kumulative Fehler setzen api_status='error' (transient, ohne Fail-Streak, kein Auto-Disable). Legacy-Fallback: enthält die error_message not found / user not found / http 404 / status 404, wird ebenfalls als „Profil existiert nicht" geparkt.

Location: app/Http/Controllers/Api/CollectorJobController.php (Methode fail), app/Support/Collector/CollectorErrorCode.php (Klassifizierung), app/Enums/CollectorErrorCategory.php

POST /api/collectors/heartbeat

Aktualisiert last_seen_at, speichert optionale Collector-Metadaten und signalisiert über should_resume, ob wartende Jobs vorliegen. Kommt max. alle 30 s (gedrosselt).

Request: meta ist optional (ältere Plugin-Versionen senden es nicht).

{
"meta": {
"status": "running",
"queue_state": "completed",
"last_error_code": null,
"consecutive_job_fail_count": 0,
"success_count": 1234,
"fail_count": 56,
"version": "1.4.0"
}
}

Response (200):

{ "status": "ok", "queued_jobs": 3, "should_resume": true }

should_resume: true, sobald wartende Jobs in der Queue stehen — ein Collector im Extended-Retry-Backoff bricht daraufhin sofort aus und leased wieder. Der queued_jobs-Count ist 30 s gecacht. Ein Heartbeat löscht zudem einen zuvor gesetzten Error-State des Clients (Recovery) und speichert das meta-Objekt auf dem CollectorClient (für das Multi-Collector-Dashboard).

Location: app/Http/Controllers/Api/CollectorClientController.php (Methode heartbeat)

GET /api/collector/ping

Leichtgewichtiger Connectivity-Check ohne Auth und ohne DB-Query. Collectors prüfen damit während des Recovery-Backoffs, ob die API wieder erreichbar ist, ohne einen gültigen Token zu brauchen. Muss schnell und billig bleiben (Throttle: 60/min).

Response (200):

{ "status": "ok", "timestamp": "2026-06-01T12:00:00Z" }

Location: app/Http/Controllers/Api/CollectorClientController.php (Methode ping)

POST /api/collectors/error

Bericht eines Collectors, der wegen eines fatalen Fehlers gestoppt hat — bei consecutive_errors oder einem Session-Fehler (dann ist error_code der jeweilige Session-Code, z.B. not_logged_in).

Request: version/stopped_at sind optional.

{
"error_code": "consecutive_errors",
"error_message": "3 consecutive failures, last: invalid_result",
"version": "1.4.0",
"stopped_at": "2026-06-01T12:34:56.000Z"
}

Die API setzt den Client auf error_state='error', re-queued dessen geleaste Jobs (ein anderer Collector übernimmt) und sendet eine Admin-Benachrichtigung. error_message wird sanitisiert geliefert (Tokens entfernt), kann aber lang sein → als Text gespeichert.

Response (200):

{ "status": "ok" }

Location: app/Http/Controllers/Api/CollectorClientController.php (Methode error)

Token Health Monitoring (Plan 37)

Per-Token-Monitoring das fehlerhafte Collector-Tokens automatisch erkennt, pausiert und nach Selbstheilung wieder aktiviert.

Funktionsweise

  1. Error-Tracking: Jeder complete()/fail() Call inkrementiert Cache-Counter pro Collector-Client (Success/Fail/Error-Code-Breakdown).
  2. Evaluation: collector:evaluate-token-health (alle 5 Min) berechnet die Fehlerrate im Rolling Window (60 Min, min. 50 Requests). Idle-Protection: Ein Collector ohne aktuelle Aktivitaet (< min_recent_activity Jobs im current hour bucket, Default 5) wird als Insufficient bewertet — also nicht suspendiert, auch wenn aeltere Failures im Sliding-Window noch sichtbar sind. So bleibt ein idle gewordener Collector aktiv und kann sofort wieder Jobs leasen, wenn neue Arbeit kommt.
  3. Auto-Suspension: Bei >40% Fehlerrate wird der Token suspendiert — der Lease-Endpoint gibt HTTP 429 mit Retry-After: 900 zurueck.
  4. Re-Test: 5 Test-Jobs werden an den suspendierten Token gesendet. Bei 80% Erfolg wird der Token automatisch reaktiviert.
  5. Re-Test-Isolation (F3): Re-Test-Jobs zaehlen nur im Retest-Tracker, nicht in der regulaeren Health-Statistik. Dadurch koennen Re-Test-Failures die Fehlerrate nicht weiter verschlechtern.

Schwellwerte

LevelFehlerrateAktion
Healthy<20%Keine
Warning20-40%Admin-Alarm (Toast)
Critical>40%Auto-Suspension + E-Mail-Alarm + Job-Umverteilung

Zusaetzlich: Error-Code-spezifische Schwellwerte (tab_closed >30%, parse_error >10%, profile_not_found >5%, scrape_error >25%).

Anomalie-Erkennung

Z-Score-basiert: Vergleich der aktuellen Fehlerrate mit dem 24h-Durchschnitt. Alarm bei Z-Score >2 (2 Standardabweichungen ueber Mittel).

Fehler-Korrelation

Erkennt ob Fehler collector-spezifisch oder plattformweit sind. Wenn >50% aller aktiven Collectors gleichzeitig >30% Fehlerrate haben → "Plattform-Problem"-Alarm statt individueller Token-Suspension.

Config

// config/collector.php → token_health
'warning_threshold' => 0.20,
'critical_threshold' => 0.40,
'min_sample_size' => 50,
'min_recent_activity' => 5, // Idle-Protection: Mindestaktivitaet im current hour bucket
'evaluation_window_minutes' => 60,
'suspension_ttl_hours' => 6,
'retest_job_count' => 5,
'retest_success_threshold' => 0.80,

Location: app/Services/Collector/CollectorTokenHealthService.php, app/Console/Commands/EvaluateTokenHealth.php

Bonus Scraping (Plan 36)

Wenn alle täglichen Pflicht-Scrapes abgearbeitet sind, nutzt das System die verbleibende Collector-Kapazität um Profile aus zukünftigen Rotation-Buckets vorab zu scrapen.

Funktionsweise

  1. Idle-Detection: Zählt ALLE offenen Non-Bonus-Jobs (Queued + Leased). System gilt als idle wenn die Queue komplett leer ist (kein einziger daily_scrape oder watcher_import Job offen). Keine konfigurierbare Toleranz — Bonus-Scrapes starten erst nach Abschluss aller regulären Jobs.

  2. Pending-Duty-Check (seit 2026-05-26 strikt): Selbst bei leerer Queue blockiert Bonus, wenn irgendein Profil heute noch nicht erfolgreich gescraped wurde UND nicht im Fail-Streak-Cooldown sitzt. Konkret zählt pendingDutyProfilesCount() Profile mit:

    • last_daily_scrape_on != today (nicht heute erfolgreich gescraped)
    • scrape_fail_streak < 3 (nicht permanent geparkt — Cooldown-Profile gelten als "gehandled")
    • UND (last_daily_scrape_on IS NULL [neu] ODER last_daily_queue_on IS NOT NULL AND last_daily_scrape_on < last_daily_queue_on [queued aber Scrape danach nicht erfolgreich])

    Damit gilt: Bonus läuft erst wenn ALLE Today-Profile entweder erfolgreich gescraped sind ODER im Cooldown geparkt sind. Profile die heute eingequeued wurden aber gerade noch in Bearbeitung sind (Worker leased, Backoff-Retry, fail_count < max) blockieren Bonus zusätzlich weil sie auch die Queue belegen → isIdle() greift.

  3. Kandidaten-Auswahl: Profile aus zukünftigen rotation_buckets, sortiert nach Bucket-Proximity und Follower-Count.

    Rotation-Bucket-Modulus (seit 2026-05-27): Die social_profiles.rotation_bucket-Spalte wird mit LCM(instagram_days=3, youtube_days=3, low_priority_days=7) = 21 als Modulus gespeichert. Konsumenten machen WHERE (rotation_bucket % rotation_days) != today_bucket — mathematisch korrekt für sowohl Regular-3-Tage- als auch Low-Prio-7-Tage-Rotation. Vorher war der Modulus MAX=7 und der Bonus-Code verglich direkt mit today % 3 → Profile waren in der falschen Bucket-Klassifizierung. Migration via php artisan profiles:backfill-rotation-buckets --force.

    Pending-Duty-Check (seit 2026-05-27 via UpdateStatusService): BonusScrapeService::pendingDutyProfilesCount() delegiert seit dem Bonus-Bug-Fix an {Instagram,YouTube}UpdateStatusService::getStatus() — dieselbe Datenquelle wie das "noch nicht aktualisiert"-Widget im Admin-Update-Status. Bonus blockt wenn not_updated_today - failed_not_updated - cooldown_not_updated > 0. Damit ist die Bonus-Gatekeeper-Logik exakt konsistent mit dem was der Admin sieht (Lücken in der pendingDuty-Detection sind ausgeschlossen).

  4. Dispatch: Jobs mit job_type=bonus_scrape, priority=-10, low_priority=true — Duty-Jobs haben immer Vorrang.

  5. Metriken-Isolation: Bonus-Scrapes inkrementieren DailyScrapeProgress nicht, damit die reguläre Fortschrittsanzeige korrekt bleibt.

Tageslimit (platform-spezifisch seit 2026-05-26)

Pro-Platform-Cache-Counter:

  • bonus_scrapes:instagram:{date} — Instagram-Bonus-Zähler
  • bonus_scrapes:youtube:{date} — YouTube-Bonus-Zähler
  • bonus_scrapes:{date} — Legacy-globaler Zähler (wird parallel hochgezählt für Backwards-Compat mit Diagnostic-Tinkern die den alten Key noch lesen)

Per Default:

  • Instagram: unbegrenzt (max_daily_jobs_instagram=0) — kein API-Quota-Kostenproblem, darf bis Tageswechsel beliebig viele Profile vorab-scrapen
  • YouTube: 30.000/Tag (max_daily_jobs_youtube=30000) — wegen API-Quota; jede Bonus-Aktion kostet 1 Unit pro 50 Videos

Counter sind seit 2026-05-26 platform-getrennt — Instagram-Bonus blockt YouTube-Cap nicht mehr (Bug, gefixt mit Commit de71506).

YouTube Bonus

Eigener Command social:bonus-youtube-scrapes (alle 10 min, 12-22 UTC). Prüft >20% Pool-Quota-Budget vor Dispatch. Re-prüft Quota alle 100 Profile.

Collector Exclusion

Bei transientem Fehler wird excluded_collector_id gesetzt — ein anderer Collector übernimmt den Job. Nach dem 2. Retry wird die Exclusion aufgehoben um Starvation bei nur 1 aktivem Collector zu verhindern.

Config

// config/postbox.php → bonus_scraping
'enabled' => true,
'min_hour_utc' => 12, // DST-aware via YouTubeQuotaSchedule (2h nach YT-Quota-Reset)
'max_daily_jobs' => 30000, // Legacy globaler Fallback (vor Plattform-Trennung)
'max_daily_jobs_instagram' => 0, // 0 = unlimited (kein API-Quota-Kostenproblem)
'max_daily_jobs_youtube' => 30000, // API-Quota-Cap pro Tag
'job_priority' => -10, // unter allen Duty-Jobs
'chunk_size' => 1000, // Bonus-Jobs pro Dispatch-Run
'youtube_min_quota_percent' => 20, // YouTube stoppt bei <20% Pool-Quota

Location: app/Services/Collector/BonusScrapeService.php, app/Console/Commands/QueueBonusYouTubeScrapes.php

Admin-Features

Platform Filter

Admins koennen die Collector Queue und Logs nach Plattform filtern (all, instagram, youtube).

Priority Boost

Einzelne Jobs koennen per Admin-UI in der Prioritaet hochgestuft werden: priority = max(queued priority) + 1. Der Job wird damit als naechstes geleased.

Rescrape Shortcut

Admins koennen in /admin/social-profiles direkt ein priorisiertes Rescrape ausloesen -- der zugehoerige Watcher wird in die Prioritaets-Queue gestellt.

Location: app/Livewire/Admin/LogQueue/Index.php, app/Livewire/Admin/SocialProfiles/Index.php