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
| Middleware | Pruefung |
|---|---|
auth:sanctum | Token existiert und ist nicht abgelaufen |
RejectRevokedTokens | Token nicht revoziert (revoked_at IS NULL) + IP-Whitelist |
throttle:collector | 600 Requests pro Minute pro Client |
TrackApiTokenUsage | Stuendliche 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):
| Endpoint | Methode | Auth | Zweck |
|---|---|---|---|
/api/collector/ping | GET | Nein | Connectivity-Check (keine DB, schnell) |
/api/collector/jobs/lease | POST | Bearer | Job leasen |
/api/collector/jobs/stats | GET | Bearer | Queue-Statistiken |
/api/collector/jobs/{job}/complete | POST | Bearer | Job-Ergebnis melden |
/api/collector/jobs/{job}/fail | POST | Bearer | Job-Fehler melden |
/api/collectors/heartbeat | POST | Bearer | Heartbeat + Meta + should_resume |
/api/collectors/error | POST | Bearer | Collector-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_url → payload.handle → payload.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_typeist seit v1.4.0 einer vonpersonal | 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 alsNULLgespeichert — niemals als geratene Zahl oder0.0= echter Nullwert (brandneuer Post ohne Likes). Wird als0gespeichert.views_countist 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 insocial_profile_daily_metrics.scrape_qualitygespeichert.< 0.8= unvollständiger Scrape.anomalies[]:{ type, severity, detail }mitseverity ∈ {info, warning, critical}. Werden idempotent insocial_profile_anomaliesgespeichert; Einträge mit ungültigerseverityoder leeremtypewerden verworfen (geloggt). Nurwarning/criticalsollten 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
| Feld | watcher_import | daily_scrape |
|---|---|---|
job_type | "watcher_import" | "daily_scrape" |
run_id | Import-Run-ID | -- |
workspace_id | Ziel-Workspace | -- |
created_by | User-ID | -- |
social_profile_id | -- | Profil-ID |
input_url | Instagram-URL | Instagram-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:
| Code | Beschreibung |
|---|---|
| 409 | Job nicht im Status leased, Lease abgelaufen, anderer Client — oder doppelter Complete-Call auf einen bereits abgeschlossenen Job |
| 422 | Validierungsfehler 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.
| Kategorie | Codes | API-Verhalten |
|---|---|---|
| Session (Collector-Problem) | account_suspended, challenge_required, action_blocked, checkpoint_required, account_disabled, automated_behavior, scraping_restricted, not_logged_in, login_redirect, consent_required | Re-Queue ohne Strike — nicht das Profil ist schuld, sondern der ausgeloggte/geblockte Collector. Profil-Status bleibt unberührt; ein gesunder Collector übernimmt. |
| Benign | profile_not_found | Park (permanent fehlgeschlagen) + Profil wird auf api_status='inactive' gesetzt (existiert nicht mehr). |
tab_closed | Re-Queue ohne Strike (der Scrape fand schlicht nicht statt). | |
| Transient | request_timeout, request_aborted, profile_redirected | Re-Queue ohne Strike (mit Backoff). |
| Kumulativ | invalid_result, parse_error, rate_limited, scrape_timeout, scrape_error, missing_profile_url, tab_create_failed + jeder unbekannte Code | Re-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
- Error-Tracking: Jeder
complete()/fail()Call inkrementiert Cache-Counter pro Collector-Client (Success/Fail/Error-Code-Breakdown). - 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_activityJobs im current hour bucket, Default 5) wird alsInsufficientbewertet — 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. - Auto-Suspension: Bei >40% Fehlerrate wird der Token suspendiert — der Lease-Endpoint gibt HTTP 429 mit
Retry-After: 900zurueck. - Re-Test: 5 Test-Jobs werden an den suspendierten Token gesendet. Bei 80% Erfolg wird der Token automatisch reaktiviert.
- 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
| Level | Fehlerrate | Aktion |
|---|---|---|
| Healthy | <20% | Keine |
| Warning | 20-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
-
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.
-
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] ODERlast_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. -
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 machenWHERE (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 mittoday % 3→ Profile waren in der falschen Bucket-Klassifizierung. Migration viaphp 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 wennnot_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). -
Dispatch: Jobs mit
job_type=bonus_scrape,priority=-10,low_priority=true— Duty-Jobs haben immer Vorrang. -
Metriken-Isolation: Bonus-Scrapes inkrementieren
DailyScrapeProgressnicht, 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ählerbonus_scrapes:youtube:{date}— YouTube-Bonus-Zählerbonus_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