|
18217
|
791
|
14
|
2026-05-11T11:04:59.229152+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778497499229_m2.jpg...
|
iTerm2
|
DEV (docker)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.32ms DONE
cache [PASSWORD_DOTS] 10.62ms DONE
compiled [PASSWORD_DOTS] 3.60ms DONE
events [PASSWORD_DOTS] 2.60ms DONE
routes [PASSWORD_DOTS] 2.72ms DONE
views [PASSWORD_DOTS] 5.95ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
HubSpot\Client\Crm\Deals\ApiException
[429] Client error: `GET [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.49ms DONE
cache [PASSWORD_DOTS] 21.31ms DONE
compiled [PASSWORD_DOTS] 3.11ms DONE
events [PASSWORD_DOTS] 5.05ms DONE
routes [PASSWORD_DOTS] 1.83ms DONE
views [PASSWORD_DOTS] 4.91ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 37.77ms DONE
cache [PASSWORD_DOTS] 58.83ms DONE
compiled [PASSWORD_DOTS] 9.93ms DONE
events [PASSWORD_DOTS] 12.23ms DONE
routes [PASSWORD_DOTS] 5.02ms DONE
views [PASSWORD_DOTS] 21.46ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.01ms DONE
cache [PASSWORD_DOTS] 16.11ms DONE
compiled [PASSWORD_DOTS] 2.91ms DONE
events [PASSWORD_DOTS] 2.27ms DONE
routes [PASSWORD_DOTS] 3.11ms DONE
views [PASSWORD_DOTS] 18.41ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
SevenShores\Hubspot\Exceptions\BadRequest
Client error: `POST [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 55.84ms DONE
cache [PASSWORD_DOTS] 108.68ms DONE
compiled [PASSWORD_DOTS] 22.07ms DONE
events [PASSWORD_DOTS] 25.86ms DONE
routes [PASSWORD_DOTS] 19.91ms DONE
views [PASSWORD_DOTS] 52.25ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-audio:worker-audio_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
TypeError
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83
79▕
80▕ // Update reference parameters
81▕ $total = $state->total;
82▕ $lastRecordId = $state->lastRecordId;
➜ 83▕ }
84▕
85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool
86▕ {
87▕ if ($state->hasReachedSafetyLimit()) {
1 app/Services/Crm/Hubspot/Client.php:195
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\Services\Crm\Hubspot\Client), [], "contact")
2 app/Services/Crm/Hubspot/Client.php:176
Jiminny\Services\Crm\Hubspot\Client::getPaginatedDataGenerator([], "contact")
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.73ms DONE
cache [PASSWORD_DOTS] 19.13ms DONE
compiled [PASSWORD_DOTS] 4.93ms DONE
events [PASSWORD_DOTS] 3.02ms DONE
routes [PASSWORD_DOTS] 5.55ms DONE
views [PASSWORD_DOTS] 6.02ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.47ms DONE
cache [PASSWORD_DOTS] 16.66ms DONE
compiled [PASSWORD_DOTS] 3.26ms DONE
events [PASSWORD_DOTS] 3.99ms DONE
routes [PASSWORD_DOTS] 3.85ms DONE
views [PASSWORD_DOTS] 4.93ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.42ms DONE
cache [PASSWORD_DOTS] 23.70ms DONE
compiled [PASSWORD_DOTS] 3.46ms DONE
events [PASSWORD_DOTS] 4.30ms DONE
routes [PASSWORD_DOTS] 8.35ms DONE
views [PASSWORD_DOTS] 8.00ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.77ms DONE
cache [PASSWORD_DOTS] 25.55ms DONE
compiled [PASSWORD_DOTS] 3.87ms DONE
events [PASSWORD_DOTS] 4.96ms DONE
routes [PASSWORD_DOTS] 2.23ms DONE
views [PASSWORD_DOTS] 7.50ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ git status
On branch JY-20725-handle-HS-search-rate-limit
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: app/Jobs/Middleware/HandleHubspotRateLimit.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Exceptions/RateLimitException.php
modified: app/Jobs/Activity/Import/MatchCrmData.php
modified: app/Jobs/Crm/MatchActivityCrmData.php
modified: app/Services/Crm/Hubspot/Client.php
modified: app/Services/Crm/Hubspot/HubspotClientInterface.php
modified: app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php
modified: app/Services/Crm/Hubspot/Pagination/PaginationState.php
modified: app/Services/Crm/Hubspot/Service.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.local.bak
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ gbr
* JY-20725-handle-HS-search-rate-limit
master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ dev
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Illuminate\Database\QueryException
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'team_id' in 'WHERE' (Connection: mysql, Host: mariadb, Port: 3306, Database: jiminny, SQL: select * from `activities` where `team_id` = 2 and `activities`.`deleted_at` is null order by `id` desc limit 100)
at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
834▕ $exceptionType = $this->isUniqueConstraintError($e)
835▕ ? UniqueConstraintViolationException::class
836▕ : QueryException::class;
837▕
➜ 838▕ throw new $exceptionType(
839▕ $this->getNameWithReadWriteType(),
840▕ $query,
841▕ $this->prepareBindings($bindings),
842▕ $e,
+11 vendor frames
12 app/Console/Commands/JiminnyDebugCommand.php:380
Illuminate\Database\Eloquent\Builder::get()
13 app/Console/Commands/JiminnyDebugCommand.php:49
Jiminny\Console\Commands\JiminnyDebugCommand::simulateMatchActivityStorm()
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.01ms DONE
cache [PASSWORD_DOTS] 38.00ms DONE
compiled [PASSWORD_DOTS] 1.78ms DONE
events [PASSWORD_DOTS] 1.13ms DONE
routes [PASSWORD_DOTS] 1.56ms DONE
views [PASSWORD_DOTS] 3.25ms DONE
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-audio:worker-audio_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.34ms DONE
cache [PASSWORD_DOTS] 11.98ms DONE
compiled [PASSWORD_DOTS] 2.10ms DONE
events [PASSWORD_DOTS] 5.31ms DONE
routes [PASSWORD_DOTS] 2.90ms DONE
views [PASSWORD_DOTS] 13.11ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.53ms DONE
cache [PASSWORD_DOTS] 128.25ms DONE
compiled [PASSWORD_DOTS] 1.73ms DONE
events [PASSWORD_DOTS] 1.07ms DONE
routes [PASSWORD_DOTS] 1.45ms DONE
views [PASSWORD_DOTS] 5.46ms DONE
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-emails:worker-emails_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData j...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.32ms DONE\n cache ............................................................................................................................... 10.62ms DONE\n compiled ............................................................................................................................. 3.60ms DONE\n events ............................................................................................................................... 2.60ms DONE\n routes ............................................................................................................................... 2.72ms DONE\n views ................................................................................................................................ 5.95ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\n\n HubSpot\\Client\\Crm\\Deals\\ApiException \n\n [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n\n at vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704\n 700▕ $options = $this->createHttpClientOption();\n 701▕ try {\n 702▕ $response = $this->client->send($request, $options);\n 703▕ } catch (RequestException $e) {\n ➜ 704▕ throw new ApiException(\n 705▕ \"[{$e->getCode()}] {$e->getMessage()}\",\n 706▕ (int) $e->getCode(),\n 707▕ $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n 708▕ $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n\n +1 vendor frames \n\n 2 app/Services/Crm/Hubspot/Client.php:212\n HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi::getById(\"374720564\", \"hs_object_id,dealname\", \"companies,contacts\")\n\n 3 app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php:130\n Jiminny\\Services\\Crm\\Hubspot\\Client::getOpportunityById(\"374720564\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.49ms DONE\n cache ............................................................................................................................... 21.31ms DONE\n compiled ............................................................................................................................. 3.11ms DONE\n events ............................................................................................................................... 5.05ms DONE\n routes ............................................................................................................................... 1.83ms DONE\n views ................................................................................................................................ 4.91ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 37.77ms DONE\n cache ............................................................................................................................... 58.83ms DONE\n compiled ............................................................................................................................. 9.93ms DONE\n events .............................................................................................................................. 12.23ms DONE\n routes ............................................................................................................................... 5.02ms DONE\n views ............................................................................................................................... 21.46ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.01ms DONE\n cache ............................................................................................................................... 16.11ms DONE\n compiled ............................................................................................................................. 2.91ms DONE\n events ............................................................................................................................... 2.27ms DONE\n routes ............................................................................................................................... 3.11ms DONE\n views ............................................................................................................................... 18.41ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n SevenShores\\Hubspot\\Exceptions\\BadRequest \n\n Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e0284-5 (truncated...)\n\n at vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24\n 20▕ }\n 21▕ \n 22▕ public static function create(RequestException $guzzleException): self\n 23▕ {\n ➜ 24▕ $e = new static(\n 25▕ static::sanitizeResponseMessage($guzzleException->getMessage()),\n 26▕ $guzzleException->getCode(),\n 27▕ $guzzleException\n 28▕ );\n\n +13 vendor frames \n\n 14 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:163\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:51\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 55.84ms DONE\n cache .............................................................................................................................. 108.68ms DONE\n compiled ............................................................................................................................ 22.07ms DONE\n events .............................................................................................................................. 25.86ms DONE\n routes .............................................................................................................................. 19.91ms DONE\n views ............................................................................................................................... 52.25ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n TypeError \n\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83\n 79▕ \n 80▕ // Update reference parameters\n 81▕ $total = $state->total;\n 82▕ $lastRecordId = $state->lastRecordId;\n ➜ 83▕ }\n 84▕ \n 85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool\n 86▕ {\n 87▕ if ($state->hasReachedSafetyLimit()) {\n\n 1 app/Services/Crm/Hubspot/Client.php:195\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), [], \"contact\")\n\n 2 app/Services/Crm/Hubspot/Client.php:176\n Jiminny\\Services\\Crm\\Hubspot\\Client::getPaginatedDataGenerator([], \"contact\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.73ms DONE\n cache ............................................................................................................................... 19.13ms DONE\n compiled ............................................................................................................................. 4.93ms DONE\n events ............................................................................................................................... 3.02ms DONE\n routes ............................................................................................................................... 5.55ms DONE\n views ................................................................................................................................ 6.02ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.47ms DONE\n cache ............................................................................................................................... 16.66ms DONE\n compiled ............................................................................................................................. 3.26ms DONE\n events ............................................................................................................................... 3.99ms DONE\n routes ............................................................................................................................... 3.85ms DONE\n views ................................................................................................................................ 4.93ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.42ms DONE\n cache ............................................................................................................................... 23.70ms DONE\n compiled ............................................................................................................................. 3.46ms DONE\n events ............................................................................................................................... 4.30ms DONE\n routes ............................................................................................................................... 8.35ms DONE\n views ................................................................................................................................ 8.00ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.77ms DONE\n cache ............................................................................................................................... 25.55ms DONE\n compiled ............................................................................................................................. 3.87ms DONE\n events ............................................................................................................................... 4.96ms DONE\n routes ............................................................................................................................... 2.23ms DONE\n views ................................................................................................................................ 7.50ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ dev\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ git status\nOn branch JY-20725-handle-HS-search-rate-limit\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: app/Jobs/Middleware/HandleHubspotRateLimit.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Exceptions/RateLimitException.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Activity/Import/MatchCrmData.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Crm/MatchActivityCrmData.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Client.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/HubspotClientInterface.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Pagination/PaginationState.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.local.bak\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ gbr\n* JY-20725-handle-HS-search-rate-limit\n master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\n\n Illuminate\\Database\\QueryException \n\n SQLSTATE[42S22]: Column not found: 1054 Unknown column 'team_id' in 'WHERE' (Connection: mysql, Host: mariadb, Port: 3306, Database: jiminny, SQL: select * from `activities` where `team_id` = 2 and `activities`.`deleted_at` is null order by `id` desc limit 100)\n\n at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838\n 834▕ $exceptionType = $this->isUniqueConstraintError($e)\n 835▕ ? UniqueConstraintViolationException::class\n 836▕ : QueryException::class;\n 837▕ \n ➜ 838▕ throw new $exceptionType(\n 839▕ $this->getNameWithReadWriteType(),\n 840▕ $query,\n 841▕ $this->prepareBindings($bindings),\n 842▕ $e,\n\n +11 vendor frames \n\n 12 app/Console/Commands/JiminnyDebugCommand.php:380\n Illuminate\\Database\\Eloquent\\Builder::get()\n\n 13 app/Console/Commands/JiminnyDebugCommand.php:49\n Jiminny\\Console\\Commands\\JiminnyDebugCommand::simulateMatchActivityStorm()\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.01ms DONE\n cache ............................................................................................................................... 38.00ms DONE\n compiled ............................................................................................................................. 1.78ms DONE\n events ............................................................................................................................... 1.13ms DONE\n routes ............................................................................................................................... 1.56ms DONE\n views ................................................................................................................................ 3.25ms DONE\n\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.34ms DONE\n cache ............................................................................................................................... 11.98ms DONE\n compiled ............................................................................................................................. 2.10ms DONE\n events ............................................................................................................................... 5.31ms DONE\n routes ............................................................................................................................... 2.90ms DONE\n views ............................................................................................................................... 13.11ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.53ms DONE\n cache .............................................................................................................................. 128.25ms DONE\n compiled ............................................................................................................................. 1.73ms DONE\n events ............................................................................................................................... 1.07ms DONE\n routes ............................................................................................................................... 1.45ms DONE\n views ................................................................................................................................ 5.46ms DONE\n\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 69.57ms DONE\n cache .............................................................................................................................. 285.32ms DONE\n compiled ............................................................................................................................ 13.02ms DONE\n events .............................................................................................................................. 11.48ms DONE\n routes ............................................................................................................................... 6.30ms DONE\n views ............................................................................................................................... 51.86ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-es-update:worker-es-update_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny#","depth":4,"on_screen":true,"value":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.32ms DONE\n cache ............................................................................................................................... 10.62ms DONE\n compiled ............................................................................................................................. 3.60ms DONE\n events ............................................................................................................................... 2.60ms DONE\n routes ............................................................................................................................... 2.72ms DONE\n views ................................................................................................................................ 5.95ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\n\n HubSpot\\Client\\Crm\\Deals\\ApiException \n\n [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n\n at vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704\n 700▕ $options = $this->createHttpClientOption();\n 701▕ try {\n 702▕ $response = $this->client->send($request, $options);\n 703▕ } catch (RequestException $e) {\n ➜ 704▕ throw new ApiException(\n 705▕ \"[{$e->getCode()}] {$e->getMessage()}\",\n 706▕ (int) $e->getCode(),\n 707▕ $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n 708▕ $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n\n +1 vendor frames \n\n 2 app/Services/Crm/Hubspot/Client.php:212\n HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi::getById(\"374720564\", \"hs_object_id,dealname\", \"companies,contacts\")\n\n 3 app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php:130\n Jiminny\\Services\\Crm\\Hubspot\\Client::getOpportunityById(\"374720564\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.49ms DONE\n cache ............................................................................................................................... 21.31ms DONE\n compiled ............................................................................................................................. 3.11ms DONE\n events ............................................................................................................................... 5.05ms DONE\n routes ............................................................................................................................... 1.83ms DONE\n views ................................................................................................................................ 4.91ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 37.77ms DONE\n cache ............................................................................................................................... 58.83ms DONE\n compiled ............................................................................................................................. 9.93ms DONE\n events .............................................................................................................................. 12.23ms DONE\n routes ............................................................................................................................... 5.02ms DONE\n views ............................................................................................................................... 21.46ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.01ms DONE\n cache ............................................................................................................................... 16.11ms DONE\n compiled ............................................................................................................................. 2.91ms DONE\n events ............................................................................................................................... 2.27ms DONE\n routes ............................................................................................................................... 3.11ms DONE\n views ............................................................................................................................... 18.41ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n SevenShores\\Hubspot\\Exceptions\\BadRequest \n\n Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e0284-5 (truncated...)\n\n at vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24\n 20▕ }\n 21▕ \n 22▕ public static function create(RequestException $guzzleException): self\n 23▕ {\n ➜ 24▕ $e = new static(\n 25▕ static::sanitizeResponseMessage($guzzleException->getMessage()),\n 26▕ $guzzleException->getCode(),\n 27▕ $guzzleException\n 28▕ );\n\n +13 vendor frames \n\n 14 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:163\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:51\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 55.84ms DONE\n cache .............................................................................................................................. 108.68ms DONE\n compiled ............................................................................................................................ 22.07ms DONE\n events .............................................................................................................................. 25.86ms DONE\n routes .............................................................................................................................. 19.91ms DONE\n views ............................................................................................................................... 52.25ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n TypeError \n\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83\n 79▕ \n 80▕ // Update reference parameters\n 81▕ $total = $state->total;\n 82▕ $lastRecordId = $state->lastRecordId;\n ➜ 83▕ }\n 84▕ \n 85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool\n 86▕ {\n 87▕ if ($state->hasReachedSafetyLimit()) {\n\n 1 app/Services/Crm/Hubspot/Client.php:195\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), [], \"contact\")\n\n 2 app/Services/Crm/Hubspot/Client.php:176\n Jiminny\\Services\\Crm\\Hubspot\\Client::getPaginatedDataGenerator([], \"contact\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.73ms DONE\n cache ............................................................................................................................... 19.13ms DONE\n compiled ............................................................................................................................. 4.93ms DONE\n events ............................................................................................................................... 3.02ms DONE\n routes ............................................................................................................................... 5.55ms DONE\n views ................................................................................................................................ 6.02ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.47ms DONE\n cache ............................................................................................................................... 16.66ms DONE\n compiled ............................................................................................................................. 3.26ms DONE\n events ............................................................................................................................... 3.99ms DONE\n routes ............................................................................................................................... 3.85ms DONE\n views ................................................................................................................................ 4.93ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.42ms DONE\n cache ............................................................................................................................... 23.70ms DONE\n compiled ............................................................................................................................. 3.46ms DONE\n events ............................................................................................................................... 4.30ms DONE\n routes ............................................................................................................................... 8.35ms DONE\n views ................................................................................................................................ 8.00ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.77ms DONE\n cache ............................................................................................................................... 25.55ms DONE\n compiled ............................................................................................................................. 3.87ms DONE\n events ............................................................................................................................... 4.96ms DONE\n routes ............................................................................................................................... 2.23ms DONE\n views ................................................................................................................................ 7.50ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ dev\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ git status\nOn branch JY-20725-handle-HS-search-rate-limit\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: app/Jobs/Middleware/HandleHubspotRateLimit.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Exceptions/RateLimitException.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Activity/Import/MatchCrmData.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Crm/MatchActivityCrmData.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Client.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/HubspotClientInterface.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Pagination/PaginationState.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.local.bak\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ gbr\n* JY-20725-handle-HS-search-rate-limit\n master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\n\n Illuminate\\Database\\QueryException \n\n SQLSTATE[42S22]: Column not found: 1054 Unknown column 'team_id' in 'WHERE' (Connection: mysql, Host: mariadb, Port: 3306, Database: jiminny, SQL: select * from `activities` where `team_id` = 2 and `activities`.`deleted_at` is null order by `id` desc limit 100)\n\n at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838\n 834▕ $exceptionType = $this->isUniqueConstraintError($e)\n 835▕ ? UniqueConstraintViolationException::class\n 836▕ : QueryException::class;\n 837▕ \n ➜ 838▕ throw new $exceptionType(\n 839▕ $this->getNameWithReadWriteType(),\n 840▕ $query,\n 841▕ $this->prepareBindings($bindings),\n 842▕ $e,\n\n +11 vendor frames \n\n 12 app/Console/Commands/JiminnyDebugCommand.php:380\n Illuminate\\Database\\Eloquent\\Builder::get()\n\n 13 app/Console/Commands/JiminnyDebugCommand.php:49\n Jiminny\\Console\\Commands\\JiminnyDebugCommand::simulateMatchActivityStorm()\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.01ms DONE\n cache ............................................................................................................................... 38.00ms DONE\n compiled ............................................................................................................................. 1.78ms DONE\n events ............................................................................................................................... 1.13ms DONE\n routes ............................................................................................................................... 1.56ms DONE\n views ................................................................................................................................ 3.25ms DONE\n\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.34ms DONE\n cache ............................................................................................................................... 11.98ms DONE\n compiled ............................................................................................................................. 2.10ms DONE\n events ............................................................................................................................... 5.31ms DONE\n routes ............................................................................................................................... 2.90ms DONE\n views ............................................................................................................................... 13.11ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.53ms DONE\n cache .............................................................................................................................. 128.25ms DONE\n compiled ............................................................................................................................. 1.73ms DONE\n events ............................................................................................................................... 1.07ms DONE\n routes ............................................................................................................................... 1.45ms DONE\n views ................................................................................................................................ 5.46ms DONE\n\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 69.57ms DONE\n cache .............................................................................................................................. 285.32ms DONE\n compiled ............................................................................................................................ 13.02ms DONE\n events .............................................................................................................................. 11.48ms DONE\n routes ............................................................................................................................... 6.30ms DONE\n views ............................................................................................................................... 51.86ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-es-update:worker-es-update_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.49468085,"top":1.0,"width":0.029920213,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-8908419307871373980
|
4435031406530046724
|
click
|
accessibility
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.32ms DONE
cache [PASSWORD_DOTS] 10.62ms DONE
compiled [PASSWORD_DOTS] 3.60ms DONE
events [PASSWORD_DOTS] 2.60ms DONE
routes [PASSWORD_DOTS] 2.72ms DONE
views [PASSWORD_DOTS] 5.95ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
HubSpot\Client\Crm\Deals\ApiException
[429] Client error: `GET [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.49ms DONE
cache [PASSWORD_DOTS] 21.31ms DONE
compiled [PASSWORD_DOTS] 3.11ms DONE
events [PASSWORD_DOTS] 5.05ms DONE
routes [PASSWORD_DOTS] 1.83ms DONE
views [PASSWORD_DOTS] 4.91ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 37.77ms DONE
cache [PASSWORD_DOTS] 58.83ms DONE
compiled [PASSWORD_DOTS] 9.93ms DONE
events [PASSWORD_DOTS] 12.23ms DONE
routes [PASSWORD_DOTS] 5.02ms DONE
views [PASSWORD_DOTS] 21.46ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.01ms DONE
cache [PASSWORD_DOTS] 16.11ms DONE
compiled [PASSWORD_DOTS] 2.91ms DONE
events [PASSWORD_DOTS] 2.27ms DONE
routes [PASSWORD_DOTS] 3.11ms DONE
views [PASSWORD_DOTS] 18.41ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
SevenShores\Hubspot\Exceptions\BadRequest
Client error: `POST [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 55.84ms DONE
cache [PASSWORD_DOTS] 108.68ms DONE
compiled [PASSWORD_DOTS] 22.07ms DONE
events [PASSWORD_DOTS] 25.86ms DONE
routes [PASSWORD_DOTS] 19.91ms DONE
views [PASSWORD_DOTS] 52.25ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-audio:worker-audio_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
TypeError
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83
79▕
80▕ // Update reference parameters
81▕ $total = $state->total;
82▕ $lastRecordId = $state->lastRecordId;
➜ 83▕ }
84▕
85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool
86▕ {
87▕ if ($state->hasReachedSafetyLimit()) {
1 app/Services/Crm/Hubspot/Client.php:195
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\Services\Crm\Hubspot\Client), [], "contact")
2 app/Services/Crm/Hubspot/Client.php:176
Jiminny\Services\Crm\Hubspot\Client::getPaginatedDataGenerator([], "contact")
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.73ms DONE
cache [PASSWORD_DOTS] 19.13ms DONE
compiled [PASSWORD_DOTS] 4.93ms DONE
events [PASSWORD_DOTS] 3.02ms DONE
routes [PASSWORD_DOTS] 5.55ms DONE
views [PASSWORD_DOTS] 6.02ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.47ms DONE
cache [PASSWORD_DOTS] 16.66ms DONE
compiled [PASSWORD_DOTS] 3.26ms DONE
events [PASSWORD_DOTS] 3.99ms DONE
routes [PASSWORD_DOTS] 3.85ms DONE
views [PASSWORD_DOTS] 4.93ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.42ms DONE
cache [PASSWORD_DOTS] 23.70ms DONE
compiled [PASSWORD_DOTS] 3.46ms DONE
events [PASSWORD_DOTS] 4.30ms DONE
routes [PASSWORD_DOTS] 8.35ms DONE
views [PASSWORD_DOTS] 8.00ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.77ms DONE
cache [PASSWORD_DOTS] 25.55ms DONE
compiled [PASSWORD_DOTS] 3.87ms DONE
events [PASSWORD_DOTS] 4.96ms DONE
routes [PASSWORD_DOTS] 2.23ms DONE
views [PASSWORD_DOTS] 7.50ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ git status
On branch JY-20725-handle-HS-search-rate-limit
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: app/Jobs/Middleware/HandleHubspotRateLimit.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Exceptions/RateLimitException.php
modified: app/Jobs/Activity/Import/MatchCrmData.php
modified: app/Jobs/Crm/MatchActivityCrmData.php
modified: app/Services/Crm/Hubspot/Client.php
modified: app/Services/Crm/Hubspot/HubspotClientInterface.php
modified: app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php
modified: app/Services/Crm/Hubspot/Pagination/PaginationState.php
modified: app/Services/Crm/Hubspot/Service.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.local.bak
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ gbr
* JY-20725-handle-HS-search-rate-limit
master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ dev
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Illuminate\Database\QueryException
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'team_id' in 'WHERE' (Connection: mysql, Host: mariadb, Port: 3306, Database: jiminny, SQL: select * from `activities` where `team_id` = 2 and `activities`.`deleted_at` is null order by `id` desc limit 100)
at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
834▕ $exceptionType = $this->isUniqueConstraintError($e)
835▕ ? UniqueConstraintViolationException::class
836▕ : QueryException::class;
837▕
➜ 838▕ throw new $exceptionType(
839▕ $this->getNameWithReadWriteType(),
840▕ $query,
841▕ $this->prepareBindings($bindings),
842▕ $e,
+11 vendor frames
12 app/Console/Commands/JiminnyDebugCommand.php:380
Illuminate\Database\Eloquent\Builder::get()
13 app/Console/Commands/JiminnyDebugCommand.php:49
Jiminny\Console\Commands\JiminnyDebugCommand::simulateMatchActivityStorm()
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.01ms DONE
cache [PASSWORD_DOTS] 38.00ms DONE
compiled [PASSWORD_DOTS] 1.78ms DONE
events [PASSWORD_DOTS] 1.13ms DONE
routes [PASSWORD_DOTS] 1.56ms DONE
views [PASSWORD_DOTS] 3.25ms DONE
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-audio:worker-audio_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.34ms DONE
cache [PASSWORD_DOTS] 11.98ms DONE
compiled [PASSWORD_DOTS] 2.10ms DONE
events [PASSWORD_DOTS] 5.31ms DONE
routes [PASSWORD_DOTS] 2.90ms DONE
views [PASSWORD_DOTS] 13.11ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.53ms DONE
cache [PASSWORD_DOTS] 128.25ms DONE
compiled [PASSWORD_DOTS] 1.73ms DONE
events [PASSWORD_DOTS] 1.07ms DONE
routes [PASSWORD_DOTS] 1.45ms DONE
views [PASSWORD_DOTS] 5.46ms DONE
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-emails:worker-emails_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData j...
|
18216
|
NULL
|
NULL
|
NULL
|
|
18219
|
790
|
16
|
2026-05-11T11:05:00.303491+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778497500303_m1.jpg...
|
iTerm2
|
DEV (docker)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.32ms DONE
cache [PASSWORD_DOTS] 10.62ms DONE
compiled [PASSWORD_DOTS] 3.60ms DONE
events [PASSWORD_DOTS] 2.60ms DONE
routes [PASSWORD_DOTS] 2.72ms DONE
views [PASSWORD_DOTS] 5.95ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
HubSpot\Client\Crm\Deals\ApiException
[429] Client error: `GET [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.49ms DONE
cache [PASSWORD_DOTS] 21.31ms DONE
compiled [PASSWORD_DOTS] 3.11ms DONE
events [PASSWORD_DOTS] 5.05ms DONE
routes [PASSWORD_DOTS] 1.83ms DONE
views [PASSWORD_DOTS] 4.91ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 37.77ms DONE
cache [PASSWORD_DOTS] 58.83ms DONE
compiled [PASSWORD_DOTS] 9.93ms DONE
events [PASSWORD_DOTS] 12.23ms DONE
routes [PASSWORD_DOTS] 5.02ms DONE
views [PASSWORD_DOTS] 21.46ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.01ms DONE
cache [PASSWORD_DOTS] 16.11ms DONE
compiled [PASSWORD_DOTS] 2.91ms DONE
events [PASSWORD_DOTS] 2.27ms DONE
routes [PASSWORD_DOTS] 3.11ms DONE
views [PASSWORD_DOTS] 18.41ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
SevenShores\Hubspot\Exceptions\BadRequest
Client error: `POST [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 55.84ms DONE
cache [PASSWORD_DOTS] 108.68ms DONE
compiled [PASSWORD_DOTS] 22.07ms DONE
events [PASSWORD_DOTS] 25.86ms DONE
routes [PASSWORD_DOTS] 19.91ms DONE
views [PASSWORD_DOTS] 52.25ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-audio:worker-audio_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
TypeError
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83
79▕
80▕ // Update reference parameters
81▕ $total = $state->total;
82▕ $lastRecordId = $state->lastRecordId;
➜ 83▕ }
84▕
85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool
86▕ {
87▕ if ($state->hasReachedSafetyLimit()) {
1 app/Services/Crm/Hubspot/Client.php:195
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\Services\Crm\Hubspot\Client), [], "contact")
2 app/Services/Crm/Hubspot/Client.php:176
Jiminny\Services\Crm\Hubspot\Client::getPaginatedDataGenerator([], "contact")
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.73ms DONE
cache [PASSWORD_DOTS] 19.13ms DONE
compiled [PASSWORD_DOTS] 4.93ms DONE
events [PASSWORD_DOTS] 3.02ms DONE
routes [PASSWORD_DOTS] 5.55ms DONE
views [PASSWORD_DOTS] 6.02ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.47ms DONE
cache [PASSWORD_DOTS] 16.66ms DONE
compiled [PASSWORD_DOTS] 3.26ms DONE
events [PASSWORD_DOTS] 3.99ms DONE
routes [PASSWORD_DOTS] 3.85ms DONE
views [PASSWORD_DOTS] 4.93ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.42ms DONE
cache [PASSWORD_DOTS] 23.70ms DONE
compiled [PASSWORD_DOTS] 3.46ms DONE
events [PASSWORD_DOTS] 4.30ms DONE
routes [PASSWORD_DOTS] 8.35ms DONE
views [PASSWORD_DOTS] 8.00ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.77ms DONE
cache [PASSWORD_DOTS] 25.55ms DONE
compiled [PASSWORD_DOTS] 3.87ms DONE
events [PASSWORD_DOTS] 4.96ms DONE
routes [PASSWORD_DOTS] 2.23ms DONE
views [PASSWORD_DOTS] 7.50ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ git status
On branch JY-20725-handle-HS-search-rate-limit
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: app/Jobs/Middleware/HandleHubspotRateLimit.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Exceptions/RateLimitException.php
modified: app/Jobs/Activity/Import/MatchCrmData.php
modified: app/Jobs/Crm/MatchActivityCrmData.php
modified: app/Services/Crm/Hubspot/Client.php
modified: app/Services/Crm/Hubspot/HubspotClientInterface.php
modified: app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php
modified: app/Services/Crm/Hubspot/Pagination/PaginationState.php
modified: app/Services/Crm/Hubspot/Service.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.local.bak
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ gbr
* JY-20725-handle-HS-search-rate-limit
master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ dev
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Illuminate\Database\QueryException
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'team_id' in 'WHERE' (Connection: mysql, Host: mariadb, Port: 3306, Database: jiminny, SQL: select * from `activities` where `team_id` = 2 and `activities`.`deleted_at` is null order by `id` desc limit 100)
at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
834▕ $exceptionType = $this->isUniqueConstraintError($e)
835▕ ? UniqueConstraintViolationException::class
836▕ : QueryException::class;
837▕
➜ 838▕ throw new $exceptionType(
839▕ $this->getNameWithReadWriteType(),
840▕ $query,
841▕ $this->prepareBindings($bindings),
842▕ $e,
+11 vendor frames
12 app/Console/Commands/JiminnyDebugCommand.php:380
Illuminate\Database\Eloquent\Builder::get()
13 app/Console/Commands/JiminnyDebugCommand.php:49
Jiminny\Console\Commands\JiminnyDebugCommand::simulateMatchActivityStorm()
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.01ms DONE
cache [PASSWORD_DOTS] 38.00ms DONE
compiled [PASSWORD_DOTS] 1.78ms DONE
events [PASSWORD_DOTS] 1.13ms DONE
routes [PASSWORD_DOTS] 1.56ms DONE
views [PASSWORD_DOTS] 3.25ms DONE
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-audio:worker-audio_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.34ms DONE
cache [PASSWORD_DOTS] 11.98ms DONE
compiled [PASSWORD_DOTS] 2.10ms DONE
events [PASSWORD_DOTS] 5.31ms DONE
routes [PASSWORD_DOTS] 2.90ms DONE
views [PASSWORD_DOTS] 13.11ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.53ms DONE
cache [PASSWORD_DOTS] 128.25ms DONE
compiled [PASSWORD_DOTS] 1.73ms DONE
events [PASSWORD_DOTS] 1.07ms DONE
routes [PASSWORD_DOTS] 1.45ms DONE
views [PASSWORD_DOTS] 5.46ms DONE
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-emails:worker-emails_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData j...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.32ms DONE\n cache ............................................................................................................................... 10.62ms DONE\n compiled ............................................................................................................................. 3.60ms DONE\n events ............................................................................................................................... 2.60ms DONE\n routes ............................................................................................................................... 2.72ms DONE\n views ................................................................................................................................ 5.95ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\n\n HubSpot\\Client\\Crm\\Deals\\ApiException \n\n [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n\n at vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704\n 700▕ $options = $this->createHttpClientOption();\n 701▕ try {\n 702▕ $response = $this->client->send($request, $options);\n 703▕ } catch (RequestException $e) {\n ➜ 704▕ throw new ApiException(\n 705▕ \"[{$e->getCode()}] {$e->getMessage()}\",\n 706▕ (int) $e->getCode(),\n 707▕ $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n 708▕ $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n\n +1 vendor frames \n\n 2 app/Services/Crm/Hubspot/Client.php:212\n HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi::getById(\"374720564\", \"hs_object_id,dealname\", \"companies,contacts\")\n\n 3 app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php:130\n Jiminny\\Services\\Crm\\Hubspot\\Client::getOpportunityById(\"374720564\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.49ms DONE\n cache ............................................................................................................................... 21.31ms DONE\n compiled ............................................................................................................................. 3.11ms DONE\n events ............................................................................................................................... 5.05ms DONE\n routes ............................................................................................................................... 1.83ms DONE\n views ................................................................................................................................ 4.91ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 37.77ms DONE\n cache ............................................................................................................................... 58.83ms DONE\n compiled ............................................................................................................................. 9.93ms DONE\n events .............................................................................................................................. 12.23ms DONE\n routes ............................................................................................................................... 5.02ms DONE\n views ............................................................................................................................... 21.46ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.01ms DONE\n cache ............................................................................................................................... 16.11ms DONE\n compiled ............................................................................................................................. 2.91ms DONE\n events ............................................................................................................................... 2.27ms DONE\n routes ............................................................................................................................... 3.11ms DONE\n views ............................................................................................................................... 18.41ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n SevenShores\\Hubspot\\Exceptions\\BadRequest \n\n Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e0284-5 (truncated...)\n\n at vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24\n 20▕ }\n 21▕ \n 22▕ public static function create(RequestException $guzzleException): self\n 23▕ {\n ➜ 24▕ $e = new static(\n 25▕ static::sanitizeResponseMessage($guzzleException->getMessage()),\n 26▕ $guzzleException->getCode(),\n 27▕ $guzzleException\n 28▕ );\n\n +13 vendor frames \n\n 14 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:163\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:51\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 55.84ms DONE\n cache .............................................................................................................................. 108.68ms DONE\n compiled ............................................................................................................................ 22.07ms DONE\n events .............................................................................................................................. 25.86ms DONE\n routes .............................................................................................................................. 19.91ms DONE\n views ............................................................................................................................... 52.25ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n TypeError \n\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83\n 79▕ \n 80▕ // Update reference parameters\n 81▕ $total = $state->total;\n 82▕ $lastRecordId = $state->lastRecordId;\n ➜ 83▕ }\n 84▕ \n 85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool\n 86▕ {\n 87▕ if ($state->hasReachedSafetyLimit()) {\n\n 1 app/Services/Crm/Hubspot/Client.php:195\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), [], \"contact\")\n\n 2 app/Services/Crm/Hubspot/Client.php:176\n Jiminny\\Services\\Crm\\Hubspot\\Client::getPaginatedDataGenerator([], \"contact\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.73ms DONE\n cache ............................................................................................................................... 19.13ms DONE\n compiled ............................................................................................................................. 4.93ms DONE\n events ............................................................................................................................... 3.02ms DONE\n routes ............................................................................................................................... 5.55ms DONE\n views ................................................................................................................................ 6.02ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.47ms DONE\n cache ............................................................................................................................... 16.66ms DONE\n compiled ............................................................................................................................. 3.26ms DONE\n events ............................................................................................................................... 3.99ms DONE\n routes ............................................................................................................................... 3.85ms DONE\n views ................................................................................................................................ 4.93ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.42ms DONE\n cache ............................................................................................................................... 23.70ms DONE\n compiled ............................................................................................................................. 3.46ms DONE\n events ............................................................................................................................... 4.30ms DONE\n routes ............................................................................................................................... 8.35ms DONE\n views ................................................................................................................................ 8.00ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.77ms DONE\n cache ............................................................................................................................... 25.55ms DONE\n compiled ............................................................................................................................. 3.87ms DONE\n events ............................................................................................................................... 4.96ms DONE\n routes ............................................................................................................................... 2.23ms DONE\n views ................................................................................................................................ 7.50ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ dev\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ git status\nOn branch JY-20725-handle-HS-search-rate-limit\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: app/Jobs/Middleware/HandleHubspotRateLimit.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Exceptions/RateLimitException.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Activity/Import/MatchCrmData.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Crm/MatchActivityCrmData.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Client.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/HubspotClientInterface.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Pagination/PaginationState.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.local.bak\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ gbr\n* JY-20725-handle-HS-search-rate-limit\n master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\n\n Illuminate\\Database\\QueryException \n\n SQLSTATE[42S22]: Column not found: 1054 Unknown column 'team_id' in 'WHERE' (Connection: mysql, Host: mariadb, Port: 3306, Database: jiminny, SQL: select * from `activities` where `team_id` = 2 and `activities`.`deleted_at` is null order by `id` desc limit 100)\n\n at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838\n 834▕ $exceptionType = $this->isUniqueConstraintError($e)\n 835▕ ? UniqueConstraintViolationException::class\n 836▕ : QueryException::class;\n 837▕ \n ➜ 838▕ throw new $exceptionType(\n 839▕ $this->getNameWithReadWriteType(),\n 840▕ $query,\n 841▕ $this->prepareBindings($bindings),\n 842▕ $e,\n\n +11 vendor frames \n\n 12 app/Console/Commands/JiminnyDebugCommand.php:380\n Illuminate\\Database\\Eloquent\\Builder::get()\n\n 13 app/Console/Commands/JiminnyDebugCommand.php:49\n Jiminny\\Console\\Commands\\JiminnyDebugCommand::simulateMatchActivityStorm()\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.01ms DONE\n cache ............................................................................................................................... 38.00ms DONE\n compiled ............................................................................................................................. 1.78ms DONE\n events ............................................................................................................................... 1.13ms DONE\n routes ............................................................................................................................... 1.56ms DONE\n views ................................................................................................................................ 3.25ms DONE\n\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.34ms DONE\n cache ............................................................................................................................... 11.98ms DONE\n compiled ............................................................................................................................. 2.10ms DONE\n events ............................................................................................................................... 5.31ms DONE\n routes ............................................................................................................................... 2.90ms DONE\n views ............................................................................................................................... 13.11ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.53ms DONE\n cache .............................................................................................................................. 128.25ms DONE\n compiled ............................................................................................................................. 1.73ms DONE\n events ............................................................................................................................... 1.07ms DONE\n routes ............................................................................................................................... 1.45ms DONE\n views ................................................................................................................................ 5.46ms DONE\n\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 69.57ms DONE\n cache .............................................................................................................................. 285.32ms DONE\n compiled ............................................................................................................................ 13.02ms DONE\n events .............................................................................................................................. 11.48ms DONE\n routes ............................................................................................................................... 6.30ms DONE\n views ............................................................................................................................... 51.86ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-es-update:worker-es-update_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny#","depth":4,"on_screen":true,"value":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.32ms DONE\n cache ............................................................................................................................... 10.62ms DONE\n compiled ............................................................................................................................. 3.60ms DONE\n events ............................................................................................................................... 2.60ms DONE\n routes ............................................................................................................................... 2.72ms DONE\n views ................................................................................................................................ 5.95ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\n\n HubSpot\\Client\\Crm\\Deals\\ApiException \n\n [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n\n at vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704\n 700▕ $options = $this->createHttpClientOption();\n 701▕ try {\n 702▕ $response = $this->client->send($request, $options);\n 703▕ } catch (RequestException $e) {\n ➜ 704▕ throw new ApiException(\n 705▕ \"[{$e->getCode()}] {$e->getMessage()}\",\n 706▕ (int) $e->getCode(),\n 707▕ $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n 708▕ $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n\n +1 vendor frames \n\n 2 app/Services/Crm/Hubspot/Client.php:212\n HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi::getById(\"374720564\", \"hs_object_id,dealname\", \"companies,contacts\")\n\n 3 app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php:130\n Jiminny\\Services\\Crm\\Hubspot\\Client::getOpportunityById(\"374720564\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.49ms DONE\n cache ............................................................................................................................... 21.31ms DONE\n compiled ............................................................................................................................. 3.11ms DONE\n events ............................................................................................................................... 5.05ms DONE\n routes ............................................................................................................................... 1.83ms DONE\n views ................................................................................................................................ 4.91ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 37.77ms DONE\n cache ............................................................................................................................... 58.83ms DONE\n compiled ............................................................................................................................. 9.93ms DONE\n events .............................................................................................................................. 12.23ms DONE\n routes ............................................................................................................................... 5.02ms DONE\n views ............................................................................................................................... 21.46ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.01ms DONE\n cache ............................................................................................................................... 16.11ms DONE\n compiled ............................................................................................................................. 2.91ms DONE\n events ............................................................................................................................... 2.27ms DONE\n routes ............................................................................................................................... 3.11ms DONE\n views ............................................................................................................................... 18.41ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n SevenShores\\Hubspot\\Exceptions\\BadRequest \n\n Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e0284-5 (truncated...)\n\n at vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24\n 20▕ }\n 21▕ \n 22▕ public static function create(RequestException $guzzleException): self\n 23▕ {\n ➜ 24▕ $e = new static(\n 25▕ static::sanitizeResponseMessage($guzzleException->getMessage()),\n 26▕ $guzzleException->getCode(),\n 27▕ $guzzleException\n 28▕ );\n\n +13 vendor frames \n\n 14 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:163\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:51\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 55.84ms DONE\n cache .............................................................................................................................. 108.68ms DONE\n compiled ............................................................................................................................ 22.07ms DONE\n events .............................................................................................................................. 25.86ms DONE\n routes .............................................................................................................................. 19.91ms DONE\n views ............................................................................................................................... 52.25ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n TypeError \n\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83\n 79▕ \n 80▕ // Update reference parameters\n 81▕ $total = $state->total;\n 82▕ $lastRecordId = $state->lastRecordId;\n ➜ 83▕ }\n 84▕ \n 85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool\n 86▕ {\n 87▕ if ($state->hasReachedSafetyLimit()) {\n\n 1 app/Services/Crm/Hubspot/Client.php:195\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), [], \"contact\")\n\n 2 app/Services/Crm/Hubspot/Client.php:176\n Jiminny\\Services\\Crm\\Hubspot\\Client::getPaginatedDataGenerator([], \"contact\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.73ms DONE\n cache ............................................................................................................................... 19.13ms DONE\n compiled ............................................................................................................................. 4.93ms DONE\n events ............................................................................................................................... 3.02ms DONE\n routes ............................................................................................................................... 5.55ms DONE\n views ................................................................................................................................ 6.02ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.47ms DONE\n cache ............................................................................................................................... 16.66ms DONE\n compiled ............................................................................................................................. 3.26ms DONE\n events ............................................................................................................................... 3.99ms DONE\n routes ............................................................................................................................... 3.85ms DONE\n views ................................................................................................................................ 4.93ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.42ms DONE\n cache ............................................................................................................................... 23.70ms DONE\n compiled ............................................................................................................................. 3.46ms DONE\n events ............................................................................................................................... 4.30ms DONE\n routes ............................................................................................................................... 8.35ms DONE\n views ................................................................................................................................ 8.00ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.77ms DONE\n cache ............................................................................................................................... 25.55ms DONE\n compiled ............................................................................................................................. 3.87ms DONE\n events ............................................................................................................................... 4.96ms DONE\n routes ............................................................................................................................... 2.23ms DONE\n views ................................................................................................................................ 7.50ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ dev\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ git status\nOn branch JY-20725-handle-HS-search-rate-limit\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: app/Jobs/Middleware/HandleHubspotRateLimit.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Exceptions/RateLimitException.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Activity/Import/MatchCrmData.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Crm/MatchActivityCrmData.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Client.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/HubspotClientInterface.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Pagination/PaginationState.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.local.bak\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ gbr\n* JY-20725-handle-HS-search-rate-limit\n master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\n\n Illuminate\\Database\\QueryException \n\n SQLSTATE[42S22]: Column not found: 1054 Unknown column 'team_id' in 'WHERE' (Connection: mysql, Host: mariadb, Port: 3306, Database: jiminny, SQL: select * from `activities` where `team_id` = 2 and `activities`.`deleted_at` is null order by `id` desc limit 100)\n\n at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838\n 834▕ $exceptionType = $this->isUniqueConstraintError($e)\n 835▕ ? UniqueConstraintViolationException::class\n 836▕ : QueryException::class;\n 837▕ \n ➜ 838▕ throw new $exceptionType(\n 839▕ $this->getNameWithReadWriteType(),\n 840▕ $query,\n 841▕ $this->prepareBindings($bindings),\n 842▕ $e,\n\n +11 vendor frames \n\n 12 app/Console/Commands/JiminnyDebugCommand.php:380\n Illuminate\\Database\\Eloquent\\Builder::get()\n\n 13 app/Console/Commands/JiminnyDebugCommand.php:49\n Jiminny\\Console\\Commands\\JiminnyDebugCommand::simulateMatchActivityStorm()\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.01ms DONE\n cache ............................................................................................................................... 38.00ms DONE\n compiled ............................................................................................................................. 1.78ms DONE\n events ............................................................................................................................... 1.13ms DONE\n routes ............................................................................................................................... 1.56ms DONE\n views ................................................................................................................................ 3.25ms DONE\n\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.34ms DONE\n cache ............................................................................................................................... 11.98ms DONE\n compiled ............................................................................................................................. 2.10ms DONE\n events ............................................................................................................................... 5.31ms DONE\n routes ............................................................................................................................... 2.90ms DONE\n views ............................................................................................................................... 13.11ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.53ms DONE\n cache .............................................................................................................................. 128.25ms DONE\n compiled ............................................................................................................................. 1.73ms DONE\n events ............................................................................................................................... 1.07ms DONE\n routes ............................................................................................................................... 1.45ms DONE\n views ................................................................................................................................ 5.46ms DONE\n\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 69.57ms DONE\n cache .............................................................................................................................. 285.32ms DONE\n compiled ............................................................................................................................ 13.02ms DONE\n events .............................................................................................................................. 11.48ms DONE\n routes ............................................................................................................................... 6.30ms DONE\n views ............................................................................................................................... 51.86ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-es-update:worker-es-update_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42604166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5625,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56666666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7027778,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.70694447,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.84305555,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8472222,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.46875,"top":0.033333335,"width":0.0625,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-8908419307871373980
|
4435031406530046724
|
visual_change
|
accessibility
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.32ms DONE
cache [PASSWORD_DOTS] 10.62ms DONE
compiled [PASSWORD_DOTS] 3.60ms DONE
events [PASSWORD_DOTS] 2.60ms DONE
routes [PASSWORD_DOTS] 2.72ms DONE
views [PASSWORD_DOTS] 5.95ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
HubSpot\Client\Crm\Deals\ApiException
[429] Client error: `GET [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.49ms DONE
cache [PASSWORD_DOTS] 21.31ms DONE
compiled [PASSWORD_DOTS] 3.11ms DONE
events [PASSWORD_DOTS] 5.05ms DONE
routes [PASSWORD_DOTS] 1.83ms DONE
views [PASSWORD_DOTS] 4.91ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 37.77ms DONE
cache [PASSWORD_DOTS] 58.83ms DONE
compiled [PASSWORD_DOTS] 9.93ms DONE
events [PASSWORD_DOTS] 12.23ms DONE
routes [PASSWORD_DOTS] 5.02ms DONE
views [PASSWORD_DOTS] 21.46ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.01ms DONE
cache [PASSWORD_DOTS] 16.11ms DONE
compiled [PASSWORD_DOTS] 2.91ms DONE
events [PASSWORD_DOTS] 2.27ms DONE
routes [PASSWORD_DOTS] 3.11ms DONE
views [PASSWORD_DOTS] 18.41ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
SevenShores\Hubspot\Exceptions\BadRequest
Client error: `POST [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 55.84ms DONE
cache [PASSWORD_DOTS] 108.68ms DONE
compiled [PASSWORD_DOTS] 22.07ms DONE
events [PASSWORD_DOTS] 25.86ms DONE
routes [PASSWORD_DOTS] 19.91ms DONE
views [PASSWORD_DOTS] 52.25ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-audio:worker-audio_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
TypeError
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83
79▕
80▕ // Update reference parameters
81▕ $total = $state->total;
82▕ $lastRecordId = $state->lastRecordId;
➜ 83▕ }
84▕
85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool
86▕ {
87▕ if ($state->hasReachedSafetyLimit()) {
1 app/Services/Crm/Hubspot/Client.php:195
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\Services\Crm\Hubspot\Client), [], "contact")
2 app/Services/Crm/Hubspot/Client.php:176
Jiminny\Services\Crm\Hubspot\Client::getPaginatedDataGenerator([], "contact")
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.73ms DONE
cache [PASSWORD_DOTS] 19.13ms DONE
compiled [PASSWORD_DOTS] 4.93ms DONE
events [PASSWORD_DOTS] 3.02ms DONE
routes [PASSWORD_DOTS] 5.55ms DONE
views [PASSWORD_DOTS] 6.02ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.47ms DONE
cache [PASSWORD_DOTS] 16.66ms DONE
compiled [PASSWORD_DOTS] 3.26ms DONE
events [PASSWORD_DOTS] 3.99ms DONE
routes [PASSWORD_DOTS] 3.85ms DONE
views [PASSWORD_DOTS] 4.93ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.42ms DONE
cache [PASSWORD_DOTS] 23.70ms DONE
compiled [PASSWORD_DOTS] 3.46ms DONE
events [PASSWORD_DOTS] 4.30ms DONE
routes [PASSWORD_DOTS] 8.35ms DONE
views [PASSWORD_DOTS] 8.00ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.77ms DONE
cache [PASSWORD_DOTS] 25.55ms DONE
compiled [PASSWORD_DOTS] 3.87ms DONE
events [PASSWORD_DOTS] 4.96ms DONE
routes [PASSWORD_DOTS] 2.23ms DONE
views [PASSWORD_DOTS] 7.50ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ git status
On branch JY-20725-handle-HS-search-rate-limit
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: app/Jobs/Middleware/HandleHubspotRateLimit.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Exceptions/RateLimitException.php
modified: app/Jobs/Activity/Import/MatchCrmData.php
modified: app/Jobs/Crm/MatchActivityCrmData.php
modified: app/Services/Crm/Hubspot/Client.php
modified: app/Services/Crm/Hubspot/HubspotClientInterface.php
modified: app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php
modified: app/Services/Crm/Hubspot/Pagination/PaginationState.php
modified: app/Services/Crm/Hubspot/Service.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.local.bak
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ gbr
* JY-20725-handle-HS-search-rate-limit
master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ dev
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Illuminate\Database\QueryException
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'team_id' in 'WHERE' (Connection: mysql, Host: mariadb, Port: 3306, Database: jiminny, SQL: select * from `activities` where `team_id` = 2 and `activities`.`deleted_at` is null order by `id` desc limit 100)
at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
834▕ $exceptionType = $this->isUniqueConstraintError($e)
835▕ ? UniqueConstraintViolationException::class
836▕ : QueryException::class;
837▕
➜ 838▕ throw new $exceptionType(
839▕ $this->getNameWithReadWriteType(),
840▕ $query,
841▕ $this->prepareBindings($bindings),
842▕ $e,
+11 vendor frames
12 app/Console/Commands/JiminnyDebugCommand.php:380
Illuminate\Database\Eloquent\Builder::get()
13 app/Console/Commands/JiminnyDebugCommand.php:49
Jiminny\Console\Commands\JiminnyDebugCommand::simulateMatchActivityStorm()
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.01ms DONE
cache [PASSWORD_DOTS] 38.00ms DONE
compiled [PASSWORD_DOTS] 1.78ms DONE
events [PASSWORD_DOTS] 1.13ms DONE
routes [PASSWORD_DOTS] 1.56ms DONE
views [PASSWORD_DOTS] 3.25ms DONE
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-audio:worker-audio_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.34ms DONE
cache [PASSWORD_DOTS] 11.98ms DONE
compiled [PASSWORD_DOTS] 2.10ms DONE
events [PASSWORD_DOTS] 5.31ms DONE
routes [PASSWORD_DOTS] 2.90ms DONE
views [PASSWORD_DOTS] 13.11ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.53ms DONE
cache [PASSWORD_DOTS] 128.25ms DONE
compiled [PASSWORD_DOTS] 1.73ms DONE
events [PASSWORD_DOTS] 1.07ms DONE
routes [PASSWORD_DOTS] 1.45ms DONE
views [PASSWORD_DOTS] 5.46ms DONE
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-emails:worker-emails_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData j...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
18255
|
790
|
33
|
2026-05-11T11:07:05.147508+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778497625147_m1.jpg...
|
iTerm2
|
DEV (docker)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.32ms DONE
cache [PASSWORD_DOTS] 10.62ms DONE
compiled [PASSWORD_DOTS] 3.60ms DONE
events [PASSWORD_DOTS] 2.60ms DONE
routes [PASSWORD_DOTS] 2.72ms DONE
views [PASSWORD_DOTS] 5.95ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
HubSpot\Client\Crm\Deals\ApiException
[429] Client error: `GET [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.49ms DONE
cache [PASSWORD_DOTS] 21.31ms DONE
compiled [PASSWORD_DOTS] 3.11ms DONE
events [PASSWORD_DOTS] 5.05ms DONE
routes [PASSWORD_DOTS] 1.83ms DONE
views [PASSWORD_DOTS] 4.91ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 37.77ms DONE
cache [PASSWORD_DOTS] 58.83ms DONE
compiled [PASSWORD_DOTS] 9.93ms DONE
events [PASSWORD_DOTS] 12.23ms DONE
routes [PASSWORD_DOTS] 5.02ms DONE
views [PASSWORD_DOTS] 21.46ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.01ms DONE
cache [PASSWORD_DOTS] 16.11ms DONE
compiled [PASSWORD_DOTS] 2.91ms DONE
events [PASSWORD_DOTS] 2.27ms DONE
routes [PASSWORD_DOTS] 3.11ms DONE
views [PASSWORD_DOTS] 18.41ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
SevenShores\Hubspot\Exceptions\BadRequest
Client error: `POST [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 55.84ms DONE
cache [PASSWORD_DOTS] 108.68ms DONE
compiled [PASSWORD_DOTS] 22.07ms DONE
events [PASSWORD_DOTS] 25.86ms DONE
routes [PASSWORD_DOTS] 19.91ms DONE
views [PASSWORD_DOTS] 52.25ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-audio:worker-audio_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
TypeError
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83
79▕
80▕ // Update reference parameters
81▕ $total = $state->total;
82▕ $lastRecordId = $state->lastRecordId;
➜ 83▕ }
84▕
85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool
86▕ {
87▕ if ($state->hasReachedSafetyLimit()) {
1 app/Services/Crm/Hubspot/Client.php:195
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\Services\Crm\Hubspot\Client), [], "contact")
2 app/Services/Crm/Hubspot/Client.php:176
Jiminny\Services\Crm\Hubspot\Client::getPaginatedDataGenerator([], "contact")
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.73ms DONE
cache [PASSWORD_DOTS] 19.13ms DONE
compiled [PASSWORD_DOTS] 4.93ms DONE
events [PASSWORD_DOTS] 3.02ms DONE
routes [PASSWORD_DOTS] 5.55ms DONE
views [PASSWORD_DOTS] 6.02ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.47ms DONE
cache [PASSWORD_DOTS] 16.66ms DONE
compiled [PASSWORD_DOTS] 3.26ms DONE
events [PASSWORD_DOTS] 3.99ms DONE
routes [PASSWORD_DOTS] 3.85ms DONE
views [PASSWORD_DOTS] 4.93ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.42ms DONE
cache [PASSWORD_DOTS] 23.70ms DONE
compiled [PASSWORD_DOTS] 3.46ms DONE
events [PASSWORD_DOTS] 4.30ms DONE
routes [PASSWORD_DOTS] 8.35ms DONE
views [PASSWORD_DOTS] 8.00ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.77ms DONE
cache [PASSWORD_DOTS] 25.55ms DONE
compiled [PASSWORD_DOTS] 3.87ms DONE
events [PASSWORD_DOTS] 4.96ms DONE
routes [PASSWORD_DOTS] 2.23ms DONE
views [PASSWORD_DOTS] 7.50ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ git status
On branch JY-20725-handle-HS-search-rate-limit
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: app/Jobs/Middleware/HandleHubspotRateLimit.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Exceptions/RateLimitException.php
modified: app/Jobs/Activity/Import/MatchCrmData.php
modified: app/Jobs/Crm/MatchActivityCrmData.php
modified: app/Services/Crm/Hubspot/Client.php
modified: app/Services/Crm/Hubspot/HubspotClientInterface.php
modified: app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php
modified: app/Services/Crm/Hubspot/Pagination/PaginationState.php
modified: app/Services/Crm/Hubspot/Service.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.local.bak
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ gbr
* JY-20725-handle-HS-search-rate-limit
master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ dev
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Illuminate\Database\QueryException
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'team_id' in 'WHERE' (Connection: mysql, Host: mariadb, Port: 3306, Database: jiminny, SQL: select * from `activities` where `team_id` = 2 and `activities`.`deleted_at` is null order by `id` desc limit 100)
at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
834▕ $exceptionType = $this->isUniqueConstraintError($e)
835▕ ? UniqueConstraintViolationException::class
836▕ : QueryException::class;
837▕
➜ 838▕ throw new $exceptionType(
839▕ $this->getNameWithReadWriteType(),
840▕ $query,
841▕ $this->prepareBindings($bindings),
842▕ $e,
+11 vendor frames
12 app/Console/Commands/JiminnyDebugCommand.php:380
Illuminate\Database\Eloquent\Builder::get()
13 app/Console/Commands/JiminnyDebugCommand.php:49
Jiminny\Console\Commands\JiminnyDebugCommand::simulateMatchActivityStorm()
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.01ms DONE
cache [PASSWORD_DOTS] 38.00ms DONE
compiled [PASSWORD_DOTS] 1.78ms DONE
events [PASSWORD_DOTS] 1.13ms DONE
routes [PASSWORD_DOTS] 1.56ms DONE
views [PASSWORD_DOTS] 3.25ms DONE
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-audio:worker-audio_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.34ms DONE
cache [PASSWORD_DOTS] 11.98ms DONE
compiled [PASSWORD_DOTS] 2.10ms DONE
events [PASSWORD_DOTS] 5.31ms DONE
routes [PASSWORD_DOTS] 2.90ms DONE
views [PASSWORD_DOTS] 13.11ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.53ms DONE
cache [PASSWORD_DOTS] 128.25ms DONE
compiled [PASSWORD_DOTS] 1.73ms DONE
events [PASSWORD_DOTS] 1.07ms DONE
routes [PASSWORD_DOTS] 1.45ms DONE
views [PASSWORD_DOTS] 5.46ms DONE
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-emails:worker-emails_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData j...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.32ms DONE\n cache ............................................................................................................................... 10.62ms DONE\n compiled ............................................................................................................................. 3.60ms DONE\n events ............................................................................................................................... 2.60ms DONE\n routes ............................................................................................................................... 2.72ms DONE\n views ................................................................................................................................ 5.95ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\n\n HubSpot\\Client\\Crm\\Deals\\ApiException \n\n [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n\n at vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704\n 700▕ $options = $this->createHttpClientOption();\n 701▕ try {\n 702▕ $response = $this->client->send($request, $options);\n 703▕ } catch (RequestException $e) {\n ➜ 704▕ throw new ApiException(\n 705▕ \"[{$e->getCode()}] {$e->getMessage()}\",\n 706▕ (int) $e->getCode(),\n 707▕ $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n 708▕ $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n\n +1 vendor frames \n\n 2 app/Services/Crm/Hubspot/Client.php:212\n HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi::getById(\"374720564\", \"hs_object_id,dealname\", \"companies,contacts\")\n\n 3 app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php:130\n Jiminny\\Services\\Crm\\Hubspot\\Client::getOpportunityById(\"374720564\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.49ms DONE\n cache ............................................................................................................................... 21.31ms DONE\n compiled ............................................................................................................................. 3.11ms DONE\n events ............................................................................................................................... 5.05ms DONE\n routes ............................................................................................................................... 1.83ms DONE\n views ................................................................................................................................ 4.91ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 37.77ms DONE\n cache ............................................................................................................................... 58.83ms DONE\n compiled ............................................................................................................................. 9.93ms DONE\n events .............................................................................................................................. 12.23ms DONE\n routes ............................................................................................................................... 5.02ms DONE\n views ............................................................................................................................... 21.46ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.01ms DONE\n cache ............................................................................................................................... 16.11ms DONE\n compiled ............................................................................................................................. 2.91ms DONE\n events ............................................................................................................................... 2.27ms DONE\n routes ............................................................................................................................... 3.11ms DONE\n views ............................................................................................................................... 18.41ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n SevenShores\\Hubspot\\Exceptions\\BadRequest \n\n Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e0284-5 (truncated...)\n\n at vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24\n 20▕ }\n 21▕ \n 22▕ public static function create(RequestException $guzzleException): self\n 23▕ {\n ➜ 24▕ $e = new static(\n 25▕ static::sanitizeResponseMessage($guzzleException->getMessage()),\n 26▕ $guzzleException->getCode(),\n 27▕ $guzzleException\n 28▕ );\n\n +13 vendor frames \n\n 14 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:163\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:51\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 55.84ms DONE\n cache .............................................................................................................................. 108.68ms DONE\n compiled ............................................................................................................................ 22.07ms DONE\n events .............................................................................................................................. 25.86ms DONE\n routes .............................................................................................................................. 19.91ms DONE\n views ............................................................................................................................... 52.25ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n TypeError \n\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83\n 79▕ \n 80▕ // Update reference parameters\n 81▕ $total = $state->total;\n 82▕ $lastRecordId = $state->lastRecordId;\n ➜ 83▕ }\n 84▕ \n 85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool\n 86▕ {\n 87▕ if ($state->hasReachedSafetyLimit()) {\n\n 1 app/Services/Crm/Hubspot/Client.php:195\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), [], \"contact\")\n\n 2 app/Services/Crm/Hubspot/Client.php:176\n Jiminny\\Services\\Crm\\Hubspot\\Client::getPaginatedDataGenerator([], \"contact\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.73ms DONE\n cache ............................................................................................................................... 19.13ms DONE\n compiled ............................................................................................................................. 4.93ms DONE\n events ............................................................................................................................... 3.02ms DONE\n routes ............................................................................................................................... 5.55ms DONE\n views ................................................................................................................................ 6.02ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.47ms DONE\n cache ............................................................................................................................... 16.66ms DONE\n compiled ............................................................................................................................. 3.26ms DONE\n events ............................................................................................................................... 3.99ms DONE\n routes ............................................................................................................................... 3.85ms DONE\n views ................................................................................................................................ 4.93ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.42ms DONE\n cache ............................................................................................................................... 23.70ms DONE\n compiled ............................................................................................................................. 3.46ms DONE\n events ............................................................................................................................... 4.30ms DONE\n routes ............................................................................................................................... 8.35ms DONE\n views ................................................................................................................................ 8.00ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.77ms DONE\n cache ............................................................................................................................... 25.55ms DONE\n compiled ............................................................................................................................. 3.87ms DONE\n events ............................................................................................................................... 4.96ms DONE\n routes ............................................................................................................................... 2.23ms DONE\n views ................................................................................................................................ 7.50ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ dev\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ git status\nOn branch JY-20725-handle-HS-search-rate-limit\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: app/Jobs/Middleware/HandleHubspotRateLimit.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Exceptions/RateLimitException.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Activity/Import/MatchCrmData.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Crm/MatchActivityCrmData.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Client.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/HubspotClientInterface.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Pagination/PaginationState.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.local.bak\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ gbr\n* JY-20725-handle-HS-search-rate-limit\n master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\n\n Illuminate\\Database\\QueryException \n\n SQLSTATE[42S22]: Column not found: 1054 Unknown column 'team_id' in 'WHERE' (Connection: mysql, Host: mariadb, Port: 3306, Database: jiminny, SQL: select * from `activities` where `team_id` = 2 and `activities`.`deleted_at` is null order by `id` desc limit 100)\n\n at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838\n 834▕ $exceptionType = $this->isUniqueConstraintError($e)\n 835▕ ? UniqueConstraintViolationException::class\n 836▕ : QueryException::class;\n 837▕ \n ➜ 838▕ throw new $exceptionType(\n 839▕ $this->getNameWithReadWriteType(),\n 840▕ $query,\n 841▕ $this->prepareBindings($bindings),\n 842▕ $e,\n\n +11 vendor frames \n\n 12 app/Console/Commands/JiminnyDebugCommand.php:380\n Illuminate\\Database\\Eloquent\\Builder::get()\n\n 13 app/Console/Commands/JiminnyDebugCommand.php:49\n Jiminny\\Console\\Commands\\JiminnyDebugCommand::simulateMatchActivityStorm()\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.01ms DONE\n cache ............................................................................................................................... 38.00ms DONE\n compiled ............................................................................................................................. 1.78ms DONE\n events ............................................................................................................................... 1.13ms DONE\n routes ............................................................................................................................... 1.56ms DONE\n views ................................................................................................................................ 3.25ms DONE\n\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.34ms DONE\n cache ............................................................................................................................... 11.98ms DONE\n compiled ............................................................................................................................. 2.10ms DONE\n events ............................................................................................................................... 5.31ms DONE\n routes ............................................................................................................................... 2.90ms DONE\n views ............................................................................................................................... 13.11ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.53ms DONE\n cache .............................................................................................................................. 128.25ms DONE\n compiled ............................................................................................................................. 1.73ms DONE\n events ............................................................................................................................... 1.07ms DONE\n routes ............................................................................................................................... 1.45ms DONE\n views ................................................................................................................................ 5.46ms DONE\n\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 69.57ms DONE\n cache .............................................................................................................................. 285.32ms DONE\n compiled ............................................................................................................................ 13.02ms DONE\n events .............................................................................................................................. 11.48ms DONE\n routes ............................................................................................................................... 6.30ms DONE\n views ............................................................................................................................... 51.86ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-es-update:worker-es-update_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny#","depth":4,"on_screen":true,"value":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.32ms DONE\n cache ............................................................................................................................... 10.62ms DONE\n compiled ............................................................................................................................. 3.60ms DONE\n events ............................................................................................................................... 2.60ms DONE\n routes ............................................................................................................................... 2.72ms DONE\n views ................................................................................................................................ 5.95ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\n\n HubSpot\\Client\\Crm\\Deals\\ApiException \n\n [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n\n at vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704\n 700▕ $options = $this->createHttpClientOption();\n 701▕ try {\n 702▕ $response = $this->client->send($request, $options);\n 703▕ } catch (RequestException $e) {\n ➜ 704▕ throw new ApiException(\n 705▕ \"[{$e->getCode()}] {$e->getMessage()}\",\n 706▕ (int) $e->getCode(),\n 707▕ $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n 708▕ $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n\n +1 vendor frames \n\n 2 app/Services/Crm/Hubspot/Client.php:212\n HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi::getById(\"374720564\", \"hs_object_id,dealname\", \"companies,contacts\")\n\n 3 app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php:130\n Jiminny\\Services\\Crm\\Hubspot\\Client::getOpportunityById(\"374720564\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.49ms DONE\n cache ............................................................................................................................... 21.31ms DONE\n compiled ............................................................................................................................. 3.11ms DONE\n events ............................................................................................................................... 5.05ms DONE\n routes ............................................................................................................................... 1.83ms DONE\n views ................................................................................................................................ 4.91ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 37.77ms DONE\n cache ............................................................................................................................... 58.83ms DONE\n compiled ............................................................................................................................. 9.93ms DONE\n events .............................................................................................................................. 12.23ms DONE\n routes ............................................................................................................................... 5.02ms DONE\n views ............................................................................................................................... 21.46ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.01ms DONE\n cache ............................................................................................................................... 16.11ms DONE\n compiled ............................................................................................................................. 2.91ms DONE\n events ............................................................................................................................... 2.27ms DONE\n routes ............................................................................................................................... 3.11ms DONE\n views ............................................................................................................................... 18.41ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n SevenShores\\Hubspot\\Exceptions\\BadRequest \n\n Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e0284-5 (truncated...)\n\n at vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24\n 20▕ }\n 21▕ \n 22▕ public static function create(RequestException $guzzleException): self\n 23▕ {\n ➜ 24▕ $e = new static(\n 25▕ static::sanitizeResponseMessage($guzzleException->getMessage()),\n 26▕ $guzzleException->getCode(),\n 27▕ $guzzleException\n 28▕ );\n\n +13 vendor frames \n\n 14 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:163\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:51\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 55.84ms DONE\n cache .............................................................................................................................. 108.68ms DONE\n compiled ............................................................................................................................ 22.07ms DONE\n events .............................................................................................................................. 25.86ms DONE\n routes .............................................................................................................................. 19.91ms DONE\n views ............................................................................................................................... 52.25ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n TypeError \n\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83\n 79▕ \n 80▕ // Update reference parameters\n 81▕ $total = $state->total;\n 82▕ $lastRecordId = $state->lastRecordId;\n ➜ 83▕ }\n 84▕ \n 85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool\n 86▕ {\n 87▕ if ($state->hasReachedSafetyLimit()) {\n\n 1 app/Services/Crm/Hubspot/Client.php:195\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), [], \"contact\")\n\n 2 app/Services/Crm/Hubspot/Client.php:176\n Jiminny\\Services\\Crm\\Hubspot\\Client::getPaginatedDataGenerator([], \"contact\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.73ms DONE\n cache ............................................................................................................................... 19.13ms DONE\n compiled ............................................................................................................................. 4.93ms DONE\n events ............................................................................................................................... 3.02ms DONE\n routes ............................................................................................................................... 5.55ms DONE\n views ................................................................................................................................ 6.02ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.47ms DONE\n cache ............................................................................................................................... 16.66ms DONE\n compiled ............................................................................................................................. 3.26ms DONE\n events ............................................................................................................................... 3.99ms DONE\n routes ............................................................................................................................... 3.85ms DONE\n views ................................................................................................................................ 4.93ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.42ms DONE\n cache ............................................................................................................................... 23.70ms DONE\n compiled ............................................................................................................................. 3.46ms DONE\n events ............................................................................................................................... 4.30ms DONE\n routes ............................................................................................................................... 8.35ms DONE\n views ................................................................................................................................ 8.00ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.77ms DONE\n cache ............................................................................................................................... 25.55ms DONE\n compiled ............................................................................................................................. 3.87ms DONE\n events ............................................................................................................................... 4.96ms DONE\n routes ............................................................................................................................... 2.23ms DONE\n views ................................................................................................................................ 7.50ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ dev\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ git status\nOn branch JY-20725-handle-HS-search-rate-limit\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: app/Jobs/Middleware/HandleHubspotRateLimit.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Exceptions/RateLimitException.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Activity/Import/MatchCrmData.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Crm/MatchActivityCrmData.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Client.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/HubspotClientInterface.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Pagination/PaginationState.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Hubspot/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.local.bak\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ gbr\n* JY-20725-handle-HS-search-rate-limit\n master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\n\n Illuminate\\Database\\QueryException \n\n SQLSTATE[42S22]: Column not found: 1054 Unknown column 'team_id' in 'WHERE' (Connection: mysql, Host: mariadb, Port: 3306, Database: jiminny, SQL: select * from `activities` where `team_id` = 2 and `activities`.`deleted_at` is null order by `id` desc limit 100)\n\n at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838\n 834▕ $exceptionType = $this->isUniqueConstraintError($e)\n 835▕ ? UniqueConstraintViolationException::class\n 836▕ : QueryException::class;\n 837▕ \n ➜ 838▕ throw new $exceptionType(\n 839▕ $this->getNameWithReadWriteType(),\n 840▕ $query,\n 841▕ $this->prepareBindings($bindings),\n 842▕ $e,\n\n +11 vendor frames \n\n 12 app/Console/Commands/JiminnyDebugCommand.php:380\n Illuminate\\Database\\Eloquent\\Builder::get()\n\n 13 app/Console/Commands/JiminnyDebugCommand.php:49\n Jiminny\\Console\\Commands\\JiminnyDebugCommand::simulateMatchActivityStorm()\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.01ms DONE\n cache ............................................................................................................................... 38.00ms DONE\n compiled ............................................................................................................................. 1.78ms DONE\n events ............................................................................................................................... 1.13ms DONE\n routes ............................................................................................................................... 1.56ms DONE\n views ................................................................................................................................ 3.25ms DONE\n\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.34ms DONE\n cache ............................................................................................................................... 11.98ms DONE\n compiled ............................................................................................................................. 2.10ms DONE\n events ............................................................................................................................... 5.31ms DONE\n routes ............................................................................................................................... 2.90ms DONE\n views ............................................................................................................................... 13.11ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.53ms DONE\n cache .............................................................................................................................. 128.25ms DONE\n compiled ............................................................................................................................. 1.73ms DONE\n events ............................................................................................................................... 1.07ms DONE\n routes ............................................................................................................................... 1.45ms DONE\n views ................................................................................................................................ 5.46ms DONE\n\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 69.57ms DONE\n cache .............................................................................................................................. 285.32ms DONE\n compiled ............................................................................................................................ 13.02ms DONE\n events .............................................................................................................................. 11.48ms DONE\n routes ............................................................................................................................... 6.30ms DONE\n views ............................................................................................................................... 51.86ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-es-update:worker-es-update_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nDispatching 100 MatchActivityCrmData jobs (portal=2)\nDone. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8908419307871373980
|
4435031406530046724
|
click
|
accessibility
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.32ms DONE
cache [PASSWORD_DOTS] 10.62ms DONE
compiled [PASSWORD_DOTS] 3.60ms DONE
events [PASSWORD_DOTS] 2.60ms DONE
routes [PASSWORD_DOTS] 2.72ms DONE
views [PASSWORD_DOTS] 5.95ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
HubSpot\Client\Crm\Deals\ApiException
[429] Client error: `GET [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.49ms DONE
cache [PASSWORD_DOTS] 21.31ms DONE
compiled [PASSWORD_DOTS] 3.11ms DONE
events [PASSWORD_DOTS] 5.05ms DONE
routes [PASSWORD_DOTS] 1.83ms DONE
views [PASSWORD_DOTS] 4.91ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 37.77ms DONE
cache [PASSWORD_DOTS] 58.83ms DONE
compiled [PASSWORD_DOTS] 9.93ms DONE
events [PASSWORD_DOTS] 12.23ms DONE
routes [PASSWORD_DOTS] 5.02ms DONE
views [PASSWORD_DOTS] 21.46ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.01ms DONE
cache [PASSWORD_DOTS] 16.11ms DONE
compiled [PASSWORD_DOTS] 2.91ms DONE
events [PASSWORD_DOTS] 2.27ms DONE
routes [PASSWORD_DOTS] 3.11ms DONE
views [PASSWORD_DOTS] 18.41ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
SevenShores\Hubspot\Exceptions\BadRequest
Client error: `POST [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 55.84ms DONE
cache [PASSWORD_DOTS] 108.68ms DONE
compiled [PASSWORD_DOTS] 22.07ms DONE
events [PASSWORD_DOTS] 25.86ms DONE
routes [PASSWORD_DOTS] 19.91ms DONE
views [PASSWORD_DOTS] 52.25ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-audio:worker-audio_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
TypeError
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83
79▕
80▕ // Update reference parameters
81▕ $total = $state->total;
82▕ $lastRecordId = $state->lastRecordId;
➜ 83▕ }
84▕
85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool
86▕ {
87▕ if ($state->hasReachedSafetyLimit()) {
1 app/Services/Crm/Hubspot/Client.php:195
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\Services\Crm\Hubspot\Client), [], "contact")
2 app/Services/Crm/Hubspot/Client.php:176
Jiminny\Services\Crm\Hubspot\Client::getPaginatedDataGenerator([], "contact")
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.73ms DONE
cache [PASSWORD_DOTS] 19.13ms DONE
compiled [PASSWORD_DOTS] 4.93ms DONE
events [PASSWORD_DOTS] 3.02ms DONE
routes [PASSWORD_DOTS] 5.55ms DONE
views [PASSWORD_DOTS] 6.02ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.47ms DONE
cache [PASSWORD_DOTS] 16.66ms DONE
compiled [PASSWORD_DOTS] 3.26ms DONE
events [PASSWORD_DOTS] 3.99ms DONE
routes [PASSWORD_DOTS] 3.85ms DONE
views [PASSWORD_DOTS] 4.93ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.42ms DONE
cache [PASSWORD_DOTS] 23.70ms DONE
compiled [PASSWORD_DOTS] 3.46ms DONE
events [PASSWORD_DOTS] 4.30ms DONE
routes [PASSWORD_DOTS] 8.35ms DONE
views [PASSWORD_DOTS] 8.00ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.77ms DONE
cache [PASSWORD_DOTS] 25.55ms DONE
compiled [PASSWORD_DOTS] 3.87ms DONE
events [PASSWORD_DOTS] 4.96ms DONE
routes [PASSWORD_DOTS] 2.23ms DONE
views [PASSWORD_DOTS] 7.50ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ git status
On branch JY-20725-handle-HS-search-rate-limit
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: app/Jobs/Middleware/HandleHubspotRateLimit.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Exceptions/RateLimitException.php
modified: app/Jobs/Activity/Import/MatchCrmData.php
modified: app/Jobs/Crm/MatchActivityCrmData.php
modified: app/Services/Crm/Hubspot/Client.php
modified: app/Services/Crm/Hubspot/HubspotClientInterface.php
modified: app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php
modified: app/Services/Crm/Hubspot/Pagination/PaginationState.php
modified: app/Services/Crm/Hubspot/Service.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.local.bak
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ gbr
* JY-20725-handle-HS-search-rate-limit
master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ dev
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Illuminate\Database\QueryException
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'team_id' in 'WHERE' (Connection: mysql, Host: mariadb, Port: 3306, Database: jiminny, SQL: select * from `activities` where `team_id` = 2 and `activities`.`deleted_at` is null order by `id` desc limit 100)
at vendor/laravel/framework/src/Illuminate/Database/Connection.php:838
834▕ $exceptionType = $this->isUniqueConstraintError($e)
835▕ ? UniqueConstraintViolationException::class
836▕ : QueryException::class;
837▕
➜ 838▕ throw new $exceptionType(
839▕ $this->getNameWithReadWriteType(),
840▕ $query,
841▕ $this->prepareBindings($bindings),
842▕ $e,
+11 vendor frames
12 app/Console/Commands/JiminnyDebugCommand.php:380
Illuminate\Database\Eloquent\Builder::get()
13 app/Console/Commands/JiminnyDebugCommand.php:49
Jiminny\Console\Commands\JiminnyDebugCommand::simulateMatchActivityStorm()
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.01ms DONE
cache [PASSWORD_DOTS] 38.00ms DONE
compiled [PASSWORD_DOTS] 1.78ms DONE
events [PASSWORD_DOTS] 1.13ms DONE
routes [PASSWORD_DOTS] 1.56ms DONE
views [PASSWORD_DOTS] 3.25ms DONE
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-audio:worker-audio_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.34ms DONE
cache [PASSWORD_DOTS] 11.98ms DONE
compiled [PASSWORD_DOTS] 2.10ms DONE
events [PASSWORD_DOTS] 5.31ms DONE
routes [PASSWORD_DOTS] 2.90ms DONE
views [PASSWORD_DOTS] 13.11ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.53ms DONE
cache [PASSWORD_DOTS] 128.25ms DONE
compiled [PASSWORD_DOTS] 1.73ms DONE
events [PASSWORD_DOTS] 1.07ms DONE
routes [PASSWORD_DOTS] 1.45ms DONE
views [PASSWORD_DOTS] 5.46ms DONE
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-emails:worker-emails_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData jobs (portal=2)
Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Dispatching 100 MatchActivityCrmData j...
|
18254
|
NULL
|
NULL
|
NULL
|
|
11405
|
516
|
28
|
2026-05-08T19:15:18.682310+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778267718682_m2.jpg...
|
Firefox
|
hPanel - Hostinger — Personal
|
True
|
hpanel.hostinger.com
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
hPanel - Hostinger
hPanel - Hostinger
Close tab
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
New Tab
New Tab
Location Logger
Location Logger
Finance Hub
Finance Hub
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.0518755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.06304868,"width":0.080784574,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"hPanel - Hostinger","depth":4,"bounds":{"left":0.0,"top":0.08459697,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"hPanel - Hostinger","depth":5,"bounds":{"left":0.013297873,"top":0.09577015,"width":0.032413565,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.10139628,"top":0.09177973,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Nginx Proxy Manager","depth":4,"bounds":{"left":0.0,"top":0.11731844,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":5,"bounds":{"left":0.013297873,"top":0.12849163,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"bounds":{"left":0.0,"top":0.15003991,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"bounds":{"left":0.013297873,"top":0.16121309,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"bounds":{"left":0.0,"top":0.18276137,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"bounds":{"left":0.013297873,"top":0.19393456,"width":0.040724736,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"bounds":{"left":0.0,"top":0.21548285,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"bounds":{"left":0.013297873,"top":0.22665602,"width":0.03756649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.2482043,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.25937748,"width":0.11469415,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"bounds":{"left":0.0,"top":0.28092578,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"bounds":{"left":0.013297873,"top":0.29209897,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Оптичен интернет за дома - EON телевизия | Vivacom | 5G","depth":4,"bounds":{"left":0.0,"top":0.31364724,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Оптичен интернет за дома - EON телевизия | Vivacom | 5G","depth":5,"bounds":{"left":0.013297873,"top":0.32482043,"width":0.105884306,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AFFiNE - All In One KnowledgeOS","depth":4,"bounds":{"left":0.0,"top":0.3463687,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AFFiNE - All In One KnowledgeOS","depth":5,"bounds":{"left":0.013297873,"top":0.3575419,"width":0.05851064,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All docs · AFFiNE","depth":4,"bounds":{"left":0.0,"top":0.3790902,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All docs · AFFiNE","depth":5,"bounds":{"left":0.013297873,"top":0.39026338,"width":0.029587766,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Payments Logger","depth":4,"bounds":{"left":0.0,"top":0.41181165,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Payments Logger","depth":5,"bounds":{"left":0.013297873,"top":0.42298484,"width":0.030086435,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - kovaliklukas@gmail.com - Gmail","depth":4,"bounds":{"left":0.0,"top":0.4445331,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - kovaliklukas@gmail.com - Gmail","depth":5,"bounds":{"left":0.013297873,"top":0.4557063,"width":0.22639628,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.4772546,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.4884278,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Location Logger","depth":4,"bounds":{"left":0.0,"top":0.509976,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Location Logger","depth":5,"bounds":{"left":0.013297873,"top":0.5211492,"width":0.028091755,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"bounds":{"left":0.0,"top":0.54269755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"bounds":{"left":0.013297873,"top":0.55387074,"width":0.021609042,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.57701516,"width":0.108211435,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-8908095982106990210
|
-7613739020711815925
|
visual_change
|
accessibility
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
hPanel - Hostinger
hPanel - Hostinger
Close tab
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
New Tab
New Tab
Location Logger
Location Logger
Finance Hub
Finance Hub
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
11407
|
515
|
29
|
2026-05-08T19:15:40.613336+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778267740613_m1.jpg...
|
Firefox
|
hPanel - Hostinger — Personal
|
True
|
hpanel.hostinger.com
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
hPanel - Hostinger
hPanel - Hostinger
Close tab
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
New Tab
New Tab
Location Logger
Location Logger
Finance Hub
Finance Hub
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"hPanel - Hostinger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"hPanel - Hostinger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Nginx Proxy Manager","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Оптичен интернет за дома - EON телевизия | Vivacom | 5G","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Оптичен интернет за дома - EON телевизия | Vivacom | 5G","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AFFiNE - All In One KnowledgeOS","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AFFiNE - All In One KnowledgeOS","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All docs · AFFiNE","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All docs · AFFiNE","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Payments Logger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Payments Logger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - kovaliklukas@gmail.com - Gmail","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - kovaliklukas@gmail.com - Gmail","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Location Logger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Location Logger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-8908095982106990210
|
-7613739020711815925
|
click
|
accessibility
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
hPanel - Hostinger
hPanel - Hostinger
Close tab
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
New Tab
New Tab
Location Logger
Location Logger
Finance Hub
Finance Hub
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden...
|
11403
|
NULL
|
NULL
|
NULL
|
|
11408
|
516
|
30
|
2026-05-08T19:15:42.929939+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778267742929_m2.jpg...
|
Firefox
|
hPanel - Hostinger — Personal
|
True
|
https://hpanel.hostinger.com/
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
hPanel - Hostinger
hPanel - Hostinger
Close tab
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
New Tab
New Tab
Location Logger
Location Logger
Finance Hub
Finance Hub
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.0518755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.06304868,"width":0.080784574,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"hPanel - Hostinger","depth":4,"bounds":{"left":0.0,"top":0.08459697,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"hPanel - Hostinger","depth":5,"bounds":{"left":0.013297873,"top":0.09577015,"width":0.032413565,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.10139628,"top":0.09177973,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Nginx Proxy Manager","depth":4,"bounds":{"left":0.0,"top":0.11731844,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":5,"bounds":{"left":0.013297873,"top":0.12849163,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"bounds":{"left":0.0,"top":0.15003991,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"bounds":{"left":0.013297873,"top":0.16121309,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"bounds":{"left":0.0,"top":0.18276137,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"bounds":{"left":0.013297873,"top":0.19393456,"width":0.040724736,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"bounds":{"left":0.0,"top":0.21548285,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"bounds":{"left":0.013297873,"top":0.22665602,"width":0.03756649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.2482043,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.25937748,"width":0.11469415,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"bounds":{"left":0.0,"top":0.28092578,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"bounds":{"left":0.013297873,"top":0.29209897,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Оптичен интернет за дома - EON телевизия | Vivacom | 5G","depth":4,"bounds":{"left":0.0,"top":0.31364724,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Оптичен интернет за дома - EON телевизия | Vivacom | 5G","depth":5,"bounds":{"left":0.013297873,"top":0.32482043,"width":0.105884306,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AFFiNE - All In One KnowledgeOS","depth":4,"bounds":{"left":0.0,"top":0.3463687,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AFFiNE - All In One KnowledgeOS","depth":5,"bounds":{"left":0.013297873,"top":0.3575419,"width":0.05851064,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All docs · AFFiNE","depth":4,"bounds":{"left":0.0,"top":0.3790902,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All docs · AFFiNE","depth":5,"bounds":{"left":0.013297873,"top":0.39026338,"width":0.029587766,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Payments Logger","depth":4,"bounds":{"left":0.0,"top":0.41181165,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Payments Logger","depth":5,"bounds":{"left":0.013297873,"top":0.42298484,"width":0.030086435,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - kovaliklukas@gmail.com - Gmail","depth":4,"bounds":{"left":0.0,"top":0.4445331,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - kovaliklukas@gmail.com - Gmail","depth":5,"bounds":{"left":0.013297873,"top":0.4557063,"width":0.22639628,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.4772546,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.4884278,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Location Logger","depth":4,"bounds":{"left":0.0,"top":0.509976,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Location Logger","depth":5,"bounds":{"left":0.013297873,"top":0.5211492,"width":0.028091755,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"bounds":{"left":0.0,"top":0.54269755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"bounds":{"left":0.013297873,"top":0.55387074,"width":0.021609042,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.57701516,"width":0.108211435,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-8908095982106990210
|
-7613739020711815925
|
visual_change
|
accessibility
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
hPanel - Hostinger
hPanel - Hostinger
Close tab
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
New Tab
New Tab
Location Logger
Location Logger
Finance Hub
Finance Hub
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
11411
|
516
|
32
|
2026-05-08T19:15:52.673112+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778267752673_m2.jpg...
|
Firefox
|
hPanel - Hostinger — Personal
|
True
|
hpanel.hostinger.com
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
hPanel - Hostinger
hPanel - Hostinger
Close tab
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
New Tab
New Tab
Location Logger
Location Logger
Finance Hub
Finance Hub
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.0518755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.06304868,"width":0.080784574,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"hPanel - Hostinger","depth":4,"bounds":{"left":0.0,"top":0.08459697,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"hPanel - Hostinger","depth":5,"bounds":{"left":0.013297873,"top":0.09577015,"width":0.032413565,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.10139628,"top":0.09177973,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Nginx Proxy Manager","depth":4,"bounds":{"left":0.0,"top":0.11731844,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":5,"bounds":{"left":0.013297873,"top":0.12849163,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"bounds":{"left":0.0,"top":0.15003991,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"bounds":{"left":0.013297873,"top":0.16121309,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"bounds":{"left":0.0,"top":0.18276137,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"bounds":{"left":0.013297873,"top":0.19393456,"width":0.040724736,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"bounds":{"left":0.0,"top":0.21548285,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"bounds":{"left":0.013297873,"top":0.22665602,"width":0.03756649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.2482043,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.25937748,"width":0.11469415,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"bounds":{"left":0.0,"top":0.28092578,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"bounds":{"left":0.013297873,"top":0.29209897,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Оптичен интернет за дома - EON телевизия | Vivacom | 5G","depth":4,"bounds":{"left":0.0,"top":0.31364724,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Оптичен интернет за дома - EON телевизия | Vivacom | 5G","depth":5,"bounds":{"left":0.013297873,"top":0.32482043,"width":0.105884306,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AFFiNE - All In One KnowledgeOS","depth":4,"bounds":{"left":0.0,"top":0.3463687,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AFFiNE - All In One KnowledgeOS","depth":5,"bounds":{"left":0.013297873,"top":0.3575419,"width":0.05851064,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All docs · AFFiNE","depth":4,"bounds":{"left":0.0,"top":0.3790902,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All docs · AFFiNE","depth":5,"bounds":{"left":0.013297873,"top":0.39026338,"width":0.029587766,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Payments Logger","depth":4,"bounds":{"left":0.0,"top":0.41181165,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Payments Logger","depth":5,"bounds":{"left":0.013297873,"top":0.42298484,"width":0.030086435,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - kovaliklukas@gmail.com - Gmail","depth":4,"bounds":{"left":0.0,"top":0.4445331,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - kovaliklukas@gmail.com - Gmail","depth":5,"bounds":{"left":0.013297873,"top":0.4557063,"width":0.22639628,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.4772546,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.4884278,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Location Logger","depth":4,"bounds":{"left":0.0,"top":0.509976,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Location Logger","depth":5,"bounds":{"left":0.013297873,"top":0.5211492,"width":0.028091755,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"bounds":{"left":0.0,"top":0.54269755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"bounds":{"left":0.013297873,"top":0.55387074,"width":0.021609042,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.57701516,"width":0.108211435,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-8908095982106990210
|
-7613739020711815925
|
visual_change
|
accessibility
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
hPanel - Hostinger
hPanel - Hostinger
Close tab
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
New Tab
New Tab
Location Logger
Location Logger
Finance Hub
Finance Hub
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
11412
|
515
|
31
|
2026-05-08T19:15:59.152750+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778267759152_m1.jpg...
|
Firefox
|
hPanel - Hostinger — Personal
|
True
|
hpanel.hostinger.com
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
hPanel - Hostinger
hPanel - Hostinger
Close tab
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
New Tab
New Tab
Location Logger
Location Logger
Finance Hub
Finance Hub
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"hPanel - Hostinger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"hPanel - Hostinger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Nginx Proxy Manager","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Оптичен интернет за дома - EON телевизия | Vivacom | 5G","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Оптичен интернет за дома - EON телевизия | Vivacom | 5G","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AFFiNE - All In One KnowledgeOS","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AFFiNE - All In One KnowledgeOS","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All docs · AFFiNE","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All docs · AFFiNE","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Payments Logger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Payments Logger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - kovaliklukas@gmail.com - Gmail","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - kovaliklukas@gmail.com - Gmail","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Location Logger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Location Logger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-8908095982106990210
|
-7613739020711815925
|
click
|
accessibility
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
hPanel - Hostinger
hPanel - Hostinger
Close tab
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
New Tab
New Tab
Location Logger
Location Logger
Finance Hub
Finance Hub
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden...
|
11409
|
NULL
|
NULL
|
NULL
|
|
11415
|
516
|
34
|
2026-05-08T19:16:18.549512+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778267778549_m2.jpg...
|
Firefox
|
hPanel - Hostinger — Personal
|
True
|
hpanel.hostinger.com
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
hPanel - Hostinger
hPanel - Hostinger
Close tab
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
New Tab
New Tab
Location Logger
Location Logger
Finance Hub
Finance Hub
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.0518755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.06304868,"width":0.080784574,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"hPanel - Hostinger","depth":4,"bounds":{"left":0.0,"top":0.08459697,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"hPanel - Hostinger","depth":5,"bounds":{"left":0.013297873,"top":0.09577015,"width":0.032413565,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.10139628,"top":0.09177973,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Nginx Proxy Manager","depth":4,"bounds":{"left":0.0,"top":0.11731844,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":5,"bounds":{"left":0.013297873,"top":0.12849163,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"bounds":{"left":0.0,"top":0.15003991,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"bounds":{"left":0.013297873,"top":0.16121309,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"bounds":{"left":0.0,"top":0.18276137,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"bounds":{"left":0.013297873,"top":0.19393456,"width":0.040724736,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"bounds":{"left":0.0,"top":0.21548285,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"bounds":{"left":0.013297873,"top":0.22665602,"width":0.03756649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.2482043,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.25937748,"width":0.11469415,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"bounds":{"left":0.0,"top":0.28092578,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"bounds":{"left":0.013297873,"top":0.29209897,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Оптичен интернет за дома - EON телевизия | Vivacom | 5G","depth":4,"bounds":{"left":0.0,"top":0.31364724,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Оптичен интернет за дома - EON телевизия | Vivacom | 5G","depth":5,"bounds":{"left":0.013297873,"top":0.32482043,"width":0.105884306,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AFFiNE - All In One KnowledgeOS","depth":4,"bounds":{"left":0.0,"top":0.3463687,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AFFiNE - All In One KnowledgeOS","depth":5,"bounds":{"left":0.013297873,"top":0.3575419,"width":0.05851064,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All docs · AFFiNE","depth":4,"bounds":{"left":0.0,"top":0.3790902,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All docs · AFFiNE","depth":5,"bounds":{"left":0.013297873,"top":0.39026338,"width":0.029587766,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Payments Logger","depth":4,"bounds":{"left":0.0,"top":0.41181165,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Payments Logger","depth":5,"bounds":{"left":0.013297873,"top":0.42298484,"width":0.030086435,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - kovaliklukas@gmail.com - Gmail","depth":4,"bounds":{"left":0.0,"top":0.4445331,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - kovaliklukas@gmail.com - Gmail","depth":5,"bounds":{"left":0.013297873,"top":0.4557063,"width":0.22639628,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.4772546,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.4884278,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Location Logger","depth":4,"bounds":{"left":0.0,"top":0.509976,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Location Logger","depth":5,"bounds":{"left":0.013297873,"top":0.5211492,"width":0.028091755,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"bounds":{"left":0.0,"top":0.54269755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"bounds":{"left":0.013297873,"top":0.55387074,"width":0.021609042,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.57701516,"width":0.108211435,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-8908095982106990210
|
-7613739020711815925
|
visual_change
|
accessibility
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
hPanel - Hostinger
hPanel - Hostinger
Close tab
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
New Tab
New Tab
Location Logger
Location Logger
Finance Hub
Finance Hub
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
11446
|
516
|
53
|
2026-05-08T19:17:27.838254+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778267847838_m2.jpg...
|
Firefox
|
hPanel - Hostinger — Personal
|
True
|
hpanel.hostinger.com
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
hPanel - Hostinger
hPanel - Hostinger
Close tab
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
New Tab
New Tab
Location Logger
Location Logger
Finance Hub
Finance Hub
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.0518755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.06304868,"width":0.080784574,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"hPanel - Hostinger","depth":4,"bounds":{"left":0.0,"top":0.08459697,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"hPanel - Hostinger","depth":5,"bounds":{"left":0.013297873,"top":0.09577015,"width":0.032413565,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.10139628,"top":0.09177973,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Nginx Proxy Manager","depth":4,"bounds":{"left":0.0,"top":0.11731844,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":5,"bounds":{"left":0.013297873,"top":0.12849163,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"bounds":{"left":0.0,"top":0.15003991,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"bounds":{"left":0.013297873,"top":0.16121309,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"bounds":{"left":0.0,"top":0.18276137,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"bounds":{"left":0.013297873,"top":0.19393456,"width":0.040724736,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"bounds":{"left":0.0,"top":0.21548285,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"bounds":{"left":0.013297873,"top":0.22665602,"width":0.03756649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.2482043,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.25937748,"width":0.11469415,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"bounds":{"left":0.0,"top":0.28092578,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"bounds":{"left":0.013297873,"top":0.29209897,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Оптичен интернет за дома - EON телевизия | Vivacom | 5G","depth":4,"bounds":{"left":0.0,"top":0.31364724,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Оптичен интернет за дома - EON телевизия | Vivacom | 5G","depth":5,"bounds":{"left":0.013297873,"top":0.32482043,"width":0.105884306,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AFFiNE - All In One KnowledgeOS","depth":4,"bounds":{"left":0.0,"top":0.3463687,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AFFiNE - All In One KnowledgeOS","depth":5,"bounds":{"left":0.013297873,"top":0.3575419,"width":0.05851064,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All docs · AFFiNE","depth":4,"bounds":{"left":0.0,"top":0.3790902,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All docs · AFFiNE","depth":5,"bounds":{"left":0.013297873,"top":0.39026338,"width":0.029587766,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Payments Logger","depth":4,"bounds":{"left":0.0,"top":0.41181165,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Payments Logger","depth":5,"bounds":{"left":0.013297873,"top":0.42298484,"width":0.030086435,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - kovaliklukas@gmail.com - Gmail","depth":4,"bounds":{"left":0.0,"top":0.4445331,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - kovaliklukas@gmail.com - Gmail","depth":5,"bounds":{"left":0.013297873,"top":0.4557063,"width":0.22639628,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.4772546,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.4884278,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Location Logger","depth":4,"bounds":{"left":0.0,"top":0.509976,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Location Logger","depth":5,"bounds":{"left":0.013297873,"top":0.5211492,"width":0.028091755,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"bounds":{"left":0.0,"top":0.54269755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"bounds":{"left":0.013297873,"top":0.55387074,"width":0.021609042,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.57701516,"width":0.108211435,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-8908095982106990210
|
-7613739020711815925
|
visual_change
|
accessibility
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
hPanel - Hostinger
hPanel - Hostinger
Close tab
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
Оптичен интернет за дома - EON телевизия | Vivacom | 5G
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
[NirDiamant/GenAI_Agents] Add SwarmScore — Portable Trust Rating for AI Agents (Issue #115) - [EMAIL] - Gmail
New Tab
New Tab
Location Logger
Location Logger
Finance Hub
Finance Hub
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden...
|
11445
|
NULL
|
NULL
|
NULL
|
|
15484
|
690
|
6
|
2026-05-11T06:54:16.492905+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778482456492_m1.jpg...
|
Notion Calendar
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Slack+Ch our*FilelEditViewGolWindowHelpNikolay Yan Slack+Ch our*FilelEditViewGolWindowHelpNikolay Yankov (Presenting)TdtC Jy 20457 ServicX Jmim83 Promshttps://fminny.atlassian.net/jira/software/c/projects/JY/boards/37D Projects4 55HPlatform Team %Q Search board80000₴8EpicvТуре ~Quick filtersAJ Panorama for CallScoring nouAUTOMATED AI SCORINGKewoy torwuw( JY-20361Upgrade to PHP 8.5PHP &S UPGRADEIn Deylд 3-18001Smart Instant Nudge Pre-teseringCOST-EFFECTIVE AND FASTER NU.…Code ReviewД JY:20123 15 11 • = OSetup test coverage forProphet in SonarMAINTENANCESBacklog#3-109511 ***= 3Al Review - O1summary Acoon kems,NeyPointsGROWTH - MAINTAIN OUR COMINIn Dev0 20506 2 • ****= 3(POC) Jiminny MCPConnector EJIMENTNRYCOMECTOIn Progress• 20625 10 11 0000ehchineonoeheehubspot accounts/contactsPLATFORM STARRLTTYIn DevC JY-20725lah|meet.google.com/mie-gawc-dsi?authuser=[EMAIL]*O Attent0 Cals -Ca Transp3 CircleCi|*xIDaily - Platform - now100% L28• Mon 11 May 9:54:159X MCP0 DerComplete sprint1 CreateSync opportunities without|CCATFORM STARTUTY!In QA8 _20352 (53 11 * = 08• Mon 11 May 9:54• эppіскL Al BookmarxsGroup: QueriesAI Reports > Empty pagedesign and promotionAJREPORTSDeployed0 -20372 /0 1l •0*0 = /Grok via AzureDeployedД-20726 1 0 •**=|Allow users to delete SSand Panorama promptswhen those are used in a...AJREPORTSDeployedX8 -20770 |1 01 .0*0 =Release AJ Panoramareports to customersAJREPORTSDeployed0 -20740 05 1) •**0=Wrong formatting forsummary in the CRMNikolay YankovGalya Dimitrova4 othersStefka Stoyanova9:54 AM | Daily - PlatformWLukas Kovalik5:49...
|
NULL
|
-8907197721416308400
|
NULL
|
visual_change
|
ocr
|
NULL
|
Slack+Ch our*FilelEditViewGolWindowHelpNikolay Yan Slack+Ch our*FilelEditViewGolWindowHelpNikolay Yankov (Presenting)TdtC Jy 20457 ServicX Jmim83 Promshttps://fminny.atlassian.net/jira/software/c/projects/JY/boards/37D Projects4 55HPlatform Team %Q Search board80000₴8EpicvТуре ~Quick filtersAJ Panorama for CallScoring nouAUTOMATED AI SCORINGKewoy torwuw( JY-20361Upgrade to PHP 8.5PHP &S UPGRADEIn Deylд 3-18001Smart Instant Nudge Pre-teseringCOST-EFFECTIVE AND FASTER NU.…Code ReviewД JY:20123 15 11 • = OSetup test coverage forProphet in SonarMAINTENANCESBacklog#3-109511 ***= 3Al Review - O1summary Acoon kems,NeyPointsGROWTH - MAINTAIN OUR COMINIn Dev0 20506 2 • ****= 3(POC) Jiminny MCPConnector EJIMENTNRYCOMECTOIn Progress• 20625 10 11 0000ehchineonoeheehubspot accounts/contactsPLATFORM STARRLTTYIn DevC JY-20725lah|meet.google.com/mie-gawc-dsi?authuser=[EMAIL]*O Attent0 Cals -Ca Transp3 CircleCi|*xIDaily - Platform - now100% L28• Mon 11 May 9:54:159X MCP0 DerComplete sprint1 CreateSync opportunities without|CCATFORM STARTUTY!In QA8 _20352 (53 11 * = 08• Mon 11 May 9:54• эppіскL Al BookmarxsGroup: QueriesAI Reports > Empty pagedesign and promotionAJREPORTSDeployed0 -20372 /0 1l •0*0 = /Grok via AzureDeployedД-20726 1 0 •**=|Allow users to delete SSand Panorama promptswhen those are used in a...AJREPORTSDeployedX8 -20770 |1 01 .0*0 =Release AJ Panoramareports to customersAJREPORTSDeployed0 -20740 05 1) •**0=Wrong formatting forsummary in the CRMNikolay YankovGalya Dimitrova4 othersStefka Stoyanova9:54 AM | Daily - PlatformWLukas Kovalik5:49...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22982
|
978
|
59
|
2026-05-12T07:28:55.192713+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570935192_m1.jpg...
|
QuickTime Player
|
PLanhat Petko interest event 2026-05-11.mp4
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
rewind
play/pause
fast forward
mute
More Controls
rewind
play/pause
fast forward
mute
More Controls
toggle full screen
show external playback menu
show external playback menu
show media selection menu
toggle picture-in-picture playback
show action menu
share
show chapter menu
zoom
zoom
playback speed
01:57
toggle elapsed time, timecode and framecount
04:55
toggle duration and remaining time
document actions
PLanhat Petko interest event 2026-05-11.mp4...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"rewind","depth":1,"bounds":{"left":0.77569443,"top":0.9266667,"width":0.017361112,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXCheckBox","text":"play/pause","depth":1,"bounds":{"left":0.8003472,"top":0.9172222,"width":0.02013889,"height":0.037777778},"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"fast forward","depth":1,"bounds":{"left":0.828125,"top":0.9266667,"width":0.017361112,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"mute","depth":1,"bounds":{"left":0.65868056,"top":0.9266667,"width":0.015625,"height":0.016666668},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"More Controls","depth":1,"bounds":{"left":0.9496528,"top":0.9261111,"width":0.0125,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXCheckBox","text":"toggle full screen","depth":1,"bounds":{"left":0.89340276,"top":0.93222225,"width":0.013888889,"height":0.022222223},"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"show external playback menu","depth":1,"bounds":{"left":0.89340276,"top":0.9261111,"width":0.013888889,"height":0.022222223},"on_screen":true,"role_description":"button","is_focused":false},{"role":"AXButton","text":"show external playback menu","depth":2,"bounds":{"left":0.89340276,"top":0.9261111,"width":0.013888889,"height":0.022222223},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"show media selection menu","depth":1,"bounds":{"left":0.89340276,"top":0.93222225,"width":0.015277778,"height":0.022222223},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"toggle picture-in-picture playback","depth":1,"bounds":{"left":0.89340276,"top":0.92444444,"width":0.017361112,"height":0.022222223},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"show action menu","depth":1,"bounds":{"left":0.89340276,"top":0.9316667,"width":0.014583333,"height":0.023333333},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"share","depth":1,"bounds":{"left":0.9232639,"top":0.9211111,"width":0.013541667,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"show chapter menu","depth":1,"bounds":{"left":0.89340276,"top":0.935,"width":0.014583333,"height":0.016666668},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXCheckBox","text":"zoom","depth":1,"bounds":{"left":0.89340276,"top":0.93,"width":0.013888889,"height":0.026666667},"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXCheckBox","text":"zoom","depth":1,"bounds":{"left":0.89340276,"top":0.93277776,"width":0.017361112,"height":0.02111111},"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"playback speed","depth":1,"bounds":{"left":0.89340276,"top":0.93277776,"width":0.013194445,"height":0.02111111},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"01:57","depth":1,"bounds":{"left":0.65868056,"top":0.9633333,"width":0.02638889,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"toggle elapsed time, timecode and framecount","depth":1,"bounds":{"left":0.66006947,"top":0.9633333,"width":0.023611112,"height":0.016666668},"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"04:55","depth":1,"bounds":{"left":0.9305556,"top":0.9633333,"width":0.031597223,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"toggle duration and remaining time","depth":1,"bounds":{"left":0.93194443,"top":0.9633333,"width":0.028819444,"height":0.016666668},"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXMenuButton","text":"document actions","depth":1,"bounds":{"left":0.6097222,"top":0.04,"width":0.0069444445,"height":0.017777778},"on_screen":true,"role_description":"menu button","is_enabled":false,"is_focused":false},{"role":"AXStaticText","text":"PLanhat Petko interest event 2026-05-11.mp4","depth":1,"bounds":{"left":0.3986111,"top":0.04,"width":0.21111111,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-8906954455826827180
|
8227836637542616676
|
click
|
hybrid
|
NULL
|
rewind
play/pause
fast forward
mute
More Controls
rewind
play/pause
fast forward
mute
More Controls
toggle full screen
show external playback menu
show external playback menu
show media selection menu
toggle picture-in-picture playback
show action menu
share
show chapter menu
zoom
zoom
playback speed
01:57
toggle elapsed time, timecode and framecount
04:55
toggle duration and remaining time
document actions
PLanhat Petko interest event 2026-05-11.mp4
QuickTime PlayerFileEditViewWindowHelpj Support Daily - in 4h 32 m100% (Tue 12 May 10:28:54• PLanhat Petko interest event 2026-05-11.mp4Screen snareChromeFileEditViewHistoryBookmarksProfilesTabWindowHelp*8• Mon 11 May 12:19Gree!Scoreandre• wilsoCall AJiminM Inbox=N=AppsBuildUser;§ New: | u User; | +Cws.planhat.com/jiminny/home/data-explorer/usagemetricdef?preview=UsageMetricDef.674543e45a4792694fe994e5WorkAIKB• ChatPlayground Al...Jiminny - Calenda…M GMail• My Calendly - Eve....= PH New Ul LoginGet Starting with J...AppsChloe Onboarding..+ CX Journey SMB....Jiminny ~Content ExplorerData ExplorerNotificationsEmail ManagerMoreSections -CS Day-to-day•Getting started Guide• Just CS Data+ Daily Operations# Weekly prepERenewals and UpsellRisk and Churn AnalyticsImplementation -Impl ProjectsTrial Opps (Under Review)Stoyan's clientsLeadershipSystem ReportsLeadership OperationsPortiolio Overview (Dashbo…NPS Report - GregClient Engagement OverviewRevenue AnalyticsE MetricB DatasetplaybackVisitedQ, PLAYBACK+ Metric6 nТугactivities. playbackVisitedOverview~ Company 2Raw Data• Playback Adoption AvgCal• Playback AdoptionCompanyBT Local Business Oxford & BucksEndUserTHRIVE• JUsers Playback AdoptionCalBT Local Business Severnside• Playback Adoption (Last 7 days)CalJibble• Playback AdoptionCalMoxsoplaybackVisitedUsConnectdReward Gateway-EdenredTeamtailorReward Gateway-EdenredScoroBT Local Business SevernsideScoroScoroCrmnUserBen CopelinLouise CameronConnor MorrisEllas Bjornstjerne EngedalTom ZiniZoe SergioAidan JonesAndrew CrabtreeThomas CrothallLewis TurnerThomas CrothallThomas CrothalEventplaybackVisitedplaybackVisitedplaybackVisitedplaybackVisitedplaybackVisitedplaybackVisitedplaybackVisitedplaybackVisitedplaybackVisitedplaybackVisitedplaybackVisitedplaybackVisitedplaybackVisitedplaybackVisitednisharWlietalE PetkoFilter by CompanyTimeMay 11, 2026 09:19:16Ingestion AtMay 11, 2026 09:19:18May 11, 2026 09:19:11May 11, 2026 09:19:18May 11, 2026 09:19:03May 11, 2026 09:19:08May 11, 2026 09-19:00May 11, 2026 09:18:58May 11, 2026 09:19:08May 11, 2026 09:19:08May 11, 2026 09:18:58May 11, 2026 09:19:08May 11, 2026 09:18-45May 11, 2026 09:18:47May 11, 2026 09:18:45May 11, 2026 09:18-34May 11, 2026 09:18:09May 11, 2026 09:18:05May 11, 2026 09:18:04May 11, 2026 09:18:02May 11, 2026 09:18:00May 11, 2026 09:18:47May T1,May 1195 Al Notes: OffMay 11,May 11,May 11May 11,Petko ...een .ГА02:1104:55ve...
|
NULL
|
/Volumes/Work/2026/PLanhat Petko interest event 20 /Volumes/Work/2026/PLanhat Petko interest event 2026-05-11.mp4...
|
NULL
|
NULL
|
|
23061
|
980
|
18
|
2026-05-12T07:31:33.832018+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778571093832_m1.jpg...
|
QuickTime Player
|
PLanhat Petko interest event 2026-05-11.mp4
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
rewind
play/pause
fast forward
mute
More Controls
rewind
play/pause
fast forward
mute
More Controls
toggle full screen
show external playback menu
show external playback menu
show media selection menu
toggle picture-in-picture playback
show action menu
share
show chapter menu
zoom
zoom
playback speed
01:57
toggle elapsed time, timecode and framecount
04:55
toggle duration and remaining time
document actions
PLanhat Petko interest event 2026-05-11.mp4...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"rewind","depth":1,"bounds":{"left":0.77569443,"top":0.9266667,"width":0.017361112,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXCheckBox","text":"play/pause","depth":1,"bounds":{"left":0.8003472,"top":0.9172222,"width":0.02013889,"height":0.037777778},"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":true},{"role":"AXButton","text":"fast forward","depth":1,"bounds":{"left":0.828125,"top":0.9266667,"width":0.017361112,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"mute","depth":1,"bounds":{"left":0.65868056,"top":0.9266667,"width":0.015625,"height":0.016666668},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"More Controls","depth":1,"bounds":{"left":0.9496528,"top":0.9261111,"width":0.0125,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXCheckBox","text":"toggle full screen","depth":1,"bounds":{"left":0.89340276,"top":0.93222225,"width":0.013888889,"height":0.022222223},"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"show external playback menu","depth":1,"bounds":{"left":0.89340276,"top":0.9261111,"width":0.013888889,"height":0.022222223},"on_screen":true,"role_description":"button","is_focused":false},{"role":"AXButton","text":"show external playback menu","depth":2,"bounds":{"left":0.89340276,"top":0.9261111,"width":0.013888889,"height":0.022222223},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"show media selection menu","depth":1,"bounds":{"left":0.89340276,"top":0.93222225,"width":0.015277778,"height":0.022222223},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"toggle picture-in-picture playback","depth":1,"bounds":{"left":0.89340276,"top":0.92444444,"width":0.017361112,"height":0.022222223},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"show action menu","depth":1,"bounds":{"left":0.89340276,"top":0.9316667,"width":0.014583333,"height":0.023333333},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"share","depth":1,"bounds":{"left":0.9232639,"top":0.9211111,"width":0.013541667,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"show chapter menu","depth":1,"bounds":{"left":0.89340276,"top":0.935,"width":0.014583333,"height":0.016666668},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXCheckBox","text":"zoom","depth":1,"bounds":{"left":0.89340276,"top":0.93,"width":0.013888889,"height":0.026666667},"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXCheckBox","text":"zoom","depth":1,"bounds":{"left":0.89340276,"top":0.93277776,"width":0.017361112,"height":0.02111111},"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"playback speed","depth":1,"bounds":{"left":0.89340276,"top":0.93277776,"width":0.013194445,"height":0.02111111},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"01:57","depth":1,"bounds":{"left":0.65868056,"top":0.9633333,"width":0.02638889,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"toggle elapsed time, timecode and framecount","depth":1,"bounds":{"left":0.66006947,"top":0.9633333,"width":0.023611112,"height":0.016666668},"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"04:55","depth":1,"bounds":{"left":0.9305556,"top":0.9633333,"width":0.031597223,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"toggle duration and remaining time","depth":1,"bounds":{"left":0.93194443,"top":0.9633333,"width":0.028819444,"height":0.016666668},"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXMenuButton","text":"document actions","depth":1,"bounds":{"left":0.6097222,"top":0.04,"width":0.0069444445,"height":0.017777778},"on_screen":true,"role_description":"menu button","is_enabled":false,"is_focused":false},{"role":"AXStaticText","text":"PLanhat Petko interest event 2026-05-11.mp4","depth":1,"bounds":{"left":0.3986111,"top":0.04,"width":0.21111111,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-8906954455826827180
|
8227836637542616676
|
click
|
hybrid
|
NULL
|
rewind
play/pause
fast forward
mute
More Controls
rewind
play/pause
fast forward
mute
More Controls
toggle full screen
show external playback menu
show external playback menu
show media selection menu
toggle picture-in-picture playback
show action menu
share
show chapter menu
zoom
zoom
playback speed
01:57
toggle elapsed time, timecode and framecount
04:55
toggle duration and remaining time
document actions
PLanhat Petko interest event 2026-05-11.mp4
QuickTime PlayerFileEditViewWindowHelp(ablj Support Daily - in 4h 29 mA100% (8•Tue 12 May 10:31:33• PLanhat Petko interest event 2026-05-11.mp4Screen snareChromeFileEditViewHistoryBookmarksProfilesTabWindowHelp*Q8• Mon 11 May 12:19Gree!Scoreandre• wilsoCall AJiminM Inbox=NX=AppsBuildu Usery§ New: | u User; | +Cws.planhat.com/jiminny/home/data-explorer/usagemetricdef?preview=UsageMetricDef.674543e45a4792694fe994e5© WorkAIKB• ChatPlayground Al...Apps+ CX Journey SMB....Jiminny ~Content ExplorerData ExplorerNotificationsEmail Manager•** MoreSections -ECS Day-to-day•Getting started Guide• Just CS Data* Daily Operations# Weekly prepRenewals and UpsellRisk and Churn AnalyticsImplementation -Impl ProjectsTrial Opps (Under Review)Stoyan's clientsLeadershipSystem ReportsLeadership OperationsPortfolio Overview (Dashbo….NPS Report - GregClient Engagement OverviewRevenue AnalyticsJiminny - Calenda….M GMail• My Calendly - Eve....= PH New Ul LoginGet Starting with J...Chloe Onboarding..E MetricB DatasetplaybackVisitedQ, PLAYBACK+ Metric6 nactivities. playbackVisitedТугOverviewRaw DataTrace~ Company 2Fiter by Company• Playback Adoption Avg• Playback AdoptionplaybackVisitedw6 2026EndUser• JUsers Playback AdoptionCal• Playback Adoption (Last 7 days)Cal• Playback AdoptionCalplaybackVisitedUsw1 2026NQ 2026w3 2026W4 2026WS 2026Wб 2026w8 2026WEEKSMINNo groups found+ Show fuh listAdd a comment40 +E. Petko6 Feb 11, 2026 - May 10, 2026w11 2026w13 2026Al Notes: OffPetko ...reen .701:5704:55ve...
|
23060
|
/Volumes/Work/2026/PLanhat Petko interest event 20 /Volumes/Work/2026/PLanhat Petko interest event 2026-05-11.mp4...
|
NULL
|
NULL
|
|
23062
|
981
|
24
|
2026-05-12T07:31:33.793870+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778571093793_m2.jpg...
|
QuickTime Player
|
PLanhat Petko interest event 2026-05-11.mp4
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rewind
play/pause
fast forward
mute
More Controls
rewind
play/pause
fast forward
mute
More Controls
toggle full screen
show external playback menu
show external playback menu
show media selection menu
toggle picture-in-picture playback
show action menu
share
show chapter menu
zoom
zoom
playback speed
01:57
toggle elapsed time, timecode and framecount
04:55
toggle duration and remaining time
document actions
PLanhat Petko interest event 2026-05-11.mp4...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"rewind","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXCheckBox","text":"play/pause","depth":1,"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":true},{"role":"AXButton","text":"fast forward","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"mute","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"More Controls","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXCheckBox","text":"toggle full screen","depth":1,"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"show external playback menu","depth":1,"on_screen":true,"role_description":"button","is_focused":false},{"role":"AXButton","text":"show external playback menu","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"show media selection menu","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"toggle picture-in-picture playback","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"show action menu","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"share","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"show chapter menu","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXCheckBox","text":"zoom","depth":1,"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXCheckBox","text":"zoom","depth":1,"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXButton","text":"playback speed","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"01:57","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"toggle elapsed time, timecode and framecount","depth":1,"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"04:55","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"toggle duration and remaining time","depth":1,"on_screen":true,"role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false},{"role":"AXMenuButton","text":"document actions","depth":1,"bounds":{"left":0.5621675,"top":1.0,"width":0.0033244682,"height":-0.028730989},"on_screen":true,"role_description":"menu button","is_enabled":false,"is_focused":false},{"role":"AXStaticText","text":"PLanhat Petko interest event 2026-05-11.mp4","depth":1,"bounds":{"left":0.46110374,"top":1.0,"width":0.10106383,"height":-0.028730989},"on_screen":true,"role_description":"text"}]...
|
-8906954455826827180
|
8227836637542616676
|
click
|
hybrid
|
NULL
|
rewind
play/pause
fast forward
mute
More Controls
rewind
play/pause
fast forward
mute
More Controls
toggle full screen
show external playback menu
show external playback menu
show media selection menu
toggle picture-in-picture playback
show action menu
share
show chapter menu
zoom
zoom
playback speed
01:57
toggle elapsed time, timecode and framecount
04:55
toggle duration and remaining time
document actions
PLanhat Petko interest event 2026-05-11.mp4
rirerox•0 lNew Tab* Jy 20820 es reindex stream mode)# (UY-20725] [HubSpot] Optimise CF@ JY-20725 add HS rate limit handllPipelines - jiminny/appPull requests - jiminny/appWY-207731 User Pilot not receivini@JY-20773 fix user pilot tracking ofrWJY-20776) Automated report - sel) TypeError: League|Flysystem|Files)TypeError: Leaque\Flysystem\FilesPlatform Sprint 3 02 - Platform Tel@JY-20625 | JY-20742 | MCP POC ЬG LLM pricing overview with the mos= Data Explorer• Loain— New Tab• = ws.planhat.com/• Goodle geminidifferent versions and regions, you can trynavigating directly. However, note thatws.planhat.com is a global entry point for theupgraded platform.• Try navigating to: https://ws.planhat.com• If your account has been migrated to theupgraded version, it should recognize yoursession or prompt you to log in.• Note: If your data is specifically hosted onune eus cluster, the system mavautomatically redirect you to a regionalversion of the upgraded Ul (e.g., ws-eu3.planhat.com)2. The In-App "Switch" (If Available)If your tenant is eligible for the upgrade but youare still in the classic "app" view, look for theseUl elements:• The Top Gray Bar: Click on your TenantName/Logo in the top-left corner. Thisoften opens the "Global Tools" menu.• Upgrade Prompts: In some instances,Planhat olaces a "Irv the new Pannat" or"Upgraded Planhat" toggle in the Helpmenu (the "?" icon in the top right) or as abanner at the too of the screen.Why use ws.olanhat.com?You likely need this URL to access newer "GlobalEnter a prompt for GeminiFastvAl lt can make mistakes so double check it Your orivaev &. GerSummarize pagemetricdef?preview=UsageMetricDef.69f2c6529c9f21b58804f123E Metric~DatasetDara explorerQ autCalendanNameA Notifications** Morev EndUser 1automated-reports-track-interestCS Dav-to-dav"Getting started Guide& Just CS Data* Dailv OverationsWeekly prep® Renewals and UpsellRisk and Churn An..ImolementationImpl ProjectsTrial Opps (Under Rev..,Stovan's clients1 metricTypeUser ActivitiesModelQ Search Jiminnyautomated-reporis-track-interestactivities.automated-reoorts-track-interestOverview Raw Data TraceEndUserMetrics05 May06 MavCommentsAdd a commonti07 MayInDAYSVhhlsuppont Dally • In 4h Z5m100% 5• lue 1z May 10•31•3.L. Lukas•• XFilter by Company€ May 05, 2026 - May 11, 202609 Mav11 Mayas aSUMacross comoaniesNo groups found+ Show full lict...
|
23054
|
/Volumes/Work/2026/PLanhat Petko interest event 20 /Volumes/Work/2026/PLanhat Petko interest event 2026-05-11.mp4...
|
NULL
|
NULL
|
|
12941
|
574
|
11
|
2026-05-09T09:58:15.596176+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-09/1778 /Users/lukas/.screenpipe/data/data/2026-05-09/1778320695596_m2.jpg...
|
Firefox
|
Table: transactions - db - Adminer — Personal
|
True
|
http://192.168.0.242:8092/?pgsql=db&username=finan
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Inbox - [EMAIL] - Gmail
Inbox - [EMAIL] - Gmail
(25) Quora
(25) Quora
Location Logger
Location Logger
Finance Hub
Finance Hub
Finance Hub
Finance Hub
Table: transactions - db - Adminer
Table: transactions - db - Adminer
Close tab
Електронно банкиране ДСК Директ от Банка ДСК
Електронно банкиране ДСК Директ от Банка ДСК
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
VIVACOM
VIVACOM
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
VIVACOM
VIVACOM
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Claude Code | Claude Platform
Claude Code | Claude Platform
Claude
Claude
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
PostgreSQL
PostgreSQL
»
db
db
»
finance_hub
finance_hub
»
public
public
» Table: transactions
Table: transactions
Table: transactions
Select data
Select data
Show structure
Show structure
Alter table
Alter table
New item
New item
Column
Type
Comment
id
integer
Auto Increment
[
nextval('transactions_id_seq')
]
date
timestamp(3)
NULL
amount
double precision
NULL
currency
text
NULL
recipient
text
NULL
owner
text
NULL
location
text
NULL
notes
text
NULL
created_at
timestamp(3)
[
CURRENT_TIMESTAMP
]
updated_at
timestamp(3)
[
CURRENT_TIMESTAMP
]
Column
id
date
amount
currency
recipient
owner
location
notes
created_at
updated_at
Type
integer
Auto Increment
[
nextval('transactions_id_seq')
]
timestamp(3)
NULL
double precision
NULL
text
NULL
text
NULL
text
NULL
text
NULL
text
NULL
timestamp(3)
[
CURRENT_TIMESTAMP
]
timestamp(3)
[
CURRENT_TIMESTAMP
]
Comment
Indexes
Indexes
PRIMARY
id
PRIMARY
id
Alter indexes
Alter indexes
Foreign keys
Foreign keys
Add foreign key
Add foreign key
Checks
Checks
Create check
Create check
Triggers
Triggers
Add trigger
Add trigger
Adminer 5.3.0
Adminer
Adminer
5.3.0
Language:
DB:
Schema:
SQL command
SQL command
Import
Import
Export
Export
Create table
Create table
select
select
_TagToTransaction
_TagToTransaction
select
select
_TagToTransactionImport
_TagToTransactionImport
select
select
_prisma_migrations
_prisma_migrations
select
select
tags
tags
select
select
transaction_imports
transaction_imports
select
select
transactions
transactions
finance
Logout
http://[IP_ADDRESS]:8092/?pgsql=db&username=finance&db=finance_hub&ns=public&table=transaction_imports...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.0518755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.06304868,"width":0.080784574,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DNS / Nameservers | Hostinger","depth":4,"bounds":{"left":0.0,"top":0.08459697,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DNS / Nameservers | Hostinger","depth":5,"bounds":{"left":0.013297873,"top":0.09577015,"width":0.053856384,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Nginx Proxy Manager","depth":4,"bounds":{"left":0.0,"top":0.11731844,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":5,"bounds":{"left":0.013297873,"top":0.12849163,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"bounds":{"left":0.0,"top":0.15003991,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"bounds":{"left":0.013297873,"top":0.16121309,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"bounds":{"left":0.0,"top":0.18276137,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"bounds":{"left":0.013297873,"top":0.19393456,"width":0.040724736,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"bounds":{"left":0.0,"top":0.21548285,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"bounds":{"left":0.013297873,"top":0.22665602,"width":0.03756649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.2482043,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.25937748,"width":0.11469415,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"bounds":{"left":0.0,"top":0.28092578,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"bounds":{"left":0.013297873,"top":0.29209897,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AFFiNE - All In One KnowledgeOS","depth":4,"bounds":{"left":0.0,"top":0.31364724,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AFFiNE - All In One KnowledgeOS","depth":5,"bounds":{"left":0.013297873,"top":0.32482043,"width":0.05851064,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All docs · AFFiNE","depth":4,"bounds":{"left":0.0,"top":0.3463687,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All docs · AFFiNE","depth":5,"bounds":{"left":0.013297873,"top":0.3575419,"width":0.029587766,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Payments Logger","depth":4,"bounds":{"left":0.0,"top":0.3790902,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Payments Logger","depth":5,"bounds":{"left":0.013297873,"top":0.39026338,"width":0.030086435,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Inbox - kovaliklukas@gmail.com - Gmail","depth":4,"bounds":{"left":0.0,"top":0.41181165,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Inbox - kovaliklukas@gmail.com - Gmail","depth":5,"bounds":{"left":0.013297873,"top":0.42298484,"width":0.06898271,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"(25) Quora","depth":4,"bounds":{"left":0.0,"top":0.4445331,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"(25) Quora","depth":5,"bounds":{"left":0.013297873,"top":0.4557063,"width":0.018949468,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Location Logger","depth":4,"bounds":{"left":0.0,"top":0.4772546,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Location Logger","depth":5,"bounds":{"left":0.013297873,"top":0.4884278,"width":0.028091755,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"bounds":{"left":0.0,"top":0.509976,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"bounds":{"left":0.013297873,"top":0.5211492,"width":0.021609042,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"bounds":{"left":0.0,"top":0.54269755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"bounds":{"left":0.013297873,"top":0.55387074,"width":0.021609042,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Table: transactions - db - Adminer","depth":4,"bounds":{"left":0.0,"top":0.575419,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Table: transactions - db - Adminer","depth":5,"bounds":{"left":0.013297873,"top":0.5865922,"width":0.059507977,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.10139628,"top":0.5826017,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Електронно банкиране ДСК Директ от Банка ДСК","depth":4,"bounds":{"left":0.0,"top":0.60814047,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Електронно банкиране ДСК Директ от Банка ДСК","depth":5,"bounds":{"left":0.013297873,"top":0.61931366,"width":0.09059176,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE","depth":4,"bounds":{"left":0.0,"top":0.6408619,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE","depth":5,"bounds":{"left":0.013297873,"top":0.6520351,"width":0.113696806,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"VIVACOM","depth":4,"bounds":{"left":0.0,"top":0.6735834,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"VIVACOM","depth":5,"bounds":{"left":0.013297873,"top":0.6847566,"width":0.016788565,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Смартфони с Unlimited план до 120 € отстъпка | Vivacom","depth":4,"bounds":{"left":0.0,"top":0.70630485,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Смартфони с Unlimited план до 120 € отстъпка | Vivacom","depth":5,"bounds":{"left":0.013297873,"top":0.71747804,"width":0.10239362,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"VIVACOM","depth":4,"bounds":{"left":0.0,"top":0.7390263,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"VIVACOM","depth":5,"bounds":{"left":0.013297873,"top":0.7501995,"width":0.016788565,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom","depth":4,"bounds":{"left":0.0,"top":0.7717478,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom","depth":5,"bounds":{"left":0.013297873,"top":0.782921,"width":0.098902926,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Claude Code | Claude Platform","depth":4,"bounds":{"left":0.0,"top":0.8044693,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Claude Code | Claude Platform","depth":5,"bounds":{"left":0.013297873,"top":0.8156425,"width":0.053357713,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Claude","depth":4,"bounds":{"left":0.0,"top":0.83719075,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Claude","depth":5,"bounds":{"left":0.013297873,"top":0.84836394,"width":0.012134309,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.87150836,"width":0.108211435,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"PostgreSQL","depth":8,"bounds":{"left":0.2130984,"top":0.055067837,"width":0.027593086,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"PostgreSQL","depth":9,"bounds":{"left":0.2130984,"top":0.055067837,"width":0.027593086,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"»","depth":8,"bounds":{"left":0.24069148,"top":0.055067837,"width":0.006482713,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"db","depth":8,"bounds":{"left":0.2471742,"top":0.055067837,"width":0.005984043,"height":0.014365523},"on_screen":true,"help_text":"Alt+Shift+1","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"db","depth":9,"bounds":{"left":0.2471742,"top":0.055067837,"width":0.005984043,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"»","depth":8,"bounds":{"left":0.25315824,"top":0.055067837,"width":0.0063164895,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"finance_hub","depth":8,"bounds":{"left":0.25947472,"top":0.055067837,"width":0.029421542,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"finance_hub","depth":9,"bounds":{"left":0.25947472,"top":0.055067837,"width":0.029421542,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"»","depth":8,"bounds":{"left":0.28889626,"top":0.055067837,"width":0.006482713,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"public","depth":8,"bounds":{"left":0.29537898,"top":0.055067837,"width":0.01412899,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"public","depth":9,"bounds":{"left":0.29537898,"top":0.055067837,"width":0.01412899,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"» Table: transactions","depth":8,"bounds":{"left":0.30950797,"top":0.055067837,"width":0.051861703,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Table: transactions","depth":7,"bounds":{"left":0.20827793,"top":0.0830008,"width":0.49850398,"height":0.04988029},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Table: transactions","depth":8,"bounds":{"left":0.21542554,"top":0.096568234,"width":0.068317816,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Select data","depth":8,"bounds":{"left":0.21426196,"top":0.14884278,"width":0.026928192,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Select data","depth":9,"bounds":{"left":0.21426196,"top":0.14884278,"width":0.026928192,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Show structure","depth":8,"bounds":{"left":0.24950133,"top":0.14884278,"width":0.041223403,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Show structure","depth":9,"bounds":{"left":0.24950133,"top":0.14884278,"width":0.041223403,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Alter table","depth":8,"bounds":{"left":0.2990359,"top":0.14884278,"width":0.024933511,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Alter table","depth":9,"bounds":{"left":0.2990359,"top":0.14884278,"width":0.024933511,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"New item","depth":8,"bounds":{"left":0.3324468,"top":0.14884278,"width":0.022606382,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New item","depth":9,"bounds":{"left":0.3324468,"top":0.14884278,"width":0.022606382,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Column","depth":11,"bounds":{"left":0.22057846,"top":0.1763767,"width":0.018284574,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Type","depth":11,"bounds":{"left":0.24650931,"top":0.1763767,"width":0.009973404,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Comment","depth":11,"bounds":{"left":0.3801529,"top":0.1763767,"width":0.020944148,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":10,"bounds":{"left":0.2159242,"top":0.19393456,"width":0.004488032,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"integer","depth":11,"bounds":{"left":0.24650931,"top":0.19393456,"width":0.015292553,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auto Increment","depth":10,"bounds":{"left":0.2632979,"top":0.19393456,"width":0.033909574,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"bounds":{"left":0.29870346,"top":0.19393456,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"nextval('transactions_id_seq')","depth":11,"bounds":{"left":0.30069813,"top":0.19393456,"width":0.07446808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"bounds":{"left":0.37516624,"top":0.19393456,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"date","depth":10,"bounds":{"left":0.2159242,"top":0.21189146,"width":0.010638298,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"bounds":{"left":0.24650931,"top":0.21189146,"width":0.029753989,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.2777593,"top":0.21189146,"width":0.011136968,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"amount","depth":10,"bounds":{"left":0.2159242,"top":0.22984837,"width":0.018450798,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"double precision","depth":11,"bounds":{"left":0.24650931,"top":0.22984837,"width":0.03523936,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.28324467,"top":0.22984837,"width":0.011136968,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"currency","depth":10,"bounds":{"left":0.2159242,"top":0.24780527,"width":0.021110373,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.24780527,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.24780527,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":10,"bounds":{"left":0.2159242,"top":0.26576218,"width":0.021276595,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.26576218,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.26576218,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"owner","depth":10,"bounds":{"left":0.2159242,"top":0.28332004,"width":0.01512633,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.28332004,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.28332004,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"location","depth":10,"bounds":{"left":0.2159242,"top":0.30127692,"width":0.019281914,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.30127692,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.30127692,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"notes","depth":10,"bounds":{"left":0.2159242,"top":0.31923383,"width":0.013297873,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.31923383,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.31923383,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"created_at","depth":10,"bounds":{"left":0.2159242,"top":0.33719075,"width":0.026097074,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"bounds":{"left":0.24650931,"top":0.33719075,"width":0.029753989,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"bounds":{"left":0.2777593,"top":0.33719075,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CURRENT_TIMESTAMP","depth":11,"bounds":{"left":0.27975398,"top":0.33719075,"width":0.054853722,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"bounds":{"left":0.33460772,"top":0.33719075,"width":0.0018284575,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"updated_at","depth":10,"bounds":{"left":0.2159242,"top":0.35514766,"width":0.027593086,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"bounds":{"left":0.24650931,"top":0.35514766,"width":0.029753989,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"bounds":{"left":0.2777593,"top":0.35514766,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CURRENT_TIMESTAMP","depth":11,"bounds":{"left":0.27975398,"top":0.35514766,"width":0.054853722,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"bounds":{"left":0.33460772,"top":0.35514766,"width":0.0018284575,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Column","depth":10,"bounds":{"left":0.22057846,"top":0.1763767,"width":0.018284574,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":10,"bounds":{"left":0.2159242,"top":0.19393456,"width":0.004488032,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"date","depth":10,"bounds":{"left":0.2159242,"top":0.21189146,"width":0.010638298,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"amount","depth":10,"bounds":{"left":0.2159242,"top":0.22984837,"width":0.018450798,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"currency","depth":10,"bounds":{"left":0.2159242,"top":0.24780527,"width":0.021110373,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":10,"bounds":{"left":0.2159242,"top":0.26576218,"width":0.021276595,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"owner","depth":10,"bounds":{"left":0.2159242,"top":0.28332004,"width":0.01512633,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"location","depth":10,"bounds":{"left":0.2159242,"top":0.30127692,"width":0.019281914,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"notes","depth":10,"bounds":{"left":0.2159242,"top":0.31923383,"width":0.013297873,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"created_at","depth":10,"bounds":{"left":0.2159242,"top":0.33719075,"width":0.026097074,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"updated_at","depth":10,"bounds":{"left":0.2159242,"top":0.35514766,"width":0.027593086,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Type","depth":10,"bounds":{"left":0.24650931,"top":0.1763767,"width":0.009973404,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"integer","depth":11,"bounds":{"left":0.24650931,"top":0.19393456,"width":0.015292553,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auto Increment","depth":10,"bounds":{"left":0.2632979,"top":0.19393456,"width":0.033909574,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"bounds":{"left":0.29870346,"top":0.19393456,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"nextval('transactions_id_seq')","depth":11,"bounds":{"left":0.30069813,"top":0.19393456,"width":0.07446808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"bounds":{"left":0.37516624,"top":0.19393456,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"bounds":{"left":0.24650931,"top":0.21189146,"width":0.029753989,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.2777593,"top":0.21189146,"width":0.011136968,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"double precision","depth":11,"bounds":{"left":0.24650931,"top":0.22984837,"width":0.03523936,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.28324467,"top":0.22984837,"width":0.011136968,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.24780527,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.24780527,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.26576218,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.26576218,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.28332004,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.28332004,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.30127692,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.30127692,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.31923383,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.31923383,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"bounds":{"left":0.24650931,"top":0.33719075,"width":0.029753989,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"bounds":{"left":0.2777593,"top":0.33719075,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CURRENT_TIMESTAMP","depth":11,"bounds":{"left":0.27975398,"top":0.33719075,"width":0.054853722,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"bounds":{"left":0.33460772,"top":0.33719075,"width":0.0018284575,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"bounds":{"left":0.24650931,"top":0.35514766,"width":0.029753989,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"bounds":{"left":0.2777593,"top":0.35514766,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CURRENT_TIMESTAMP","depth":11,"bounds":{"left":0.27975398,"top":0.35514766,"width":0.054853722,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"bounds":{"left":0.33460772,"top":0.35514766,"width":0.0018284575,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Comment","depth":10,"bounds":{"left":0.3801529,"top":0.1763767,"width":0.020944148,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Indexes","depth":7,"bounds":{"left":0.21426196,"top":0.3858739,"width":0.49251994,"height":0.018754989},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Indexes","depth":8,"bounds":{"left":0.21426196,"top":0.3858739,"width":0.024601065,"height":0.018355945},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PRIMARY","depth":10,"bounds":{"left":0.2159242,"top":0.4177973,"width":0.022772606,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":10,"bounds":{"left":0.24168883,"top":0.4177973,"width":0.0038231383,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PRIMARY","depth":10,"bounds":{"left":0.2159242,"top":0.4177973,"width":0.022772606,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":10,"bounds":{"left":0.24168883,"top":0.4177973,"width":0.0038231383,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Alter indexes","depth":8,"bounds":{"left":0.21426196,"top":0.4425379,"width":0.03125,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Alter indexes","depth":9,"bounds":{"left":0.21426196,"top":0.4425379,"width":0.03125,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Foreign keys","depth":7,"bounds":{"left":0.21426196,"top":0.47206703,"width":0.49251994,"height":0.018754989},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Foreign keys","depth":8,"bounds":{"left":0.21426196,"top":0.47206703,"width":0.03956117,"height":0.018355945},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add foreign key","depth":8,"bounds":{"left":0.21426196,"top":0.5,"width":0.037898935,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add foreign key","depth":9,"bounds":{"left":0.21426196,"top":0.5,"width":0.037898935,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Checks","depth":7,"bounds":{"left":0.21426196,"top":0.5291301,"width":0.49251994,"height":0.018754989},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Checks","depth":8,"bounds":{"left":0.21426196,"top":0.52952915,"width":0.022107713,"height":0.018355945},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Create check","depth":8,"bounds":{"left":0.21426196,"top":0.55706304,"width":0.03125,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create check","depth":9,"bounds":{"left":0.21426196,"top":0.55706304,"width":0.03125,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Triggers","depth":7,"bounds":{"left":0.21426196,"top":0.58619314,"width":0.49251994,"height":0.018754989},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Triggers","depth":8,"bounds":{"left":0.21426196,"top":0.5865922,"width":0.024933511,"height":0.018355945},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add trigger","depth":8,"bounds":{"left":0.21426196,"top":0.6141261,"width":0.026928192,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add trigger","depth":9,"bounds":{"left":0.21426196,"top":0.6141261,"width":0.026928192,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Adminer 5.3.0","depth":8,"bounds":{"left":0.113696806,"top":0.0830008,"width":0.0909242,"height":0.04988029},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"Adminer","depth":9,"bounds":{"left":0.11851729,"top":0.096568234,"width":0.038231384,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Adminer","depth":10,"bounds":{"left":0.12649602,"top":0.096568234,"width":0.03025266,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5.3.0","depth":9,"bounds":{"left":0.15924202,"top":0.10335196,"width":0.011635638,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Language:","depth":11,"bounds":{"left":0.11851729,"top":0.056664005,"width":0.027094414,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DB:","depth":11,"bounds":{"left":0.11851729,"top":0.1424581,"width":0.010804521,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Schema:","depth":11,"bounds":{"left":0.11851729,"top":0.16001596,"width":0.022938829,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SQL command","depth":9,"bounds":{"left":0.11851729,"top":0.1963288,"width":0.034906916,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQL command","depth":10,"bounds":{"left":0.11851729,"top":0.1963288,"width":0.034906916,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Import","depth":9,"bounds":{"left":0.16173537,"top":0.1963288,"width":0.01662234,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Import","depth":10,"bounds":{"left":0.16173537,"top":0.1963288,"width":0.01662234,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Export","depth":9,"bounds":{"left":0.11851729,"top":0.21069433,"width":0.015625,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Export","depth":10,"bounds":{"left":0.11851729,"top":0.21069433,"width":0.015625,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Create table","depth":9,"bounds":{"left":0.14245346,"top":0.21069433,"width":0.029421542,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create table","depth":10,"bounds":{"left":0.14245346,"top":0.21069433,"width":0.029421542,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"bounds":{"left":0.11851729,"top":0.2442139,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"bounds":{"left":0.11851729,"top":0.2442139,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"_TagToTransaction","depth":10,"bounds":{"left":0.1341423,"top":0.2442139,"width":0.043882977,"height":0.014365523},"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"_TagToTransaction","depth":11,"bounds":{"left":0.1341423,"top":0.2442139,"width":0.043882977,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"bounds":{"left":0.11851729,"top":0.2585794,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"bounds":{"left":0.11851729,"top":0.2585794,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"_TagToTransactionImport","depth":10,"bounds":{"left":0.1341423,"top":0.2585794,"width":0.060339097,"height":0.014365523},"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"_TagToTransactionImport","depth":11,"bounds":{"left":0.1341423,"top":0.2585794,"width":0.060339097,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"bounds":{"left":0.11851729,"top":0.27294493,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"bounds":{"left":0.11851729,"top":0.27294493,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"_prisma_migrations","depth":10,"bounds":{"left":0.1341423,"top":0.27294493,"width":0.047706116,"height":0.014365523},"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"_prisma_migrations","depth":11,"bounds":{"left":0.1341423,"top":0.27294493,"width":0.047706116,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"bounds":{"left":0.11851729,"top":0.28731045,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"bounds":{"left":0.11851729,"top":0.28731045,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"tags","depth":10,"bounds":{"left":0.1341423,"top":0.28731045,"width":0.010139627,"height":0.014365523},"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"tags","depth":11,"bounds":{"left":0.1341423,"top":0.28731045,"width":0.010139627,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"bounds":{"left":0.11851729,"top":0.30167598,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"bounds":{"left":0.11851729,"top":0.30167598,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"transaction_imports","depth":10,"bounds":{"left":0.1341423,"top":0.30167598,"width":0.048038565,"height":0.014365523},"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"transaction_imports","depth":11,"bounds":{"left":0.1341423,"top":0.30167598,"width":0.048038565,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"bounds":{"left":0.11851729,"top":0.3160415,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"bounds":{"left":0.11851729,"top":0.3160415,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"transactions","depth":10,"bounds":{"left":0.1341423,"top":0.3160415,"width":0.03324468,"height":0.014365523},"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"transactions","depth":11,"bounds":{"left":0.1341423,"top":0.3160415,"width":0.03324468,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"finance","depth":9,"bounds":{"left":0.66954786,"top":0.057861134,"width":0.018949468,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Logout","depth":9,"bounds":{"left":0.68849736,"top":0.057462092,"width":0.018284574,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"http://192.168.0.242:8092/?pgsql=db&username=finance&db=finance_hub&ns=public&table=transaction_imports","depth":5,"bounds":{"left":0.11502659,"top":0.9876297,"width":0.19913563,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-8906692636764843871
|
3035567681474669887
|
click
|
accessibility
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Inbox - [EMAIL] - Gmail
Inbox - [EMAIL] - Gmail
(25) Quora
(25) Quora
Location Logger
Location Logger
Finance Hub
Finance Hub
Finance Hub
Finance Hub
Table: transactions - db - Adminer
Table: transactions - db - Adminer
Close tab
Електронно банкиране ДСК Директ от Банка ДСК
Електронно банкиране ДСК Директ от Банка ДСК
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
VIVACOM
VIVACOM
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
VIVACOM
VIVACOM
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Claude Code | Claude Platform
Claude Code | Claude Platform
Claude
Claude
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
PostgreSQL
PostgreSQL
»
db
db
»
finance_hub
finance_hub
»
public
public
» Table: transactions
Table: transactions
Table: transactions
Select data
Select data
Show structure
Show structure
Alter table
Alter table
New item
New item
Column
Type
Comment
id
integer
Auto Increment
[
nextval('transactions_id_seq')
]
date
timestamp(3)
NULL
amount
double precision
NULL
currency
text
NULL
recipient
text
NULL
owner
text
NULL
location
text
NULL
notes
text
NULL
created_at
timestamp(3)
[
CURRENT_TIMESTAMP
]
updated_at
timestamp(3)
[
CURRENT_TIMESTAMP
]
Column
id
date
amount
currency
recipient
owner
location
notes
created_at
updated_at
Type
integer
Auto Increment
[
nextval('transactions_id_seq')
]
timestamp(3)
NULL
double precision
NULL
text
NULL
text
NULL
text
NULL
text
NULL
text
NULL
timestamp(3)
[
CURRENT_TIMESTAMP
]
timestamp(3)
[
CURRENT_TIMESTAMP
]
Comment
Indexes
Indexes
PRIMARY
id
PRIMARY
id
Alter indexes
Alter indexes
Foreign keys
Foreign keys
Add foreign key
Add foreign key
Checks
Checks
Create check
Create check
Triggers
Triggers
Add trigger
Add trigger
Adminer 5.3.0
Adminer
Adminer
5.3.0
Language:
DB:
Schema:
SQL command
SQL command
Import
Import
Export
Export
Create table
Create table
select
select
_TagToTransaction
_TagToTransaction
select
select
_TagToTransactionImport
_TagToTransactionImport
select
select
_prisma_migrations
_prisma_migrations
select
select
tags
tags
select
select
transaction_imports
transaction_imports
select
select
transactions
transactions
finance
Logout
http://[IP_ADDRESS]:8092/?pgsql=db&username=finance&db=finance_hub&ns=public&table=transaction_imports...
|
12940
|
NULL
|
NULL
|
NULL
|
|
12942
|
573
|
16
|
2026-05-09T09:58:15.596152+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-09/1778 /Users/lukas/.screenpipe/data/data/2026-05-09/1778320695596_m1.jpg...
|
Firefox
|
Table: transactions - db - Adminer — Personal
|
True
|
http://192.168.0.242:8092/?pgsql=db&username=finan
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Inbox - [EMAIL] - Gmail
Inbox - [EMAIL] - Gmail
(25) Quora
(25) Quora
Location Logger
Location Logger
Finance Hub
Finance Hub
Finance Hub
Finance Hub
Table: transactions - db - Adminer
Table: transactions - db - Adminer
Close tab
Електронно банкиране ДСК Директ от Банка ДСК
Електронно банкиране ДСК Директ от Банка ДСК
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
VIVACOM
VIVACOM
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
VIVACOM
VIVACOM
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Claude Code | Claude Platform
Claude Code | Claude Platform
Claude
Claude
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
PostgreSQL
PostgreSQL
»
db
db
»
finance_hub
finance_hub
»
public
public
» Table: transactions
Table: transactions
Table: transactions
Select data
Select data
Show structure
Show structure
Alter table
Alter table
New item
New item
Column
Type
Comment
id
integer
Auto Increment
[
nextval('transactions_id_seq')
]
date
timestamp(3)
NULL
amount
double precision
NULL
currency
text
NULL
recipient
text
NULL
owner
text
NULL
location
text
NULL
notes
text
NULL
created_at
timestamp(3)
[
CURRENT_TIMESTAMP
]
updated_at
timestamp(3)
[
CURRENT_TIMESTAMP
]
Column
id
date
amount
currency
recipient
owner
location
notes
created_at
updated_at
Type
integer
Auto Increment
[
nextval('transactions_id_seq')
]
timestamp(3)
NULL
double precision
NULL
text
NULL
text
NULL
text
NULL
text
NULL
text
NULL
timestamp(3)
[
CURRENT_TIMESTAMP
]
timestamp(3)
[
CURRENT_TIMESTAMP
]
Comment
Indexes
Indexes
PRIMARY
id
PRIMARY
id
Alter indexes
Alter indexes
Foreign keys
Foreign keys
Add foreign key
Add foreign key
Checks
Checks
Create check
Create check
Triggers
Triggers
Add trigger
Add trigger
Adminer 5.3.0
Adminer
Adminer
5.3.0
Language:
DB:
Schema:
SQL command
SQL command
Import
Import
Export
Export
Create table
Create table
select
select
_TagToTransaction
_TagToTransaction
select
select
_TagToTransactionImport
_TagToTransactionImport
select
select
_prisma_migrations
_prisma_migrations
select
select
tags
tags
select
select
transaction_imports
transaction_imports
select
select
transactions
transactions
finance
Logout
http://[IP_ADDRESS]:8092/?pgsql=db&username=finance&db=finance_hub&ns=public&table=transaction_imports...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DNS / Nameservers | Hostinger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DNS / Nameservers | Hostinger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Nginx Proxy Manager","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AFFiNE - All In One KnowledgeOS","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AFFiNE - All In One KnowledgeOS","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All docs · AFFiNE","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All docs · AFFiNE","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Payments Logger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Payments Logger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Inbox - kovaliklukas@gmail.com - Gmail","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Inbox - kovaliklukas@gmail.com - Gmail","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"(25) Quora","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"(25) Quora","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Location Logger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Location Logger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Table: transactions - db - Adminer","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Table: transactions - db - Adminer","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Електронно банкиране ДСК Директ от Банка ДСК","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Електронно банкиране ДСК Директ от Банка ДСК","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"VIVACOM","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"VIVACOM","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Смартфони с Unlimited план до 120 € отстъпка | Vivacom","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Смартфони с Unlimited план до 120 € отстъпка | Vivacom","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"VIVACOM","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"VIVACOM","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Claude Code | Claude Platform","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Claude Code | Claude Platform","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Claude","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Claude","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"PostgreSQL","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"PostgreSQL","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"»","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"db","depth":8,"on_screen":true,"help_text":"Alt+Shift+1","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"db","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"»","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"finance_hub","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"finance_hub","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"»","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"public","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"public","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"» Table: transactions","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Table: transactions","depth":7,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Table: transactions","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Select data","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Select data","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Show structure","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Show structure","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Alter table","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Alter table","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"New item","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New item","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Column","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Type","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Comment","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"integer","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auto Increment","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"nextval('transactions_id_seq')","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"date","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"amount","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"double precision","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"currency","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"owner","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"location","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"notes","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"created_at","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CURRENT_TIMESTAMP","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"updated_at","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CURRENT_TIMESTAMP","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Column","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"date","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"amount","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"currency","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"owner","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"location","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"notes","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"created_at","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"updated_at","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Type","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"integer","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auto Increment","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"nextval('transactions_id_seq')","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"double precision","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CURRENT_TIMESTAMP","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CURRENT_TIMESTAMP","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Comment","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Indexes","depth":7,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Indexes","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PRIMARY","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PRIMARY","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Alter indexes","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Alter indexes","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Foreign keys","depth":7,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Foreign keys","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add foreign key","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add foreign key","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Checks","depth":7,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Checks","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Create check","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create check","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Triggers","depth":7,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Triggers","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add trigger","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add trigger","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Adminer 5.3.0","depth":8,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"Adminer","depth":9,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Adminer","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5.3.0","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Language:","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DB:","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Schema:","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SQL command","depth":9,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQL command","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Import","depth":9,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Import","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Export","depth":9,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Export","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Create table","depth":9,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create table","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"_TagToTransaction","depth":10,"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"_TagToTransaction","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"_TagToTransactionImport","depth":10,"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"_TagToTransactionImport","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"_prisma_migrations","depth":10,"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"_prisma_migrations","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"tags","depth":10,"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"tags","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"transaction_imports","depth":10,"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXStaticText","text":"transaction_imports","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"transactions","depth":10,"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"transactions","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"finance","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Logout","depth":9,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"http://192.168.0.242:8092/?pgsql=db&username=finance&db=finance_hub&ns=public&table=transaction_imports","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-8906692636764843871
|
3035567681474669887
|
click
|
accessibility
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Inbox - [EMAIL] - Gmail
Inbox - [EMAIL] - Gmail
(25) Quora
(25) Quora
Location Logger
Location Logger
Finance Hub
Finance Hub
Finance Hub
Finance Hub
Table: transactions - db - Adminer
Table: transactions - db - Adminer
Close tab
Електронно банкиране ДСК Директ от Банка ДСК
Електронно банкиране ДСК Директ от Банка ДСК
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
VIVACOM
VIVACOM
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
VIVACOM
VIVACOM
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Claude Code | Claude Platform
Claude Code | Claude Platform
Claude
Claude
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
PostgreSQL
PostgreSQL
»
db
db
»
finance_hub
finance_hub
»
public
public
» Table: transactions
Table: transactions
Table: transactions
Select data
Select data
Show structure
Show structure
Alter table
Alter table
New item
New item
Column
Type
Comment
id
integer
Auto Increment
[
nextval('transactions_id_seq')
]
date
timestamp(3)
NULL
amount
double precision
NULL
currency
text
NULL
recipient
text
NULL
owner
text
NULL
location
text
NULL
notes
text
NULL
created_at
timestamp(3)
[
CURRENT_TIMESTAMP
]
updated_at
timestamp(3)
[
CURRENT_TIMESTAMP
]
Column
id
date
amount
currency
recipient
owner
location
notes
created_at
updated_at
Type
integer
Auto Increment
[
nextval('transactions_id_seq')
]
timestamp(3)
NULL
double precision
NULL
text
NULL
text
NULL
text
NULL
text
NULL
text
NULL
timestamp(3)
[
CURRENT_TIMESTAMP
]
timestamp(3)
[
CURRENT_TIMESTAMP
]
Comment
Indexes
Indexes
PRIMARY
id
PRIMARY
id
Alter indexes
Alter indexes
Foreign keys
Foreign keys
Add foreign key
Add foreign key
Checks
Checks
Create check
Create check
Triggers
Triggers
Add trigger
Add trigger
Adminer 5.3.0
Adminer
Adminer
5.3.0
Language:
DB:
Schema:
SQL command
SQL command
Import
Import
Export
Export
Create table
Create table
select
select
_TagToTransaction
_TagToTransaction
select
select
_TagToTransactionImport
_TagToTransactionImport
select
select
_prisma_migrations
_prisma_migrations
select
select
tags
tags
select
select
transaction_imports
transaction_imports
select
select
transactions
transactions
finance
Logout
http://[IP_ADDRESS]:8092/?pgsql=db&username=finance&db=finance_hub&ns=public&table=transaction_imports...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
13032
|
578
|
12
|
2026-05-09T10:05:00.195079+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-09/1778 /Users/lukas/.screenpipe/data/data/2026-05-09/1778321100195_m2.jpg...
|
Firefox
|
Table: transactions - db - Adminer — Personal
|
True
|
http://192.168.0.242:8092/?pgsql=db&username=finan
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Inbox - [EMAIL] - Gmail
Inbox - [EMAIL] - Gmail
(25) Quora
(25) Quora
Location Logger
Location Logger
Finance Hub
Finance Hub
Finance Hub
Finance Hub
Table: transactions - db - Adminer
Table: transactions - db - Adminer
Close tab
Електронно банкиране ДСК Директ от Банка ДСК
Електронно банкиране ДСК Директ от Банка ДСК
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
VIVACOM
VIVACOM
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
VIVACOM
VIVACOM
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Claude Code | Claude Platform
Claude Code | Claude Platform
Claude
Claude
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
PostgreSQL
PostgreSQL
»
db
db
»
finance_hub
finance_hub
»
public
public
» Table: transactions
Table: transactions
Table: transactions
Select data
Select data
Show structure
Show structure
Alter table
Alter table
New item
New item
Column
Type
Comment
id
integer
Auto Increment
[
nextval('transactions_id_seq')
]
date
timestamp(3)
NULL
amount
double precision
NULL
currency
text
NULL
recipient
text
NULL
owner
text
NULL
location
text
NULL
notes
text
NULL
created_at
timestamp(3)
[
CURRENT_TIMESTAMP
]
updated_at
timestamp(3)
[
CURRENT_TIMESTAMP
]
Column
id
date
amount
currency
recipient
owner
location
notes
created_at
updated_at
Type
integer
Auto Increment
[
nextval('transactions_id_seq')
]
timestamp(3)
NULL
double precision
NULL
text
NULL
text
NULL
text
NULL
text
NULL
text
NULL
timestamp(3)
[
CURRENT_TIMESTAMP
]
timestamp(3)
[
CURRENT_TIMESTAMP
]
Comment
Indexes
Indexes
PRIMARY
id
PRIMARY
id
Alter indexes
Alter indexes
Foreign keys
Foreign keys
Add foreign key
Add foreign key
Checks
Checks
Create check
Create check
Triggers
Triggers
Add trigger
Add trigger
Adminer 5.3.0
Adminer
Adminer
5.3.0
Language:
DB:
Schema:
SQL command
SQL command
Import
Import
Export
Export
Create table
Create table
select
select
_TagToTransaction
_TagToTransaction
select
select
_TagToTransactionImport
_TagToTransactionImport
select
select
_prisma_migrations
_prisma_migrations
select
select
tags
tags
select
select
transaction_imports
transaction_imports
select
select
transactions
transactions
finance
Logout
http://[IP_ADDRESS]:8092/?pgsql=db&username=finance&db=finance_hub&ns=public&table=transaction_imports...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.0518755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.06304868,"width":0.080784574,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DNS / Nameservers | Hostinger","depth":4,"bounds":{"left":0.0,"top":0.08459697,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DNS / Nameservers | Hostinger","depth":5,"bounds":{"left":0.013297873,"top":0.09577015,"width":0.053856384,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Nginx Proxy Manager","depth":4,"bounds":{"left":0.0,"top":0.11731844,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":5,"bounds":{"left":0.013297873,"top":0.12849163,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"bounds":{"left":0.0,"top":0.15003991,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"bounds":{"left":0.013297873,"top":0.16121309,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"bounds":{"left":0.0,"top":0.18276137,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"bounds":{"left":0.013297873,"top":0.19393456,"width":0.040724736,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"bounds":{"left":0.0,"top":0.21548285,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"bounds":{"left":0.013297873,"top":0.22665602,"width":0.03756649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.2482043,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.25937748,"width":0.11469415,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"bounds":{"left":0.0,"top":0.28092578,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"bounds":{"left":0.013297873,"top":0.29209897,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AFFiNE - All In One KnowledgeOS","depth":4,"bounds":{"left":0.0,"top":0.31364724,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AFFiNE - All In One KnowledgeOS","depth":5,"bounds":{"left":0.013297873,"top":0.32482043,"width":0.05851064,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All docs · AFFiNE","depth":4,"bounds":{"left":0.0,"top":0.3463687,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All docs · AFFiNE","depth":5,"bounds":{"left":0.013297873,"top":0.3575419,"width":0.029587766,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Payments Logger","depth":4,"bounds":{"left":0.0,"top":0.3790902,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Payments Logger","depth":5,"bounds":{"left":0.013297873,"top":0.39026338,"width":0.030086435,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Inbox - kovaliklukas@gmail.com - Gmail","depth":4,"bounds":{"left":0.0,"top":0.41181165,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Inbox - kovaliklukas@gmail.com - Gmail","depth":5,"bounds":{"left":0.013297873,"top":0.42298484,"width":0.06898271,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"(25) Quora","depth":4,"bounds":{"left":0.0,"top":0.4445331,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"(25) Quora","depth":5,"bounds":{"left":0.013297873,"top":0.4557063,"width":0.018949468,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Location Logger","depth":4,"bounds":{"left":0.0,"top":0.4772546,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Location Logger","depth":5,"bounds":{"left":0.013297873,"top":0.4884278,"width":0.028091755,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"bounds":{"left":0.0,"top":0.509976,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"bounds":{"left":0.013297873,"top":0.5211492,"width":0.021609042,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"bounds":{"left":0.0,"top":0.54269755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"bounds":{"left":0.013297873,"top":0.55387074,"width":0.021609042,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Table: transactions - db - Adminer","depth":4,"bounds":{"left":0.0,"top":0.575419,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Table: transactions - db - Adminer","depth":5,"bounds":{"left":0.013297873,"top":0.5865922,"width":0.059507977,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.10139628,"top":0.5826017,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Електронно банкиране ДСК Директ от Банка ДСК","depth":4,"bounds":{"left":0.0,"top":0.60814047,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Електронно банкиране ДСК Директ от Банка ДСК","depth":5,"bounds":{"left":0.013297873,"top":0.61931366,"width":0.09059176,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE","depth":4,"bounds":{"left":0.0,"top":0.6408619,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE","depth":5,"bounds":{"left":0.013297873,"top":0.6520351,"width":0.113696806,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"VIVACOM","depth":4,"bounds":{"left":0.0,"top":0.6735834,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"VIVACOM","depth":5,"bounds":{"left":0.013297873,"top":0.6847566,"width":0.016788565,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Смартфони с Unlimited план до 120 € отстъпка | Vivacom","depth":4,"bounds":{"left":0.0,"top":0.70630485,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Смартфони с Unlimited план до 120 € отстъпка | Vivacom","depth":5,"bounds":{"left":0.013297873,"top":0.71747804,"width":0.10239362,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"VIVACOM","depth":4,"bounds":{"left":0.0,"top":0.7390263,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"VIVACOM","depth":5,"bounds":{"left":0.013297873,"top":0.7501995,"width":0.016788565,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom","depth":4,"bounds":{"left":0.0,"top":0.7717478,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom","depth":5,"bounds":{"left":0.013297873,"top":0.782921,"width":0.098902926,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Claude Code | Claude Platform","depth":4,"bounds":{"left":0.0,"top":0.8044693,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Claude Code | Claude Platform","depth":5,"bounds":{"left":0.013297873,"top":0.8156425,"width":0.053357713,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Claude","depth":4,"bounds":{"left":0.0,"top":0.83719075,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Claude","depth":5,"bounds":{"left":0.013297873,"top":0.84836394,"width":0.012134309,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.87150836,"width":0.108211435,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"PostgreSQL","depth":8,"bounds":{"left":0.2130984,"top":0.055067837,"width":0.027593086,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"PostgreSQL","depth":9,"bounds":{"left":0.2130984,"top":0.055067837,"width":0.027593086,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"»","depth":8,"bounds":{"left":0.24069148,"top":0.055067837,"width":0.006482713,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"db","depth":8,"bounds":{"left":0.2471742,"top":0.055067837,"width":0.005984043,"height":0.014365523},"on_screen":true,"help_text":"Alt+Shift+1","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"db","depth":9,"bounds":{"left":0.2471742,"top":0.055067837,"width":0.005984043,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"»","depth":8,"bounds":{"left":0.25315824,"top":0.055067837,"width":0.0063164895,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"finance_hub","depth":8,"bounds":{"left":0.25947472,"top":0.055067837,"width":0.029421542,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"finance_hub","depth":9,"bounds":{"left":0.25947472,"top":0.055067837,"width":0.029421542,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"»","depth":8,"bounds":{"left":0.28889626,"top":0.055067837,"width":0.006482713,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"public","depth":8,"bounds":{"left":0.29537898,"top":0.055067837,"width":0.01412899,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"public","depth":9,"bounds":{"left":0.29537898,"top":0.055067837,"width":0.01412899,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"» Table: transactions","depth":8,"bounds":{"left":0.30950797,"top":0.055067837,"width":0.051861703,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Table: transactions","depth":7,"bounds":{"left":0.20827793,"top":0.0830008,"width":0.49850398,"height":0.04988029},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Table: transactions","depth":8,"bounds":{"left":0.21542554,"top":0.096568234,"width":0.068317816,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Select data","depth":8,"bounds":{"left":0.21426196,"top":0.14884278,"width":0.026928192,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Select data","depth":9,"bounds":{"left":0.21426196,"top":0.14884278,"width":0.026928192,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Show structure","depth":8,"bounds":{"left":0.24950133,"top":0.14884278,"width":0.041223403,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Show structure","depth":9,"bounds":{"left":0.24950133,"top":0.14884278,"width":0.041223403,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Alter table","depth":8,"bounds":{"left":0.2990359,"top":0.14884278,"width":0.024933511,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Alter table","depth":9,"bounds":{"left":0.2990359,"top":0.14884278,"width":0.024933511,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"New item","depth":8,"bounds":{"left":0.3324468,"top":0.14884278,"width":0.022606382,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New item","depth":9,"bounds":{"left":0.3324468,"top":0.14884278,"width":0.022606382,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Column","depth":11,"bounds":{"left":0.22057846,"top":0.1763767,"width":0.018284574,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Type","depth":11,"bounds":{"left":0.24650931,"top":0.1763767,"width":0.009973404,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Comment","depth":11,"bounds":{"left":0.3801529,"top":0.1763767,"width":0.020944148,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":10,"bounds":{"left":0.2159242,"top":0.19393456,"width":0.004488032,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"integer","depth":11,"bounds":{"left":0.24650931,"top":0.19393456,"width":0.015292553,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auto Increment","depth":10,"bounds":{"left":0.2632979,"top":0.19393456,"width":0.033909574,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"bounds":{"left":0.29870346,"top":0.19393456,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"nextval('transactions_id_seq')","depth":11,"bounds":{"left":0.30069813,"top":0.19393456,"width":0.07446808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"bounds":{"left":0.37516624,"top":0.19393456,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"date","depth":10,"bounds":{"left":0.2159242,"top":0.21189146,"width":0.010638298,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"bounds":{"left":0.24650931,"top":0.21189146,"width":0.029753989,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.2777593,"top":0.21189146,"width":0.011136968,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"amount","depth":10,"bounds":{"left":0.2159242,"top":0.22984837,"width":0.018450798,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"double precision","depth":11,"bounds":{"left":0.24650931,"top":0.22984837,"width":0.03523936,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.28324467,"top":0.22984837,"width":0.011136968,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"currency","depth":10,"bounds":{"left":0.2159242,"top":0.24780527,"width":0.021110373,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.24780527,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.24780527,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":10,"bounds":{"left":0.2159242,"top":0.26576218,"width":0.021276595,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.26576218,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.26576218,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"owner","depth":10,"bounds":{"left":0.2159242,"top":0.28332004,"width":0.01512633,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.28332004,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.28332004,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"location","depth":10,"bounds":{"left":0.2159242,"top":0.30127692,"width":0.019281914,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.30127692,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.30127692,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"notes","depth":10,"bounds":{"left":0.2159242,"top":0.31923383,"width":0.013297873,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.31923383,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.31923383,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"created_at","depth":10,"bounds":{"left":0.2159242,"top":0.33719075,"width":0.026097074,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"bounds":{"left":0.24650931,"top":0.33719075,"width":0.029753989,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"bounds":{"left":0.2777593,"top":0.33719075,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CURRENT_TIMESTAMP","depth":11,"bounds":{"left":0.27975398,"top":0.33719075,"width":0.054853722,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"bounds":{"left":0.33460772,"top":0.33719075,"width":0.0018284575,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"updated_at","depth":10,"bounds":{"left":0.2159242,"top":0.35514766,"width":0.027593086,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"bounds":{"left":0.24650931,"top":0.35514766,"width":0.029753989,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"bounds":{"left":0.2777593,"top":0.35514766,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CURRENT_TIMESTAMP","depth":11,"bounds":{"left":0.27975398,"top":0.35514766,"width":0.054853722,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"bounds":{"left":0.33460772,"top":0.35514766,"width":0.0018284575,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Column","depth":10,"bounds":{"left":0.22057846,"top":0.1763767,"width":0.018284574,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":10,"bounds":{"left":0.2159242,"top":0.19393456,"width":0.004488032,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"date","depth":10,"bounds":{"left":0.2159242,"top":0.21189146,"width":0.010638298,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"amount","depth":10,"bounds":{"left":0.2159242,"top":0.22984837,"width":0.018450798,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"currency","depth":10,"bounds":{"left":0.2159242,"top":0.24780527,"width":0.021110373,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":10,"bounds":{"left":0.2159242,"top":0.26576218,"width":0.021276595,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"owner","depth":10,"bounds":{"left":0.2159242,"top":0.28332004,"width":0.01512633,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"location","depth":10,"bounds":{"left":0.2159242,"top":0.30127692,"width":0.019281914,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"notes","depth":10,"bounds":{"left":0.2159242,"top":0.31923383,"width":0.013297873,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"created_at","depth":10,"bounds":{"left":0.2159242,"top":0.33719075,"width":0.026097074,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"updated_at","depth":10,"bounds":{"left":0.2159242,"top":0.35514766,"width":0.027593086,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Type","depth":10,"bounds":{"left":0.24650931,"top":0.1763767,"width":0.009973404,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"integer","depth":11,"bounds":{"left":0.24650931,"top":0.19393456,"width":0.015292553,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auto Increment","depth":10,"bounds":{"left":0.2632979,"top":0.19393456,"width":0.033909574,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"bounds":{"left":0.29870346,"top":0.19393456,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"nextval('transactions_id_seq')","depth":11,"bounds":{"left":0.30069813,"top":0.19393456,"width":0.07446808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"bounds":{"left":0.37516624,"top":0.19393456,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"bounds":{"left":0.24650931,"top":0.21189146,"width":0.029753989,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.2777593,"top":0.21189146,"width":0.011136968,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"double precision","depth":11,"bounds":{"left":0.24650931,"top":0.22984837,"width":0.03523936,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.28324467,"top":0.22984837,"width":0.011136968,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.24780527,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.24780527,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.26576218,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.26576218,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.28332004,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.28332004,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.30127692,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.30127692,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.24650931,"top":0.31923383,"width":0.008477394,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NULL","depth":10,"bounds":{"left":0.25648272,"top":0.31923383,"width":0.011303191,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"bounds":{"left":0.24650931,"top":0.33719075,"width":0.029753989,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"bounds":{"left":0.2777593,"top":0.33719075,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CURRENT_TIMESTAMP","depth":11,"bounds":{"left":0.27975398,"top":0.33719075,"width":0.054853722,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"bounds":{"left":0.33460772,"top":0.33719075,"width":0.0018284575,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"timestamp(3)","depth":11,"bounds":{"left":0.24650931,"top":0.35514766,"width":0.029753989,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[","depth":11,"bounds":{"left":0.2777593,"top":0.35514766,"width":0.0019946808,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CURRENT_TIMESTAMP","depth":11,"bounds":{"left":0.27975398,"top":0.35514766,"width":0.054853722,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"]","depth":11,"bounds":{"left":0.33460772,"top":0.35514766,"width":0.0018284575,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Comment","depth":10,"bounds":{"left":0.3801529,"top":0.1763767,"width":0.020944148,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Indexes","depth":7,"bounds":{"left":0.21426196,"top":0.3858739,"width":0.49251994,"height":0.018754989},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Indexes","depth":8,"bounds":{"left":0.21426196,"top":0.3858739,"width":0.024601065,"height":0.018355945},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PRIMARY","depth":10,"bounds":{"left":0.2159242,"top":0.4177973,"width":0.022772606,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":10,"bounds":{"left":0.24168883,"top":0.4177973,"width":0.0038231383,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PRIMARY","depth":10,"bounds":{"left":0.2159242,"top":0.4177973,"width":0.022772606,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"id","depth":10,"bounds":{"left":0.24168883,"top":0.4177973,"width":0.0038231383,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Alter indexes","depth":8,"bounds":{"left":0.21426196,"top":0.4425379,"width":0.03125,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Alter indexes","depth":9,"bounds":{"left":0.21426196,"top":0.4425379,"width":0.03125,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Foreign keys","depth":7,"bounds":{"left":0.21426196,"top":0.47206703,"width":0.49251994,"height":0.018754989},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Foreign keys","depth":8,"bounds":{"left":0.21426196,"top":0.47206703,"width":0.03956117,"height":0.018355945},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add foreign key","depth":8,"bounds":{"left":0.21426196,"top":0.5,"width":0.037898935,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add foreign key","depth":9,"bounds":{"left":0.21426196,"top":0.5,"width":0.037898935,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Checks","depth":7,"bounds":{"left":0.21426196,"top":0.5291301,"width":0.49251994,"height":0.018754989},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Checks","depth":8,"bounds":{"left":0.21426196,"top":0.52952915,"width":0.022107713,"height":0.018355945},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Create check","depth":8,"bounds":{"left":0.21426196,"top":0.55706304,"width":0.03125,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create check","depth":9,"bounds":{"left":0.21426196,"top":0.55706304,"width":0.03125,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Triggers","depth":7,"bounds":{"left":0.21426196,"top":0.58619314,"width":0.49251994,"height":0.018754989},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Triggers","depth":8,"bounds":{"left":0.21426196,"top":0.5865922,"width":0.024933511,"height":0.018355945},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add trigger","depth":8,"bounds":{"left":0.21426196,"top":0.6141261,"width":0.026928192,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add trigger","depth":9,"bounds":{"left":0.21426196,"top":0.6141261,"width":0.026928192,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Adminer 5.3.0","depth":8,"bounds":{"left":0.113696806,"top":0.0830008,"width":0.0909242,"height":0.04988029},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"Adminer","depth":9,"bounds":{"left":0.11851729,"top":0.096568234,"width":0.038231384,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Adminer","depth":10,"bounds":{"left":0.12649602,"top":0.096568234,"width":0.03025266,"height":0.021548284},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5.3.0","depth":9,"bounds":{"left":0.15924202,"top":0.10335196,"width":0.011635638,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Language:","depth":11,"bounds":{"left":0.11851729,"top":0.056664005,"width":0.027094414,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DB:","depth":11,"bounds":{"left":0.11851729,"top":0.1424581,"width":0.010804521,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Schema:","depth":11,"bounds":{"left":0.11851729,"top":0.16001596,"width":0.022938829,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SQL command","depth":9,"bounds":{"left":0.11851729,"top":0.1963288,"width":0.034906916,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQL command","depth":10,"bounds":{"left":0.11851729,"top":0.1963288,"width":0.034906916,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Import","depth":9,"bounds":{"left":0.16173537,"top":0.1963288,"width":0.01662234,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Import","depth":10,"bounds":{"left":0.16173537,"top":0.1963288,"width":0.01662234,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Export","depth":9,"bounds":{"left":0.11851729,"top":0.21069433,"width":0.015625,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Export","depth":10,"bounds":{"left":0.11851729,"top":0.21069433,"width":0.015625,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Create table","depth":9,"bounds":{"left":0.14245346,"top":0.21069433,"width":0.029421542,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create table","depth":10,"bounds":{"left":0.14245346,"top":0.21069433,"width":0.029421542,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"bounds":{"left":0.11851729,"top":0.2442139,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"bounds":{"left":0.11851729,"top":0.2442139,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"_TagToTransaction","depth":10,"bounds":{"left":0.1341423,"top":0.2442139,"width":0.043882977,"height":0.014365523},"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"_TagToTransaction","depth":11,"bounds":{"left":0.1341423,"top":0.2442139,"width":0.043882977,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"bounds":{"left":0.11851729,"top":0.2585794,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"bounds":{"left":0.11851729,"top":0.2585794,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"_TagToTransactionImport","depth":10,"bounds":{"left":0.1341423,"top":0.2585794,"width":0.060339097,"height":0.014365523},"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"_TagToTransactionImport","depth":11,"bounds":{"left":0.1341423,"top":0.2585794,"width":0.060339097,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"bounds":{"left":0.11851729,"top":0.27294493,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"bounds":{"left":0.11851729,"top":0.27294493,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"_prisma_migrations","depth":10,"bounds":{"left":0.1341423,"top":0.27294493,"width":0.047706116,"height":0.014365523},"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"_prisma_migrations","depth":11,"bounds":{"left":0.1341423,"top":0.27294493,"width":0.047706116,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"bounds":{"left":0.11851729,"top":0.28731045,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"bounds":{"left":0.11851729,"top":0.28731045,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"tags","depth":10,"bounds":{"left":0.1341423,"top":0.28731045,"width":0.010139627,"height":0.014365523},"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"tags","depth":11,"bounds":{"left":0.1341423,"top":0.28731045,"width":0.010139627,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"bounds":{"left":0.11851729,"top":0.30167598,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"bounds":{"left":0.11851729,"top":0.30167598,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"transaction_imports","depth":10,"bounds":{"left":0.1341423,"top":0.30167598,"width":0.048038565,"height":0.014365523},"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"transaction_imports","depth":11,"bounds":{"left":0.1341423,"top":0.30167598,"width":0.048038565,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"select","depth":10,"bounds":{"left":0.11851729,"top":0.3160415,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"Select data","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"select","depth":11,"bounds":{"left":0.11851729,"top":0.3160415,"width":0.013796543,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"transactions","depth":10,"bounds":{"left":0.1341423,"top":0.3160415,"width":0.03324468,"height":0.014365523},"on_screen":true,"help_text":"Show structure","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"transactions","depth":11,"bounds":{"left":0.1341423,"top":0.3160415,"width":0.03324468,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"finance","depth":9,"bounds":{"left":0.66954786,"top":0.057861134,"width":0.018949468,"height":0.014365523},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Logout","depth":9,"bounds":{"left":0.68849736,"top":0.057462092,"width":0.018284574,"height":0.017557861},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"http://192.168.0.242:8092/?pgsql=db&username=finance&db=finance_hub&ns=public&table=transaction_imports","depth":5,"bounds":{"left":0.11502659,"top":0.9876297,"width":0.19913563,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-8906692636764843871
|
3035567681474669887
|
click
|
accessibility
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Inbox - [EMAIL] - Gmail
Inbox - [EMAIL] - Gmail
(25) Quora
(25) Quora
Location Logger
Location Logger
Finance Hub
Finance Hub
Finance Hub
Finance Hub
Table: transactions - db - Adminer
Table: transactions - db - Adminer
Close tab
Електронно банкиране ДСК Директ от Банка ДСК
Електронно банкиране ДСК Директ от Банка ДСК
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
VIVACOM
VIVACOM
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
VIVACOM
VIVACOM
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Claude Code | Claude Platform
Claude Code | Claude Platform
Claude
Claude
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
PostgreSQL
PostgreSQL
»
db
db
»
finance_hub
finance_hub
»
public
public
» Table: transactions
Table: transactions
Table: transactions
Select data
Select data
Show structure
Show structure
Alter table
Alter table
New item
New item
Column
Type
Comment
id
integer
Auto Increment
[
nextval('transactions_id_seq')
]
date
timestamp(3)
NULL
amount
double precision
NULL
currency
text
NULL
recipient
text
NULL
owner
text
NULL
location
text
NULL
notes
text
NULL
created_at
timestamp(3)
[
CURRENT_TIMESTAMP
]
updated_at
timestamp(3)
[
CURRENT_TIMESTAMP
]
Column
id
date
amount
currency
recipient
owner
location
notes
created_at
updated_at
Type
integer
Auto Increment
[
nextval('transactions_id_seq')
]
timestamp(3)
NULL
double precision
NULL
text
NULL
text
NULL
text
NULL
text
NULL
text
NULL
timestamp(3)
[
CURRENT_TIMESTAMP
]
timestamp(3)
[
CURRENT_TIMESTAMP
]
Comment
Indexes
Indexes
PRIMARY
id
PRIMARY
id
Alter indexes
Alter indexes
Foreign keys
Foreign keys
Add foreign key
Add foreign key
Checks
Checks
Create check
Create check
Triggers
Triggers
Add trigger
Add trigger
Adminer 5.3.0
Adminer
Adminer
5.3.0
Language:
DB:
Schema:
SQL command
SQL command
Import
Import
Export
Export
Create table
Create table
select
select
_TagToTransaction
_TagToTransaction
select
select
_TagToTransactionImport
_TagToTransactionImport
select
select
_prisma_migrations
_prisma_migrations
select
select
tags
tags
select
select
transaction_imports
transaction_imports
select
select
transactions
transactions
finance
Logout
http://[IP_ADDRESS]:8092/?pgsql=db&username=finance&db=finance_hub&ns=public&table=transaction_imports...
|
13031
|
NULL
|
NULL
|
NULL
|
|
2489
|
106
|
25
|
2026-05-07T11:22:31.849829+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778152951849_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Open sidebar
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in
hubspot:rl:meta:{portalId}
. This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.
For search specifically, since headers don't come back, the local counter
is
the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but
not
on 429 (you really did make that request).
How to work with multiple jobs
How to work with multiple jobs
The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:
Concurrency cap
via
Queue::throttle()
or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.
Per-tenant fairness
— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.
Priority lanes
— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:
hubspot-priority
and
hubspot-bulk
, with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).
Backoff on 429
— respect the
Retry-After
header. If absent, exponential with jitter (e.g.,
min(2^attempt * 250ms, 30s) ± 20%
). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.
How to work with paginated requests
How to work with paginated requests
The trap is treating "fetch all" as one logical operation. Each page is its own API call and competes for tokens with everything else.
Two practical rules:
Don't hold the worker idle while paginating.
If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the
processing
of page N as a separate job, and queue a "fetch page N+1" job with a delay equal to the wait time. Each page becomes its own atomic unit.
Always batch where HubSpot offers it.
POST /crm/v3/objects/{type}/batch/{read|update|create}
accepts up to 100 IDs per call. For your "patching IDs" flow this is the difference between 600 calls and 6.
Walkthrough: 600 opportunities
Walkthrough: 600 opportunities
Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.
Sane flow:
Identify deltas
— if you can avoid Search at all, do. Use
lastmodifieddate
filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.
Batch read
current state —
POST /crm/v3/objects/deals/batch/read
with 100 IDs per call → 6 calls. Burst weight 6.
Compute diff
in your code (no API).
Batch update
—
POST /crm/v3/objects/deals/batch/update
with 100 per call → 6 calls.
Associations
—
POST /crm/v4/associations/{from}/{to}/batch/create
if needed → another few batch calls.
Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.
Recognition: pre-check denies if
<weight>
slots aren't free; on 429 the response surfaces
policyName
(DAILY/SECONDLY) — log it, push the job back with
Retry-After
delay, increment a metric so you can alarm on sustained 429 rate.
Pitfalls & bottlenecks worth pre-empting
Pitfalls & bottlenecks worth pre-empting
Daily quota is shared across the whole portal
, including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.
Search is uniquely fragile
: separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by
hs_object_id
ranges or by
lastmodifieddate
windows.
Retry storms after HubSpot incidents
— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).
Webhook → sync → webhook loops
— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property
jy_last_sync_id
) and ignore webhooks where it just changed.
Token refresh
— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =
expires_in - 60s
, single-flight the refresh (only one worker refreshes; others wait on a lock).
Search latency for fresh writes
— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.
Clock skew
— Redis is authoritative. Don't use
time()
from each worker for window scoring; use
redis.call('TIME')
inside the Lua script.
Dead workers with held tokens
— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.
The 5% error rule
counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.
Phantom contention with Laravel
RateLimited
middleware
: it's per-job-class by default, not per-portal. If your
SyncDealJob
and
UpdateActivityJob
are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single
HubspotRateLimiter
service that everything routes through.
If you want, I can sketch the Lua script for the sliding-window acquire and the Laravel service wrapper around it.
Copy
Give positive feedback
Give negative feedback
Retry
You said: What will happen if there are two different types of rate limits?
You said: What will happen if there are two different types of rate limits?
What will happen if there are two different types of rate limits?
Search requests that have 5 requests per second
The other rate limit that has, let's say, 100 requests per 10 seconds How do these work together? Is there some conflict? Can I reach the limit with one request and block either as well, or are these separated? Can I still continue making search requests once I hit the normal requests limits?
13:40
Retry
Edit
Copy
Claude responded: They're independent buckets — with one shared bucket on top.
Claude responded: They're independent buckets — with one shared bucket on top.
Analyzed independent rate limit buckets and their practical interactions
Analyzed independent rate limit buckets and their practical interactions
They're independent buckets — with one shared bucket on top.
The three buckets and what each call hits
The three buckets and what each call hits
Copy to clipboard
┌──────────────────┐
Search request ──→ │ Search: 5/sec │ ──┐
└──────────────────┘ │
├──→ ┌─────────────────┐
┌──────────────────┐ │ │ Daily: 250k–1M │
Non-search call ──→ │ Burst: 190/10s │ ──┘ │ (per portal) │
└──────────────────┘ └─────────────────┘
A search call is only metered against the search bucket and the daily bucket. A non-search call (batch read/update, single PATCH, associations, etc.) is only metered against the burst bucket and the daily bucket. The two short-window buckets do not share counters.
What this means practically
What this means practically
Hitting search secondly limit (5/s):
Only further
/search
calls 429. Your batch reads, batch updates, single PATCHes, association calls — all keep working at full burst speed. This is actually useful: if your sync is bottlenecked on Search, you can pivot to using
lastmodifieddate
filters via batch endpoints, or use the IDs you already have to do batch reads, while Search cools off.
Hitting burst limit (190/10s):
Only non-search calls 429. You can still issue Search calls at 5/s. Useful for: continuing to identify deltas for queueing while in-flight updates drain, or refreshing a small set of records via Search even though your write pipeline is paused.
Hitting daily limit:
Everything stops. Search and non-search alike. Doesn't unblock until midnight in your portal's configured timezone — and remember this is shared across every private app in the portal, plus n8n flows, marketing tools, anything else hitting the API with that account.
So can one type of request block the other?
So can one type of request block the other?
Search → Burst:
No.
Saturating Search at 50 calls per 10s leaves your full 190/10s burst untouched.
Burst → Search:
No.
Saturating burst leaves Search's 5/s untouched.
Either → Daily:
Yes
, but only by contributing to the same daily counter. It's volume that kills you here, not the type.
In theory you can sustain ~50 search + ~190 other =
~240 calls per 10 seconds
indefinitely (until daily runs out), because the two buckets bill independently.
Implementation consequence
Implementation consequence
Your rate limiter needs to know which bucket a request hits before it acquires. Cleanest pattern:
Copy to clipboard
php
$limiter
->
acquire
(
'search'
,
$weight
=
1
)
;
// for search endpoints only
$limiter
->
acquire
(
'burst'
,
$weight
=
1
)
;
// for everything else
$limiter
->
acquire
(
'daily'
,
$weight
=
1
)
;
// every call, both kinds
In Redis: three separate keys, three separate Lua acquire scripts (or one script with a bucket parameter). A search call calls
acquire('search')
+
acquire('daily')
. A batch update calls
acquire('burst')
+
acquire('daily')
.
One subtle pitfall: when you backoff on a 429,
inspect
policyName
in the response body
to know which bucket actually fired. If you see
SECONDLY
from a search endpoint, you only need to delay search-bound jobs — burst-bound jobs in the queue can keep running. If you blanket-pause everything on any 429, you waste capacity on the bucket that's still healthy. Two queues (or two throttle keys) make this clean; one global pause is the lazy version.
The other subtlety: the burst response gives you
X-HubSpot-RateLimit-Remaining
to clamp local state against, but search doesn't return those headers. So your search counter is unverified — be slightly more conservative there (e.g., budget 4/s instead of 5) to absorb the unaccounted-for calls.
Copy
Give positive feedback
Give negative feedback
Retry
You said: Ok, so what will happen each time I make a request?
You said: Ok, so what will happen each time I make a request?
Ok, so what will happen each time I make a request? Let's say I have 100,000 requests to make that are instant. Do I need to increment each request based on the type? For example, if there is a search request, I need to increment both the search bucket and also the daily bucket. If it's a burst, then I can go to the daily and burst buckets. If I need to check if it can make a call, do I need to see both the daily and search, or burst, respectively? Isn't that too heavy for the infrastructure, adding additional load to each request?
13:44
Retry
Edit
Copy
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Analyzed per-request overhead concerns and quantified Redis performance impact
Analyzed per-request overhead concerns and quantified Redis performance impact
Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out. But the overhead is much smaller than it sounds, and importantly, it's nowhere near your bottleneck.
What "two buckets per request" actually costs
What "two buckets per request" actually costs
Each request makes
one round trip to Redis
— not two — because you fold both bucket checks into a single Lua script that runs atomically server-side:
Copy to clipboard
lua
-- KEYS[1] = burst bucket key (or search)
-- KEYS[2] = daily bucket key
-- ARGV[1] = now (ms) ARGV[2] = window_ms
-- ARGV[3] = bucket_max ARGV[4] = daily_max
-- ARGV[5] = request_id ARGV[6] = daily_ttl
-- Trim sliding window
redis
.
call
(
'ZREMRANGEBYSCORE'
,
KEYS
[
1
]
,
0
,
ARGV
[
1
]
-
ARGV
[
2
]
)
local
burst_used
=
redis
.
call
(
'ZCARD'
,
KEYS
[
1
]
)
local
daily_used
=
tonumber
(
redis
.
call
(
'GET'
,
KEYS
[
2
]
)
or
'0'
)
if
burst_used
>=
tonumber
(
ARGV
[
3
]
)
then
-- Tell caller how long to sleep until oldest entry expires
local
oldest
=
redis
.
call
(
'ZRANGE'
,
KEYS
[
1
]
,
0
,
0
,
'WITHSCORES'
)
return
{
0
,
'BURST'
,
(
oldest
[
2
]
+
ARGV
[
2
]
)
-
ARGV
[
1
]
}
end
if
daily_used
>=
tonumber
(
ARGV
[
4
]
)
then
return
{
0
,
'DAILY'
,
-
1
}
end
redis
.
call
(
'ZADD'
,
KEYS
[
1
]
,
ARGV
[
1
]
,
ARGV
[
5
]
)
redis
.
call
(
'PEXPIRE'
,
KEYS
[
1
]
,
ARGV
[
2
]
+
1000
)
local
d
=
redis
.
call
(
'INCR'
,
KEYS
[
2
]
)
if
d
==
1
then
redis
.
call
(
'EXPIRE'
,
KEYS
[
2
]
,
ARGV
[
6
]
)
end
return
{
1
,
'OK'
,
tonumber
(...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.029587766,"top":0.03830806,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.030585106,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.10538564,"top":0.06703911,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.1349734,"top":0.06703911,"width":0.0063164895,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.10538564,"top":0.079010375,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"bounds":{"left":0.029920213,"top":0.02793296,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.004986702,"top":0.059856344,"width":0.025930852,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.03158245,"top":0.059856344,"width":0.03125,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.0631649,"top":0.059856344,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.0043218085,"top":0.08938547,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.018949468,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.003656915,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.01761968,"top":0.0933759,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.08178192,"top":0.0933759,"width":0.006981383,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.0043218085,"top":0.110135674,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.0043218085,"top":0.1300878,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.0043218085,"top":0.15003991,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.0063164895,"top":0.18914606,"width":0.08377659,"height":0.013567438},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.0043218085,"top":0.20590582,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.08344415,"top":0.20909816,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.0043218085,"top":0.22745411,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.08344415,"top":0.22984837,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.0063164895,"top":0.25698325,"width":0.06349734,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.07114362,"top":0.25698325,"width":0.018949468,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.0043218085,"top":0.27294493,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.08344415,"top":0.27613726,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.0043218085,"top":0.29449323,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.08344415,"top":0.29768556,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.0043218085,"top":0.31524342,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.08344415,"top":0.31843576,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.0043218085,"top":0.3367917,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.08344415,"top":0.33998403,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.0043218085,"top":0.3575419,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.08344415,"top":0.36073422,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.0043218085,"top":0.3790902,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.08344415,"top":0.38228253,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.0043218085,"top":0.39984038,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.08344415,"top":0.40303272,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.0043218085,"top":0.42138866,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.08344415,"top":0.4237829,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.0043218085,"top":0.44213888,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.08344415,"top":0.44533122,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.0043218085,"top":0.46288908,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.08344415,"top":0.4660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.0043218085,"top":0.48443735,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.08344415,"top":0.48762968,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.0043218085,"top":0.5051876,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.08344415,"top":0.5083799,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"bounds":{"left":0.0043218085,"top":0.52673584,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"bounds":{"left":0.08344415,"top":0.52992815,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"bounds":{"left":0.0043218085,"top":0.547486,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"bounds":{"left":0.08344415,"top":0.5506784,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"bounds":{"left":0.0043218085,"top":0.56903434,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"bounds":{"left":0.08344415,"top":0.57222664,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"bounds":{"left":0.0043218085,"top":0.5897845,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"bounds":{"left":0.08344415,"top":0.59297687,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"bounds":{"left":0.0043218085,"top":0.6113328,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"bounds":{"left":0.08344415,"top":0.61452514,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"bounds":{"left":0.0043218085,"top":0.632083,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"bounds":{"left":0.08344415,"top":0.63527536,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"bounds":{"left":0.0043218085,"top":0.65363127,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"bounds":{"left":0.08344415,"top":0.65682364,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"bounds":{"left":0.0043218085,"top":0.6743815,"width":0.08643617,"height":0.011173184},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"bounds":{"left":0.08344415,"top":0.6775738,"width":0.005984043,"height":0.007980846},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"bounds":{"left":0.0043218085,"top":0.6943336,"width":0.037898935,"height":0.01915403},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"bounds":{"left":0.08277926,"top":0.6943336,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":19,"bounds":{"left":0.043218084,"top":0.02793296,"width":0.09773936,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":21,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.09507979,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":41,"bounds":{"left":0.048204787,"top":0.031923383,"width":0.09142287,"height":0.014365523}}],"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.14128989,"top":0.02793296,"width":0.0066489363,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open sidebar","depth":21,"bounds":{"left":0.47340426,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":21,"bounds":{"left":0.48537233,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"You said: So just a solution for rate limit implementation.","depth":20,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: So just a solution for rate limit implementation.","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating activity (engagement, meeting, call, note)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"searching for a company or contact","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating deal","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"patching IDs\nReview what the limit is at the moment in the HubSpot documentation and then think of the solution:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"where to store it","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to read it","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with multiple jobs","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":25,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). Example of such a error Client error:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST https://api.hubapi.com/crm/v3/objects/contact/search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"resulted in a","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"429 Too Many Requests","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:42","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Current HubSpot limits (Apr 2026)","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Current HubSpot limits (Apr 2026)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The burst window is per app; the daily quota is per portal.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". That's the limit the 429 in your example is hitting (","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"secondly limit","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/crm/v3/objects/contact/search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). It's a separate bucket from the 190/10s, and critically, search responses do not include the","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-*","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"headers","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— you can't read remaining capacity off the response, you have to track it yourself.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Non-search endpoints return","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", and","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The daily headers are not returned for OAuth-authenticated calls","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— that's enforced for marketplace listing, but it's also a sane operational target.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So you have","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"four buckets","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Where to store it","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Where to store it","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EXPIRE","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"plus Lua scripting for true CAS semantics.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Key layout I'd use:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:search:{portalId} ZSET (sliding 1s)","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:meta:{portalId} HASH (last seen headers)","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The two short-window buckets are sliding-window-log (Redis ZSET, score =","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"microtime","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The daily bucket is a plain","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with TTL set to seconds-until-midnight in","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubSpot's account timezone","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to read it","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to read it","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two-phase: optimistic pre-check, authoritative post-update.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Before a call, run the Lua acquire script. If it returns \"denied,\" sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"After the call, parse","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and store them in","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:meta:{portalId}","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For search specifically, since headers don't come back, the local counter","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"is","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"not","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on 429 (you really did make that request).","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to work with multiple jobs","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to work with multiple jobs","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Concurrency cap","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"via","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Queue::throttle()","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Per-tenant fairness","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Priority lanes","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot-priority","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot-bulk","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Backoff on 429","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— respect the","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"header. If absent, exponential with jitter (e.g.,","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"min(2^attempt * 250ms, 30s) ± 20%","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to work with paginated requests","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to work with paginated requests","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The trap is treating \"fetch all\" as one logical operation. Each page is its own API call and competes for tokens with everything else.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two practical rules:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Don't hold the worker idle while paginating.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"processing","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"of page N as a separate job, and queue a \"fetch page N+1\" job with a delay equal to the wait time. Each page becomes its own atomic unit.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Always batch where HubSpot offers it.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/{type}/batch/{read|update|create}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"accepts up to 100 IDs per call. For your \"patching IDs\" flow this is the difference between 600 calls and 6.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Walkthrough: 600 opportunities","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Walkthrough: 600 opportunities","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sane flow:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Identify deltas","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— if you can avoid Search at all, do. Use","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Batch read","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"current state —","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/deals/batch/read","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with 100 IDs per call → 6 calls. Burst weight 6.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Compute diff","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"in your code (no API).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Batch update","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/deals/batch/update","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with 100 per call → 6 calls.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Associations","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v4/associations/{from}/{to}/batch/create","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if needed → another few batch calls.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Recognition: pre-check denies if","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"<weight>","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"slots aren't free; on 429 the response surfaces","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"policyName","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(DAILY/SECONDLY) — log it, push the job back with","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"delay, increment a metric so you can alarm on sustained 429 rate.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Pitfalls & bottlenecks worth pre-empting","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Pitfalls & bottlenecks worth pre-empting","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily quota is shared across the whole portal","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search is uniquely fragile","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hs_object_id","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ranges or by","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"windows.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry storms after HubSpot incidents","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Webhook → sync → webhook loops","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"jy_last_sync_id","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":") and ignore webhooks where it just changed.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Token refresh","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"expires_in - 60s","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", single-flight the refresh (only one worker refreshes; others wait on a lock).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search latency for fresh writes","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Clock skew","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— Redis is authoritative. Don't use","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"time()","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from each worker for window scoring; use","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis.call('TIME')","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"inside the Lua script.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dead workers with held tokens","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The 5% error rule","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Phantom contention with Laravel","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimited","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"middleware","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": it's per-job-class by default, not per-portal. If your","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncDealJob","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateActivityJob","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotRateLimiter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"service that everything routes through.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"If you want, I can sketch the Lua script for the sliding-window acquire and the Laravel service wrapper around it.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give positive feedback","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give negative feedback","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"You said: What will happen if there are two different types of rate limits?","depth":20,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: What will happen if there are two different types of rate limits?","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"What will happen if there are two different types of rate limits?","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search requests that have 5 requests per second","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The other rate limit that has, let's say, 100 requests per 10 seconds How do these work together? Is there some conflict? Can I reach the limit with one request and block either as well, or are these separated? Can I still continue making search requests once I hit the normal requests limits?","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"13:40","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: They're independent buckets — with one shared bucket on top.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: They're independent buckets — with one shared bucket on top.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Analyzed independent rate limit buckets and their practical interactions","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzed independent rate limit buckets and their practical interactions","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"They're independent buckets — with one shared bucket on top.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"The three buckets and what each call hits","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"The three buckets and what each call hits","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"┌──────────────────┐","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search request ──→ │ Search: 5/sec │ ──┐","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"└──────────────────┘ │","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"├──→ ┌─────────────────┐","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"┌──────────────────┐ │ │ Daily: 250k–1M │","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Non-search call ──→ │ Burst: 190/10s │ ──┘ │ (per portal) │","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"└──────────────────┘ └─────────────────┘","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"A search call is only metered against the search bucket and the daily bucket. A non-search call (batch read/update, single PATCH, associations, etc.) is only metered against the burst bucket and the daily bucket. The two short-window buckets do not share counters.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"What this means practically","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"What this means practically","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hitting search secondly limit (5/s):","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Only further","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"calls 429. Your batch reads, batch updates, single PATCHes, association calls — all keep working at full burst speed. This is actually useful: if your sync is bottlenecked on Search, you can pivot to using","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"filters via batch endpoints, or use the IDs you already have to do batch reads, while Search cools off.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hitting burst limit (190/10s):","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Only non-search calls 429. You can still issue Search calls at 5/s. Useful for: continuing to identify deltas for queueing while in-flight updates drain, or refreshing a small set of records via Search even though your write pipeline is paused.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hitting daily limit:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Everything stops. Search and non-search alike. Doesn't unblock until midnight in your portal's configured timezone — and remember this is shared across every private app in the portal, plus n8n flows, marketing tools, anything else hitting the API with that account.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"So can one type of request block the other?","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"So can one type of request block the other?","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search → Burst:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"No.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saturating Search at 50 calls per 10s leaves your full 190/10s burst untouched.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst → Search:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"No.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saturating burst leaves Search's 5/s untouched.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Either → Daily:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Yes","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", but only by contributing to the same daily counter. It's volume that kills you here, not the type.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"In theory you can sustain ~50 search + ~190 other =","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"~240 calls per 10 seconds","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"indefinitely (until daily runs out), because the two buckets bill independently.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Implementation consequence","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Implementation consequence","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Your rate limiter needs to know which bucket a request hits before it acquires. Cleanest pattern:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"php","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$limiter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'search'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$weight","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// for search endpoints only","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$limiter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'burst'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$weight","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// for everything else","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$limiter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'daily'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$weight","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// every call, both kinds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"In Redis: three separate keys, three separate Lua acquire scripts (or one script with a bucket parameter). A search call calls","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('search')","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('daily')","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". A batch update calls","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('burst')","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('daily')","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"One subtle pitfall: when you backoff on a 429,","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"inspect","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"policyName","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"in the response body","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to know which bucket actually fired. If you see","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SECONDLY","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from a search endpoint, you only need to delay search-bound jobs — burst-bound jobs in the queue can keep running. If you blanket-pause everything on any 429, you waste capacity on the bucket that's still healthy. Two queues (or two throttle keys) make this clean; one global pause is the lazy version.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The other subtlety: the burst response gives you","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to clamp local state against, but search doesn't return those headers. So your search counter is unverified — be slightly more conservative there (e.g., budget 4/s instead of 5) to absorb the unaccounted-for calls.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give positive feedback","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give negative feedback","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"You said: Ok, so what will happen each time I make a request?","depth":20,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: Ok, so what will happen each time I make a request?","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Ok, so what will happen each time I make a request? Let's say I have 100,000 requests to make that are instant. Do I need to increment each request based on the type? For example, if there is a search request, I need to increment both the search bucket and also the daily bucket. If it's a burst, then I can go to the daily and burst buckets. If I need to check if it can make a call, do I need to see both the daily and search, or burst, respectively? Isn't that too heavy for the infrastructure, adding additional load to each request?","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"13:44","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Analyzed per-request overhead concerns and quantified Redis performance impact","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzed per-request overhead concerns and quantified Redis performance impact","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out. But the overhead is much smaller than it sounds, and importantly, it's nowhere near your bottleneck.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"What \"two buckets per request\" actually costs","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"What \"two buckets per request\" actually costs","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Each request makes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"one round trip to Redis","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— not two — because you fold both bucket checks into a single Lua script that runs atomically server-side:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"lua","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- KEYS[1] = burst bucket key (or search)","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- KEYS[2] = daily bucket key","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- ARGV[1] = now (ms) ARGV[2] = window_ms","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- ARGV[3] = bucket_max ARGV[4] = daily_max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- ARGV[5] = request_id ARGV[6] = daily_ttl","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- Trim sliding window","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'ZREMRANGEBYSCORE'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"local","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"burst_used","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'ZCARD'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"local","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"daily_used","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"tonumber","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'GET'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"or","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'0'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"burst_used","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":">=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"tonumber","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"then","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- Tell caller how long to sleep until oldest entry expires","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"local","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"oldest","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'ZRANGE'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'WITHSCORES'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'BURST'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"oldest","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"end","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"daily_used","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":">=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"tonumber","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"then","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'DAILY'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"end","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'ZADD'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'PEXPIRE'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1000","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"local","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"d","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'INCR'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"d","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"==","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"then","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'EXPIRE'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"end","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'OK'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"tonumber","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"}]...
|
-8905506865488421941
|
-8642777718357894060
|
visual_change
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Open sidebar
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in
hubspot:rl:meta:{portalId}
. This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.
For search specifically, since headers don't come back, the local counter
is
the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but
not
on 429 (you really did make that request).
How to work with multiple jobs
How to work with multiple jobs
The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:
Concurrency cap
via
Queue::throttle()
or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.
Per-tenant fairness
— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.
Priority lanes
— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:
hubspot-priority
and
hubspot-bulk
, with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).
Backoff on 429
— respect the
Retry-After
header. If absent, exponential with jitter (e.g.,
min(2^attempt * 250ms, 30s) ± 20%
). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.
How to work with paginated requests
How to work with paginated requests
The trap is treating "fetch all" as one logical operation. Each page is its own API call and competes for tokens with everything else.
Two practical rules:
Don't hold the worker idle while paginating.
If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the
processing
of page N as a separate job, and queue a "fetch page N+1" job with a delay equal to the wait time. Each page becomes its own atomic unit.
Always batch where HubSpot offers it.
POST /crm/v3/objects/{type}/batch/{read|update|create}
accepts up to 100 IDs per call. For your "patching IDs" flow this is the difference between 600 calls and 6.
Walkthrough: 600 opportunities
Walkthrough: 600 opportunities
Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.
Sane flow:
Identify deltas
— if you can avoid Search at all, do. Use
lastmodifieddate
filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.
Batch read
current state —
POST /crm/v3/objects/deals/batch/read
with 100 IDs per call → 6 calls. Burst weight 6.
Compute diff
in your code (no API).
Batch update
—
POST /crm/v3/objects/deals/batch/update
with 100 per call → 6 calls.
Associations
—
POST /crm/v4/associations/{from}/{to}/batch/create
if needed → another few batch calls.
Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.
Recognition: pre-check denies if
<weight>
slots aren't free; on 429 the response surfaces
policyName
(DAILY/SECONDLY) — log it, push the job back with
Retry-After
delay, increment a metric so you can alarm on sustained 429 rate.
Pitfalls & bottlenecks worth pre-empting
Pitfalls & bottlenecks worth pre-empting
Daily quota is shared across the whole portal
, including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.
Search is uniquely fragile
: separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by
hs_object_id
ranges or by
lastmodifieddate
windows.
Retry storms after HubSpot incidents
— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).
Webhook → sync → webhook loops
— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property
jy_last_sync_id
) and ignore webhooks where it just changed.
Token refresh
— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =
expires_in - 60s
, single-flight the refresh (only one worker refreshes; others wait on a lock).
Search latency for fresh writes
— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.
Clock skew
— Redis is authoritative. Don't use
time()
from each worker for window scoring; use
redis.call('TIME')
inside the Lua script.
Dead workers with held tokens
— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.
The 5% error rule
counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.
Phantom contention with Laravel
RateLimited
middleware
: it's per-job-class by default, not per-portal. If your
SyncDealJob
and
UpdateActivityJob
are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single
HubspotRateLimiter
service that everything routes through.
If you want, I can sketch the Lua script for the sliding-window acquire and the Laravel service wrapper around it.
Copy
Give positive feedback
Give negative feedback
Retry
You said: What will happen if there are two different types of rate limits?
You said: What will happen if there are two different types of rate limits?
What will happen if there are two different types of rate limits?
Search requests that have 5 requests per second
The other rate limit that has, let's say, 100 requests per 10 seconds How do these work together? Is there some conflict? Can I reach the limit with one request and block either as well, or are these separated? Can I still continue making search requests once I hit the normal requests limits?
13:40
Retry
Edit
Copy
Claude responded: They're independent buckets — with one shared bucket on top.
Claude responded: They're independent buckets — with one shared bucket on top.
Analyzed independent rate limit buckets and their practical interactions
Analyzed independent rate limit buckets and their practical interactions
They're independent buckets — with one shared bucket on top.
The three buckets and what each call hits
The three buckets and what each call hits
Copy to clipboard
┌──────────────────┐
Search request ──→ │ Search: 5/sec │ ──┐
└──────────────────┘ │
├──→ ┌─────────────────┐
┌──────────────────┐ │ │ Daily: 250k–1M │
Non-search call ──→ │ Burst: 190/10s │ ──┘ │ (per portal) │
└──────────────────┘ └─────────────────┘
A search call is only metered against the search bucket and the daily bucket. A non-search call (batch read/update, single PATCH, associations, etc.) is only metered against the burst bucket and the daily bucket. The two short-window buckets do not share counters.
What this means practically
What this means practically
Hitting search secondly limit (5/s):
Only further
/search
calls 429. Your batch reads, batch updates, single PATCHes, association calls — all keep working at full burst speed. This is actually useful: if your sync is bottlenecked on Search, you can pivot to using
lastmodifieddate
filters via batch endpoints, or use the IDs you already have to do batch reads, while Search cools off.
Hitting burst limit (190/10s):
Only non-search calls 429. You can still issue Search calls at 5/s. Useful for: continuing to identify deltas for queueing while in-flight updates drain, or refreshing a small set of records via Search even though your write pipeline is paused.
Hitting daily limit:
Everything stops. Search and non-search alike. Doesn't unblock until midnight in your portal's configured timezone — and remember this is shared across every private app in the portal, plus n8n flows, marketing tools, anything else hitting the API with that account.
So can one type of request block the other?
So can one type of request block the other?
Search → Burst:
No.
Saturating Search at 50 calls per 10s leaves your full 190/10s burst untouched.
Burst → Search:
No.
Saturating burst leaves Search's 5/s untouched.
Either → Daily:
Yes
, but only by contributing to the same daily counter. It's volume that kills you here, not the type.
In theory you can sustain ~50 search + ~190 other =
~240 calls per 10 seconds
indefinitely (until daily runs out), because the two buckets bill independently.
Implementation consequence
Implementation consequence
Your rate limiter needs to know which bucket a request hits before it acquires. Cleanest pattern:
Copy to clipboard
php
$limiter
->
acquire
(
'search'
,
$weight
=
1
)
;
// for search endpoints only
$limiter
->
acquire
(
'burst'
,
$weight
=
1
)
;
// for everything else
$limiter
->
acquire
(
'daily'
,
$weight
=
1
)
;
// every call, both kinds
In Redis: three separate keys, three separate Lua acquire scripts (or one script with a bucket parameter). A search call calls
acquire('search')
+
acquire('daily')
. A batch update calls
acquire('burst')
+
acquire('daily')
.
One subtle pitfall: when you backoff on a 429,
inspect
policyName
in the response body
to know which bucket actually fired. If you see
SECONDLY
from a search endpoint, you only need to delay search-bound jobs — burst-bound jobs in the queue can keep running. If you blanket-pause everything on any 429, you waste capacity on the bucket that's still healthy. Two queues (or two throttle keys) make this clean; one global pause is the lazy version.
The other subtlety: the burst response gives you
X-HubSpot-RateLimit-Remaining
to clamp local state against, but search doesn't return those headers. So your search counter is unverified — be slightly more conservative there (e.g., budget 4/s instead of 5) to absorb the unaccounted-for calls.
Copy
Give positive feedback
Give negative feedback
Retry
You said: Ok, so what will happen each time I make a request?
You said: Ok, so what will happen each time I make a request?
Ok, so what will happen each time I make a request? Let's say I have 100,000 requests to make that are instant. Do I need to increment each request based on the type? For example, if there is a search request, I need to increment both the search bucket and also the daily bucket. If it's a burst, then I can go to the daily and burst buckets. If I need to check if it can make a call, do I need to see both the daily and search, or burst, respectively? Isn't that too heavy for the infrastructure, adding additional load to each request?
13:44
Retry
Edit
Copy
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Analyzed per-request overhead concerns and quantified Redis performance impact
Analyzed per-request overhead concerns and quantified Redis performance impact
Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out. But the overhead is much smaller than it sounds, and importantly, it's nowhere near your bottleneck.
What "two buckets per request" actually costs
What "two buckets per request" actually costs
Each request makes
one round trip to Redis
— not two — because you fold both bucket checks into a single Lua script that runs atomically server-side:
Copy to clipboard
lua
-- KEYS[1] = burst bucket key (or search)
-- KEYS[2] = daily bucket key
-- ARGV[1] = now (ms) ARGV[2] = window_ms
-- ARGV[3] = bucket_max ARGV[4] = daily_max
-- ARGV[5] = request_id ARGV[6] = daily_ttl
-- Trim sliding window
redis
.
call
(
'ZREMRANGEBYSCORE'
,
KEYS
[
1
]
,
0
,
ARGV
[
1
]
-
ARGV
[
2
]
)
local
burst_used
=
redis
.
call
(
'ZCARD'
,
KEYS
[
1
]
)
local
daily_used
=
tonumber
(
redis
.
call
(
'GET'
,
KEYS
[
2
]
)
or
'0'
)
if
burst_used
>=
tonumber
(
ARGV
[
3
]
)
then
-- Tell caller how long to sleep until oldest entry expires
local
oldest
=
redis
.
call
(
'ZRANGE'
,
KEYS
[
1
]
,
0
,
0
,
'WITHSCORES'
)
return
{
0
,
'BURST'
,
(
oldest
[
2
]
+
ARGV
[
2
]
)
-
ARGV
[
1
]
}
end
if
daily_used
>=
tonumber
(
ARGV
[
4
]
)
then
return
{
0
,
'DAILY'
,
-
1
}
end
redis
.
call
(
'ZADD'
,
KEYS
[
1
]
,
ARGV
[
1
]
,
ARGV
[
5
]
)
redis
.
call
(
'PEXPIRE'
,
KEYS
[
1
]
,
ARGV
[
2
]
+
1000
)
local
d
=
redis
.
call
(
'INCR'
,
KEYS
[
2
]
)
if
d
==
1
then
redis
.
call
(
'EXPIRE'
,
KEYS
[
2
]
,
ARGV
[
6
]
)
end
return
{
1
,
'OK'
,
tonumber
(...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
18173
|
788
|
17
|
2026-05-11T11:01:34.508847+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778497294508_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKERO ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKERO ₴1DEV (docker)$2APP (-zsh)883viewsjiminny-worker-processing-2:jiminny-worker-processing-2_00: stoppedjiminny-worker-processing-3:jiminny-worker-processing-3_00: stoppedjiminny-worker-processing-4: jiminny-worker-processing-4_00: stoppedjiminny-worker-processing-5:jiminny-worker-processing-5_00:jiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: stoppedworker-analytics:worker-analytics_00: stoppedworker-conferences:worker-conferences_00: stoppedworker-crm-update:worker-crm-update_00: stoppedworker-download:worker-download_00: stoppedworker-emails:worker-emails_00:worker-nudges:worker-nudges_00: stoppedworker:worker_00: stoppedworker-audio:worker-audio_00: stoppedworker-calendar:worker-calendar_00: stoppedworker-crm-sync:worker-crm-sync_00: stoppedartisan-schedule:artisan-schedule_00: stoppedworker-es-update:worker-es-update_00: stoppedjiminny-worker-processing-1:jiminny-worker-processing-1_00: stoppedartisan-schedule:artisan-schedule_00: startedjiminny-worker-processing-1:jiminny-worker-processing-1_00: startedjiminny-worker-processing-2:jiminny-worker-processing-2_00: startedjiminny-worker-processing-3:jiminny-worker-processing-3_00: startedjiminny-worker-processing-4:jiminny-worker-processing-4_00: startedjiminny-worker-processing-5:jiminny-worker-processing-5_00:jiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: startedworker:worker_00: startedworker-analytics:worker-analytics_00: startedworker-audio:worker-audio_00: startedworker-calendar:worker-calendar_00: startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00: startedworker-emails:worker-emails_00: startedworker-es-update:worker-es-update_00: startedworker-nudges:worker-nudges_00: startedroot@docker_lamp_1:/home/jiminny#php artisan jiminny:debugDispatching 100 MatchActivityCrmData jobs (portal=2)Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugDispatching 100 MatchActivityCrmData jobs (portal=2)Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.root@docker_lamp_1:/home/jiminny# ]DEV (d)-zshHomeDMsActivityLater...alo)Preparation for Refi... in 59 m100% <78• Mon 11 May 14:01:34→Describe what you are looking forJiminny ...# contusion-clinic# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...ToastHomeMessages^ Direct messages®. Aneliya Angelova®. Galya Dimitrova I& Petko Kashinski&. Stefka StoyanovaVasil Vasilev&. Nikolay IvanovAneliya Angelova, ...Stoyan Tanev®. VesE Lukas Kovalik y... 0AboutFriday, May 8th~iminnyAdded by Toast for GitHubToday ~Toast APP10:00 AMReviewapp#12059 Jy 20820 es reindexstream model hydration2 days old • 12 files changed •@Vasil VasilevAdded by Toast for GitHubResolve Conflictsapp#11443 Test hublets latency5 months old • 20 files changed#11327 JY-19501 webhookbased opportunity syncShow moreAdded by Toast for GitHubNeeds Loveapp#12024 JY-20773 fix user pilottracking ofr automated reportgenerated12 days old • 1 file changedE:: AppsMessage ToastJira CloudToast+AaConala CalaNew...
|
NULL
|
-8905419098328085519
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKERO ₴1DEV SlackFileEditViewGoHistoryWindowHelpDOCKERO ₴1DEV (docker)$2APP (-zsh)883viewsjiminny-worker-processing-2:jiminny-worker-processing-2_00: stoppedjiminny-worker-processing-3:jiminny-worker-processing-3_00: stoppedjiminny-worker-processing-4: jiminny-worker-processing-4_00: stoppedjiminny-worker-processing-5:jiminny-worker-processing-5_00:jiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: stoppedworker-analytics:worker-analytics_00: stoppedworker-conferences:worker-conferences_00: stoppedworker-crm-update:worker-crm-update_00: stoppedworker-download:worker-download_00: stoppedworker-emails:worker-emails_00:worker-nudges:worker-nudges_00: stoppedworker:worker_00: stoppedworker-audio:worker-audio_00: stoppedworker-calendar:worker-calendar_00: stoppedworker-crm-sync:worker-crm-sync_00: stoppedartisan-schedule:artisan-schedule_00: stoppedworker-es-update:worker-es-update_00: stoppedjiminny-worker-processing-1:jiminny-worker-processing-1_00: stoppedartisan-schedule:artisan-schedule_00: startedjiminny-worker-processing-1:jiminny-worker-processing-1_00: startedjiminny-worker-processing-2:jiminny-worker-processing-2_00: startedjiminny-worker-processing-3:jiminny-worker-processing-3_00: startedjiminny-worker-processing-4:jiminny-worker-processing-4_00: startedjiminny-worker-processing-5:jiminny-worker-processing-5_00:jiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: startedworker:worker_00: startedworker-analytics:worker-analytics_00: startedworker-audio:worker-audio_00: startedworker-calendar:worker-calendar_00: startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00: startedworker-emails:worker-emails_00: startedworker-es-update:worker-es-update_00: startedworker-nudges:worker-nudges_00: startedroot@docker_lamp_1:/home/jiminny#php artisan jiminny:debugDispatching 100 MatchActivityCrmData jobs (portal=2)Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugDispatching 100 MatchActivityCrmData jobs (portal=2)Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.root@docker_lamp_1:/home/jiminny# ]DEV (d)-zshHomeDMsActivityLater...alo)Preparation for Refi... in 59 m100% <78• Mon 11 May 14:01:34→Describe what you are looking forJiminny ...# contusion-clinic# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...ToastHomeMessages^ Direct messages®. Aneliya Angelova®. Galya Dimitrova I& Petko Kashinski&. Stefka StoyanovaVasil Vasilev&. Nikolay IvanovAneliya Angelova, ...Stoyan Tanev®. VesE Lukas Kovalik y... 0AboutFriday, May 8th~iminnyAdded by Toast for GitHubToday ~Toast APP10:00 AMReviewapp#12059 Jy 20820 es reindexstream model hydration2 days old • 12 files changed •@Vasil VasilevAdded by Toast for GitHubResolve Conflictsapp#11443 Test hublets latency5 months old • 20 files changed#11327 JY-19501 webhookbased opportunity syncShow moreAdded by Toast for GitHubNeeds Loveapp#12024 JY-20773 fix user pilottracking ofr automated reportgenerated12 days old • 1 file changedE:: AppsMessage ToastJira CloudToast+AaConala CalaNew...
|
18171
|
NULL
|
NULL
|
NULL
|
|
20106
|
864
|
12
|
2026-05-11T14:31:40.097382+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778509900097_m1.jpg...
|
PhpStorm
|
Push Commits to app
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Show Diff
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button"}]...
|
-8905307785746533391
|
-6617048611967383443
|
click
|
hybrid
|
NULL
|
Show Diff
iTerm2ShellEditViewSessionScriptsProfile Show Diff
iTerm2ShellEditViewSessionScriptsProfilesWindowHelpAPP (docker)APP (docker)-zsh+DOCKER-232,9 +235,11 @@₴81DEV (docker)₴284* @param string SobjectType The object type ('deals', 'companies', 'contacts', 'calls')* @param array<string, mixed› Spayload The search payload with filters, sorts, properties, etc.ereturn array Thesearch response with'results'total', 'paging'keys* @throws RateLimitException When rate limit is hit* @throws HubspotException On API errors* @return array The search response with 'results', 'total','paging' keyspublic function search(string SobjectType, array Spayload): array-zsh>0 ll*5screenpipe"100% <78• Mon 11 May 17:31:39T81O 886-zsh*7 |+APPend diff4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)begin diff--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php+++/home/jiminny/app/Console/Commands/JiminnyDebugCommand.php-359,11 +359,11 @ScrmService = ScrmResolver->prepareCrmService();-//-//for (Si = 0; si < 3; Si++) {if (Si%25 ===0) {Sthis->info("Syncing opportunity {$1}");Sthis->info("Matching contact {$i}");-//-//.ScrmService->syncOpportunity('374720564');if ($i % 25 === 0) {Sthis->info("Syncing opportunity {$1}");Sthis->info("Matching contact {Si}");////ScrmService-›syncOpportunity('374720564');ScrmService-›matchByName('Robot');end diffFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1...
|
20104
|
NULL
|
NULL
|
NULL
|
|
2992
|
120
|
4
|
2026-05-07T11:55:46.053839+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154946053_m2.jpg...
|
PhpStorm
|
faVsco.js – OpportunitySyncTrait.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
33
2...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.4005984,"top":0.09736632,"width":0.28257978,"height":0.8818835},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"33","depth":4,"bounds":{"left":0.33277926,"top":0.2490024,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.34507978,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"}]...
|
-8905231361930287567
|
-8132359382463312958
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
33
2
PhostormINavigarecodeFV faVsco.jsProledeyT Deleteermentytrai.pnp© ActivityProviderRegistry.pC ActivityProviderService.pl(c) CallDenormalizerkeglstry© CrmOwnerResolver.phpn DatalmnortHandlerintertaAddkateLimitcommand.phpMeetingBotService.php© ParticipantConsentServict© ParticipantsService.phpT ResponseValidationTrait.pT SalesforceGetUserTrait.plT SfDenormaliserMainCrmD© TrackRecordingFileSizeSe© TrackRecordingSizeEnfor(*) ValidateEmitProspectEver> C AjReports>D Avatai› D CalendarHubspotwebhookbatchsyncstrateav.onImportopportunity8atch.onoImportBatchJobTrait.phpC) Middleware/RateLimited.phpC Http/RateLimited.phpC) BaseRateLimiter.ohp(C) Service.phpSyncCrmEntitiesTrait.pho© OpportunitvSyncTest.phpC) SvncTeamMetadata.oho(C) RateLimit.ohoC RateLimiterinstance.ohdtnait Onnortun:tvSvnctraitt Ovateeuuntyson TradteSyncExcebtion (\Throwable Se, arpay Sparameters): VoidA 33A33 12X23 M V125* dinhericdoc127128 6oubuc tunction syncupportunitystrino scrmio: Uoportuniry› Conterence, 0 Crm• Bulhorn$this->client->getOpportunityById($crmId, ['hs_object_id', 'dealname']);131D Closem CoobenSstrateav = sthis->opportunntvsvncStrateavResolver->resolve01 CrmObiectsstrateov: OnnortunitvsvncStrateov?esolver:STNGLE SYNC OPPORTUNITY STRATEGYAID DecorateActivitv135DummyHelpersm HuhsnotSparameters =1D AccountSyncStrategyD ActionsD ContactSyncStrategy>DDTOIm Ciolde'erm id' => Sermidltry 1if (! $strategy instanceof HubspotSingleSyncStrategy) {throw new InvalidArgumentException(> @ Journal0 Metadatav @ OpportunitySvncStrate'Strategy must by HubspotSingleSyncStra› concerns© HubspotLastModifie© HubspotLastModifie© HubspotLastModifie© HubspotLastModifie$hsOpportunity = $strategy->fetchOpportunity($parameters):} catch (\HubSpot\Client\Crm\Deals\ApiException $e) {schis-> logger->into'• $this->getDisplayName(() . '] Opportunity not found'. ['teamId' => Sthis->team->qetUuido"crmlo => scrmloh'reason' => $e->qetMessageOC) =uosootLast Moditie@ HubspotSinaleSvncC HubsootSvncStrate@ HubspotWebhookBreturn null:PadinationM ProspectSearchStratecShsdoportunitvi'associations'l= Sthis->convertlealAssociationsdoogM RedisM ServiceTraitoT OnnortunitvSvnctrreturn Sthis->imoort0rUodatedooortunztv(Shsdoportunitv):N SuncCrmEntitiesTraN. SuncFieldsTrait nhnarAube for IDE suanestionsa Detect more seawritvlssues lin vour D.Dffiles lltn SonarAube Claud for free //lDownload SonarOmbe Server /llllear more /llDonit ask adain /itodav 105251>0 lbl= custom.log X = laravel.logA console [STAGING]A SF [jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]* console [eu)CascadeHubspot Rate Limitind"suppont Dally • In om100% L2Thu 7 May 14:55:45AskJiminnyReportActivityServiceTest v+0 ..Cascade Code xKick off a new project. Make changesacross your entre codeoase• HubSpot Rate Limiting IntegrationFixing Favicon Inconsistency© Fix Flaky Automated Reports TestsAsk anvthina (84L)< CodeC AdantiveWN Windsurf Toams 121-15 UTF.8io 4 spaces...
|
2991
|
NULL
|
NULL
|
NULL
|
|
6318
|
264
|
0
|
2026-05-07T18:15:01.692254+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778177701692_m2.jpg...
|
PhpStorm
|
faVsco.js – ConferenceCrmMatcherJob.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormINavigarecodeProiect v© DetachActivityObje PhostormINavigarecodeProiect v© DetachActivityObject.php© ActivityChangeCatego© SyncOpportunitiesJob.php• Cllentonp©Assignownersnip.ongC DelereAcuivities.onpC Delete leamenurnDatac) Delele l eamskerention© HardDeleteActivities.pc) Hubspot/Service.phpC) PayloadBuilder.php(c) CrmActivityService.phoC) CachedCrmServiceDecorator.pho© HardDeleteActivity.pnj ( Hubspot/.../SyncCrmEntitiesTrait.php© MatchMeetingOwner.r© Pipedrive/Service.phpServicelntertace.php© OpportunitySyncTest.phpclass ConferencecrmMatcherJob imolements Shou doueueC) ReindexForAccountJoloublic function handledC) ReindexForcontactJoiC) ReindexForgroupJob.r© ReindexForUser.Job.phC) RetrvActivitvSvnc.lob.n@ SvncActivity.php© TeardownStream.php•IM A AutomationIM AiRenorts> D AudioMAutomatedRenorts(C) RequestGenerateAck.lil© RequestGenerateRepo© SendReportExpiringSoC) SendReport.loh nhn© SendReportMailJob.phc senakeponinorcenera• C Calendaraermv 0 Deletec) DeleteAccountJob.c) DeleteContactJob.rw Deletecrmentitvurac) Deletelead.oo.oho© DeleteOpportunityC) VerifvActivitvCrmirv a Hubsoot> TraitsC) FetchMeraedObiecC) Hubsnot AonUninst@ ImnortAccountRatoN. ImnortRatch.lohTraC) ImnortContactRateC) Imnort@nnortunitvf# ProcessHubsnotWeC) ProceccinternalWelC DrococcMornedobie DrococeWohhookse) lindataDonlWohha) M CalocforcdtrySuser = Sactivitv->aetUser@:SpersistedAttendees = $this->collectParticipants(Sactivity):i€ Cemntv(SnensistedAttendeps))₫Slogger->info('[ConferenceCrmMatcherJob] No valid participants to process', [=> $this->activityId,ScrmService = ScrmResolver->resolveForUser(Suser):SimportParticipants = Sthis->createImportParticipants(Sactivity. Suser. SpersistedAttendees):SimportParticipants->setcrmservicescrmservicenSimportParticipants->retreshcrmbataoSloager->info('[ConfernceCrmMatcherJobl Refresh activity erm data finished'. [l'activity id' => sthis-›activitvid.'particivants count' => countSoersistedAttendees).} catch (SocialAccountTokenInvalidException $e) {Sloagen->error('ConferencecrmMatcherJ0b CRM token invalid'.=> Se->aetMessade@i.i// Prevent retries when no social account is available.Sthis->fail(Se):catch Eycontion Col d$logger->error('[ConferenceCrmMatcherJob] Failed to refresh activity crm data', [tactivity idt -s Cthic-sactivitutdl'error' => $e->qetMessage0.'trace' => $e->qetTraceAsStringO.D:tlof2 edits yAccept File &+X Reiect File t8@+ 6 of 7 files →throw SearQube for INE suadections: Netect more cecurity iccuec in vour DHP filec II Try SonarQube Cloud for fres /I Download oarQuhe Sorver |l Iearn more /I Don't ack adain (todav 10-25)© ProviderRateLimiter.php X = custom.logElaravel.log4 SF jiminny@localhost]A HS_local [jiminny@localhost]tiò accounts fürA console (PROD]# console [eu)A console [STAGING]use...class Providerratelimitenorotected RateLimiter SrateLimiter:public function __construct(RateLimiter $rateLimiter)(...}public function canMakeRequest(RateLimited $provider): bool/** Ovar RateLimitInterface SrateLimit */foreach (Sprovider->getRateLimits as $rateLimit) ‹$key = SrateLimit->getKeyO:if (Sthis->rateLimiter->tooManyAttempts($key, SrateLimit->getQuotaO)) ‹return true:pubLic tunction requestavallableln kateLimited Sprovider : 1ntreturn Sprovider->getRateLimits(->isNotEmotv@Sprovider->getRateLimitso(RateLimitInterface SrateLimit): int => Sthis->rateLimiter->availableIn(SrateLir->maxonublic function incrementRequestCountRateLimited Sorovider)• void** Avan Ratel imitIntenface Sratel imit *^foreach (Sprovider->getRateLimits as $rateLimit){Sthicesnasmit-›getKey, $rateLimit->getWindow0):TO lo100% 12Thu 7 May 21:15:02U AskJiminnyReportActivityServiceTest vCascadeHubspot Rate Limiting• Hubspot Rate Limit+0 ..ContinueCommand cd, areo, headop /yoer /bxev/nint 1 grep -t Trst Th iC -ActivityServie\|craActavityService" -nc luderi x- phpuSKipwA You've used 93% of your quota. Quota resets May 8, 11:00 AM GMT+3.app/Jobs/Crm/ MatchActivityCrmData.php +6app/Jobs/Activity/ ConferenceCrmMatcherJob.php +6aoo/lobs/Crm/l#SvncHubspotObiects.ohv +6oort/@ HydrateCrmDataByExternalCallid.Job.oho +6app/Jobs/Middleware/ HandleHubspotRateLimit.php +44onntlshelCrm/ß CunaOnnartunitineloh.nhnrdlAsk anvthina (84L)C° AdantiveView al•Reiect alllAccent alli• oW Windsurf Teams 1:1 UTF-8 P 4 spaces...
|
NULL
|
-8903636769225120449
|
NULL
|
click
|
ocr
|
NULL
|
PhostormINavigarecodeProiect v© DetachActivityObje PhostormINavigarecodeProiect v© DetachActivityObject.php© ActivityChangeCatego© SyncOpportunitiesJob.php• Cllentonp©Assignownersnip.ongC DelereAcuivities.onpC Delete leamenurnDatac) Delele l eamskerention© HardDeleteActivities.pc) Hubspot/Service.phpC) PayloadBuilder.php(c) CrmActivityService.phoC) CachedCrmServiceDecorator.pho© HardDeleteActivity.pnj ( Hubspot/.../SyncCrmEntitiesTrait.php© MatchMeetingOwner.r© Pipedrive/Service.phpServicelntertace.php© OpportunitySyncTest.phpclass ConferencecrmMatcherJob imolements Shou doueueC) ReindexForAccountJoloublic function handledC) ReindexForcontactJoiC) ReindexForgroupJob.r© ReindexForUser.Job.phC) RetrvActivitvSvnc.lob.n@ SvncActivity.php© TeardownStream.php•IM A AutomationIM AiRenorts> D AudioMAutomatedRenorts(C) RequestGenerateAck.lil© RequestGenerateRepo© SendReportExpiringSoC) SendReport.loh nhn© SendReportMailJob.phc senakeponinorcenera• C Calendaraermv 0 Deletec) DeleteAccountJob.c) DeleteContactJob.rw Deletecrmentitvurac) Deletelead.oo.oho© DeleteOpportunityC) VerifvActivitvCrmirv a Hubsoot> TraitsC) FetchMeraedObiecC) Hubsnot AonUninst@ ImnortAccountRatoN. ImnortRatch.lohTraC) ImnortContactRateC) Imnort@nnortunitvf# ProcessHubsnotWeC) ProceccinternalWelC DrococcMornedobie DrococeWohhookse) lindataDonlWohha) M CalocforcdtrySuser = Sactivitv->aetUser@:SpersistedAttendees = $this->collectParticipants(Sactivity):i€ Cemntv(SnensistedAttendeps))₫Slogger->info('[ConferenceCrmMatcherJob] No valid participants to process', [=> $this->activityId,ScrmService = ScrmResolver->resolveForUser(Suser):SimportParticipants = Sthis->createImportParticipants(Sactivity. Suser. SpersistedAttendees):SimportParticipants->setcrmservicescrmservicenSimportParticipants->retreshcrmbataoSloager->info('[ConfernceCrmMatcherJobl Refresh activity erm data finished'. [l'activity id' => sthis-›activitvid.'particivants count' => countSoersistedAttendees).} catch (SocialAccountTokenInvalidException $e) {Sloagen->error('ConferencecrmMatcherJ0b CRM token invalid'.=> Se->aetMessade@i.i// Prevent retries when no social account is available.Sthis->fail(Se):catch Eycontion Col d$logger->error('[ConferenceCrmMatcherJob] Failed to refresh activity crm data', [tactivity idt -s Cthic-sactivitutdl'error' => $e->qetMessage0.'trace' => $e->qetTraceAsStringO.D:tlof2 edits yAccept File &+X Reiect File t8@+ 6 of 7 files →throw SearQube for INE suadections: Netect more cecurity iccuec in vour DHP filec II Try SonarQube Cloud for fres /I Download oarQuhe Sorver |l Iearn more /I Don't ack adain (todav 10-25)© ProviderRateLimiter.php X = custom.logElaravel.log4 SF jiminny@localhost]A HS_local [jiminny@localhost]tiò accounts fürA console (PROD]# console [eu)A console [STAGING]use...class Providerratelimitenorotected RateLimiter SrateLimiter:public function __construct(RateLimiter $rateLimiter)(...}public function canMakeRequest(RateLimited $provider): bool/** Ovar RateLimitInterface SrateLimit */foreach (Sprovider->getRateLimits as $rateLimit) ‹$key = SrateLimit->getKeyO:if (Sthis->rateLimiter->tooManyAttempts($key, SrateLimit->getQuotaO)) ‹return true:pubLic tunction requestavallableln kateLimited Sprovider : 1ntreturn Sprovider->getRateLimits(->isNotEmotv@Sprovider->getRateLimitso(RateLimitInterface SrateLimit): int => Sthis->rateLimiter->availableIn(SrateLir->maxonublic function incrementRequestCountRateLimited Sorovider)• void** Avan Ratel imitIntenface Sratel imit *^foreach (Sprovider->getRateLimits as $rateLimit){Sthicesnasmit-›getKey, $rateLimit->getWindow0):TO lo100% 12Thu 7 May 21:15:02U AskJiminnyReportActivityServiceTest vCascadeHubspot Rate Limiting• Hubspot Rate Limit+0 ..ContinueCommand cd, areo, headop /yoer /bxev/nint 1 grep -t Trst Th iC -ActivityServie\|craActavityService" -nc luderi x- phpuSKipwA You've used 93% of your quota. Quota resets May 8, 11:00 AM GMT+3.app/Jobs/Crm/ MatchActivityCrmData.php +6app/Jobs/Activity/ ConferenceCrmMatcherJob.php +6aoo/lobs/Crm/l#SvncHubspotObiects.ohv +6oort/@ HydrateCrmDataByExternalCallid.Job.oho +6app/Jobs/Middleware/ HandleHubspotRateLimit.php +44onntlshelCrm/ß CunaOnnartunitineloh.nhnrdlAsk anvthina (84L)C° AdantiveView al•Reiect alllAccent alli• oW Windsurf Teams 1:1 UTF-8 P 4 spaces...
|
6317
|
NULL
|
NULL
|
NULL
|
|
14495
|
644
|
8
|
2026-05-09T18:02:14.953927+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-09/1778 /Users/lukas/.screenpipe/data/data/2026-05-09/1778349734953_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
дeV FINANCE [SSH: NAS]y finance-huhlbackenotronten дeV FINANCE [SSH: NAS]y finance-huhlbackenotrontend> mcp> scripts• .env.env.example• gitianore# docker-compose.vmlO README.md(} sms export.isonselectionViewlerminalWindowmelpo .envNOTIFIER URL=[URL_WITH_CREDENTIALS] 0100% 152sat y May 21:02.1008 00*mA .•<> Edit automatically8 Sign In...
|
NULL
|
-8901626392127212151
|
NULL
|
idle
|
ocr
|
NULL
|
дeV FINANCE [SSH: NAS]y finance-huhlbackenotronten дeV FINANCE [SSH: NAS]y finance-huhlbackenotrontend> mcp> scripts• .env.env.example• gitianore# docker-compose.vmlO README.md(} sms export.isonselectionViewlerminalWindowmelpo .envNOTIFIER URL=[URL_WITH_CREDENTIALS] 0100% 152sat y May 21:02.1008 00*mA .•<> Edit automatically8 Sign In...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
16518
|
741
|
16
|
2026-05-11T09:02:03.651331+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778490123651_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"65","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Cache::get($cacheKey);\n if (is_int($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Cache::put($cacheKey, $retryAfter, $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n//\n// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n// $body = json_decode((string) $response->getBody(), true);\n//\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n// \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
-8901204211021107095
|
6378618611371477348
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
65
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Cache;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Cache::get($cacheKey);
if (is_int($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Cache::put($cacheKey, $retryAfter, $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
//
// $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
// $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
// $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
// $body = json_decode((string) $response->getBody(), true);
//
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
// \Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4931
|
178
|
13
|
2026-05-07T14:39:37.643048+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164777643_m2.jpg...
|
Firefox
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira — Work...
|
True
|
jiminny.atlassian.net/jira/software/c/projects/JY/ jiminny.atlassian.net/jira/software/c/projects/JY/boards/37...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Close tab
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Sentry
Sentry
Pull requests · jiminny/app
Pull requests · jiminny/app
Userpilot | Ask Jiminny Report Generated
Userpilot | Ask Jiminny Report Generated
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Jiminny
Jiminny
Search the CRM - HubSpot docs
Search the CRM - HubSpot docs
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)
Teams
, (opens new window)
open menu
open menu
Customise sidebar
Customise sidebar
Resize side navigation panel
Spaces
Spaces
/
Jiminny (New)
Jiminny (New)
Platform Team
Platform Team
Add people
Add people
Board actions
Board actions
Share
Automation
Give feedback
Give feedback
Enter full screen
Enter full screen
Summary
Summary
Timeline
Timeline
Backlog
Backlog
Active sprints...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.34773937,"top":0.0518755,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":5,"bounds":{"left":0.36103722,"top":0.06304868,"width":0.10106383,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.41505983,"top":0.05905826,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":4,"bounds":{"left":0.34773937,"top":0.08459697,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":5,"bounds":{"left":0.36103722,"top":0.09577015,"width":0.4644282,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Service-Desk - Queues - Platform team - Service space - Jira","depth":4,"bounds":{"left":0.34773937,"top":0.11731844,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Service-Desk - Queues - Platform team - Service space - Jira","depth":5,"bounds":{"left":0.36103722,"top":0.12849163,"width":0.10721409,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app","depth":4,"bounds":{"left":0.34773937,"top":0.15003991,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app","depth":5,"bounds":{"left":0.36103722,"top":0.16121309,"width":0.17037898,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Sentry","depth":4,"bounds":{"left":0.34773937,"top":0.18276137,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Sentry","depth":5,"bounds":{"left":0.36103722,"top":0.19393456,"width":0.011303191,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · jiminny/app","depth":4,"bounds":{"left":0.34773937,"top":0.21548285,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · jiminny/app","depth":5,"bounds":{"left":0.36103722,"top":0.22665602,"width":0.04537899,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Userpilot | Ask Jiminny Report Generated","depth":4,"bounds":{"left":0.34773937,"top":0.2482043,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Userpilot | Ask Jiminny Report Generated","depth":5,"bounds":{"left":0.36103722,"top":0.25937748,"width":0.07164229,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"bounds":{"left":0.34773937,"top":0.28092578,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"bounds":{"left":0.36103722,"top":0.29209897,"width":0.19331782,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"bounds":{"left":0.34773937,"top":0.31364724,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"bounds":{"left":0.36103722,"top":0.32482043,"width":0.013131649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Search the CRM - HubSpot docs","depth":4,"bounds":{"left":0.34773937,"top":0.3463687,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Search the CRM - HubSpot docs","depth":5,"bounds":{"left":0.36103722,"top":0.3575419,"width":0.05651596,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.35056517,"top":0.38068634,"width":0.07413564,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.35056517,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.3615359,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.3726729,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.38380983,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.3949468,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to:","depth":9,"bounds":{"left":0.43799868,"top":0.07861133,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Top Bar","depth":10,"bounds":{"left":0.43799868,"top":0.097765364,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Top Bar","depth":11,"bounds":{"left":0.43799868,"top":0.097765364,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Sidebar","depth":10,"bounds":{"left":0.43799868,"top":0.11691939,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Sidebar","depth":11,"bounds":{"left":0.43799868,"top":0.11691939,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Main Content","depth":10,"bounds":{"left":0.43799868,"top":0.13607343,"width":0.029421542,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Main Content","depth":11,"bounds":{"left":0.43799868,"top":0.13607343,"width":0.029421542,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Space navigation","depth":10,"bounds":{"left":0.43799868,"top":0.15522745,"width":0.037898935,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Space navigation","depth":11,"bounds":{"left":0.43799868,"top":0.15522745,"width":0.037898935,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Collapse sidebar [","depth":9,"bounds":{"left":0.43134972,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Collapse sidebar [","depth":11,"bounds":{"left":0.43650267,"top":0.06344773,"width":0.039727394,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Switch sites or apps","depth":10,"bounds":{"left":0.44331783,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Switch sites or apps","depth":12,"bounds":{"left":0.44847074,"top":0.06344773,"width":0.044215426,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Go to your Jira homepage","depth":9,"bounds":{"left":0.4566157,"top":0.057861134,"width":0.029421542,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXComboBox","text":"Search, press enter to navigate to advanced search with your text query","depth":11,"bounds":{"left":0.57862365,"top":0.06264964,"width":0.24268617,"height":0.015961692},"on_screen":true,"help_text":"","placeholder":"Search","role_description":"combo box","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Create","depth":10,"bounds":{"left":0.829621,"top":0.057861134,"width":0.030086435,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create","depth":12,"bounds":{"left":0.8409242,"top":0.06384677,"width":0.014793883,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Rovo Ask Rovo","depth":12,"bounds":{"left":0.91223407,"top":0.057861134,"width":0.035904255,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ask Rovo","depth":14,"bounds":{"left":0.92353725,"top":0.06384677,"width":0.020611702,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Notifications","depth":12,"bounds":{"left":0.9494681,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Notifications","depth":14,"bounds":{"left":0.954621,"top":0.06344773,"width":0.027759308,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Help","depth":12,"bounds":{"left":0.96143615,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Help","depth":14,"bounds":{"left":0.9665891,"top":0.06344773,"width":0.010139627,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Settings","depth":12,"bounds":{"left":0.9734042,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Settings","depth":14,"bounds":{"left":0.97855717,"top":0.06344773,"width":0.017952127,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"lukas.kovalik@jiminny.com","depth":12,"bounds":{"left":0.98537236,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"lukas.kovalik@jiminny.com","depth":14,"bounds":{"left":0.99052525,"top":0.06344773,"width":0.009474754,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"For you","depth":12,"bounds":{"left":0.43134972,"top":0.09976058,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"For you","depth":15,"bounds":{"left":0.44198802,"top":0.10574621,"width":0.01662234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Recent","depth":12,"bounds":{"left":0.43134972,"top":0.12529927,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Recent","depth":15,"bounds":{"left":0.44198802,"top":0.13128492,"width":0.015458777,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Starred","depth":12,"bounds":{"left":0.43134972,"top":0.15083799,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Starred","depth":15,"bounds":{"left":0.44198802,"top":0.15682362,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Apps","depth":12,"bounds":{"left":0.43134972,"top":0.1763767,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Apps","depth":15,"bounds":{"left":0.44198802,"top":0.18236233,"width":0.011635638,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Apps","depth":13,"bounds":{"left":0.5008311,"top":0.17956904,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Apps","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Spaces","depth":12,"bounds":{"left":0.43134972,"top":0.2019154,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXStaticText","text":"Spaces","depth":15,"bounds":{"left":0.44198802,"top":0.20790103,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Create space","depth":13,"bounds":{"left":0.48420876,"top":0.20510775,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create space","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for spaces","depth":13,"bounds":{"left":0.49351728,"top":0.20510775,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for spaces","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Recent","depth":16,"bounds":{"left":0.43733376,"top":0.23423783,"width":0.013464096,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Jiminny (New)","depth":17,"bounds":{"left":0.4353391,"top":0.2529928,"width":0.0674867,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny (New)","depth":20,"bounds":{"left":0.4459774,"top":0.25897846,"width":0.032081116,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Jiminny (New)","depth":18,"bounds":{"left":0.43666887,"top":0.25618514,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXMenuButton","text":"Create board","depth":18,"bounds":{"left":0.48420876,"top":0.25618514,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Create board","depth":20,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Jiminny (New)","depth":18,"bounds":{"left":0.49351728,"top":0.25618514,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Jiminny (New)","depth":20,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Platform Team","depth":19,"bounds":{"left":0.43932846,"top":0.27853152,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Team","depth":22,"bounds":{"left":0.44996676,"top":0.28451717,"width":0.032247342,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":20,"bounds":{"left":0.5008311,"top":0.28172386,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":22,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Capture Team","depth":19,"bounds":{"left":0.43932846,"top":0.30407023,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Capture Team","depth":22,"bounds":{"left":0.44996676,"top":0.31005585,"width":0.03125,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":20,"bounds":{"left":0.5008311,"top":0.30726257,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":22,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Enterprise Stability Issues 🤕","depth":19,"bounds":{"left":0.43932846,"top":0.32960895,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Enterprise Stability Issues 🤕","depth":22,"bounds":{"left":0.44996676,"top":0.33559456,"width":0.050531916,"height":0.030726258},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":20,"bounds":{"left":0.5008311,"top":0.33280128,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":22,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Processing Team","depth":19,"bounds":{"left":0.43932846,"top":0.35514766,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Processing Team","depth":22,"bounds":{"left":0.44996676,"top":0.36113328,"width":0.038231384,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":20,"bounds":{"left":0.5008311,"top":0.35834,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":22,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SE Kanban","depth":19,"bounds":{"left":0.43932846,"top":0.38068634,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SE Kanban","depth":22,"bounds":{"left":0.44996676,"top":0.386672,"width":0.024102394,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":20,"bounds":{"left":0.5008311,"top":0.38387868,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":22,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Service-Desk","depth":17,"bounds":{"left":0.4353391,"top":0.40622506,"width":0.0674867,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Service-Desk","depth":20,"bounds":{"left":0.4459774,"top":0.4122107,"width":0.03025266,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Service-Desk","depth":18,"bounds":{"left":0.5021609,"top":0.4094174,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Service-Desk","depth":20,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"More spaces","depth":17,"bounds":{"left":0.4353391,"top":0.43176377,"width":0.0674867,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More spaces","depth":20,"bounds":{"left":0.4459774,"top":0.43774942,"width":0.028756648,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Filters","depth":12,"bounds":{"left":0.43134972,"top":0.45730248,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Filters","depth":15,"bounds":{"left":0.44198802,"top":0.4632881,"width":0.013796543,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Filters","depth":13,"bounds":{"left":0.5008311,"top":0.46049482,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Filters","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dashboards","depth":12,"bounds":{"left":0.43134972,"top":0.4828412,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Dashboards","depth":15,"bounds":{"left":0.44198802,"top":0.4888268,"width":0.026761968,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Create dashboard","depth":13,"bounds":{"left":0.5028258,"top":0.48603353,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create dashboard","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Dashboards","depth":13,"bounds":{"left":0.51013964,"top":0.48603353,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Dashboards","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Operations","depth":12,"bounds":{"left":0.43134972,"top":0.5083799,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Operations","depth":15,"bounds":{"left":0.44198802,"top":0.5143655,"width":0.02443484,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Operations","depth":13,"bounds":{"left":0.5008311,"top":0.51157224,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Operations","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Confluence , (opens new window)","depth":13,"bounds":{"left":0.43134972,"top":0.5434956,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Confluence","depth":17,"bounds":{"left":0.44198802,"top":0.5494813,"width":0.025764627,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", (opens new window)","depth":15,"bounds":{"left":0.43134972,"top":0.55706304,"width":0.04837101,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Teams , (opens new window)","depth":13,"bounds":{"left":0.43134972,"top":0.56903434,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Teams","depth":17,"bounds":{"left":0.44198802,"top":0.57501996,"width":0.014793883,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", (opens new window)","depth":15,"bounds":{"left":0.43134972,"top":0.5826017,"width":0.04837101,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"open menu","depth":14,"bounds":{"left":0.4915226,"top":0.57222664,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"open menu","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Customise sidebar","depth":12,"bounds":{"left":0.43134972,"top":0.60415006,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Customise sidebar","depth":15,"bounds":{"left":0.44198802,"top":0.6101357,"width":0.04155585,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Resize side navigation panel","depth":13,"bounds":{"left":0.55867684,"top":0.0981644,"width":0.062333778,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Spaces","depth":13,"bounds":{"left":0.51512635,"top":0.09976058,"width":0.016289894,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Spaces","depth":15,"bounds":{"left":0.51512635,"top":0.102553874,"width":0.016289894,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":13,"bounds":{"left":0.53457445,"top":0.102553874,"width":0.0016622341,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Jiminny (New)","depth":13,"bounds":{"left":0.539395,"top":0.09976058,"width":0.03174867,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny (New)","depth":15,"bounds":{"left":0.539395,"top":0.102553874,"width":0.03174867,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Platform Team","depth":10,"bounds":{"left":0.51512635,"top":0.12210695,"width":0.045877658,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Platform Team","depth":11,"bounds":{"left":0.51512635,"top":0.12210695,"width":0.045877658,"height":0.019553073},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add people","depth":10,"bounds":{"left":0.56299865,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add people","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":10,"bounds":{"left":0.5756317,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Share","depth":10,"bounds":{"left":0.94148934,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Automation","depth":10,"bounds":{"left":0.95478725,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give feedback","depth":10,"bounds":{"left":0.9680851,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Give feedback","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Enter full screen","depth":10,"bounds":{"left":0.98138297,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Enter full screen","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Summary","depth":13,"bounds":{"left":0.5124667,"top":0.14764565,"width":0.035904255,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Summary","depth":15,"bounds":{"left":0.52377,"top":0.15363128,"width":0.021276595,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Timeline","depth":13,"bounds":{"left":0.5497008,"top":0.14764565,"width":0.03357713,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Timeline","depth":15,"bounds":{"left":0.561004,"top":0.15363128,"width":0.018949468,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Backlog","depth":13,"bounds":{"left":0.5846077,"top":0.14764565,"width":0.032413565,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Backlog","depth":15,"bounds":{"left":0.5959109,"top":0.15363128,"width":0.017785905,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Active sprints","depth":13,"bounds":{"left":0.61835104,"top":0.14764565,"width":0.045212764,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"}]...
|
-8900173637054785448
|
6293058154025644164
|
visual_change
|
accessibility
|
NULL
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Close tab
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
Service-Desk - Queues - Platform team - Service space - Jira
Service-Desk - Queues - Platform team - Service space - Jira
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app
Sentry
Sentry
Pull requests · jiminny/app
Pull requests · jiminny/app
Userpilot | Ask Jiminny Report Generated
Userpilot | Ask Jiminny Report Generated
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Jiminny
Jiminny
Search the CRM - HubSpot docs
Search the CRM - HubSpot docs
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)
Teams
, (opens new window)
open menu
open menu
Customise sidebar
Customise sidebar
Resize side navigation panel
Spaces
Spaces
/
Jiminny (New)
Jiminny (New)
Platform Team
Platform Team
Add people
Add people
Board actions
Board actions
Share
Automation
Give feedback
Give feedback
Enter full screen
Enter full screen
Summary
Summary
Timeline
Timeline
Backlog
Backlog
Active sprints...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
9434
|
425
|
11
|
2026-05-08T12:50:21.108452+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778244621108_m1.jpg...
|
Firefox
|
Jy 20820 es reindex stream model hydration by Vasi Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app — Work...
|
True
|
github.com/jiminny/app/pull/12059/changes#diff-f77 github.com/jiminny/app/pull/12059/changes#diff-f77c5793308caa3e764a033422493121a1329dd953a89f3dde4a9386622f22f9...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Unnamed Group
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST [URL_WITH_CREDENTIALS] -5,69 +5,64 @@5namespace Jiminny\Component\ES\Processor\Actions;5namespace Jiminny\Component\ES\Processor\Actions;667use Elastica\Document;7use Elastica\Document;8-use Illuminate\Support\Collection;9use Jiminny\Component\ElasticSearch\Contract\Searchable;8use Jiminny\Component\ElasticSearch\Contract\Searchable;10use Jiminny\Component\ES\Processor\DTOs\DocumentLoad;9use Jiminny\Component\ES\Processor\DTOs\DocumentLoad;11use Jiminny\Component\ES\Processor\DTOs\SimpleCollection;10use Jiminny\Component\ES\Processor\DTOs\SimpleCollection;12use Jiminny\Component\ES\Processor\EntityQueryBuilder;11use Jiminny\Component\ES\Processor\EntityQueryBuilder;13-use Jiminny\Component\ES\Processor\Traits\SkipActivityTrait;14use Jiminny\Exceptions\SyncActivityException;12use Jiminny\Exceptions\SyncActivityException;15use Jiminny\Models\Model;13use Jiminny\Models\Model;16use Sentry\Laravel\Facade as Sentry;14use Sentry\Laravel\Facade as Sentry;15+use Throwable;171618class LoadDocumentsAction17class LoadDocumentsAction19{18{20-use SkipActivityTrait;19+public function __construct(21-20+private readonly EntityQueryBuilder $queryBuilder22-private const int RDS_CHUNK_SIZE = 250;21+ ) {22+ }232324-/**25- * @codeCoverageIgnore26- */27public function loadDocuments(string $updateTarget, array $entityIdsList): DocumentLoad24public function loadDocuments(string $updateTarget, array $entityIdsList): DocumentLoad28 {25 {29$documentsToUpdate = new SimpleCollection();26$documentsToUpdate = new SimpleCollection();30$documentsToDelete = new SimpleCollection();27$documentsToDelete = new SimpleCollection();312832-// do get mariadb data29+$query = $this->queryBuilder->getEntityQuery($updateTarget, $entityIdsList);33-$query = EntityQueryBuilder::getEntityQuery($updateTarget, $entityIdsList);343035-$query->chunkByIdDesc(31+/** @var Model&Searchable $entityModel */36-self::RDS_CHUNK_SIZE,32+foreach ($query->cursor() as $entityModel) {37-function (Collection $entityModels) use ($documentsToUpdate, $documentsToDelete) {33+if ($entityModel->isDeleted()) {38-/** @var Model&Searchable $entityForDeletion */34+/**39-foreach ($entityModels->whereNotNull('deleted_at') as $entityForDeletion) {35+ * Cleanup (from ElasticSearch) scheduled entities that were recently deleted.40-$documentsToDelete->add($entityForDeletion->getId());36+ * After a deletion, no more updates on a record are expected, so the operation is considered final,41- }37+ * unless the record is restored.42-38+ */43-/** @var Model&Searchable $entityModel */39+$documentsToDelete->add($entityModel->getId());44-foreach ($entityModels->whereNull('deleted_at') as $entityModel) {40+ } else {45-if (self::shouldSkipActivity($entityModel)) {41+try {46-/**42+$documentsToUpdate->add(47- * If the activity type is in the skip list, we should not push it for indexing.43+new Document((string) $entityModel->getId(), $entityModel->getIndexableAttributes())48- * If an ES record already exists, we should remove it to free up storage and processing power44+ );49- */45+ } catch (Throwable $error) {50-$documentsToDelete->add($entityModel->getId());46+ Sentry::captureException(51-47+new SyncActivityException(52-continue;48+'ES entity async RDS build data failed',53- }49+$error->getCode(),54-50+$error55-try {51+ )56-$documentsToUpdate->add(52+ );57-new Document((string) $entityModel->getId(), $entityModel->getIndexableAttributes())58- );59- } catch (\Throwable $error) {60- Sentry::captureException(61-new SyncActivityException(62-'ES entity async RDS build data failed',63-$error->getCode(),64-$error65- )66- );67- }68 }53 }69 }54 }70- );55+56+/**57+ * Clean up fragmented memory.58+ * Dropping relations and unsetting the entity model after hydration and usage,59+ * allows GC to remove heap memory allocations, and recycle already allocated memory,60+ * instead of allocating more memory from OS.61+ * Unset operations signal GC to collect destroyed object memory62+ */63+$entityModel->setRelations([]);64+ unset($entityModel);65+ }716672return new DocumentLoad($documentsToUpdate, $documentsToDelete);67return new DocumentLoad($documentsToUpdate, $documentsToDelete);73 }68 }</selection>” selected. Please summarize the selection using precise and concise language. Use headers and bulleted lists in the summary, to make it scannable. Maintain the meaning and factual accuracy.
You said
I’m on page “<tabTitle>Jy 20820 es reindex stream model hydration by Vasi</tabTitle>” with “<selection>@@ -5,69 +5,64 @@5namespace Jiminny\Component\ES\Processor\Actions;5namespace Jiminny\Component\ES\Processor\Actions;667use Elastica\Document;7use Elastica\Document;8-use Illuminate\Support\Collection;9use Jiminny\Component\ElasticSearch\Contract\Searchable;8use Jiminny\Component\ElasticSearch\Contract\Searchable;10use Jiminny\Component\ES\Processor\DTOs\DocumentLoad;9use Jiminny\Component\ES\Processor\DTOs\DocumentLoad;11use Jiminny\Component\ES\Processor\DTOs\SimpleCollection;10use Jiminny\Component\ES\Processor\DTOs\SimpleCollection;12use Jiminny\Component\ES\Processor\EntityQueryBuilder;11use Jiminny\Component\ES\Processor\EntityQueryBuilder;13-use Jiminny\Component\ES\Processor\Traits\SkipActivityTrait;14use Jiminny\Exceptions\SyncActivityException;12use Jiminny\Exceptions\SyncActivityException;15use Jiminny\Models\Model;13use Jiminny\Models\Model;16use Sentry\Laravel\Facade as Sentry;14use Sentry\Laravel\Facade as Sentry;15+use Throwable;171618class LoadDocumentsAction17class LoadDocumentsAction19{18{20-use SkipActivityTrait;19+public function __construct(21-20+private readonly EntityQueryBuilder $queryBuilder22-private const int RDS_CHUNK_SIZE = 250;21+ ) {22+ }232324-/**25- * @codeCoverageIgnore26- */27public function loadDocuments(string $updateTarget, array $entityIdsList): DocumentLoad24public function loadDocuments(string $updateTarget, array $entityIdsList): DocumentLoad28 {25 {29$documentsToUpdate = new SimpleCollection();26$documentsToUpdate = new SimpleCollection();30$documentsToDelete = new SimpleCollection();27$documentsToDelete = new SimpleCollection();312832-// do get mariadb data29+$query = $this->queryBuilder->getEntityQuery($updateTarget, $entityIdsList);33-$query = EntityQueryBuilder::getEntityQuery($updateTarget, $entityIdsList);343035-$query->chunkByIdDesc(31+/** @var Model&Searchable $entityModel */36-self::RDS_CHUNK_SIZE,32+foreach ($query->cursor() as $entityModel) {37-function (Collection $entityModels) use ($documentsToUpdate, $documentsToDelete) {33+if ($entityModel->isDeleted()) {38-/** @var Model&Searchable $entityForDeletion */34+/**39-foreach ($entityModels->whereNotNull('deleted_at') as $entityForDeletion) {35+ * Cleanup (from ElasticSearch) scheduled entities that were recently deleted.40-$documentsToDelete->add($entityForDeletion->getId());36+ * After a deletion, no more updates on a record are expected, so the operation is considered final,41- }37+ * unless the record is restored.42-38+ */43-/** @var Model&Searchable $entityModel */39+$documentsToDelete->add($entityModel->getId());44-foreach ($entityModels->whereNull('deleted_at') as $entityModel) {40+ } else {45-if (self::shouldSkipActivity($entityModel)) {41+try {46-/**42+$documentsToUpdate->add(47- * If the activity type is in the skip list, we should not push it for indexing.43+new Document((string) $entityModel->getId(), $entityModel->getIndexableAttributes())48- * If an ES record already exists, we should remove it to free up storage and processing power44+ );49- */45+ } catch (Throwable $error) {50-$documentsToDelete->add($entityModel->getId());46+ Sentry::captureException(51-47+new SyncActivityException(52-continue;48+'ES entity async RDS build data failed',53- }49+$error->getCode(),54-50+$error55-try {51+ )56-$documentsToUpdate->add(52+ );57-new Document((string) $entityModel->getId(), $entityModel->getIndexableAttributes())58- );59- } catch (\Throwable $error) {60- Sentry::captureException(61-new SyncActivityException(62-'ES entity async RDS build data failed',63-$error->getCode(),64-$error65- )66- );67- }68 }53 }69 }54 }70- );55+56+/**57+ * Clean up fragmented memory.58+ * Dropping relations and unsetting the entity model after hydration and usage,59+ * allows GC to remove heap memory allocations, and recycle already allocated memory,60+ * instead of allocating more memory from OS.61+ * Unset operations signal GC to collect destroyed object memory62+ */63+$entityModel->setRelations([]);64+ unset($entityModel);65+ }716672return new DocumentLoad($documentsToUpdate, $documentsToDelete);67return new DocumentLoad($documentsToUpdate, $documentsToDelete);73 }68 }</selection>” selected.
Please summarize the selection using precise and concise language. Use headers and bulleted lists in the summary, to make it scannable. Maintain the meaning and factual accuracy.
Collapse
Listen
Show more options
Show thinking
Gemini said
Gemini said
Overview
Overview
This diff refactors the
LoadDocumentsAction
class to optimize memory usage and data processing. It transitions the database query execution from batch chunking to lazy loading using cursors, introduces explicit memory management techniques, and updates the class structure to use dependency injection.
Architectural & Structural Changes
Architectural & Structural Changes
Dependency Injection:
Injected
EntityQueryBuilder
via the constructor instead of calling it statically.
Removed Traits & Constants:
Removed the
SkipActivityTrait
entirely (and its associated skip logic), as well as the
RDS_CHUNK_SIZE
constant.
Namespace Imports:
Added the
Throwable
import and removed
Illuminate\Support\Collection
.
Data Processing Refactor
Data Processing Refactor
Cursor Implementation:
Replaced
chunkByIdDesc
with
cursor()
, iterating through records sequentially rather than loading batches of 250 into memory.
Simplified Deletion Checks:
Replaced Laravel collection filtering (
whereNotNull('deleted_at')
) with a direct
$entityModel->isDeleted()
check on each model during iteration.
Removed Skip Logic:
Removed the
shouldSkipActivity
check, meaning all non-deleted models are now directly processed for updates.
Exception Handling:
Updated the catch block to use the imported
Throwable
interface rather than the fully qualified
\Throwable
.
Memory Optimization
Memory Optimization
Explicit Garbage Collection:
Added specific cleanup steps inside the iteration loop. Dropping loaded relationships (
$entityModel->setRelations([])
) and unsetting the model (
unset($entityModel)...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Unnamed Group","depth":4,"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXRadioButton","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Service-Desk - Queues - Platform team - Service space - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Service-Desk - Queues - Platform team - Service space - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 20807 check various issues with stages by nikolaybiaivanov · Pull Request #12041 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Feed — jiminny — Sentry","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Feed — jiminny — Sentry","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20818 move ask jiminny reports to its own datadog metric by LakyLak · Pull Request #12056 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20818 move ask jiminny reports to its own datadog metric by LakyLak · Pull Request #12056 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Userpilot | Ask Jiminny Report Generated","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Userpilot | Ask Jiminny Report Generated","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Problem loading page","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Problem loading page","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Search the CRM - HubSpot docs","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Search the CRM - HubSpot docs","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jiminny","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AI Features | Datadog","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AI Features | Datadog","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jy 20493 smart instant nudge pre filtering by nikolaybiaivanov · Pull Request #12053 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 20493 smart instant nudge pre filtering by nikolaybiaivanov · Pull Request #12053 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Close Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0013888889,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"AI Chat settings","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"WORK, Google Account: lukas.kovalik@jiminny.com","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Main menu","depth":12,"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Chat","depth":12,"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open menu for conversation actions.","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Conversation with Gemini","depth":15,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation with Gemini","depth":16,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy prompt","depth":21,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"You said I’m on page “<tabTitle>Jy 20820 es reindex stream model hydration by Vasi</tabTitle>” with “<selection>@@ -5,69 +5,64 @@5namespace Jiminny\\Component\\ES\\Processor\\Actions;5namespace Jiminny\\Component\\ES\\Processor\\Actions;667use Elastica\\Document;7use Elastica\\Document;8-use Illuminate\\Support\\Collection;9use Jiminny\\Component\\ElasticSearch\\Contract\\Searchable;8use Jiminny\\Component\\ElasticSearch\\Contract\\Searchable;10use Jiminny\\Component\\ES\\Processor\\DTOs\\DocumentLoad;9use Jiminny\\Component\\ES\\Processor\\DTOs\\DocumentLoad;11use Jiminny\\Component\\ES\\Processor\\DTOs\\SimpleCollection;10use Jiminny\\Component\\ES\\Processor\\DTOs\\SimpleCollection;12use Jiminny\\Component\\ES\\Processor\\EntityQueryBuilder;11use Jiminny\\Component\\ES\\Processor\\EntityQueryBuilder;13-use Jiminny\\Component\\ES\\Processor\\Traits\\SkipActivityTrait;14use Jiminny\\Exceptions\\SyncActivityException;12use Jiminny\\Exceptions\\SyncActivityException;15use Jiminny\\Models\\Model;13use Jiminny\\Models\\Model;16use Sentry\\Laravel\\Facade as Sentry;14use Sentry\\Laravel\\Facade as Sentry;15+use Throwable;171618class LoadDocumentsAction17class LoadDocumentsAction19{18{20-use SkipActivityTrait;19+public function __construct(21-20+private readonly EntityQueryBuilder $queryBuilder22-private const int RDS_CHUNK_SIZE = 250;21+ ) {22+ }232324-/**25- * @codeCoverageIgnore26- */27public function loadDocuments(string $updateTarget, array $entityIdsList): DocumentLoad24public function loadDocuments(string $updateTarget, array $entityIdsList): DocumentLoad28 {25 {29$documentsToUpdate = new SimpleCollection();26$documentsToUpdate = new SimpleCollection();30$documentsToDelete = new SimpleCollection();27$documentsToDelete = new SimpleCollection();312832-// do get mariadb data29+$query = $this->queryBuilder->getEntityQuery($updateTarget, $entityIdsList);33-$query = EntityQueryBuilder::getEntityQuery($updateTarget, $entityIdsList);343035-$query->chunkByIdDesc(31+/** @var Model&Searchable $entityModel */36-self::RDS_CHUNK_SIZE,32+foreach ($query->cursor() as $entityModel) {37-function (Collection $entityModels) use ($documentsToUpdate, $documentsToDelete) {33+if ($entityModel->isDeleted()) {38-/** @var Model&Searchable $entityForDeletion */34+/**39-foreach ($entityModels->whereNotNull('deleted_at') as $entityForDeletion) {35+ * Cleanup (from ElasticSearch) scheduled entities that were recently deleted.40-$documentsToDelete->add($entityForDeletion->getId());36+ * After a deletion, no more updates on a record are expected, so the operation is considered final,41- }37+ * unless the record is restored.42-38+ */43-/** @var Model&Searchable $entityModel */39+$documentsToDelete->add($entityModel->getId());44-foreach ($entityModels->whereNull('deleted_at') as $entityModel) {40+ } else {45-if (self::shouldSkipActivity($entityModel)) {41+try {46-/**42+$documentsToUpdate->add(47- * If the activity type is in the skip list, we should not push it for indexing.43+new Document((string) $entityModel->getId(), $entityModel->getIndexableAttributes())48- * If an ES record already exists, we should remove it to free up storage and processing power44+ );49- */45+ } catch (Throwable $error) {50-$documentsToDelete->add($entityModel->getId());46+ Sentry::captureException(51-47+new SyncActivityException(52-continue;48+'ES entity async RDS build data failed',53- }49+$error->getCode(),54-50+$error55-try {51+ )56-$documentsToUpdate->add(52+ );57-new Document((string) $entityModel->getId(), $entityModel->getIndexableAttributes())58- );59- } catch (\\Throwable $error) {60- Sentry::captureException(61-new SyncActivityException(62-'ES entity async RDS build data failed',63-$error->getCode(),64-$error65- )66- );67- }68 }53 }69 }54 }70- );55+56+/**57+ * Clean up fragmented memory.58+ * Dropping relations and unsetting the entity model after hydration and usage,59+ * allows GC to remove heap memory allocations, and recycle already allocated memory,60+ * instead of allocating more memory from OS.61+ * Unset operations signal GC to collect destroyed object memory62+ */63+$entityModel->setRelations([]);64+ unset($entityModel);65+ }716672return new DocumentLoad($documentsToUpdate, $documentsToDelete);67return new DocumentLoad($documentsToUpdate, $documentsToDelete);73 }68 }</selection>” selected. Please summarize the selection using precise and concise language. Use headers and bulleted lists in the summary, to make it scannable. Maintain the meaning and factual accuracy.","depth":21,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"You said","depth":23,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"I’m on page “<tabTitle>Jy 20820 es reindex stream model hydration by Vasi</tabTitle>” with “<selection>@@ -5,69 +5,64 @@5namespace Jiminny\\Component\\ES\\Processor\\Actions;5namespace Jiminny\\Component\\ES\\Processor\\Actions;667use Elastica\\Document;7use Elastica\\Document;8-use Illuminate\\Support\\Collection;9use Jiminny\\Component\\ElasticSearch\\Contract\\Searchable;8use Jiminny\\Component\\ElasticSearch\\Contract\\Searchable;10use Jiminny\\Component\\ES\\Processor\\DTOs\\DocumentLoad;9use Jiminny\\Component\\ES\\Processor\\DTOs\\DocumentLoad;11use Jiminny\\Component\\ES\\Processor\\DTOs\\SimpleCollection;10use Jiminny\\Component\\ES\\Processor\\DTOs\\SimpleCollection;12use Jiminny\\Component\\ES\\Processor\\EntityQueryBuilder;11use Jiminny\\Component\\ES\\Processor\\EntityQueryBuilder;13-use Jiminny\\Component\\ES\\Processor\\Traits\\SkipActivityTrait;14use Jiminny\\Exceptions\\SyncActivityException;12use Jiminny\\Exceptions\\SyncActivityException;15use Jiminny\\Models\\Model;13use Jiminny\\Models\\Model;16use Sentry\\Laravel\\Facade as Sentry;14use Sentry\\Laravel\\Facade as Sentry;15+use Throwable;171618class LoadDocumentsAction17class LoadDocumentsAction19{18{20-use SkipActivityTrait;19+public function __construct(21-20+private readonly EntityQueryBuilder $queryBuilder22-private const int RDS_CHUNK_SIZE = 250;21+ ) {22+ }232324-/**25- * @codeCoverageIgnore26- */27public function loadDocuments(string $updateTarget, array $entityIdsList): DocumentLoad24public function loadDocuments(string $updateTarget, array $entityIdsList): DocumentLoad28 {25 {29$documentsToUpdate = new SimpleCollection();26$documentsToUpdate = new SimpleCollection();30$documentsToDelete = new SimpleCollection();27$documentsToDelete = new SimpleCollection();312832-// do get mariadb data29+$query = $this->queryBuilder->getEntityQuery($updateTarget, $entityIdsList);33-$query = EntityQueryBuilder::getEntityQuery($updateTarget, $entityIdsList);343035-$query->chunkByIdDesc(31+/** @var Model&Searchable $entityModel */36-self::RDS_CHUNK_SIZE,32+foreach ($query->cursor() as $entityModel) {37-function (Collection $entityModels) use ($documentsToUpdate, $documentsToDelete) {33+if ($entityModel->isDeleted()) {38-/** @var Model&Searchable $entityForDeletion */34+/**39-foreach ($entityModels->whereNotNull('deleted_at') as $entityForDeletion) {35+ * Cleanup (from ElasticSearch) scheduled entities that were recently deleted.40-$documentsToDelete->add($entityForDeletion->getId());36+ * After a deletion, no more updates on a record are expected, so the operation is considered final,41- }37+ * unless the record is restored.42-38+ */43-/** @var Model&Searchable $entityModel */39+$documentsToDelete->add($entityModel->getId());44-foreach ($entityModels->whereNull('deleted_at') as $entityModel) {40+ } else {45-if (self::shouldSkipActivity($entityModel)) {41+try {46-/**42+$documentsToUpdate->add(47- * If the activity type is in the skip list, we should not push it for indexing.43+new Document((string) $entityModel->getId(), $entityModel->getIndexableAttributes())48- * If an ES record already exists, we should remove it to free up storage and processing power44+ );49- */45+ } catch (Throwable $error) {50-$documentsToDelete->add($entityModel->getId());46+ Sentry::captureException(51-47+new SyncActivityException(52-continue;48+'ES entity async RDS build data failed',53- }49+$error->getCode(),54-50+$error55-try {51+ )56-$documentsToUpdate->add(52+ );57-new Document((string) $entityModel->getId(), $entityModel->getIndexableAttributes())58- );59- } catch (\\Throwable $error) {60- Sentry::captureException(61-new SyncActivityException(62-'ES entity async RDS build data failed',63-$error->getCode(),64-$error65- )66- );67- }68 }53 }69 }54 }70- );55+56+/**57+ * Clean up fragmented memory.58+ * Dropping relations and unsetting the entity model after hydration and usage,59+ * allows GC to remove heap memory allocations, and recycle already allocated memory,60+ * instead of allocating more memory from OS.61+ * Unset operations signal GC to collect destroyed object memory62+ */63+$entityModel->setRelations([]);64+ unset($entityModel);65+ }716672return new DocumentLoad($documentsToUpdate, $documentsToDelete);67return new DocumentLoad($documentsToUpdate, $documentsToDelete);73 }68 }</selection>” selected.","depth":23,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Please summarize the selection using precise and concise language. Use headers and bulleted lists in the summary, to make it scannable. Maintain the meaning and factual accuracy.","depth":23,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Collapse","depth":21,"on_screen":false,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Listen","depth":22,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Show more options","depth":20,"on_screen":false,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Show thinking","depth":25,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Gemini said","depth":20,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini said","depth":21,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Overview","depth":23,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Overview","depth":24,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"This diff refactors the","depth":24,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"LoadDocumentsAction","depth":25,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"class to optimize memory usage and data processing. It transitions the database query execution from batch chunking to lazy loading using cursors, introduces explicit memory management techniques, and updates the class structure to use dependency injection.","depth":24,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Architectural & Structural Changes","depth":23,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Architectural & Structural Changes","depth":24,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Dependency Injection:","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Injected","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"EntityQueryBuilder","depth":27,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"via the constructor instead of calling it statically.","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Removed Traits & Constants:","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Removed the","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SkipActivityTrait","depth":27,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"entirely (and its associated skip logic), as well as the","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"RDS_CHUNK_SIZE","depth":27,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"constant.","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Namespace Imports:","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Added the","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Throwable","depth":27,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"import and removed","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Illuminate\\Support\\Collection","depth":27,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Data Processing Refactor","depth":23,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Data Processing Refactor","depth":24,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Cursor Implementation:","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Replaced","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"chunkByIdDesc","depth":27,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"with","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"cursor()","depth":27,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", iterating through records sequentially rather than loading batches of 250 into memory.","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Simplified Deletion Checks:","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Replaced Laravel collection filtering (","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"whereNotNull('deleted_at')","depth":27,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") with a direct","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$entityModel->isDeleted()","depth":27,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"check on each model during iteration.","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Removed Skip Logic:","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Removed the","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"shouldSkipActivity","depth":27,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"check, meaning all non-deleted models are now directly processed for updates.","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Exception Handling:","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Updated the catch block to use the imported","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Throwable","depth":27,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"interface rather than the fully qualified","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\\Throwable","depth":27,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Memory Optimization","depth":23,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Memory Optimization","depth":24,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Explicit Garbage Collection:","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Added specific cleanup steps inside the iteration loop. Dropping loaded relationships (","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"$entityModel->setRelations([])","depth":27,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") and unsetting the model (","depth":26,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"unset($entityModel)","depth":27,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-8900144491009625320
|
-1205540267655220827
|
idle
|
accessibility
|
NULL
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Unnamed Group
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST [URL_WITH_CREDENTIALS] -5,69 +5,64 @@5namespace Jiminny\Component\ES\Processor\Actions;5namespace Jiminny\Component\ES\Processor\Actions;667use Elastica\Document;7use Elastica\Document;8-use Illuminate\Support\Collection;9use Jiminny\Component\ElasticSearch\Contract\Searchable;8use Jiminny\Component\ElasticSearch\Contract\Searchable;10use Jiminny\Component\ES\Processor\DTOs\DocumentLoad;9use Jiminny\Component\ES\Processor\DTOs\DocumentLoad;11use Jiminny\Component\ES\Processor\DTOs\SimpleCollection;10use Jiminny\Component\ES\Processor\DTOs\SimpleCollection;12use Jiminny\Component\ES\Processor\EntityQueryBuilder;11use Jiminny\Component\ES\Processor\EntityQueryBuilder;13-use Jiminny\Component\ES\Processor\Traits\SkipActivityTrait;14use Jiminny\Exceptions\SyncActivityException;12use Jiminny\Exceptions\SyncActivityException;15use Jiminny\Models\Model;13use Jiminny\Models\Model;16use Sentry\Laravel\Facade as Sentry;14use Sentry\Laravel\Facade as Sentry;15+use Throwable;171618class LoadDocumentsAction17class LoadDocumentsAction19{18{20-use SkipActivityTrait;19+public function __construct(21-20+private readonly EntityQueryBuilder $queryBuilder22-private const int RDS_CHUNK_SIZE = 250;21+ ) {22+ }232324-/**25- * @codeCoverageIgnore26- */27public function loadDocuments(string $updateTarget, array $entityIdsList): DocumentLoad24public function loadDocuments(string $updateTarget, array $entityIdsList): DocumentLoad28 {25 {29$documentsToUpdate = new SimpleCollection();26$documentsToUpdate = new SimpleCollection();30$documentsToDelete = new SimpleCollection();27$documentsToDelete = new SimpleCollection();312832-// do get mariadb data29+$query = $this->queryBuilder->getEntityQuery($updateTarget, $entityIdsList);33-$query = EntityQueryBuilder::getEntityQuery($updateTarget, $entityIdsList);343035-$query->chunkByIdDesc(31+/** @var Model&Searchable $entityModel */36-self::RDS_CHUNK_SIZE,32+foreach ($query->cursor() as $entityModel) {37-function (Collection $entityModels) use ($documentsToUpdate, $documentsToDelete) {33+if ($entityModel->isDeleted()) {38-/** @var Model&Searchable $entityForDeletion */34+/**39-foreach ($entityModels->whereNotNull('deleted_at') as $entityForDeletion) {35+ * Cleanup (from ElasticSearch) scheduled entities that were recently deleted.40-$documentsToDelete->add($entityForDeletion->getId());36+ * After a deletion, no more updates on a record are expected, so the operation is considered final,41- }37+ * unless the record is restored.42-38+ */43-/** @var Model&Searchable $entityModel */39+$documentsToDelete->add($entityModel->getId());44-foreach ($entityModels->whereNull('deleted_at') as $entityModel) {40+ } else {45-if (self::shouldSkipActivity($entityModel)) {41+try {46-/**42+$documentsToUpdate->add(47- * If the activity type is in the skip list, we should not push it for indexing.43+new Document((string) $entityModel->getId(), $entityModel->getIndexableAttributes())48- * If an ES record already exists, we should remove it to free up storage and processing power44+ );49- */45+ } catch (Throwable $error) {50-$documentsToDelete->add($entityModel->getId());46+ Sentry::captureException(51-47+new SyncActivityException(52-continue;48+'ES entity async RDS build data failed',53- }49+$error->getCode(),54-50+$error55-try {51+ )56-$documentsToUpdate->add(52+ );57-new Document((string) $entityModel->getId(), $entityModel->getIndexableAttributes())58- );59- } catch (\Throwable $error) {60- Sentry::captureException(61-new SyncActivityException(62-'ES entity async RDS build data failed',63-$error->getCode(),64-$error65- )66- );67- }68 }53 }69 }54 }70- );55+56+/**57+ * Clean up fragmented memory.58+ * Dropping relations and unsetting the entity model after hydration and usage,59+ * allows GC to remove heap memory allocations, and recycle already allocated memory,60+ * instead of allocating more memory from OS.61+ * Unset operations signal GC to collect destroyed object memory62+ */63+$entityModel->setRelations([]);64+ unset($entityModel);65+ }716672return new DocumentLoad($documentsToUpdate, $documentsToDelete);67return new DocumentLoad($documentsToUpdate, $documentsToDelete);73 }68 }</selection>” selected. Please summarize the selection using precise and concise language. Use headers and bulleted lists in the summary, to make it scannable. Maintain the meaning and factual accuracy.
You said
I’m on page “<tabTitle>Jy 20820 es reindex stream model hydration by Vasi</tabTitle>” with “<selection>@@ -5,69 +5,64 @@5namespace Jiminny\Component\ES\Processor\Actions;5namespace Jiminny\Component\ES\Processor\Actions;667use Elastica\Document;7use Elastica\Document;8-use Illuminate\Support\Collection;9use Jiminny\Component\ElasticSearch\Contract\Searchable;8use Jiminny\Component\ElasticSearch\Contract\Searchable;10use Jiminny\Component\ES\Processor\DTOs\DocumentLoad;9use Jiminny\Component\ES\Processor\DTOs\DocumentLoad;11use Jiminny\Component\ES\Processor\DTOs\SimpleCollection;10use Jiminny\Component\ES\Processor\DTOs\SimpleCollection;12use Jiminny\Component\ES\Processor\EntityQueryBuilder;11use Jiminny\Component\ES\Processor\EntityQueryBuilder;13-use Jiminny\Component\ES\Processor\Traits\SkipActivityTrait;14use Jiminny\Exceptions\SyncActivityException;12use Jiminny\Exceptions\SyncActivityException;15use Jiminny\Models\Model;13use Jiminny\Models\Model;16use Sentry\Laravel\Facade as Sentry;14use Sentry\Laravel\Facade as Sentry;15+use Throwable;171618class LoadDocumentsAction17class LoadDocumentsAction19{18{20-use SkipActivityTrait;19+public function __construct(21-20+private readonly EntityQueryBuilder $queryBuilder22-private const int RDS_CHUNK_SIZE = 250;21+ ) {22+ }232324-/**25- * @codeCoverageIgnore26- */27public function loadDocuments(string $updateTarget, array $entityIdsList): DocumentLoad24public function loadDocuments(string $updateTarget, array $entityIdsList): DocumentLoad28 {25 {29$documentsToUpdate = new SimpleCollection();26$documentsToUpdate = new SimpleCollection();30$documentsToDelete = new SimpleCollection();27$documentsToDelete = new SimpleCollection();312832-// do get mariadb data29+$query = $this->queryBuilder->getEntityQuery($updateTarget, $entityIdsList);33-$query = EntityQueryBuilder::getEntityQuery($updateTarget, $entityIdsList);343035-$query->chunkByIdDesc(31+/** @var Model&Searchable $entityModel */36-self::RDS_CHUNK_SIZE,32+foreach ($query->cursor() as $entityModel) {37-function (Collection $entityModels) use ($documentsToUpdate, $documentsToDelete) {33+if ($entityModel->isDeleted()) {38-/** @var Model&Searchable $entityForDeletion */34+/**39-foreach ($entityModels->whereNotNull('deleted_at') as $entityForDeletion) {35+ * Cleanup (from ElasticSearch) scheduled entities that were recently deleted.40-$documentsToDelete->add($entityForDeletion->getId());36+ * After a deletion, no more updates on a record are expected, so the operation is considered final,41- }37+ * unless the record is restored.42-38+ */43-/** @var Model&Searchable $entityModel */39+$documentsToDelete->add($entityModel->getId());44-foreach ($entityModels->whereNull('deleted_at') as $entityModel) {40+ } else {45-if (self::shouldSkipActivity($entityModel)) {41+try {46-/**42+$documentsToUpdate->add(47- * If the activity type is in the skip list, we should not push it for indexing.43+new Document((string) $entityModel->getId(), $entityModel->getIndexableAttributes())48- * If an ES record already exists, we should remove it to free up storage and processing power44+ );49- */45+ } catch (Throwable $error) {50-$documentsToDelete->add($entityModel->getId());46+ Sentry::captureException(51-47+new SyncActivityException(52-continue;48+'ES entity async RDS build data failed',53- }49+$error->getCode(),54-50+$error55-try {51+ )56-$documentsToUpdate->add(52+ );57-new Document((string) $entityModel->getId(), $entityModel->getIndexableAttributes())58- );59- } catch (\Throwable $error) {60- Sentry::captureException(61-new SyncActivityException(62-'ES entity async RDS build data failed',63-$error->getCode(),64-$error65- )66- );67- }68 }53 }69 }54 }70- );55+56+/**57+ * Clean up fragmented memory.58+ * Dropping relations and unsetting the entity model after hydration and usage,59+ * allows GC to remove heap memory allocations, and recycle already allocated memory,60+ * instead of allocating more memory from OS.61+ * Unset operations signal GC to collect destroyed object memory62+ */63+$entityModel->setRelations([]);64+ unset($entityModel);65+ }716672return new DocumentLoad($documentsToUpdate, $documentsToDelete);67return new DocumentLoad($documentsToUpdate, $documentsToDelete);73 }68 }</selection>” selected.
Please summarize the selection using precise and concise language. Use headers and bulleted lists in the summary, to make it scannable. Maintain the meaning and factual accuracy.
Collapse
Listen
Show more options
Show thinking
Gemini said
Gemini said
Overview
Overview
This diff refactors the
LoadDocumentsAction
class to optimize memory usage and data processing. It transitions the database query execution from batch chunking to lazy loading using cursors, introduces explicit memory management techniques, and updates the class structure to use dependency injection.
Architectural & Structural Changes
Architectural & Structural Changes
Dependency Injection:
Injected
EntityQueryBuilder
via the constructor instead of calling it statically.
Removed Traits & Constants:
Removed the
SkipActivityTrait
entirely (and its associated skip logic), as well as the
RDS_CHUNK_SIZE
constant.
Namespace Imports:
Added the
Throwable
import and removed
Illuminate\Support\Collection
.
Data Processing Refactor
Data Processing Refactor
Cursor Implementation:
Replaced
chunkByIdDesc
with
cursor()
, iterating through records sequentially rather than loading batches of 250 into memory.
Simplified Deletion Checks:
Replaced Laravel collection filtering (
whereNotNull('deleted_at')
) with a direct
$entityModel->isDeleted()
check on each model during iteration.
Removed Skip Logic:
Removed the
shouldSkipActivity
check, meaning all non-deleted models are now directly processed for updates.
Exception Handling:
Updated the catch block to use the imported
Throwable
interface rather than the fully qualified
\Throwable
.
Memory Optimization
Memory Optimization
Explicit Garbage Collection:
Added specific cleanup steps inside the iteration loop. Dropping loaded relationships (
$entityModel->setRelations([])
) and unsetting the model (
unset($entityModel)...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
24974
|
1049
|
17
|
2026-05-12T10:30:03.567661+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778581803567_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorTOOISWindo PhostormVIewINavicarecodeLaravelKeractorTOOISWindowmelpFV faVsco.js?9 JY-20725-handle-HS-search-rateProleteyC) AutomatedReportGenerated.onpM+licenses.md© AutomatedReportResult.php xM Makerillepackage-ock.isonsiphoE phpstan.neon.distnamespace Jiminny Models=nhostan-naseline.neor<$ phpunit.xmlTaraw_sqL_query.sq• use ....MLREADME.mdsonar-project.properties= test.py<> Untitled Diagram.xmlIs vetur.config.jsMI WERHOOK SIL TEPING IMDLEM› ih External Librariesv E Scratches and Consolesv C Database ConsolesV AtU& console CUllA DEAL RISKS (EU1LDIZUIAEU TEULliminnv@localhost& console liminny@localho4 D/liminny@localhostAHS_local liliminnv@localha# S= liminnv@llocalhostizoho dev liminnv@llocalhV APROD# console (PRODIA console 1 pRODI4 DI (PROD]→#OAServices+,o,cv O DatabaseV dEU&console 3 sv Aliminnv@localhostAHSocal 1s 665 msASFv APROD« console# STAGINGIconsole…Docker* Jiminnu Models Automatedreportresult* dorovertu int 51d* dorovertu strind suuld* doropertu int Sreoort id* doropertu strinalnul Sname* Goropertu int Sstatus* Goropertu int Sreason* @property string Smedia_type* anronentu intinur Snarent 7o* @property arraylnull $payload* @property Carbon|null Srequested_at* @property Carbon/null $generated_at* @property Carbon|null $sent_at* @property Carbon|null $created_at* dohopenc carboninuce supoarea_at* donoрenuehed oiminny rodels Aucomareukeport srepora* doventered Aucomareareportkesulc null sourent* @pronertu-read |Illuminate Database\Eloquent\Collection<int, AutomatedReportResult> Schildrenclass AutomatedReportResult extends ModelOutoutitiminny automated renort resultsmW 1row.GgDidIouuid WUTD with time-low and time-hiah swanned)l(• report idI nameI media typeparent idfin status( reason1 payloadI responseM requested atM aenenated at: Support Daily - in 1h 30 m100% 5• Tue 12 May 13:30:03Fixing Automated Rep+0 ..w48v1V1AV=custom.log=laravel.logA SF (jiminny@localhost]4 HS_local [jiminny@localhost]« console [PROD] X 4 console [EU]cascadeA console [STAGING]=643644[PHONE]5165.659= 65%[6587838Planhat Event PlaybadSo jiminny040 A1 A40 V 64 ^select * from activities where crm_configuration_ id = 537 and provider = 'aircall'ana account1d = 1y005058 order by updacedar descselect * from contacts where crm_confiquration id = 537 and id = 35957759selecc * rrom accouncs where crm contlquracion 10 = 55/ and 10 = 17005058rt results where 1d = 19761select * from automated reports where 1d = 5835select * from activity_searches where id = 87714:select * from activity search_filters where activity search 1d = 877141SELEC * FROM activities WHERE uuid to bind 88274672-202d-4162-9d04-73ff5f0566a9' = uuildlor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid:SELECT * FROM crm_configurations WHERE provider = 'hubspot';select * from rate Limits:select * from autolated_report_results where media type = 'pdf' and status = 2and id IN (18, 1872);select * from automated_reports where id = 54;SELECT * FROM users WHERE id IN (24623, 29443, 29613):SELECT * FROM automated_report_results WHERE uuid_to_bin(822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;Inoughttor is>kead Automatedreporiskepositorv.ohgThouaht for 3slAnalysisRoot Cause:SendReport.Job.oho:80 calls getMediaPath(Sreport) which returns null when odf url is missina from the response• Passing null to Storage::disk()->exists() triggers the Flysystem type error1. Report has media type: pdt but responsel pdt url is null2. getPdfUrl() returns null (line 374-375 in AutomatedReportResult.php)getMediaPath() returns null (line 2266-2281 in AutomatedReportsService.php)Cronjob Behavior: The send cronjob (AutomatedReportsSendCommand. php:58) uses getGeneratedNotSentResults()which filters• Reiect all | Accept allAsk anything (8 +L)<> Code SWE-1.6CSVv# Q HEAФ1872822fa41h-afd3-43a9-a248-86h0e36f3131Coaching Profiles - 6 - 12 Apr 2026 - Client Success. UK Salesodf<null>E"team Eid":1,"request 4d":"822fa41b-afd3-43a9-a248-86b0e36f3131","report type":"coaching profiiles", "media types"""pdf", "podcast™l,"from date":"2026-04-06100:00:00+00:00","to date":"2026-04-12723:59:59400:00","group Eds"A191,2),"calt deal stage"U ,"current deal stage"E"request Sidw:W822faLib-afd3-43a9-a228-86h0036f3131W,"status"•"compieted™ "timestamo":W2026-04-13101411848.648399-00:004.W93 UNWW83AVViiminny.cilient-dataV/Sf0f4810-7077-4086-8f69-93429ae4d70hV/renontsVB22faLib-afd3-43a9-a228-86h0e36f331 MD™, "nenont tvne":"coachine2026-04-13 01:00•572026-04-13 01•11•48W Windsurf Teamo...
|
NULL
|
-8899959655967195678
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorTOOISWindo PhostormVIewINavicarecodeLaravelKeractorTOOISWindowmelpFV faVsco.js?9 JY-20725-handle-HS-search-rateProleteyC) AutomatedReportGenerated.onpM+licenses.md© AutomatedReportResult.php xM Makerillepackage-ock.isonsiphoE phpstan.neon.distnamespace Jiminny Models=nhostan-naseline.neor<$ phpunit.xmlTaraw_sqL_query.sq• use ....MLREADME.mdsonar-project.properties= test.py<> Untitled Diagram.xmlIs vetur.config.jsMI WERHOOK SIL TEPING IMDLEM› ih External Librariesv E Scratches and Consolesv C Database ConsolesV AtU& console CUllA DEAL RISKS (EU1LDIZUIAEU TEULliminnv@localhost& console liminny@localho4 D/liminny@localhostAHS_local liliminnv@localha# S= liminnv@llocalhostizoho dev liminnv@llocalhV APROD# console (PRODIA console 1 pRODI4 DI (PROD]→#OAServices+,o,cv O DatabaseV dEU&console 3 sv Aliminnv@localhostAHSocal 1s 665 msASFv APROD« console# STAGINGIconsole…Docker* Jiminnu Models Automatedreportresult* dorovertu int 51d* dorovertu strind suuld* doropertu int Sreoort id* doropertu strinalnul Sname* Goropertu int Sstatus* Goropertu int Sreason* @property string Smedia_type* anronentu intinur Snarent 7o* @property arraylnull $payload* @property Carbon|null Srequested_at* @property Carbon/null $generated_at* @property Carbon|null $sent_at* @property Carbon|null $created_at* dohopenc carboninuce supoarea_at* donoрenuehed oiminny rodels Aucomareukeport srepora* doventered Aucomareareportkesulc null sourent* @pronertu-read |Illuminate Database\Eloquent\Collection<int, AutomatedReportResult> Schildrenclass AutomatedReportResult extends ModelOutoutitiminny automated renort resultsmW 1row.GgDidIouuid WUTD with time-low and time-hiah swanned)l(• report idI nameI media typeparent idfin status( reason1 payloadI responseM requested atM aenenated at: Support Daily - in 1h 30 m100% 5• Tue 12 May 13:30:03Fixing Automated Rep+0 ..w48v1V1AV=custom.log=laravel.logA SF (jiminny@localhost]4 HS_local [jiminny@localhost]« console [PROD] X 4 console [EU]cascadeA console [STAGING]=643644[PHONE]5165.659= 65%[6587838Planhat Event PlaybadSo jiminny040 A1 A40 V 64 ^select * from activities where crm_configuration_ id = 537 and provider = 'aircall'ana account1d = 1y005058 order by updacedar descselect * from contacts where crm_confiquration id = 537 and id = 35957759selecc * rrom accouncs where crm contlquracion 10 = 55/ and 10 = 17005058rt results where 1d = 19761select * from automated reports where 1d = 5835select * from activity_searches where id = 87714:select * from activity search_filters where activity search 1d = 877141SELEC * FROM activities WHERE uuid to bind 88274672-202d-4162-9d04-73ff5f0566a9' = uuildlor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid:SELECT * FROM crm_configurations WHERE provider = 'hubspot';select * from rate Limits:select * from autolated_report_results where media type = 'pdf' and status = 2and id IN (18, 1872);select * from automated_reports where id = 54;SELECT * FROM users WHERE id IN (24623, 29443, 29613):SELECT * FROM automated_report_results WHERE uuid_to_bin(822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;Inoughttor is>kead Automatedreporiskepositorv.ohgThouaht for 3slAnalysisRoot Cause:SendReport.Job.oho:80 calls getMediaPath(Sreport) which returns null when odf url is missina from the response• Passing null to Storage::disk()->exists() triggers the Flysystem type error1. Report has media type: pdt but responsel pdt url is null2. getPdfUrl() returns null (line 374-375 in AutomatedReportResult.php)getMediaPath() returns null (line 2266-2281 in AutomatedReportsService.php)Cronjob Behavior: The send cronjob (AutomatedReportsSendCommand. php:58) uses getGeneratedNotSentResults()which filters• Reiect all | Accept allAsk anything (8 +L)<> Code SWE-1.6CSVv# Q HEAФ1872822fa41h-afd3-43a9-a248-86h0e36f3131Coaching Profiles - 6 - 12 Apr 2026 - Client Success. UK Salesodf<null>E"team Eid":1,"request 4d":"822fa41b-afd3-43a9-a248-86b0e36f3131","report type":"coaching profiiles", "media types"""pdf", "podcast™l,"from date":"2026-04-06100:00:00+00:00","to date":"2026-04-12723:59:59400:00","group Eds"A191,2),"calt deal stage"U ,"current deal stage"E"request Sidw:W822faLib-afd3-43a9-a228-86h0036f3131W,"status"•"compieted™ "timestamo":W2026-04-13101411848.648399-00:004.W93 UNWW83AVViiminny.cilient-dataV/Sf0f4810-7077-4086-8f69-93429ae4d70hV/renontsVB22faLib-afd3-43a9-a228-86h0e36f331 MD™, "nenont tvne":"coachine2026-04-13 01:00•572026-04-13 01•11•48W Windsurf Teamo...
|
24972
|
NULL
|
NULL
|
NULL
|
|
25386
|
1067
|
27
|
2026-05-12T11:16:48.157282+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778584608157_m2.jpg...
|
Firefox
|
[JY-20776] Automated report - sentry - Jira — Work
|
True
|
jiminny.atlassian.net/browse/JY-20776
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
[JY-20776] Automated report - sentry - Jira
jiminn [JY-20776] Automated report - sentry - Jira
jiminny.atlassian.net
Unnamed Group
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
CloudWatch | us-east-2
CloudWatch | us-east-2
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Close tab
Jiminny
Jiminny
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
Platform Team - Backlog - Jira
Platform Team - Backlog - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Sidebar
Sidebar
Top Bar
Top Bar
Main Content
Main Content
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)
Teams
, (opens new window)
open menu
open menu
Customise sidebar
Customise sidebar
Resize side navigation panel
Spaces
Spaces
/
Jiminny (New) Jiminny (New)
Jiminny (New)
/
Epic - Change parent
JY-18631
JY-18631
/
Bug - Change work type
JY-20776
JY-20776
Copy link
Automated report - sentry- Summary, edit
Automated report - sentry
Automated report - sentry
Add or create work related to this Bug
Add or create work related to this Bug
View app actions
View app actions
Collapse Key details Key details
Collapse Key details
Collapse Key details
Key details
Description
Description
Edit Description, edit
We still get
Sentry
Sentry
error when attempting to send report result without pdf url.
We need to mark such a report as failed so it is not picked up for sending again in one hour.
fix the issue so we can have a generated report for the customer
Steps to reproduce
Steps to reproduce
More information about
Edit Steps to reproduce, edit
None
Actual outcome
More information about
Edit Actual outcome
Add text
Expected outcome
More information about
Edit Expected outcome
Add text
Subtasks
Subtasks
Add subtask
Add subtask
Linked work items
Linked work items
Add linked work item
Add linked work item
Collapse Activity Activity
Collapse Activity
Collapse Activity
Activity
All
All
Comments
Comments...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"bounds":{"left":0.08361037,"top":0.28451717,"width":0.080119684,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"jiminny.atlassian.net","depth":4,"bounds":{"left":0.08361037,"top":0.2952913,"width":0.03507314,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Unnamed Group","depth":4,"bounds":{"left":0.0028257978,"top":0.057063047,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"bounds":{"left":0.0028257978,"top":0.08060654,"width":0.07679521,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"bounds":{"left":0.015957447,"top":0.09217877,"width":0.40492022,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"CloudWatch | us-east-2","depth":4,"bounds":{"left":0.0028257978,"top":0.11332801,"width":0.07679521,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"CloudWatch | us-east-2","depth":5,"bounds":{"left":0.015957447,"top":0.12490024,"width":0.04138963,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":4,"bounds":{"left":0.0,"top":0.14604948,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.15762171,"width":0.16140293,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.17877094,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.19034317,"width":0.18816489,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"bounds":{"left":0.0,"top":0.21149242,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.22306465,"width":0.1200133,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.2442139,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.25578612,"width":0.1931516,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"bounds":{"left":0.0,"top":0.27693537,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.28850758,"width":0.07646277,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.06732048,"top":0.28411812,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"Jiminny","depth":4,"bounds":{"left":0.0,"top":0.30965683,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny","depth":5,"bounds":{"left":0.013297873,"top":0.32122904,"width":0.013131649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.3423783,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.35395053,"width":0.18816489,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":4,"bounds":{"left":0.0,"top":0.37509975,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[SRD-6793] Les Mills activity types not pulling in - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.386672,"width":0.09524601,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Platform Team - Backlog - Jira","depth":4,"bounds":{"left":0.0,"top":0.40782124,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Team - Backlog - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.41939345,"width":0.053025264,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"bounds":{"left":0.0,"top":0.4405427,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.4521149,"width":0.1200133,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.47326416,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.4848364,"width":0.1931516,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.5059856,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.51755786,"width":0.039228722,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"bounds":{"left":0.0,"top":0.5387071,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"bounds":{"left":0.013297873,"top":0.5502793,"width":0.18816489,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.57302475,"width":0.07413564,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to:","depth":9,"bounds":{"left":0.090259306,"top":0.07861133,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Sidebar","depth":10,"bounds":{"left":0.090259306,"top":0.097765364,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Sidebar","depth":11,"bounds":{"left":0.090259306,"top":0.097765364,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Top Bar","depth":10,"bounds":{"left":0.090259306,"top":0.11691939,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Top Bar","depth":11,"bounds":{"left":0.090259306,"top":0.11691939,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Main Content","depth":10,"bounds":{"left":0.090259306,"top":0.13607343,"width":0.029421542,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Main Content","depth":11,"bounds":{"left":0.090259306,"top":0.13607343,"width":0.029421542,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Collapse sidebar [","depth":9,"bounds":{"left":0.08361037,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Collapse sidebar [","depth":11,"bounds":{"left":0.0887633,"top":0.06344773,"width":0.039727394,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Switch sites or apps","depth":10,"bounds":{"left":0.095578454,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Switch sites or apps","depth":12,"bounds":{"left":0.10073138,"top":0.06344773,"width":0.044215426,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Go to your Jira homepage","depth":9,"bounds":{"left":0.10887633,"top":0.057861134,"width":0.029421542,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXComboBox","text":"Search, press enter to navigate to advanced search with your text query","depth":11,"bounds":{"left":0.20694813,"top":0.06264964,"width":0.13829787,"height":0.015961692},"on_screen":true,"help_text":"","placeholder":"Search","role_description":"combo box","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Create","depth":10,"bounds":{"left":0.35355717,"top":0.057861134,"width":0.030086435,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create","depth":12,"bounds":{"left":0.3648604,"top":0.06384677,"width":0.014793883,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Rovo Ask Rovo","depth":12,"bounds":{"left":0.41206783,"top":0.057861134,"width":0.036070477,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ask Rovo","depth":14,"bounds":{"left":0.42337102,"top":0.06384677,"width":0.020777926,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Notifications","depth":12,"bounds":{"left":0.44946808,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Notifications","depth":14,"bounds":{"left":0.45462102,"top":0.06344773,"width":0.027759308,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Help","depth":12,"bounds":{"left":0.46143618,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Help","depth":14,"bounds":{"left":0.4665891,"top":0.06344773,"width":0.010139627,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Settings","depth":12,"bounds":{"left":0.47340426,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Settings","depth":14,"bounds":{"left":0.47855717,"top":0.06344773,"width":0.017952127,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"lukas.kovalik@jiminny.com","depth":12,"bounds":{"left":0.48537233,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"lukas.kovalik@jiminny.com","depth":14,"bounds":{"left":0.49052528,"top":0.06344773,"width":0.05867686,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"For you","depth":12,"bounds":{"left":0.08361037,"top":0.09976058,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"For you","depth":15,"bounds":{"left":0.09424867,"top":0.10574621,"width":0.01662234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Recent","depth":12,"bounds":{"left":0.08361037,"top":0.12529927,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Recent","depth":15,"bounds":{"left":0.09424867,"top":0.13128492,"width":0.015458777,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Starred","depth":12,"bounds":{"left":0.08361037,"top":0.15083799,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Starred","depth":15,"bounds":{"left":0.09424867,"top":0.15682362,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Apps","depth":12,"bounds":{"left":0.08361037,"top":0.1763767,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Apps","depth":15,"bounds":{"left":0.09424867,"top":0.18236233,"width":0.011635638,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Apps","depth":13,"bounds":{"left":0.15309176,"top":0.17956904,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Apps","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Spaces","depth":12,"bounds":{"left":0.08361037,"top":0.2019154,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXStaticText","text":"Spaces","depth":15,"bounds":{"left":0.09424867,"top":0.20790103,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Create space","depth":13,"bounds":{"left":0.13646941,"top":0.20510775,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create space","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for spaces","depth":13,"bounds":{"left":0.14577793,"top":0.20510775,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for spaces","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Recent","depth":16,"bounds":{"left":0.08959442,"top":0.23423783,"width":0.013464096,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Jiminny (New)","depth":17,"bounds":{"left":0.08759973,"top":0.2529928,"width":0.0674867,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny (New)","depth":20,"bounds":{"left":0.09823803,"top":0.25897846,"width":0.032081116,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Jiminny (New)","depth":18,"bounds":{"left":0.08892952,"top":0.25618514,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXMenuButton","text":"Create board","depth":18,"bounds":{"left":0.13646941,"top":0.25618514,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Create board","depth":20,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Jiminny (New)","depth":18,"bounds":{"left":0.14577793,"top":0.25618514,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Jiminny (New)","depth":20,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Platform Team","depth":19,"bounds":{"left":0.09158909,"top":0.27853152,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Team","depth":22,"bounds":{"left":0.1022274,"top":0.28451717,"width":0.032247342,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":20,"bounds":{"left":0.15309176,"top":0.28172386,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":22,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Capture Team","depth":19,"bounds":{"left":0.09158909,"top":0.30407023,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Capture Team","depth":22,"bounds":{"left":0.1022274,"top":0.31005585,"width":0.03125,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":20,"bounds":{"left":0.15309176,"top":0.30726257,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":22,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Enterprise Stability Issues 🤕","depth":19,"bounds":{"left":0.09158909,"top":0.32960895,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Enterprise Stability Issues 🤕","depth":22,"bounds":{"left":0.1022274,"top":0.33559456,"width":0.050531916,"height":0.030726258},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":20,"bounds":{"left":0.15309176,"top":0.33280128,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":22,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Processing Team","depth":19,"bounds":{"left":0.09158909,"top":0.35514766,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Processing Team","depth":22,"bounds":{"left":0.1022274,"top":0.36113328,"width":0.038231384,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":20,"bounds":{"left":0.15309176,"top":0.35834,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":22,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SE Kanban","depth":19,"bounds":{"left":0.09158909,"top":0.38068634,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SE Kanban","depth":22,"bounds":{"left":0.1022274,"top":0.386672,"width":0.024102394,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":20,"bounds":{"left":0.15309176,"top":0.38387868,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":22,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Service-Desk","depth":17,"bounds":{"left":0.08759973,"top":0.40622506,"width":0.0674867,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Service-Desk","depth":20,"bounds":{"left":0.09823803,"top":0.4122107,"width":0.03025266,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Service-Desk","depth":18,"bounds":{"left":0.15442154,"top":0.4094174,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Service-Desk","depth":20,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"More spaces","depth":17,"bounds":{"left":0.08759973,"top":0.43176377,"width":0.0674867,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More spaces","depth":20,"bounds":{"left":0.09823803,"top":0.43774942,"width":0.028756648,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Filters","depth":12,"bounds":{"left":0.08361037,"top":0.45730248,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Filters","depth":15,"bounds":{"left":0.09424867,"top":0.4632881,"width":0.013796543,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Filters","depth":13,"bounds":{"left":0.15309176,"top":0.46049482,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Filters","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dashboards","depth":12,"bounds":{"left":0.08361037,"top":0.4828412,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Dashboards","depth":15,"bounds":{"left":0.09424867,"top":0.4888268,"width":0.026761968,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Create dashboard","depth":13,"bounds":{"left":0.15508644,"top":0.48603353,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create dashboard","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Dashboards","depth":13,"bounds":{"left":0.16240026,"top":0.48603353,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Dashboards","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Operations","depth":12,"bounds":{"left":0.08361037,"top":0.5083799,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Operations","depth":15,"bounds":{"left":0.09424867,"top":0.5143655,"width":0.02443484,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Operations","depth":13,"bounds":{"left":0.15309176,"top":0.51157224,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Operations","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Confluence , (opens new window)","depth":13,"bounds":{"left":0.08361037,"top":0.5434956,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Confluence","depth":17,"bounds":{"left":0.09424867,"top":0.5494813,"width":0.025764627,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", (opens new window)","depth":15,"bounds":{"left":0.08361037,"top":0.55706304,"width":0.04837101,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Teams , (opens new window)","depth":13,"bounds":{"left":0.08361037,"top":0.56903434,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Teams","depth":17,"bounds":{"left":0.09424867,"top":0.57501996,"width":0.014793883,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", (opens new window)","depth":15,"bounds":{"left":0.08361037,"top":0.5826017,"width":0.04837101,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"open menu","depth":14,"bounds":{"left":0.14378324,"top":0.57222664,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"open menu","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Customise sidebar","depth":12,"bounds":{"left":0.08361037,"top":0.60415006,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Customise sidebar","depth":15,"bounds":{"left":0.09424867,"top":0.6101357,"width":0.04155585,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Resize side navigation panel","depth":13,"bounds":{"left":0.2109375,"top":0.0981644,"width":0.062333778,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Spaces","depth":15,"bounds":{"left":0.17137633,"top":0.10933759,"width":0.013962766,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Spaces","depth":17,"bounds":{"left":0.17137633,"top":0.11292897,"width":0.013962766,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":15,"bounds":{"left":0.18716756,"top":0.11173184,"width":0.0016622341,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Jiminny (New) Jiminny (New)","depth":15,"bounds":{"left":0.19265293,"top":0.10933759,"width":0.034574468,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny (New)","depth":17,"bounds":{"left":0.19996676,"top":0.11292897,"width":0.027260639,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":15,"bounds":{"left":0.22905585,"top":0.11173184,"width":0.0016622341,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Epic - Change parent","depth":15,"bounds":{"left":0.23254654,"top":0.10933759,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"JY-18631","depth":15,"bounds":{"left":0.24052526,"top":0.10933759,"width":0.017121011,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-18631","depth":17,"bounds":{"left":0.24052526,"top":0.11292897,"width":0.017121011,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":15,"bounds":{"left":0.25947472,"top":0.11173184,"width":0.0016622341,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Bug - Change work type","depth":15,"bounds":{"left":0.2629654,"top":0.10933759,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"JY-20776","depth":15,"bounds":{"left":0.27094415,"top":0.10933759,"width":0.018284574,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20776","depth":17,"bounds":{"left":0.27094415,"top":0.11292897,"width":0.018284574,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy link","depth":16,"bounds":{"left":0.28789893,"top":0.11213089,"width":0.005319149,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Automated report - sentry- Summary, edit","depth":11,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Automated report - sentry","depth":11,"bounds":{"left":0.17204122,"top":0.1396648,"width":0.09823803,"height":0.022346368},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Automated report - sentry","depth":12,"bounds":{"left":0.17204122,"top":0.13926576,"width":0.09823803,"height":0.023543496},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Add or create work related to this Bug","depth":12,"bounds":{"left":0.17137633,"top":0.17158818,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Add or create work related to this Bug","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"View app actions","depth":12,"bounds":{"left":0.1846742,"top":0.17158818,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"View app actions","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Collapse Key details Key details","depth":11,"bounds":{"left":0.16339761,"top":0.20989625,"width":0.22057846,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXButton","text":"Collapse Key details","depth":13,"bounds":{"left":0.16206782,"top":0.21308859,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Collapse Key details","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Key details","depth":14,"bounds":{"left":0.17137633,"top":0.2150838,"width":0.02825798,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Description","depth":12,"bounds":{"left":0.17137633,"top":0.2386273,"width":0.025598405,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Description","depth":13,"bounds":{"left":0.17137633,"top":0.23902634,"width":0.025598405,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Edit Description, edit","depth":13,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"We still get","depth":14,"bounds":{"left":0.17204122,"top":0.26097366,"width":0.02543218,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Sentry","depth":14,"bounds":{"left":0.1974734,"top":0.26097366,"width":0.014461436,"height":0.01396648},"on_screen":true,"help_text":"https://jiminny.sentry.io/issues/6873095751?project=82419","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Sentry","depth":15,"bounds":{"left":0.1974734,"top":0.26097366,"width":0.014461436,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"error when attempting to send report result without pdf url.","depth":14,"bounds":{"left":0.21193483,"top":0.26097366,"width":0.13081782,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"We need to mark such a report as failed so it is not picked up for sending again in one hour.","depth":16,"bounds":{"left":0.18001994,"top":0.2897047,"width":0.2009641,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"fix the issue so we can have a generated report for the customer","depth":16,"bounds":{"left":0.18001994,"top":0.3120511,"width":0.14212102,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Steps to reproduce","depth":12,"bounds":{"left":0.17137633,"top":0.349162,"width":0.042386968,"height":0.015163607},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Steps to reproduce","depth":13,"bounds":{"left":0.17137633,"top":0.3499601,"width":0.042386968,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More information about","depth":12,"bounds":{"left":0.21509309,"top":0.35115722,"width":0.005319149,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit Steps to reproduce, edit","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"None","depth":13,"bounds":{"left":0.17137633,"top":0.37310454,"width":0.011801862,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Actual outcome","depth":13,"bounds":{"left":0.17137633,"top":0.4177973,"width":0.034906916,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More information about","depth":12,"bounds":{"left":0.21027261,"top":0.41859537,"width":0.005319149,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit Actual outcome","depth":13,"bounds":{"left":0.25648272,"top":0.424581,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add text","depth":14,"bounds":{"left":0.25648272,"top":0.4177973,"width":0.018450798,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Expected outcome","depth":13,"bounds":{"left":0.17137633,"top":0.45610535,"width":0.04155585,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More information about","depth":12,"bounds":{"left":0.21692154,"top":0.45690343,"width":0.005319149,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit Expected outcome","depth":13,"bounds":{"left":0.25648272,"top":0.46288908,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add text","depth":14,"bounds":{"left":0.25648272,"top":0.45610535,"width":0.018450798,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Subtasks","depth":11,"bounds":{"left":0.17137633,"top":0.50957704,"width":0.023936171,"height":0.015961692},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Subtasks","depth":12,"bounds":{"left":0.17137633,"top":0.50957704,"width":0.023936171,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add subtask","depth":12,"bounds":{"left":0.16738696,"top":0.5303272,"width":0.035405584,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add subtask","depth":14,"bounds":{"left":0.17137633,"top":0.5359138,"width":0.027426861,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Linked work items","depth":11,"bounds":{"left":0.17137633,"top":0.57501996,"width":0.04654255,"height":0.015961692},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Linked work items","depth":12,"bounds":{"left":0.17137633,"top":0.57501996,"width":0.04654255,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add linked work item","depth":12,"bounds":{"left":0.16738696,"top":0.59417397,"width":0.05418883,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add linked work item","depth":14,"bounds":{"left":0.17137633,"top":0.5997606,"width":0.046210106,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Collapse Activity Activity","depth":12,"bounds":{"left":0.16339761,"top":0.6388667,"width":0.22057846,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXButton","text":"Collapse Activity","depth":14,"bounds":{"left":0.16206782,"top":0.6420591,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Collapse Activity","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Activity","depth":15,"bounds":{"left":0.17137633,"top":0.64365524,"width":0.019946808,"height":0.01556265},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All","depth":14,"bounds":{"left":0.17237367,"top":0.669992,"width":0.01412899,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"radio button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All","depth":16,"bounds":{"left":0.17669548,"top":0.67318434,"width":0.005485372,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Comments","depth":14,"bounds":{"left":0.18716756,"top":0.669992,"width":0.032413565,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"radio button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Comments","depth":16,"bounds":{"left":0.19148937,"top":0.67318434,"width":0.023769947,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-8899612689302358520
|
6219984736995864740
|
visual_change
|
accessibility
|
NULL
|
[JY-20776] Automated report - sentry - Jira
jiminn [JY-20776] Automated report - sentry - Jira
jiminny.atlassian.net
Unnamed Group
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
CloudWatch | us-east-2
CloudWatch | us-east-2
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
Close tab
Jiminny
Jiminny
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
[SRD-6793] Les Mills activity types not pulling in - Jira
[SRD-6793] Les Mills activity types not pulling in - Jira
Platform Team - Backlog - Jira
Platform Team - Backlog - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking for automated report generated by LakyLak · Pull Request #12024 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Sidebar
Sidebar
Top Bar
Top Bar
Main Content
Main Content
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)
Teams
, (opens new window)
open menu
open menu
Customise sidebar
Customise sidebar
Resize side navigation panel
Spaces
Spaces
/
Jiminny (New) Jiminny (New)
Jiminny (New)
/
Epic - Change parent
JY-18631
JY-18631
/
Bug - Change work type
JY-20776
JY-20776
Copy link
Automated report - sentry- Summary, edit
Automated report - sentry
Automated report - sentry
Add or create work related to this Bug
Add or create work related to this Bug
View app actions
View app actions
Collapse Key details Key details
Collapse Key details
Collapse Key details
Key details
Description
Description
Edit Description, edit
We still get
Sentry
Sentry
error when attempting to send report result without pdf url.
We need to mark such a report as failed so it is not picked up for sending again in one hour.
fix the issue so we can have a generated report for the customer
Steps to reproduce
Steps to reproduce
More information about
Edit Steps to reproduce, edit
None
Actual outcome
More information about
Edit Actual outcome
Add text
Expected outcome
More information about
Edit Expected outcome
Add text
Subtasks
Subtasks
Add subtask
Add subtask
Linked work items
Linked work items
Add linked work item
Add linked work item
Collapse Activity Activity
Collapse Activity
Collapse Activity
Activity
All
All
Comments
Comments...
|
25385
|
NULL
|
NULL
|
NULL
|
|
12703
|
562
|
5
|
2026-05-09T09:25:07.589193+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-09/1778 /Users/lukas/.screenpipe/data/data/2026-05-09/1778318707589_m2.jpg...
|
Code
|
Design new payment-logge… — finance [SSH: nas]
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: finance [SSH: nas]
Explorer Section: finance [SSH: nas]
FINANCE [SSH: NAS]
auth
dsk-uploader
finance-hub
backend
frontend
.env
.env.example
.gitignore
docker-compose.yml
payments-logger
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
docker-compose.yml, Editor Group 1
.env, Editor Group 1
report(1).csv, Editor Group 1
report(2).csv, Editor Group 1
"Дата","Основание","Наредител/Получател","Номер сметка на наредителя / получателя","Вид на трансакцията","Свързваща референция","Валутен курс","Сума във валутата на превода","Дебит EUR","Кредит EUR"
"08.05.2026","400915xxxxxx4447 ПОС 08.05.2026 18:46<br/>Авт. код: 36C1UB","","","","","","","5,02",""
"08.05.2026","400915xxxxxx4447 ПОС 08.05.2026 18:36<br/>Авт. код: 5CK9TB","","","","","","","15,46",""
"08.05.2026","400915xxxxxx4447 ПОС 08.05.2026 18:07<br/>Авт. код: 8FK4TB","","","","","","","9,04",""
"08.05.2026","400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 08.05.2026 19:32<br/>Авт. код: 6NM31C<br/>Номер на у-во: V5411421","BGR SOFIYA LIDL BALGARIYA EOOD UL TODOR","","КАРТОВА ОПЕРАЦИЯ","","","","67,81",""
"08.05.2026","400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 07.05.2026 09:02<br/>Авт. код: 7NE8N7<br/>Номер на у-во: 10101446","BGR SOFIA CBA EKO MARKET","","КАРТОВА ОПЕРАЦИЯ","","","","5,51",""
"08.05.2026","400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 06.05.2026 13:16<br/>Авт. код: 1TV886<br/>Номер на у-во: 06030191","POL BALICE Lagardere Travel R KR3","","КАРТОВА ОПЕРАЦИЯ","","","","5,49",""
"Дата","Основание","Наредител/Получател","Номер сметка на наредителя / получателя","Вид на трансакцията","Свързваща референция","Валутен курс","Сума във валутата на превода","Дебит EUR","Кредит EUR"
"08.05.2026","400915xxxxxx4447 ПОС 08.05.2026 18:46<br/>Авт. код: 36C1UB","","","","","","","5,02",""
"08.05.2026","400915xxxxxx4447 ПОС 08.05.2026 18:36<br/>Авт. код: 5CK9TB","","","","","","","15,46",""
"08.05.2026","400915xxxxxx4447 ПОС 08.05.2026 18:07<br/>Авт. код: 8FK4TB","","","","","","","9,04",""
"08.05.2026","400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 08.05.2026 19:32<br/>Авт. код: 6NM31C<br/>Номер на у-во: V5411421","BGR SOFIYA LIDL BALGARIYA EOOD UL TODOR","","КАРТОВА ОПЕРАЦИЯ","","","","67,81",""
"08.05.2026","400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 07.05.2026 09:02<br/>Авт. код: 7NE8N7<br/>Номер на у-во: 10101446","BGR SOFIA CBA EKO MARKET","","КАРТОВА ОПЕРАЦИЯ","","","","5,51",""
"08.05.2026","400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 06.05.2026 13:16<br/>Авт. код: 1TV886<br/>Номер на у-во: 06030191","POL BALICE Lagardere Travel R KR3","","КАРТОВА ОПЕРАЦИЯ","","","","5,49",""
Design new payment-logge…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports
PORTS
remote SSH: nas
SSH: nas
No Problems
0
0
No Ports Forwarded
0
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Design new payment-logger and dsk-uploader hybrid app
Session history
New session
Message actions
payments.js
payments.js
ets create a new app that should be combination of payment-logger and dsk-uploader. It should have authorization via authentik (auth folder). All three folders (payment-logger, dsk-uploader and auth) are just refference these will be removed later. Auth project is separated it lives on its own. First reveiw them and see how these should be combined. It will be whole new app (also the folder name). Think very carefully of whatr these two apps do and how cold they be combined. THerer should be common db and uploader should store data the same way the /ingest does. It should be properly marked in UI if it is upload or ingest or both. FIrst think of tech stack and plan carefully.
Thinking
Thinking
I'll explore all three reference projects in parallel to understand their structure before planning.
Agent:
Explore payment-logger app structure and functionality
IN
Explore the /volume2/docker/finance/payments-logger directory thoroughly. I need to understand:
1. Tech stack (frontend framework, backend framework, database)
2. What the app does - its core purpose and features
3. Database schema - all tables, relationships
4. API routes - all endpoints, their purpose and request/response shapes
5. How data flows (ingest, storage, retrieval)
6. Any auth/middleware setup
7. Docker/deployment setup
8. Key files and their roles
Report with: directory structure, tech stack summary, database schema details, all API endpoints listed, UI features, and how data is ingested. Be thorough on the /ingest endpoint specifically.
Agent:
Explore dsk-uploader app structure and functionality
IN
Explore the /volume2/docker/finance/dsk-uploader directory thoroughly. I need to understand:
1. Tech stack (frontend framework, backend framework, database)
2. What the app does - its core purpose and features
3. Database schema - all tables, relationships
4. API routes - all endpoints, their purpose and request/response shapes
5. How file uploads work - what files, what format, how parsed
6. Any auth/middleware setup
7. Docker/deployment setup
8. Key files and their roles
Report with: directory structure, tech stack summary, database schema details, all API endpoints listed, UI features, how file upload parsing works, and what data is extracted from uploads. Be very thorough about the data model and what fields are stored.
Agent:
Explore auth folder and authentik integration
IN
Explore the /volume2/docker/finance/auth directory thoroughly. I need to understand:
1. What auth system is being used (Authentik)
2. How authentication/authorization is implemented - OAuth2, OIDC, session tokens, JWT?
3. Middleware - how is auth enforced on routes?
4. Any frontend auth flow - login redirect, token handling, protected routes
5. Environment variables related to auth
6. Docker/deployment setup for the auth service itself
7. Any example of how other apps integrate with this auth
Report with: directory structure, auth flow description, middleware code, environment variable names, and integration patterns. This is a reference for building auth into a new combined app.
Message actions
payments.js
payments.js
continue
Thinking
Thinking
Launching parallel exploration of all three reference projects.
Agent:
Explore payment-logger app structure
IN
Explore the /volume2/docker/finance/payments-logger directory thoroughly. I need to understand:
1. Full directory structure (tree)
2. Tech stack (frontend framework, backend framework, database, etc.)
3. What the app does - its core functionality
4. How data is ingested (look at /ingest route especially)
5. Database schema (what tables/collections exist)
6. API routes and their purposes
7. Frontend pages and components
8. Docker setup
9. Any auth/session handling
Please read key files: package.json files, main entry points, route files, database schema files, docker-compose.yml, and any frontend components. Be thorough - I need a complete picture to plan a new combined app.
Agent:
Explore dsk-uploader app structure
IN
Explore the /volume2/docker/finance/dsk-uploader directory thoroughly. I need to understand:
1. Full directory structure (tree)
2. Tech stack (frontend framework, backend framework, database, etc.)
3. What the app does - its core functionality
4. How file uploads work (what format, what processing happens)
5. Database schema (what tables/collections exist)
6. API routes and their purposes
7. Frontend pages and components
8. Docker setup
9. Any auth/session handling
Please read key files: package.json files, main entry points, route files, database schema files, docker-compose.yml, and any frontend components. Be thorough - I need a complete picture to plan a new combined app.
Agent:
Explore auth project structure
IN
Explore the /volume2/docker/finance/auth directory thoroughly. I need to understand:
1. Full directory structure (tree)
2. What authentication system is used (authentik integration details)
3. How OAuth/OIDC flow works
4. Any middleware or helper functions for protecting routes
5. Session management approach
6. How frontend handles auth state
7. Any example of how other apps integrate with this auth system
8. Docker setup and environment variables
Please read all files carefully - this auth system will be integrated into a new combined app. I need to understand exactly how to use it.
Thinking
Thinking
Now I have a complete picture of all three. Let me design the plan.
Agent:
Design combined app architecture
IN
Design a detailed implementation plan for a new combined web app that merges two existing applications. Here is full context:
---
## App 1: payments-logger (Node.js)
- **Purpose**: Ingest Bulgarian bank SMS → parse → store in PostgreSQL → review/tag/notify in React UI
- **Stack**: Node.js 20 + Express + Prisma 5 + PostgreSQL 16 + React 18 + Vite + Tailwind CSS
- **Auth**: Custom JWT (bcryptjs + jose), tokens in localStorage
- **Ingest**: POST /api/payments/ingest (public) — SMS text OR structured JSON (Apple Wallet)
- **Schema** (PostgreSQL via Prisma):
- `payments` table: id, raw_message, date, type (POS/ATM/INTERNET/ECOM/P2P/WALLET), card, recipient, amount, balance, status (UNPROCESSED/SENT/SKIPPED), notifyPhone, notifiedAt, created_at, updated_at
- `tags` table: id, name, color — M2M with payments via `_PaymentToTag`
- `users` table: id, username, hashed_password (this will be REMOVED)
- **UI**: Single-page React app — PaymentTable (sortable, filterable, taggable), FilterBar, status actions (send/skip), notification system
- **Parser** (backend/src/parser.js): Regex parser for Bulgarian DSK Bank SMS, extracts date/time (DD/MM/YYYY HH:MM), card mask, transaction type, recipient, amount, balance
## App 2: dsk-uploader (Python/Flask)
- **Purpose**: Upload DSK bank CSV exports → parse/normalize → upload to Notion database
- **Stack**: Python 3.11 + Flask + Pandas + Custom Notion SDK + Bootstrap 5
- **Auth**: None (open)
- **CSV format** (DSK Bank Bulgarian format, columns):
- `Дата` (date, DD.MM.YYYY)
- `Вид на трансакцията` (transaction type, Bulgarian)
- `Основание` (reason/description — contains card number regex: `^\d{6}x{6}\d{4}$`)
- `Дебит BGN` (debit amount, may be empty)
- `Кредит BGN` (credit amount, may be empty)
- `Наредител/Получател` (orderer/recipient name)
- `Номер сметка на наредителя / получателя` (account number)
- **Processing**: merge multiple CSVs, normalize dates, extract card numbers from reason via regex, auto-generate tags (keyword heuristics: ЗАПЛАТА→Salary, NETFLIX→Subscriptions, etc.), filter internal transfers
- **Output**: Notion database pages (this will be REPLACED with local PostgreSQL)
## App 3: auth (Authentik)
- **Mode**: Proxy mode via NPM (forward auth)
- **How it works**: NPM intercepts all requests, calls Authentik outpost's auth endpoint. On success, NPM injects headers into proxied request:
- `X-authentik-username`
- `X-authentik-email`
- `X-authentik-groups`
- **No code integration needed** in the app itself — just trust these headers from NPM
- **Logout**: Redirect user to `/outpost.goauthentik.io/sign_out`
---
## What the New Combined App Must Do
1. **Single PostgreSQL database** for all transactions
2. **SMS Ingest** (public endpoint) — same as payments-logger /ingest, source=INGEST
3. **CSV Upload** (authenticated) — parse DSK CSV files, store transactions with source=UPLOAD into the same DB schema
4. **Unified UI** — show all payments in one table regardless of source
5. **Source indicator** in UI — badge showing "INGEST", "UPLOAD", or "BOTH" (if duplicate detection added)
6. **Source filter** in FilterBar — filter by All/Ingest/Upload
7. **Authentik auth** — remove custom JWT entirely; trust X-authentik-* headers from NPM
8. **All existing payments-logger features**: tagging, send notification, skip, status tracking, filters, sorting, pagination
9. **Remove Notion dependency entirely** — all data stays in local PostgreSQL
---
## Technical Constraints
- Keep Node.js backend (not Python) — extend payments-logger's stack
- CSV parsing: use `csv-parse` npm package (handles encoding, streaming)
- File upload: use `multer` npm middleware
- Auth middleware: read X-authentik-username header, 401 if missing (but /api/payments/ingest stays public)
- Add `source` enum field (INGEST/UPLOAD) to payments schema
- Add `currency` field (EUR for SMS ingest, BGN for CSV uploads) — or just store as-is
- Remove `users` table from schema
---
## Please Design
1. Complete folder structure for the new app (suggest name: "finance-hub" or similar — note this is a placeholder, user will confirm)
2. Prisma schema with new fields (`source`, `currency`)
3. All backend API routes
4. CSV parser design in Node.js (port of the Python logic)
5. Authentik auth middleware
6. Frontend component structure (React) — what's reused from payments-logger, what's new
7. Docker Compose setup
8. Environment variables needed
9. Step-by-step implementation order
10. Verification checklist
Be specific about file paths and code structure. This will be used to write actual implementation code.
Thinking
Thinking
Good plan. Let me ask the one thing I need before writing the final plan.
AskUserQuestion
OUT
User has answered your questions: "What should the new combined app be called? This will be the folder name under /volume2/docker/finance/ and the app branding."="finance-hub". You can now continue with the user's answers in mind.
Thinking
Thinking
Writing the final plan now.
Thinking
Thinking
Write
ets-create-a-new-generic-sun.md
ets-create-a-new-generic-sun.md
326 lines
# finance-hub — Implementation Plan
## Context
Two separate finance apps exist as references and will be retired:
- **payments-logger** (Node.js/React): Ingests Bulgarian bank SMS notifications, stores payments in PostgreSQL, provides a review/tag/notify UI.
- **dsk-uploader** (Python/Flask): Uploads DSK bank CSV exports, parses them, sends to Notion.
The new app **finance-hub** (`/volume2/docker/finance/finance-hub/`) replaces both with a single unified system: all transactions (SMS-ingested and CSV-uploaded) land in one PostgreSQL database and one React UI. Authentication is delegated entirely to Authentik (proxy mode via NPM — no custom JWT).
---
## Tech Stack
| Layer | Choice | Rationale |
|-------|--------|-----------|
| Backend | Node.js 20 + Express + Prisma 5 | Extends payments-logger; no Python needed |
| CSV parsing | `csv-parse` + `iconv-lite` | Handles Bulgarian cp1251 encoding, RFC4180 quoted fields |
| File upload | `multer` (memory storage) | Bank CSVs are small (<500 KB), no disk cleanup needed |
| Database | PostgreSQL 16 | Same as payments-logger |
| Frontend | React 18 + Vite + Tailwind CSS + Lucide React | Same as payments-logger |
| Auth | Authentik proxy via NPM headers | No custom JWT; removes users table entirely |
| Container | Docker Compose | Same pattern as both reference apps |
**Removed vs payments-logger:** `bcryptjs`, `jose`, `users` table, login form, JWT storage.
**Removed vs dsk-uploader:** Python/Flask, Pandas, Notion SDK, all Notion references.
**Added:** `csv-parse`, `iconv-lite`, `multer`, `source` + `currency` fields, `UploadPanel` component.
---
## Folder Structure
```
/volume2/docker/finance/finance-hub/
├── docker-compose.yml
├── .env
├── .env.example
├── .gitignore
├── backend/
│ ├── Dockerfile
│ ├── package.json
│ ├── prisma/
│ │ ├── schema.prisma
│ │ └── migrations/
│ │ ├── migration_lock.toml
│ │ └── 20260508_init/
│ │ └── migration.sql
│ └── src/
│ ├── index.js ← entry point (Authentik middleware wired here)
│ ├── auth.js ← Authentik header middleware (replaces JWT auth)
│ ├── parser.js ← SMS parser (copy verbatim from payments-logger)
│ ├── csvParser.js ← NEW: DSK CSV parser (port of Python dskuploader.py)
│ └── routes/
│ ├── payments.js ← existing routes + source/currency additions
│ └── upload.js ← NEW: POST /api/upload/csv
└── frontend/
├── Dockerfile
├── package.json
├── vite.config.js
├── tailwind.config.js
├── postcss.config.js
├── index.html
└── src/
├── main.jsx ← remove AuthProvider wrapper
├── index.css
├── App.jsx ← remove auth state, add Upload tab toggle
└── components/
├── FilterBar.jsx ← add source filter select
├── PaymentTable.jsx ← add Source badge column + currency display
├── PaymentCard.jsx ← minor source badge addition
├── PaymentList.jsx ← unchanged
└── UploadPanel.jsx ← NEW: drag-and-drop CSV upload UI
```
---
## Database Schema (Prisma)
File: `backend/prisma/schema.prisma`
```prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Payment {
id Int @id @default(autoincrement())
rawMessage String @map("raw_message")
date DateTime?
type String?
card String?
recipient String?
amount Float?
currency String? @default("EUR")
balance Float?
source Source @default(INGEST)
status Status @default(UNPROCESSED)
tags Tag[]
notifiedAt DateTime? @map("notified_at")
notifyPhone String? @map("notify_phone")
debitBgn Float? @map("debit_bgn")
creditBgn Float? @map("credit_bgn")
transactionType String? @map("transaction_type")
payerAccount String? @map("payer_account")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("payments")
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
color String @default("#6b7280")
payments Payment[]
@@map("tags")
}
enum Status { UNPROCESSED SENT SKIPPED }
enum Source { INGEST UPLOAD }
```
**Key decisions:**
- No `User` model — Authentik owns identity.
- `currency`: `EUR` for SMS ingest, `BGN` for CSV uploads.
- `debitBgn`, `creditBgn`, `transactionType`, `payerAccount`: nullable CSV-only columns; INGEST rows store nulls. Avoids a union query for the unified list view.
- `balance` is always null for CSV rows (DSK export does not include running balance).
- Fresh consolidated migration — no data migration from reference apps required.
---
## API Routes
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | /api/health | public | Health check |
| POST | /api/payments/ingest | public | SMS or structured ingest (source=INGEST) |
| GET | /api/payments | required | List with filters/sort/pagination (+ source filter) |
| GET | /api/payments/meta/tags | required | All tags |
| GET | /api/payments/meta/filters | required | Filter options incl. `sources` array |
| GET | /api/payments/:id | required | Single payment |
| PATCH | /api/payments/:id | required | Update status |
| DELETE | /api/payments/:id | required | Delete |
| POST | /api/payments/:id/send | required | Send notification |
| POST | /api/payments/:id/skip | required | Skip |
| POST | /api/payments/:id/tags | required | Add/upsert tag |
| DELETE | /api/payments/:id/tags/:tagId | required | Remove tag |
| POST | /api/upload/csv | required | DSK CSV file upload (source=UPLOAD) |
---
## Key Implementation Details
### auth.js (replaces entire old auth module)
```js
const PUBLIC_PATHS = new Set(['/api/health', '/api/payments/ingest']);
function authentikMiddleware(req, res, next) {
if (PUBLIC_PATHS.has(req.path)) return next();
const username = req.headers['x-authentik-username'];
if (!username) return res.status(401).json({ error: 'Unauthorized' });
req.user = {
username,
email: req.headers['x-authentik-email'] || null,
groups: (req.headers['x-authentik-groups'] || '').split(',').map(g => g.trim()).filter(Boolean),
};
next();
}
module.exports = { authentikMiddleware };
```
### csvParser.js (port of dskuploader.py)
- `iconv-lite` decodes buffer as cp1251 (DSK Bank export encoding), falls back to UTF-8
- `csv-parse` parses the decoded text with `columns: true`
- Columns: `Дата`, `Вид на трансакцията`, `Основание`, `Дебит BGN`, `Кредит BGN`, `Наредител/Получател`, `Номер сметка на наредителя / получателя`
- Card extraction: regex `/^\d{6}x{6}\d{4}$/` on first token of `Основание`
- Skips rows where `Вид на трансакцията === 'ТРАНСФЕР СОБСТВЕНИ СМЕТКИ'`
- Auto-tags via keyword rules (ЗАПЛАТА→Salary, LIDL→Groceries, NETFLIX→Subscriptions, etc.) — same logic as Python `generate_tags()`
- Returns `{ rows: PaymentData[], skipped: number, errors: string[] }`
### payments.js changes from payments-logger
1. Add `source: 'INGEST'` and `currency` to the `/ingest` create call
2. Add `source` to the `GET /` where clause filter
3. Add `sources` to `meta/filters` response
4. Currency-aware amount formatting in notification message
5. Remove all JWT/auth references (no `/auth/register`, `/auth/login`)
### upload.js (new)
- `multer` memory storage, max 10 files × 10 MB
- Calls `parseDskCsv(buffer)` per file
- Upserts tags via `prisma.tag.upsert` then connects
- Returns `{ imported, skipped, errors, payments[] }`
### Frontend changes
- **Delete**: `auth.js`, `AuthProvider.jsx`
- **main.jsx**: Remove `<AuthProvider>` wrapper
- **App.jsx**: Replace `authFetch` with plain `fetch` (Authentik session cookie travels automatically); logout → `window.location.href = '/outpost.goauthentik.io/sign_out'`; add "Payments" / "Upload CSV" tab toggle
- **FilterBar.jsx**: Add source `<select>` (All / SMS Ingest / CSV Upload); widen grid to 5 cols
- **PaymentTable.jsx**: Add `Source` column with `SMS` (indigo) / `CSV` (emerald) badge; show `${amount} ${currency}` in amount cell
- **UploadPanel.jsx** (new): Drag-and-drop zone + file list + import button + result summary with error accordion; calls `POST /api/upload/csv` with `FormData`
---
## Docker Compose
```yaml
services:
db:
image: postgres:16-alpine
environment: { POSTGRES_USER: finance, POSTGRES_PASSWORD: [PASSWORD] POSTGRES_DB: finance_hub }
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck: { test: pg_isready -U finance -d finance_hub, interval: 5s }
backend:
build: ./backend
environment:
DATABASE_URL: [CONNECTION_STRING]
PORT: "3001"
NOTIFIER_URL: ${NOTIFIER_URL}
NOTIFIER_CHANNEL: ${NOTIFIER_CHANNEL:-viber}
NOTIFY_DEFAULT_PHONE: ${NOTIFY_DEFAULT_PHONE}
TZ: ${TZ:-Europe/Sofia}
ports: ["${BACKEND_PORT:-3001}:3001"]
depends_on: { db: { condition: service_healthy } }
frontend:
build: ./frontend
ports: ["${FRONTEND_PORT:-5175}:5173"]
depends_on: [backend]
volumes: { pgdata: }
```
**NPM setup:** Frontend port proxied through Authentik forward auth. Backend port exposed separately (unprotected) so the public `/api/payments/ingest` remains reachable by iOS Shortcuts / SMS gateways without login.
---
## Environment Variables (.env)
```bash
[ENV_SECRET]
NOTIFIER_URL=[URL_WITH_CREDENTIALS] @default(autoincrement())
rawMessage String @map("raw_message")
date DateTime?
type String?
card String?
recipient String?
amount Float?
currency String? @default("EUR")
balance Float?
source Source @default(INGEST)
status Status @default(UNPROCESSED)
tags Tag[]
notifiedAt DateTime? @map("notified_at")
notifyPhone String? @map("notify_phone")
debitBgn Float? @map("debit_bgn")
creditBgn Float? @map("credit_bgn")
transactionType String? @map("transaction_type")
payerAccount String? @map("payer_account")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("payments")
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
color String @default("#6b7280")
payments Payment[]
@@map("tags")
}
enum Status {
UNPROCESSED
SENT
SKIPPED
}
enum Source {
INGEST
UPLOAD
}
Write
migration.sql
migration.sql
55 lines
-- CreateEnum
CREATE TYPE "Status" AS ENUM ('UNPROCESSED', 'SENT', 'SKIPPED');
CREATE TYPE "Source" AS ENUM ('INGEST', 'UPLOAD');
-- CreateTable
CREATE TABLE "payments" (
"id" SERIAL PRIMARY KEY,
"raw_message" TEXT NOT NULL,
"date" TIMESTAMP(3),
"type" TEXT,
"card" TEXT,
"recipient" TEXT,
"amount" DOUBLE PRECISION,
"currency" TEXT DEFAULT 'EUR',
"balance" DOUBLE PRECISION,
"source" "Source" NOT NULL DEFAULT 'INGEST',
"status" "Status" NOT NULL DEFAULT 'UNPROCESSED',
"notified_at" TIMESTAMP(3),
"notify_phone" TEXT,
"debit_bgn" DOUBLE PRECISION,
"credit_bgn" DOUBLE PRECISION,
"transaction_type" TEXT,
"payer_account" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "tags" (
"id" SERIAL PRIMARY KEY,
"name" TEXT NOT NULL,
"color" TEXT NOT NULL DEFAULT '#6b7280'
);
-- CreateUniqueIndex
CREATE UNIQUE INDEX "tags_name_key" ON "tags"("name");
-- CreateTable (M2M join)
CREATE TABLE "_PaymentToTag" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_PaymentToTag_AB_pkey" PRIMARY KEY ("A", "B")
);
CREATE INDEX "_PaymentToTag_B_index" ON "_PaymentToTag"("B");
-- AddForeignKey
ALTER TABLE "_PaymentToTag"
ADD CONSTRAINT "_PaymentToTag_A_fkey"
FOREIGN KEY ("A") REFERENCES "payments"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "_PaymentToTag"
ADD CONSTRAINT "_PaymentToTag_B_fkey"
FOREIGN KEY ("B") REFERENCES "tags"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Write
migration_lock.toml
migration_lock.toml
4 lines
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
Update Todos
Create folder structure and root config files
Write backend Prisma schema and migration
Write backend package.json and Dockerfile
Write backend src files (auth, parser, csvParser, index)
Write backend routes (payments.js, upload.js)
Write frontend config files
Write frontend React components
Build and verify with docker compose
Write
package.json
package.json
26 lines
{
"name": "finance-hub-backend",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate deploy"
},
"dependencies": {
"@prisma/client": "^5.22.0",
"cors": "^2.8.5",
"csv-parse": "^5.5.6",
"express": "^4.21.1",
"express-rate-limit": "^7.4.0",
"iconv-lite": "^0.6.3",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.1"
},
"devDependencies": {
"nodemon": "^3.1.7",
"prisma": "^5.22.0"
}
}
Write
Dockerfile
Dockerfile
18 lines
FROM node:20-alpine
RUN apk add --no-cache openssl
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY prisma ./prisma
RUN npx prisma generate
COPY src ./src
EXPOSE 3001
CMD ["sh", "-c", "npx prisma migrate deploy && node src/index.js"]
Update Todos...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Explorer (⇧⌘E)","depth":19,"bounds":{"left":0.0,"top":0.047885075,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":true},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.057462092,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Search (⇧⌘F)","depth":19,"bounds":{"left":0.0,"top":0.08619314,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.09577015,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Source Control (⌃⇧G)","depth":19,"bounds":{"left":0.0,"top":0.1245012,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.13407822,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Run and Debug (⇧⌘D)","depth":19,"bounds":{"left":0.0,"top":0.16280925,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.17238627,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Remote Explorer","depth":19,"bounds":{"left":0.0,"top":0.20111732,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.21069433,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Extensions (⇧⌘X) - 2 require update","depth":19,"bounds":{"left":0.0,"top":0.23942538,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.2490024,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":22,"bounds":{"left":0.009640957,"top":0.2601756,"width":0.0019946808,"height":0.008778931},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Claude Code","depth":19,"bounds":{"left":0.0,"top":0.27773345,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Containers","depth":19,"bounds":{"left":0.0,"top":0.3160415,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"EXPLORER","depth":17,"bounds":{"left":0.022606382,"top":0.047885075,"width":0.018949468,"height":0.02793296},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"EXPLORER","depth":18,"bounds":{"left":0.022606382,"top":0.056664005,"width":0.018949468,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.022606382,"top":0.056664005,"width":0.0023271276,"height":0.0103751}},{"char_start":1,"char_count":7,"bounds":{"left":0.024933511,"top":0.056664005,"width":0.01662234,"height":0.0103751}}],"role_description":"text"},{"role":"AXButton","text":"Explorer Section: finance [SSH: nas]","depth":21,"bounds":{"left":0.015957447,"top":0.07581804,"width":0.09940159,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXStaticText","text":"","depth":23,"bounds":{"left":0.01662234,"top":0.07821229,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"Explorer Section: finance [SSH: nas]","depth":22,"bounds":{"left":0.022606382,"top":0.07581804,"width":0.039228722,"height":0.017557861},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"FINANCE [SSH: NAS]","depth":23,"bounds":{"left":0.022606382,"top":0.079010375,"width":0.039228722,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.022606382,"top":0.07980846,"width":0.0023271276,"height":0.0103751}},{"char_start":1,"char_count":17,"bounds":{"left":0.024933511,"top":0.07980846,"width":0.036901597,"height":0.0103751}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"bounds":{"left":0.019614361,"top":0.09577015,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"auth","depth":27,"bounds":{"left":0.025930852,"top":0.09577015,"width":0.008976064,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.025930852,"top":0.096568234,"width":0.0023271276,"height":0.011971269}},{"char_start":1,"char_count":3,"bounds":{"left":0.02825798,"top":0.096568234,"width":0.0066489363,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"bounds":{"left":0.019614361,"top":0.11332801,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"dsk-uploader","depth":27,"bounds":{"left":0.025930852,"top":0.11332801,"width":0.026928192,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.025930852,"top":0.11412609,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":11,"bounds":{"left":0.028590426,"top":0.11412609,"width":0.024268618,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"bounds":{"left":0.019614361,"top":0.13088587,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"finance-hub","depth":27,"bounds":{"left":0.025930852,"top":0.13088587,"width":0.024268618,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.025930852,"top":0.13168396,"width":0.0016622341,"height":0.011971269}},{"char_start":1,"char_count":10,"bounds":{"left":0.027593086,"top":0.13168396,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"bounds":{"left":0.022273935,"top":0.14844373,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":27,"bounds":{"left":0.028590426,"top":0.14844373,"width":0.017287234,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.028590426,"top":0.14924182,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":6,"bounds":{"left":0.03125,"top":0.14924182,"width":0.01462766,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"bounds":{"left":0.022273935,"top":0.1660016,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"frontend","depth":27,"bounds":{"left":0.028590426,"top":0.1660016,"width":0.017287234,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.028590426,"top":0.16679968,"width":0.0016622341,"height":0.011971269}},{"char_start":1,"char_count":7,"bounds":{"left":0.03025266,"top":0.16679968,"width":0.015625,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.021276595,"top":0.1819633,"width":0.0063164895,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env","depth":27,"bounds":{"left":0.028590426,"top":0.18355946,"width":0.00831117,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.028590426,"top":0.18435754,"width":0.0013297872,"height":0.011971269}},{"char_start":1,"char_count":3,"bounds":{"left":0.029920213,"top":0.18435754,"width":0.006981383,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.021276595,"top":0.19952115,"width":0.0063164895,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.example","depth":27,"bounds":{"left":0.028590426,"top":0.20111732,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.028590426,"top":0.2019154,"width":0.0013297872,"height":0.011971269}},{"char_start":1,"char_count":11,"bounds":{"left":0.029920213,"top":0.2019154,"width":0.024933511,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.021276595,"top":0.21707901,"width":0.0063164895,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".gitignore","depth":27,"bounds":{"left":0.028590426,"top":0.21867518,"width":0.018949468,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.028590426,"top":0.21947326,"width":0.0013297872,"height":0.011971269}},{"char_start":1,"char_count":9,"bounds":{"left":0.029920213,"top":0.21947326,"width":0.017952127,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.021276595,"top":0.23463687,"width":0.0063164895,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"docker-compose.yml","depth":27,"bounds":{"left":0.028590426,"top":0.23623304,"width":0.042220745,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.028590426,"top":0.23703113,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":17,"bounds":{"left":0.03125,"top":0.23703113,"width":0.03956117,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"bounds":{"left":0.019614361,"top":0.25379092,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"payments-logger","depth":27,"bounds":{"left":0.025930852,"top":0.25379092,"width":0.034574468,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.025930852,"top":0.254589,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":14,"bounds":{"left":0.028590426,"top":0.254589,"width":0.031914894,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Outline Section","depth":21,"bounds":{"left":0.015957447,"top":0.9473264,"width":0.09940159,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"bounds":{"left":0.01662234,"top":0.9497207,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"OUTLINE","depth":22,"bounds":{"left":0.022606382,"top":0.9473264,"width":0.01662234,"height":0.017557861},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"OUTLINE","depth":23,"bounds":{"left":0.022606382,"top":0.95131683,"width":0.01662234,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.022606382,"top":0.95131683,"width":0.0029920214,"height":0.0103751}},{"char_start":1,"char_count":6,"bounds":{"left":0.025598405,"top":0.95131683,"width":0.013630319,"height":0.0103751}}],"role_description":"text"},{"role":"AXButton","text":"Timeline Section","depth":21,"bounds":{"left":0.015957447,"top":0.9648843,"width":0.09940159,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"bounds":{"left":0.01662234,"top":0.96727854,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"TIMELINE","depth":22,"bounds":{"left":0.022606382,"top":0.9648843,"width":0.01761968,"height":0.017557861},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"TIMELINE","depth":23,"bounds":{"left":0.022606382,"top":0.9688747,"width":0.01761968,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.022606382,"top":0.9688747,"width":0.0026595744,"height":0.0103751}},{"char_start":1,"char_count":7,"bounds":{"left":0.025265958,"top":0.9688747,"width":0.015292553,"height":0.0103751}}],"role_description":"text"},{"role":"AXRadioButton","text":"docker-compose.yml, Editor Group 1","depth":28,"bounds":{"left":0.11569149,"top":0.047885075,"width":0.0625,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":".env, Editor Group 1","depth":28,"bounds":{"left":0.17785904,"top":0.047885075,"width":0.040226065,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"report(1).csv, Editor Group 1","depth":28,"bounds":{"left":0.21775267,"top":0.047885075,"width":0.046210106,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXRadioButton","text":"report(2).csv, Editor Group 1","depth":28,"bounds":{"left":0.26396278,"top":0.047885075,"width":0.046875,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":29,"bounds":{"left":0.13264628,"top":0.07821229,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":29,"bounds":{"left":0.14827128,"top":0.07821229,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":29,"bounds":{"left":0.17586437,"top":0.07821229,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"\"Дата\",\"Основание\",\"Наредител/Получател\",\"Номер сметка на наредителя / получателя\",\"Вид на трансакцията\",\"Свързваща референция\",\"Валутен курс\",\"Сума във валутата на превода\",\"Дебит EUR\",\"Кредит EUR\"\n\"08.05.2026\",\"400915xxxxxx4447 ПОС 08.05.2026 18:46<br/>Авт. код: 36C1UB\",\"\",\"\",\"\",\"\",\"\",\"\",\"5,02\",\"\"\n\"08.05.2026\",\"400915xxxxxx4447 ПОС 08.05.2026 18:36<br/>Авт. код: 5CK9TB\",\"\",\"\",\"\",\"\",\"\",\"\",\"15,46\",\"\"\n\"08.05.2026\",\"400915xxxxxx4447 ПОС 08.05.2026 18:07<br/>Авт. код: 8FK4TB\",\"\",\"\",\"\",\"\",\"\",\"\",\"9,04\",\"\"\n\"08.05.2026\",\"400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 08.05.2026 19:32<br/>Авт. код: 6NM31C<br/>Номер на у-во: V5411421\",\"BGR SOFIYA LIDL BALGARIYA EOOD UL TODOR\",\"\",\"КАРТОВА ОПЕРАЦИЯ\",\"\",\"\",\"\",\"67,81\",\"\"\n\"08.05.2026\",\"400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 07.05.2026 09:02<br/>Авт. код: 7NE8N7<br/>Номер на у-во: 10101446\",\"BGR SOFIA CBA EKO MARKET\",\"\",\"КАРТОВА ОПЕРАЦИЯ\",\"\",\"\",\"\",\"5,51\",\"\"\n\"08.05.2026\",\"400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 06.05.2026 13:16<br/>Авт. код: 1TV886<br/>Номер на у-во: 06030191\",\"POL BALICE Lagardere Travel R KR3\",\"\",\"КАРТОВА ОПЕРАЦИЯ\",\"\",\"\",\"\",\"5,49\",\"\"","depth":28,"bounds":{"left":0.11569149,"top":0.0933759,"width":0.38031915,"height":0.0007980846},"on_screen":true,"value":"\"Дата\",\"Основание\",\"Наредител/Получател\",\"Номер сметка на наредителя / получателя\",\"Вид на трансакцията\",\"Свързваща референция\",\"Валутен курс\",\"Сума във валутата на превода\",\"Дебит EUR\",\"Кредит EUR\"\n\"08.05.2026\",\"400915xxxxxx4447 ПОС 08.05.2026 18:46<br/>Авт. код: 36C1UB\",\"\",\"\",\"\",\"\",\"\",\"\",\"5,02\",\"\"\n\"08.05.2026\",\"400915xxxxxx4447 ПОС 08.05.2026 18:36<br/>Авт. код: 5CK9TB\",\"\",\"\",\"\",\"\",\"\",\"\",\"15,46\",\"\"\n\"08.05.2026\",\"400915xxxxxx4447 ПОС 08.05.2026 18:07<br/>Авт. код: 8FK4TB\",\"\",\"\",\"\",\"\",\"\",\"\",\"9,04\",\"\"\n\"08.05.2026\",\"400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 08.05.2026 19:32<br/>Авт. код: 6NM31C<br/>Номер на у-во: V5411421\",\"BGR SOFIYA LIDL BALGARIYA EOOD UL TODOR\",\"\",\"КАРТОВА ОПЕРАЦИЯ\",\"\",\"\",\"\",\"67,81\",\"\"\n\"08.05.2026\",\"400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 07.05.2026 09:02<br/>Авт. код: 7NE8N7<br/>Номер на у-во: 10101446\",\"BGR SOFIA CBA EKO MARKET\",\"\",\"КАРТОВА ОПЕРАЦИЯ\",\"\",\"\",\"\",\"5,51\",\"\"\n\"08.05.2026\",\"400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 06.05.2026 13:16<br/>Авт. код: 1TV886<br/>Номер на у-во: 06030191\",\"POL BALICE Lagardere Travel R KR3\",\"\",\"КАРТОВА ОПЕРАЦИЯ\",\"\",\"\",\"\",\"5,49\",\"\"","role_description":"editor","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"\"Дата\",\"Основание\",\"Наредител/Получател\",\"Номер сметка на наредителя / получателя\",\"Вид на трансакцията\",\"Свързваща референция\",\"Валутен курс\",\"Сума във валутата на превода\",\"Дебит EUR\",\"Кредит EUR\"\n\"08.05.2026\",\"400915xxxxxx4447 ПОС 08.05.2026 18:46<br/>Авт. код: 36C1UB\",\"\",\"\",\"\",\"\",\"\",\"\",\"5,02\",\"\"\n\"08.05.2026\",\"400915xxxxxx4447 ПОС 08.05.2026 18:36<br/>Авт. код: 5CK9TB\",\"\",\"\",\"\",\"\",\"\",\"\",\"15,46\",\"\"\n\"08.05.2026\",\"400915xxxxxx4447 ПОС 08.05.2026 18:07<br/>Авт. код: 8FK4TB\",\"\",\"\",\"\",\"\",\"\",\"\",\"9,04\",\"\"\n\"08.05.2026\",\"400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 08.05.2026 19:32<br/>Авт. код: 6NM31C<br/>Номер на у-во: V5411421\",\"BGR SOFIYA LIDL BALGARIYA EOOD UL TODOR\",\"\",\"КАРТОВА ОПЕРАЦИЯ\",\"\",\"\",\"\",\"67,81\",\"\"\n\"08.05.2026\",\"400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 07.05.2026 09:02<br/>Авт. код: 7NE8N7<br/>Номер на у-во: 10101446\",\"BGR SOFIA CBA EKO MARKET\",\"\",\"КАРТОВА ОПЕРАЦИЯ\",\"\",\"\",\"\",\"5,51\",\"\"\n\"08.05.2026\",\"400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 06.05.2026 13:16<br/>Авт. код: 1TV886<br/>Номер на у-во: 06030191\",\"POL BALICE Lagardere Travel R KR3\",\"\",\"КАРТОВА ОПЕРАЦИЯ\",\"\",\"\",\"\",\"5,49\",\"\"","depth":29,"bounds":{"left":0.11569149,"top":0.0933759,"width":0.38031915,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Design new payment-logge…, Editor Group 2","depth":28,"bounds":{"left":0.5578458,"top":0.047885075,"width":0.07912234,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXRadioButton","text":"Problems (⇧⌘M)","depth":22,"bounds":{"left":0.118351065,"top":0.7278532,"width":0.027925532,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"PROBLEMS","depth":24,"bounds":{"left":0.122340426,"top":0.7366321,"width":0.019946808,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Output (⇧⌘U)","depth":22,"bounds":{"left":0.14594415,"top":0.7278532,"width":0.023603724,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUTPUT","depth":24,"bounds":{"left":0.14993352,"top":0.7366321,"width":0.015625,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Debug Console (⇧⌘Y)","depth":22,"bounds":{"left":0.16921543,"top":0.7278532,"width":0.039893616,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DEBUG CONSOLE","depth":24,"bounds":{"left":0.1732048,"top":0.7366321,"width":0.031914894,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Terminal (⌃`)","depth":22,"bounds":{"left":0.2087766,"top":0.7278532,"width":0.026595745,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":true},{"role":"AXStaticText","text":"TERMINAL","depth":24,"bounds":{"left":0.21276596,"top":0.7366321,"width":0.01861702,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.2130984,"top":0.73743016,"width":0.0023271276,"height":0.0103751}},{"char_start":1,"char_count":7,"bounds":{"left":0.21542554,"top":0.73743016,"width":0.016289894,"height":0.0103751}}],"role_description":"text"},{"role":"AXRadioButton","text":"Ports","depth":22,"bounds":{"left":0.23537233,"top":0.7278532,"width":0.020279255,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"PORTS","depth":24,"bounds":{"left":0.2393617,"top":0.7366321,"width":0.012300532,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"remote SSH: nas","depth":16,"bounds":{"left":0.0006648936,"top":0.98244214,"width":0.028590426,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.0033244682,"top":0.9848364,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SSH: nas","depth":17,"bounds":{"left":0.008643617,"top":0.9856345,"width":0.017952127,"height":0.011173184},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.008643617,"top":0.9856345,"width":0.0013297872,"height":0.011173184}},{"char_start":1,"char_count":7,"bounds":{"left":0.009973404,"top":0.9856345,"width":0.01462766,"height":0.011173184}}],"role_description":"text"},{"role":"AXButton","text":"No Problems","depth":16,"bounds":{"left":0.03025266,"top":0.98244214,"width":0.022606382,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.031914894,"top":0.9848364,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"bounds":{"left":0.03723404,"top":0.9856345,"width":0.004986702,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.041888297,"top":0.9848364,"width":0.0056515955,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"bounds":{"left":0.04720745,"top":0.9856345,"width":0.0039893617,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"No Ports Forwarded","depth":16,"bounds":{"left":0.054521278,"top":0.98244214,"width":0.012632979,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.05618351,"top":0.9848364,"width":0.0056515955,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"bounds":{"left":0.061502658,"top":0.9856345,"width":0.0039893617,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Notifications","depth":16,"bounds":{"left":0.9886968,"top":0.98244214,"width":0.010638298,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sign In","depth":16,"bounds":{"left":0.9650931,"top":0.98244214,"width":0.022606382,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.96675533,"top":0.9848364,"width":0.0056515955,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Sign In","depth":17,"bounds":{"left":0.97207445,"top":0.9856345,"width":0.013962766,"height":0.011173184},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.97207445,"top":0.9856345,"width":0.0013297872,"height":0.011173184}},{"char_start":1,"char_count":6,"bounds":{"left":0.9734042,"top":0.9856345,"width":0.010638298,"height":0.011173184}}],"role_description":"text"},{"role":"AXStaticText","text":"Info: Setting up SSH Host nas: Setting up SSH tunnel","depth":12,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Design new payment-logger and dsk-uploader hybrid app","depth":19,"bounds":{"left":0.56017286,"top":0.08060654,"width":0.099734046,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Session history","depth":19,"bounds":{"left":0.97839093,"top":0.08060654,"width":0.00930851,"height":0.022346368},"on_screen":true,"help_text":"Session history","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New session","depth":19,"bounds":{"left":0.9890292,"top":0.08060654,"width":0.00930851,"height":0.022346368},"on_screen":true,"help_text":"New session","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Message actions","depth":24,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"payments.js","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"payments.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ets create a new app that should be combination of payment-logger and dsk-uploader. It should have authorization via authentik (auth folder). All three folders (payment-logger, dsk-uploader and auth) are just refference these will be removed later. Auth project is separated it lives on its own. First reveiw them and see how these should be combined. It will be whole new app (also the folder name). Think very carefully of whatr these two apps do and how cold they be combined. THerer should be common db and uploader should store data the same way the /ingest does. It should be properly marked in UI if it is upload or ingest or both. FIrst think of tech stack and plan carefully.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"I'll explore all three reference projects in parallel to understand their structure before planning.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Agent:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Explore payment-logger app structure and functionality","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Explore the /volume2/docker/finance/payments-logger directory thoroughly. I need to understand:\n1. Tech stack (frontend framework, backend framework, database)\n2. What the app does - its core purpose and features\n3. Database schema - all tables, relationships\n4. API routes - all endpoints, their purpose and request/response shapes\n5. How data flows (ingest, storage, retrieval)\n6. Any auth/middleware setup\n7. Docker/deployment setup\n8. Key files and their roles\n\nReport with: directory structure, tech stack summary, database schema details, all API endpoints listed, UI features, and how data is ingested. Be thorough on the /ingest endpoint specifically.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Agent:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Explore dsk-uploader app structure and functionality","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Explore the /volume2/docker/finance/dsk-uploader directory thoroughly. I need to understand:\n1. Tech stack (frontend framework, backend framework, database)\n2. What the app does - its core purpose and features\n3. Database schema - all tables, relationships\n4. API routes - all endpoints, their purpose and request/response shapes\n5. How file uploads work - what files, what format, how parsed\n6. Any auth/middleware setup\n7. Docker/deployment setup\n8. Key files and their roles\n\nReport with: directory structure, tech stack summary, database schema details, all API endpoints listed, UI features, how file upload parsing works, and what data is extracted from uploads. Be very thorough about the data model and what fields are stored.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Agent:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Explore auth folder and authentik integration","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Explore the /volume2/docker/finance/auth directory thoroughly. I need to understand:\n1. What auth system is being used (Authentik)\n2. How authentication/authorization is implemented - OAuth2, OIDC, session tokens, JWT?\n3. Middleware - how is auth enforced on routes?\n4. Any frontend auth flow - login redirect, token handling, protected routes\n5. Environment variables related to auth\n6. Docker/deployment setup for the auth service itself\n7. Any example of how other apps integrate with this auth\n\nReport with: directory structure, auth flow description, middleware code, environment variable names, and integration patterns. This is a reference for building auth into a new combined app.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Message actions","depth":24,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"payments.js","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"payments.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"continue","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Launching parallel exploration of all three reference projects.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Agent:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Explore payment-logger app structure","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Explore the /volume2/docker/finance/payments-logger directory thoroughly. I need to understand:\n1. Full directory structure (tree)\n2. Tech stack (frontend framework, backend framework, database, etc.)\n3. What the app does - its core functionality\n4. How data is ingested (look at /ingest route especially)\n5. Database schema (what tables/collections exist)\n6. API routes and their purposes\n7. Frontend pages and components\n8. Docker setup\n9. Any auth/session handling\n\nPlease read key files: package.json files, main entry points, route files, database schema files, docker-compose.yml, and any frontend components. Be thorough - I need a complete picture to plan a new combined app.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Agent:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Explore dsk-uploader app structure","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Explore the /volume2/docker/finance/dsk-uploader directory thoroughly. I need to understand:\n1. Full directory structure (tree)\n2. Tech stack (frontend framework, backend framework, database, etc.)\n3. What the app does - its core functionality\n4. How file uploads work (what format, what processing happens)\n5. Database schema (what tables/collections exist)\n6. API routes and their purposes\n7. Frontend pages and components\n8. Docker setup\n9. Any auth/session handling\n\nPlease read key files: package.json files, main entry points, route files, database schema files, docker-compose.yml, and any frontend components. Be thorough - I need a complete picture to plan a new combined app.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Agent:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Explore auth project structure","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Explore the /volume2/docker/finance/auth directory thoroughly. I need to understand:\n1. Full directory structure (tree)\n2. What authentication system is used (authentik integration details)\n3. How OAuth/OIDC flow works\n4. Any middleware or helper functions for protecting routes\n5. Session management approach\n6. How frontend handles auth state\n7. Any example of how other apps integrate with this auth system\n8. Docker setup and environment variables\n\nPlease read all files carefully - this auth system will be integrated into a new combined app. I need to understand exactly how to use it.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Now I have a complete picture of all three. Let me design the plan.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Agent:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Design combined app architecture","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Design a detailed implementation plan for a new combined web app that merges two existing applications. Here is full context:\n\n---\n\n## App 1: payments-logger (Node.js)\n- **Purpose**: Ingest Bulgarian bank SMS → parse → store in PostgreSQL → review/tag/notify in React UI\n- **Stack**: Node.js 20 + Express + Prisma 5 + PostgreSQL 16 + React 18 + Vite + Tailwind CSS\n- **Auth**: Custom JWT (bcryptjs + jose), tokens in localStorage\n- **Ingest**: POST /api/payments/ingest (public) — SMS text OR structured JSON (Apple Wallet)\n- **Schema** (PostgreSQL via Prisma):\n - `payments` table: id, raw_message, date, type (POS/ATM/INTERNET/ECOM/P2P/WALLET), card, recipient, amount, balance, status (UNPROCESSED/SENT/SKIPPED), notifyPhone, notifiedAt, created_at, updated_at\n - `tags` table: id, name, color — M2M with payments via `_PaymentToTag`\n - `users` table: id, username, hashed_password (this will be REMOVED)\n- **UI**: Single-page React app — PaymentTable (sortable, filterable, taggable), FilterBar, status actions (send/skip), notification system\n- **Parser** (backend/src/parser.js): Regex parser for Bulgarian DSK Bank SMS, extracts date/time (DD/MM/YYYY HH:MM), card mask, transaction type, recipient, amount, balance\n\n## App 2: dsk-uploader (Python/Flask)\n- **Purpose**: Upload DSK bank CSV exports → parse/normalize → upload to Notion database\n- **Stack**: Python 3.11 + Flask + Pandas + Custom Notion SDK + Bootstrap 5\n- **Auth**: None (open)\n- **CSV format** (DSK Bank Bulgarian format, columns):\n - `Дата` (date, DD.MM.YYYY)\n - `Вид на трансакцията` (transaction type, Bulgarian)\n - `Основание` (reason/description — contains card number regex: `^\\d{6}x{6}\\d{4}$`)\n - `Дебит BGN` (debit amount, may be empty)\n - `Кредит BGN` (credit amount, may be empty)\n - `Наредител/Получател` (orderer/recipient name)\n - `Номер сметка на наредителя / получателя` (account number)\n- **Processing**: merge multiple CSVs, normalize dates, extract card numbers from reason via regex, auto-generate tags (keyword heuristics: ЗАПЛАТА→Salary, NETFLIX→Subscriptions, etc.), filter internal transfers\n- **Output**: Notion database pages (this will be REPLACED with local PostgreSQL)\n\n## App 3: auth (Authentik)\n- **Mode**: Proxy mode via NPM (forward auth)\n- **How it works**: NPM intercepts all requests, calls Authentik outpost's auth endpoint. On success, NPM injects headers into proxied request:\n - `X-authentik-username` \n - `X-authentik-email`\n - `X-authentik-groups`\n- **No code integration needed** in the app itself — just trust these headers from NPM\n- **Logout**: Redirect user to `/outpost.goauthentik.io/sign_out`\n\n---\n\n## What the New Combined App Must Do\n\n1. **Single PostgreSQL database** for all transactions\n2. **SMS Ingest** (public endpoint) — same as payments-logger /ingest, source=INGEST\n3. **CSV Upload** (authenticated) — parse DSK CSV files, store transactions with source=UPLOAD into the same DB schema\n4. **Unified UI** — show all payments in one table regardless of source\n5. **Source indicator** in UI — badge showing \"INGEST\", \"UPLOAD\", or \"BOTH\" (if duplicate detection added)\n6. **Source filter** in FilterBar — filter by All/Ingest/Upload\n7. **Authentik auth** — remove custom JWT entirely; trust X-authentik-* headers from NPM\n8. **All existing payments-logger features**: tagging, send notification, skip, status tracking, filters, sorting, pagination\n9. **Remove Notion dependency entirely** — all data stays in local PostgreSQL\n\n---\n\n## Technical Constraints\n\n- Keep Node.js backend (not Python) — extend payments-logger's stack\n- CSV parsing: use `csv-parse` npm package (handles encoding, streaming)\n- File upload: use `multer` npm middleware\n- Auth middleware: read X-authentik-username header, 401 if missing (but /api/payments/ingest stays public)\n- Add `source` enum field (INGEST/UPLOAD) to payments schema\n- Add `currency` field (EUR for SMS ingest, BGN for CSV uploads) — or just store as-is\n- Remove `users` table from schema\n\n---\n\n## Please Design\n\n1. Complete folder structure for the new app (suggest name: \"finance-hub\" or similar — note this is a placeholder, user will confirm)\n2. Prisma schema with new fields (`source`, `currency`)\n3. All backend API routes\n4. CSV parser design in Node.js (port of the Python logic)\n5. Authentik auth middleware\n6. Frontend component structure (React) — what's reused from payments-logger, what's new\n7. Docker Compose setup\n8. Environment variables needed\n9. Step-by-step implementation order\n10. Verification checklist\n\nBe specific about file paths and code structure. This will be used to write actual implementation code.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Good plan. Let me ask the one thing I need before writing the final plan.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskUserQuestion","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"User has answered your questions: \"What should the new combined app be called? This will be the folder name under /volume2/docker/finance/ and the app branding.\"=\"finance-hub\". You can now continue with the user's answers in mind.","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Writing the final plan now.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"ets-create-a-new-generic-sun.md","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"ets-create-a-new-generic-sun.md","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"326 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"# finance-hub — Implementation Plan\n\n## Context\n\nTwo separate finance apps exist as references and will be retired:\n- **payments-logger** (Node.js/React): Ingests Bulgarian bank SMS notifications, stores payments in PostgreSQL, provides a review/tag/notify UI.\n- **dsk-uploader** (Python/Flask): Uploads DSK bank CSV exports, parses them, sends to Notion.\n\nThe new app **finance-hub** (`/volume2/docker/finance/finance-hub/`) replaces both with a single unified system: all transactions (SMS-ingested and CSV-uploaded) land in one PostgreSQL database and one React UI. Authentication is delegated entirely to Authentik (proxy mode via NPM — no custom JWT).\n\n---\n\n## Tech Stack\n\n| Layer | Choice | Rationale |\n|-------|--------|-----------|\n| Backend | Node.js 20 + Express + Prisma 5 | Extends payments-logger; no Python needed |\n| CSV parsing | `csv-parse` + `iconv-lite` | Handles Bulgarian cp1251 encoding, RFC4180 quoted fields |\n| File upload | `multer` (memory storage) | Bank CSVs are small (<500 KB), no disk cleanup needed |\n| Database | PostgreSQL 16 | Same as payments-logger |\n| Frontend | React 18 + Vite + Tailwind CSS + Lucide React | Same as payments-logger |\n| Auth | Authentik proxy via NPM headers | No custom JWT; removes users table entirely |\n| Container | Docker Compose | Same pattern as both reference apps |\n\n**Removed vs payments-logger:** `bcryptjs`, `jose`, `users` table, login form, JWT storage. \n**Removed vs dsk-uploader:** Python/Flask, Pandas, Notion SDK, all Notion references. \n**Added:** `csv-parse`, `iconv-lite`, `multer`, `source` + `currency` fields, `UploadPanel` component.\n\n---\n\n## Folder Structure\n\n```\n/volume2/docker/finance/finance-hub/\n├── docker-compose.yml\n├── .env\n├── .env.example\n├── .gitignore\n├── backend/\n│ ├── Dockerfile\n│ ├── package.json\n│ ├── prisma/\n│ │ ├── schema.prisma\n│ │ └── migrations/\n│ │ ├── migration_lock.toml\n│ │ └── 20260508_init/\n│ │ └── migration.sql\n│ └── src/\n│ ├── index.js ← entry point (Authentik middleware wired here)\n│ ├── auth.js ← Authentik header middleware (replaces JWT auth)\n│ ├── parser.js ← SMS parser (copy verbatim from payments-logger)\n│ ├── csvParser.js ← NEW: DSK CSV parser (port of Python dskuploader.py)\n│ └── routes/\n│ ├── payments.js ← existing routes + source/currency additions\n│ └── upload.js ← NEW: POST /api/upload/csv\n└── frontend/\n ├── Dockerfile\n ├── package.json\n ├── vite.config.js\n ├── tailwind.config.js\n ├── postcss.config.js\n ├── index.html\n └── src/\n ├── main.jsx ← remove AuthProvider wrapper\n ├── index.css\n ├── App.jsx ← remove auth state, add Upload tab toggle\n └── components/\n ├── FilterBar.jsx ← add source filter select\n ├── PaymentTable.jsx ← add Source badge column + currency display\n ├── PaymentCard.jsx ← minor source badge addition\n ├── PaymentList.jsx ← unchanged\n └── UploadPanel.jsx ← NEW: drag-and-drop CSV upload UI\n```\n\n---\n\n## Database Schema (Prisma)\n\nFile: `backend/prisma/schema.prisma`\n\n```prisma\ngenerator client {\n provider = \"prisma-client-js\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel Payment {\n id Int @id @default(autoincrement())\n rawMessage String @map(\"raw_message\")\n date DateTime?\n type String?\n card String?\n recipient String?\n amount Float?\n currency String? @default(\"EUR\")\n balance Float?\n source Source @default(INGEST)\n status Status @default(UNPROCESSED)\n tags Tag[]\n notifiedAt DateTime? @map(\"notified_at\")\n notifyPhone String? @map(\"notify_phone\")\n debitBgn Float? @map(\"debit_bgn\")\n creditBgn Float? @map(\"credit_bgn\")\n transactionType String? @map(\"transaction_type\")\n payerAccount String? @map(\"payer_account\")\n createdAt DateTime @default(now()) @map(\"created_at\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n @@map(\"payments\")\n}\n\nmodel Tag {\n id Int @id @default(autoincrement())\n name String @unique\n color String @default(\"#6b7280\")\n payments Payment[]\n\n @@map(\"tags\")\n}\n\nenum Status { UNPROCESSED SENT SKIPPED }\nenum Source { INGEST UPLOAD }\n```\n\n**Key decisions:**\n- No `User` model — Authentik owns identity.\n- `currency`: `EUR` for SMS ingest, `BGN` for CSV uploads.\n- `debitBgn`, `creditBgn`, `transactionType`, `payerAccount`: nullable CSV-only columns; INGEST rows store nulls. Avoids a union query for the unified list view.\n- `balance` is always null for CSV rows (DSK export does not include running balance).\n- Fresh consolidated migration — no data migration from reference apps required.\n\n---\n\n## API Routes\n\n| Method | Path | Auth | Description |\n|--------|------|------|-------------|\n| GET | /api/health | public | Health check |\n| POST | /api/payments/ingest | public | SMS or structured ingest (source=INGEST) |\n| GET | /api/payments | required | List with filters/sort/pagination (+ source filter) |\n| GET | /api/payments/meta/tags | required | All tags |\n| GET | /api/payments/meta/filters | required | Filter options incl. `sources` array |\n| GET | /api/payments/:id | required | Single payment |\n| PATCH | /api/payments/:id | required | Update status |\n| DELETE | /api/payments/:id | required | Delete |\n| POST | /api/payments/:id/send | required | Send notification |\n| POST | /api/payments/:id/skip | required | Skip |\n| POST | /api/payments/:id/tags | required | Add/upsert tag |\n| DELETE | /api/payments/:id/tags/:tagId | required | Remove tag |\n| POST | /api/upload/csv | required | DSK CSV file upload (source=UPLOAD) |\n\n---\n\n## Key Implementation Details\n\n### auth.js (replaces entire old auth module)\n```js\nconst PUBLIC_PATHS = new Set(['/api/health', '/api/payments/ingest']);\n\nfunction authentikMiddleware(req, res, next) {\n if (PUBLIC_PATHS.has(req.path)) return next();\n const username = req.headers['x-authentik-username'];\n if (!username) return res.status(401).json({ error: 'Unauthorized' });\n req.user = {\n username,\n email: req.headers['x-authentik-email'] || null,\n groups: (req.headers['x-authentik-groups'] || '').split(',').map(g => g.trim()).filter(Boolean),\n };\n next();\n}\nmodule.exports = { authentikMiddleware };\n```\n\n### csvParser.js (port of dskuploader.py)\n- `iconv-lite` decodes buffer as cp1251 (DSK Bank export encoding), falls back to UTF-8\n- `csv-parse` parses the decoded text with `columns: true`\n- Columns: `Дата`, `Вид на трансакцията`, `Основание`, `Дебит BGN`, `Кредит BGN`, `Наредител/Получател`, `Номер сметка на наредителя / получателя`\n- Card extraction: regex `/^\\d{6}x{6}\\d{4}$/` on first token of `Основание`\n- Skips rows where `Вид на трансакцията === 'ТРАНСФЕР СОБСТВЕНИ СМЕТКИ'`\n- Auto-tags via keyword rules (ЗАПЛАТА→Salary, LIDL→Groceries, NETFLIX→Subscriptions, etc.) — same logic as Python `generate_tags()`\n- Returns `{ rows: PaymentData[], skipped: number, errors: string[] }`\n\n### payments.js changes from payments-logger\n1. Add `source: 'INGEST'` and `currency` to the `/ingest` create call\n2. Add `source` to the `GET /` where clause filter\n3. Add `sources` to `meta/filters` response\n4. Currency-aware amount formatting in notification message\n5. Remove all JWT/auth references (no `/auth/register`, `/auth/login`)\n\n### upload.js (new)\n- `multer` memory storage, max 10 files × 10 MB\n- Calls `parseDskCsv(buffer)` per file\n- Upserts tags via `prisma.tag.upsert` then connects\n- Returns `{ imported, skipped, errors, payments[] }`\n\n### Frontend changes\n- **Delete**: `auth.js`, `AuthProvider.jsx`\n- **main.jsx**: Remove `<AuthProvider>` wrapper\n- **App.jsx**: Replace `authFetch` with plain `fetch` (Authentik session cookie travels automatically); logout → `window.location.href = '/outpost.goauthentik.io/sign_out'`; add \"Payments\" / \"Upload CSV\" tab toggle\n- **FilterBar.jsx**: Add source `<select>` (All / SMS Ingest / CSV Upload); widen grid to 5 cols\n- **PaymentTable.jsx**: Add `Source` column with `SMS` (indigo) / `CSV` (emerald) badge; show `${amount} ${currency}` in amount cell\n- **UploadPanel.jsx** (new): Drag-and-drop zone + file list + import button + result summary with error accordion; calls `POST /api/upload/csv` with `FormData`\n\n---\n\n## Docker Compose\n\n```yaml\nservices:\n db:\n image: postgres:16-alpine\n environment: { POSTGRES_USER: finance, POSTGRES_PASSWORD: ${DB_PASSWORD}, POSTGRES_DB: finance_hub }\n volumes: [pgdata:/var/lib/postgresql/data]\n healthcheck: { test: pg_isready -U finance -d finance_hub, interval: 5s }\n\n backend:\n build: ./backend\n environment:\n DATABASE_URL: postgresql://finance:${DB_PASSWORD}@db:5432/finance_hub\n PORT: \"3001\"\n NOTIFIER_URL: ${NOTIFIER_URL}\n NOTIFIER_CHANNEL: ${NOTIFIER_CHANNEL:-viber}\n NOTIFY_DEFAULT_PHONE: ${NOTIFY_DEFAULT_PHONE}\n TZ: ${TZ:-Europe/Sofia}\n ports: [\"${BACKEND_PORT:-3001}:3001\"]\n depends_on: { db: { condition: service_healthy } }\n\n frontend:\n build: ./frontend\n ports: [\"${FRONTEND_PORT:-5175}:5173\"]\n depends_on: [backend]\n\nvolumes: { pgdata: }\n```\n\n**NPM setup:** Frontend port proxied through Authentik forward auth. Backend port exposed separately (unprotected) so the public `/api/payments/ingest` remains reachable by iOS Shortcuts / SMS gateways without login.\n\n---\n\n## Environment Variables (.env)\n\n```bash\nDB_PASSWORD=change_me\nNOTIFIER_URL=https://notifier.lakylak.xyz\nNOTIFIER_CHANNEL=viber\nNOTIFY_DEFAULT_PHONE=+359000000000\nTZ=Europe/Sofia\nBACKEND_PORT=3001\nFRONTEND_PORT=5175\n```\n\nRemoved vs payments-logger: `JWT_SECRET`, `JWT_EXPIRE_MINUTES`.\n\n---\n\n## Implementation Order\n\n1. Create folder structure and write `docker-compose.yml`, `.env.example`, `.gitignore`\n2. Write `backend/prisma/schema.prisma` and migration SQL\n3. Write `backend/package.json` (add `csv-parse`, `iconv-lite`, `multer`; remove `bcryptjs`, `jose`)\n4. Write `backend/Dockerfile`\n5. Write `backend/src/auth.js` (Authentik middleware)\n6. Copy `backend/src/parser.js` verbatim from payments-logger\n7. Write `backend/src/csvParser.js` (Python port)\n8. Write `backend/src/routes/upload.js`\n9. Write `backend/src/routes/payments.js` (from payments-logger + 5 changes listed above)\n10. Write `backend/src/index.js`\n11. Copy frontend base from payments-logger; delete `auth.js` + `AuthProvider.jsx`\n12. Update `main.jsx`, `App.jsx`, `FilterBar.jsx`, `PaymentTable.jsx`\n13. Write `frontend/src/components/UploadPanel.jsx`\n14. Update `frontend/vite.config.js`\n15. `docker compose build && docker compose up -d`\n16. Run verification checklist\n\n---\n\n## Verification\n\n**Schema**\n- `payments` table has `source`, `currency`, `debit_bgn`, `credit_bgn`, `transaction_type`, `payer_account`\n- No `users` table; `Source` enum exists\n\n**Auth**\n- `GET /api/payments` → 401 without `x-authentik-username` header\n- `POST /api/payments/ingest` → 201 without any header\n- `curl -H \"x-authentik-username: test\" localhost:3001/api/payments` → 200\n\n**SMS Ingest**\n- DSK POS SMS → `source=INGEST`, `currency=EUR`, correct amount/card/recipient\n- Apple Wallet structured body → `type=WALLET`, `source=INGEST`\n- Rate limiter → 429 after 200 req/min\n\n**CSV Upload**\n- Single DSK CSV → rows with `source=UPLOAD`, `currency=BGN`\n- Multiple CSVs in one request → all merged\n- `ТРАНСФЕР СОБСТВЕНИ СМЕТКИ` rows counted in `skipped`, not imported\n- Card regex match from `Основание` column\n- Auto-tags applied (ЗАПЛАТА→Salary, LIDL→Groceries)\n- cp1251 file imports correctly\n\n**UI**\n- Source badge visible in payment table (SMS indigo, CSV emerald)\n- Source dropdown in FilterBar filters correctly\n- Amount shows correct currency per row\n- Upload Panel: drag-and-drop works, result summary shows counts\n- Logout redirects to Authentik sign_out\n- No login form shown (Authentik handles it at NPM level)\n- Tags, send, skip, delete all still work\n\n## Reference Files\n\n- `/volume2/docker/finance/payments-logger/backend/src/routes/payments.js`\n- `/volume2/docker/finance/payments-logger/backend/src/parser.js`\n- `/volume2/docker/finance/payments-logger/backend/src/index.js`\n- `/volume2/docker/finance/payments-logger/backend/src/auth.js`\n- `/volume2/docker/finance/payments-logger/backend/prisma/schema.prisma`\n- `/volume2/docker/finance/payments-logger/frontend/src/App.jsx`\n- `/volume2/docker/finance/payments-logger/frontend/src/components/PaymentTable.jsx`\n- `/volume2/docker/finance/payments-logger/frontend/src/components/FilterBar.jsx`\n- `/volume2/docker/finance/dsk-uploader/dskuploader.py`\n- `/volume2/docker/finance/auth/docker-compose.yml`","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Claude’s Plan","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"ets-create-a-new-generic-sun.md","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"ets-create-a-new-generic-sun.md","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Stayed in plan mode","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Good question. Here's the full breakdown:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Backend Tech Stack — Options","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Backend Tech Stack — Options","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Option A: Node.js + Express + Prisma (my recommendation)","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Option A: Node.js + Express + Prisma (my recommendation)","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Why:","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"payments-logger is already Node.js/Express/Prisma. The new app is 80% payments-logger with CSV upload added on top. Keeping the same stack means:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Copy","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"parser.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"verbatim, zero rewrite","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Copy the entire","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"routes/payments.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with minimal edits","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Same Prisma schema workflow, same migrations, same Docker image","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The CSV parsing logic (the only Python-specific thing) ports cleanly to","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"csv-parse","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— it's just regex + string manipulation, nothing pandas-specific","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Trade-off:","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Have to port the Python CSV parsing to JS. It's ~150 lines and mostly string manipulation — straightforward.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Option B: Python + FastAPI + SQLAlchemy","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Option B: Python + FastAPI + SQLAlchemy","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Rewrite everything in Python. dsk-uploader is Python, so CSV parsing is already done.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Pro:","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Keep the dsk-uploader CSV logic as-is (no porting). Python has better data science libraries if you later add analytics.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Con:","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Lose the entire payments-logger backend (routes, parser, Prisma schema) — full rewrite. FastAPI + SQLAlchemy is a different mental model than Express + Prisma. Frontend stays React anyway, so the stack split doesn't simplify anything.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Verdict:","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"More work overall, gains almost nothing.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Option C: Hybrid microservices (Node.js backend + Python CSV service)","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Option C: Hybrid microservices (Node.js backend + Python CSV service)","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Keep Node.js as the main API, add a separate Python FastAPI service just for CSV parsing. They share the DB.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Pro:","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Zero porting — Python CSV code used as-is.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Con:","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two backend services to maintain, deploy, and keep in sync. Inter-service HTTP calls. Two Dockerfiles. For ~150 lines of CSV logic, this is massively over-engineered.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Verdict:","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Not worth it.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Option D: Python + Flask (exactly like dsk-uploader)","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Option D: Python + Flask (exactly like dsk-uploader)","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Extend dsk-uploader into a full payments app.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Pro:","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Pandas for CSV, no porting.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Con:","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Jinja2 templates instead of React (or add React separately). Would need to reimplement the entire payments-logger (SMS parser, notification system, tag management, pagination/filtering). Flask's Jinja templating is far less capable than the existing React UI. The React frontend is the stronger UI foundation.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Verdict:","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Worst of all options — rebuild the harder parts (UI, SMS parser, notification) for no gain.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Summary","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Summary","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Option","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CSV porting effort","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"App rewrite effort","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Result quality","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"A (Node.js)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Medium (150 lines)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Minimal","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Best","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"B (Python FastAPI)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"None","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"High (full backend)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Worse","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"C (Hybrid)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"None","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Low + infra overhead","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Overkill","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"D (Python Flask)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"None","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Very high","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Worst","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Option","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"A (Node.js)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"B (Python FastAPI)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"C (Hybrid)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"D (Python Flask)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CSV porting effort","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Medium (150 lines)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"None","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"None","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"None","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"App rewrite effort","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Minimal","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"High (full backend)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Low + infra overhead","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Very high","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Result quality","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Best","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Worse","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Overkill","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Worst","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Option","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CSV porting effort","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"App rewrite effort","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Result quality","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The CSV parser port (the only cost of Option A) is a one-time ~150 line exercise. Everything else in the plan reuses payments-logger code directly. That's why Node.js is the clear winner here.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Want to adjust the stack, or shall I proceed with Option A?","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Message actions","depth":24,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"ok sounds good. Implement all you suggested","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Starting implementation. Let me first read all the reference files I'll need to copy from.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"parser.js","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"parser.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"payments.js","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"payments.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"index.js","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"index.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"App.jsx","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"App.jsx","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"FilterBar.jsx","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"FilterBar.jsx","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"PaymentTable.jsx","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"PaymentTable.jsx","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Now let me read the remaining frontend files and the Dockerfile from payments-logger.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"main.jsx","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"main.jsx","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"index.css","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"index.css","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"package.json","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"package.json","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Dockerfile","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Dockerfile","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Dockerfile","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Dockerfile","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"vite.config.js","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"vite.config.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"tailwind.config.js","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"tailwind.config.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"postcss.config.js","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"postcss.config.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"index.html","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"index.html","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"dskuploader.py","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"dskuploader.py","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Let me also grab the remaining frontend components.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"PaymentCard.jsx","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"PaymentCard.jsx","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"PaymentList.jsx","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"PaymentList.jsx","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"schema.prisma","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"schema.prisma","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Good, I have everything I need. Let me set up the todo list and start building.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update Todos","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Create folder structure and root config files","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write backend Prisma schema and migration","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write backend package.json and Dockerfile","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write backend src files (auth, parser, csvParser, index)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write backend routes (payments.js, upload.js)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write frontend config files","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write frontend React components","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Build and verify with docker compose","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Create finance-hub directory structure","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"mkdir -p /volume2/docker/finance/finance-hub/backend/src/routes \\\n /volume2/docker/finance/finance-hub/backend/prisma/migrations/20260508_init \\\n /volume2/docker/finance/finance-hub/frontend/src/components && echo \"done\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"done","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"docker-compose.yml","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"docker-compose.yml","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"45 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"version: '3.8'\n\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_USER: finance\n POSTGRES_PASSWORD: ${DB_PASSWORD}\n POSTGRES_DB: finance_hub\n volumes:\n - pgdata:/var/lib/postgresql/data\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U finance -d finance_hub\"]\n interval: 5s\n timeout: 5s\n retries: 5\n\n backend:\n build: ./backend\n restart: unless-stopped\n environment:\n DATABASE_URL: postgresql://finance:${DB_PASSWORD}@db:5432/finance_hub\n PORT: \"3001\"\n NOTIFIER_URL: ${NOTIFIER_URL}\n NOTIFIER_CHANNEL: ${NOTIFIER_CHANNEL:-viber}\n NOTIFY_DEFAULT_PHONE: ${NOTIFY_DEFAULT_PHONE}\n TZ: ${TZ:-Europe/Sofia}\n ports:\n - \"${BACKEND_PORT:-3001}:3001\"\n depends_on:\n db:\n condition: service_healthy\n\n frontend:\n build: ./frontend\n restart: unless-stopped\n ports:\n - \"${FRONTEND_PORT:-5175}:5173\"\n depends_on:\n - backend\n\nvolumes:\n pgdata:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":".env.example","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":".env.example","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"15 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"# Database\nDB_PASSWORD=change_me_strong_password\n\n# Notifier service (for SMS payment notifications)\nNOTIFIER_URL=https://notifier.lakylak.xyz\nNOTIFIER_CHANNEL=viber\nNOTIFY_DEFAULT_PHONE=+359000000000\n\n# Timezone (used by SMS parser for Bulgarian bank timestamps)\nTZ=Europe/Sofia\n\n# Ports (optional — defaults shown)\nBACKEND_PORT=3001\nFRONTEND_PORT=5175","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":".gitignore","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":".gitignore","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"5 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".env\nnode_modules/\ndist/\n*.log","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update Todos","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Create folder structure and root config files","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write backend Prisma schema and migration","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write backend package.json and Dockerfile","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write backend src files (auth, parser, csvParser, index)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write backend routes (payments.js, upload.js)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write frontend config files","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write frontend React components","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Build and verify with docker compose","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"schema.prisma","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"schema.prisma","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"54 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"generator client {\n provider = \"prisma-client-js\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel Payment {\n id Int @id @default(autoincrement())\n rawMessage String @map(\"raw_message\")\n date DateTime?\n type String?\n card String?\n recipient String?\n amount Float?\n currency String? @default(\"EUR\")\n balance Float?\n source Source @default(INGEST)\n status Status @default(UNPROCESSED)\n tags Tag[]\n notifiedAt DateTime? @map(\"notified_at\")\n notifyPhone String? @map(\"notify_phone\")\n debitBgn Float? @map(\"debit_bgn\")\n creditBgn Float? @map(\"credit_bgn\")\n transactionType String? @map(\"transaction_type\")\n payerAccount String? @map(\"payer_account\")\n createdAt DateTime @default(now()) @map(\"created_at\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n @@map(\"payments\")\n}\n\nmodel Tag {\n id Int @id @default(autoincrement())\n name String @unique\n color String @default(\"#6b7280\")\n payments Payment[]\n\n @@map(\"tags\")\n}\n\nenum Status {\n UNPROCESSED\n SENT\n SKIPPED\n}\n\nenum Source {\n INGEST\n UPLOAD\n}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"migration.sql","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"migration.sql","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"55 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- CreateEnum\nCREATE TYPE \"Status\" AS ENUM ('UNPROCESSED', 'SENT', 'SKIPPED');\nCREATE TYPE \"Source\" AS ENUM ('INGEST', 'UPLOAD');\n\n-- CreateTable\nCREATE TABLE \"payments\" (\n \"id\" SERIAL PRIMARY KEY,\n \"raw_message\" TEXT NOT NULL,\n \"date\" TIMESTAMP(3),\n \"type\" TEXT,\n \"card\" TEXT,\n \"recipient\" TEXT,\n \"amount\" DOUBLE PRECISION,\n \"currency\" TEXT DEFAULT 'EUR',\n \"balance\" DOUBLE PRECISION,\n \"source\" \"Source\" NOT NULL DEFAULT 'INGEST',\n \"status\" \"Status\" NOT NULL DEFAULT 'UNPROCESSED',\n \"notified_at\" TIMESTAMP(3),\n \"notify_phone\" TEXT,\n \"debit_bgn\" DOUBLE PRECISION,\n \"credit_bgn\" DOUBLE PRECISION,\n \"transaction_type\" TEXT,\n \"payer_account\" TEXT,\n \"created_at\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n \"updated_at\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\n-- CreateTable\nCREATE TABLE \"tags\" (\n \"id\" SERIAL PRIMARY KEY,\n \"name\" TEXT NOT NULL,\n \"color\" TEXT NOT NULL DEFAULT '#6b7280'\n);\n\n-- CreateUniqueIndex\nCREATE UNIQUE INDEX \"tags_name_key\" ON \"tags\"(\"name\");\n\n-- CreateTable (M2M join)\nCREATE TABLE \"_PaymentToTag\" (\n \"A\" INTEGER NOT NULL,\n \"B\" INTEGER NOT NULL,\n CONSTRAINT \"_PaymentToTag_AB_pkey\" PRIMARY KEY (\"A\", \"B\")\n);\n\nCREATE INDEX \"_PaymentToTag_B_index\" ON \"_PaymentToTag\"(\"B\");\n\n-- AddForeignKey\nALTER TABLE \"_PaymentToTag\"\n ADD CONSTRAINT \"_PaymentToTag_A_fkey\"\n FOREIGN KEY (\"A\") REFERENCES \"payments\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\nALTER TABLE \"_PaymentToTag\"\n ADD CONSTRAINT \"_PaymentToTag_B_fkey\"\n FOREIGN KEY (\"B\") REFERENCES \"tags\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"migration_lock.toml","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"migration_lock.toml","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"4 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"# Please do not edit this file manually\n# It should be added in your version-control system (e.g., Git)\nprovider = \"postgresql\"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update Todos","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Create folder structure and root config files","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write backend Prisma schema and migration","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write backend package.json and Dockerfile","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write backend src files (auth, parser, csvParser, index)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write backend routes (payments.js, upload.js)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write frontend config files","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write frontend React components","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Build and verify with docker compose","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"package.json","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"package.json","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"26 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{\n \"name\": \"finance-hub-backend\",\n \"version\": \"1.0.0\",\n \"main\": \"src/index.js\",\n \"scripts\": {\n \"start\": \"node src/index.js\",\n \"dev\": \"nodemon src/index.js\",\n \"prisma:generate\": \"prisma generate\",\n \"prisma:migrate\": \"prisma migrate deploy\"\n },\n \"dependencies\": {\n \"@prisma/client\": \"^5.22.0\",\n \"cors\": \"^2.8.5\",\n \"csv-parse\": \"^5.5.6\",\n \"express\": \"^4.21.1\",\n \"express-rate-limit\": \"^7.4.0\",\n \"iconv-lite\": \"^0.6.3\",\n \"morgan\": \"^1.10.0\",\n \"multer\": \"^1.4.5-lts.1\"\n },\n \"devDependencies\": {\n \"nodemon\": \"^3.1.7\",\n \"prisma\": \"^5.22.0\"\n }\n}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Dockerfile","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Dockerfile","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"18 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FROM node:20-alpine\n\nRUN apk add --no-cache openssl\n\nWORKDIR /app\n\nCOPY package.json package-lock.json* ./\nRUN npm install\n\nCOPY prisma ./prisma\nRUN npx prisma generate\n\nCOPY src ./src\n\nEXPOSE 3001\n\nCMD [\"sh\", \"-c\", \"npx prisma migrate deploy && node src/index.js\"]","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update Todos","depth":25,"on_screen":false,"role_description":"text"}]...
|
-8898839276653562392
|
6376942343888890845
|
click
|
accessibility
|
NULL
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: finance [SSH: nas]
Explorer Section: finance [SSH: nas]
FINANCE [SSH: NAS]
auth
dsk-uploader
finance-hub
backend
frontend
.env
.env.example
.gitignore
docker-compose.yml
payments-logger
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
docker-compose.yml, Editor Group 1
.env, Editor Group 1
report(1).csv, Editor Group 1
report(2).csv, Editor Group 1
"Дата","Основание","Наредител/Получател","Номер сметка на наредителя / получателя","Вид на трансакцията","Свързваща референция","Валутен курс","Сума във валутата на превода","Дебит EUR","Кредит EUR"
"08.05.2026","400915xxxxxx4447 ПОС 08.05.2026 18:46<br/>Авт. код: 36C1UB","","","","","","","5,02",""
"08.05.2026","400915xxxxxx4447 ПОС 08.05.2026 18:36<br/>Авт. код: 5CK9TB","","","","","","","15,46",""
"08.05.2026","400915xxxxxx4447 ПОС 08.05.2026 18:07<br/>Авт. код: 8FK4TB","","","","","","","9,04",""
"08.05.2026","400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 08.05.2026 19:32<br/>Авт. код: 6NM31C<br/>Номер на у-во: V5411421","BGR SOFIYA LIDL BALGARIYA EOOD UL TODOR","","КАРТОВА ОПЕРАЦИЯ","","","","67,81",""
"08.05.2026","400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 07.05.2026 09:02<br/>Авт. код: 7NE8N7<br/>Номер на у-во: 10101446","BGR SOFIA CBA EKO MARKET","","КАРТОВА ОПЕРАЦИЯ","","","","5,51",""
"08.05.2026","400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 06.05.2026 13:16<br/>Авт. код: 1TV886<br/>Номер на у-во: 06030191","POL BALICE Lagardere Travel R KR3","","КАРТОВА ОПЕРАЦИЯ","","","","5,49",""
"Дата","Основание","Наредител/Получател","Номер сметка на наредителя / получателя","Вид на трансакцията","Свързваща референция","Валутен курс","Сума във валутата на превода","Дебит EUR","Кредит EUR"
"08.05.2026","400915xxxxxx4447 ПОС 08.05.2026 18:46<br/>Авт. код: 36C1UB","","","","","","","5,02",""
"08.05.2026","400915xxxxxx4447 ПОС 08.05.2026 18:36<br/>Авт. код: 5CK9TB","","","","","","","15,46",""
"08.05.2026","400915xxxxxx4447 ПОС 08.05.2026 18:07<br/>Авт. код: 8FK4TB","","","","","","","9,04",""
"08.05.2026","400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 08.05.2026 19:32<br/>Авт. код: 6NM31C<br/>Номер на у-во: V5411421","BGR SOFIYA LIDL BALGARIYA EOOD UL TODOR","","КАРТОВА ОПЕРАЦИЯ","","","","67,81",""
"08.05.2026","400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 07.05.2026 09:02<br/>Авт. код: 7NE8N7<br/>Номер на у-во: 10101446","BGR SOFIA CBA EKO MARKET","","КАРТОВА ОПЕРАЦИЯ","","","","5,51",""
"08.05.2026","400915xxxxxx4447 ПЛАЩАНЕ НА ПОС 06.05.2026 13:16<br/>Авт. код: 1TV886<br/>Номер на у-во: 06030191","POL BALICE Lagardere Travel R KR3","","КАРТОВА ОПЕРАЦИЯ","","","","5,49",""
Design new payment-logge…, Editor Group 2
Problems (⇧⌘M)
PROBLEMS
Output (⇧⌘U)
OUTPUT
Debug Console (⇧⌘Y)
DEBUG CONSOLE
Terminal (⌃`)
TERMINAL
Ports
PORTS
remote SSH: nas
SSH: nas
No Problems
0
0
No Ports Forwarded
0
Notifications
Sign In
Sign In
Info: Setting up SSH Host nas: Setting up SSH tunnel
Design new payment-logger and dsk-uploader hybrid app
Session history
New session
Message actions
payments.js
payments.js
ets create a new app that should be combination of payment-logger and dsk-uploader. It should have authorization via authentik (auth folder). All three folders (payment-logger, dsk-uploader and auth) are just refference these will be removed later. Auth project is separated it lives on its own. First reveiw them and see how these should be combined. It will be whole new app (also the folder name). Think very carefully of whatr these two apps do and how cold they be combined. THerer should be common db and uploader should store data the same way the /ingest does. It should be properly marked in UI if it is upload or ingest or both. FIrst think of tech stack and plan carefully.
Thinking
Thinking
I'll explore all three reference projects in parallel to understand their structure before planning.
Agent:
Explore payment-logger app structure and functionality
IN
Explore the /volume2/docker/finance/payments-logger directory thoroughly. I need to understand:
1. Tech stack (frontend framework, backend framework, database)
2. What the app does - its core purpose and features
3. Database schema - all tables, relationships
4. API routes - all endpoints, their purpose and request/response shapes
5. How data flows (ingest, storage, retrieval)
6. Any auth/middleware setup
7. Docker/deployment setup
8. Key files and their roles
Report with: directory structure, tech stack summary, database schema details, all API endpoints listed, UI features, and how data is ingested. Be thorough on the /ingest endpoint specifically.
Agent:
Explore dsk-uploader app structure and functionality
IN
Explore the /volume2/docker/finance/dsk-uploader directory thoroughly. I need to understand:
1. Tech stack (frontend framework, backend framework, database)
2. What the app does - its core purpose and features
3. Database schema - all tables, relationships
4. API routes - all endpoints, their purpose and request/response shapes
5. How file uploads work - what files, what format, how parsed
6. Any auth/middleware setup
7. Docker/deployment setup
8. Key files and their roles
Report with: directory structure, tech stack summary, database schema details, all API endpoints listed, UI features, how file upload parsing works, and what data is extracted from uploads. Be very thorough about the data model and what fields are stored.
Agent:
Explore auth folder and authentik integration
IN
Explore the /volume2/docker/finance/auth directory thoroughly. I need to understand:
1. What auth system is being used (Authentik)
2. How authentication/authorization is implemented - OAuth2, OIDC, session tokens, JWT?
3. Middleware - how is auth enforced on routes?
4. Any frontend auth flow - login redirect, token handling, protected routes
5. Environment variables related to auth
6. Docker/deployment setup for the auth service itself
7. Any example of how other apps integrate with this auth
Report with: directory structure, auth flow description, middleware code, environment variable names, and integration patterns. This is a reference for building auth into a new combined app.
Message actions
payments.js
payments.js
continue
Thinking
Thinking
Launching parallel exploration of all three reference projects.
Agent:
Explore payment-logger app structure
IN
Explore the /volume2/docker/finance/payments-logger directory thoroughly. I need to understand:
1. Full directory structure (tree)
2. Tech stack (frontend framework, backend framework, database, etc.)
3. What the app does - its core functionality
4. How data is ingested (look at /ingest route especially)
5. Database schema (what tables/collections exist)
6. API routes and their purposes
7. Frontend pages and components
8. Docker setup
9. Any auth/session handling
Please read key files: package.json files, main entry points, route files, database schema files, docker-compose.yml, and any frontend components. Be thorough - I need a complete picture to plan a new combined app.
Agent:
Explore dsk-uploader app structure
IN
Explore the /volume2/docker/finance/dsk-uploader directory thoroughly. I need to understand:
1. Full directory structure (tree)
2. Tech stack (frontend framework, backend framework, database, etc.)
3. What the app does - its core functionality
4. How file uploads work (what format, what processing happens)
5. Database schema (what tables/collections exist)
6. API routes and their purposes
7. Frontend pages and components
8. Docker setup
9. Any auth/session handling
Please read key files: package.json files, main entry points, route files, database schema files, docker-compose.yml, and any frontend components. Be thorough - I need a complete picture to plan a new combined app.
Agent:
Explore auth project structure
IN
Explore the /volume2/docker/finance/auth directory thoroughly. I need to understand:
1. Full directory structure (tree)
2. What authentication system is used (authentik integration details)
3. How OAuth/OIDC flow works
4. Any middleware or helper functions for protecting routes
5. Session management approach
6. How frontend handles auth state
7. Any example of how other apps integrate with this auth system
8. Docker setup and environment variables
Please read all files carefully - this auth system will be integrated into a new combined app. I need to understand exactly how to use it.
Thinking
Thinking
Now I have a complete picture of all three. Let me design the plan.
Agent:
Design combined app architecture
IN
Design a detailed implementation plan for a new combined web app that merges two existing applications. Here is full context:
---
## App 1: payments-logger (Node.js)
- **Purpose**: Ingest Bulgarian bank SMS → parse → store in PostgreSQL → review/tag/notify in React UI
- **Stack**: Node.js 20 + Express + Prisma 5 + PostgreSQL 16 + React 18 + Vite + Tailwind CSS
- **Auth**: Custom JWT (bcryptjs + jose), tokens in localStorage
- **Ingest**: POST /api/payments/ingest (public) — SMS text OR structured JSON (Apple Wallet)
- **Schema** (PostgreSQL via Prisma):
- `payments` table: id, raw_message, date, type (POS/ATM/INTERNET/ECOM/P2P/WALLET), card, recipient, amount, balance, status (UNPROCESSED/SENT/SKIPPED), notifyPhone, notifiedAt, created_at, updated_at
- `tags` table: id, name, color — M2M with payments via `_PaymentToTag`
- `users` table: id, username, hashed_password (this will be REMOVED)
- **UI**: Single-page React app — PaymentTable (sortable, filterable, taggable), FilterBar, status actions (send/skip), notification system
- **Parser** (backend/src/parser.js): Regex parser for Bulgarian DSK Bank SMS, extracts date/time (DD/MM/YYYY HH:MM), card mask, transaction type, recipient, amount, balance
## App 2: dsk-uploader (Python/Flask)
- **Purpose**: Upload DSK bank CSV exports → parse/normalize → upload to Notion database
- **Stack**: Python 3.11 + Flask + Pandas + Custom Notion SDK + Bootstrap 5
- **Auth**: None (open)
- **CSV format** (DSK Bank Bulgarian format, columns):
- `Дата` (date, DD.MM.YYYY)
- `Вид на трансакцията` (transaction type, Bulgarian)
- `Основание` (reason/description — contains card number regex: `^\d{6}x{6}\d{4}$`)
- `Дебит BGN` (debit amount, may be empty)
- `Кредит BGN` (credit amount, may be empty)
- `Наредител/Получател` (orderer/recipient name)
- `Номер сметка на наредителя / получателя` (account number)
- **Processing**: merge multiple CSVs, normalize dates, extract card numbers from reason via regex, auto-generate tags (keyword heuristics: ЗАПЛАТА→Salary, NETFLIX→Subscriptions, etc.), filter internal transfers
- **Output**: Notion database pages (this will be REPLACED with local PostgreSQL)
## App 3: auth (Authentik)
- **Mode**: Proxy mode via NPM (forward auth)
- **How it works**: NPM intercepts all requests, calls Authentik outpost's auth endpoint. On success, NPM injects headers into proxied request:
- `X-authentik-username`
- `X-authentik-email`
- `X-authentik-groups`
- **No code integration needed** in the app itself — just trust these headers from NPM
- **Logout**: Redirect user to `/outpost.goauthentik.io/sign_out`
---
## What the New Combined App Must Do
1. **Single PostgreSQL database** for all transactions
2. **SMS Ingest** (public endpoint) — same as payments-logger /ingest, source=INGEST
3. **CSV Upload** (authenticated) — parse DSK CSV files, store transactions with source=UPLOAD into the same DB schema
4. **Unified UI** — show all payments in one table regardless of source
5. **Source indicator** in UI — badge showing "INGEST", "UPLOAD", or "BOTH" (if duplicate detection added)
6. **Source filter** in FilterBar — filter by All/Ingest/Upload
7. **Authentik auth** — remove custom JWT entirely; trust X-authentik-* headers from NPM
8. **All existing payments-logger features**: tagging, send notification, skip, status tracking, filters, sorting, pagination
9. **Remove Notion dependency entirely** — all data stays in local PostgreSQL
---
## Technical Constraints
- Keep Node.js backend (not Python) — extend payments-logger's stack
- CSV parsing: use `csv-parse` npm package (handles encoding, streaming)
- File upload: use `multer` npm middleware
- Auth middleware: read X-authentik-username header, 401 if missing (but /api/payments/ingest stays public)
- Add `source` enum field (INGEST/UPLOAD) to payments schema
- Add `currency` field (EUR for SMS ingest, BGN for CSV uploads) — or just store as-is
- Remove `users` table from schema
---
## Please Design
1. Complete folder structure for the new app (suggest name: "finance-hub" or similar — note this is a placeholder, user will confirm)
2. Prisma schema with new fields (`source`, `currency`)
3. All backend API routes
4. CSV parser design in Node.js (port of the Python logic)
5. Authentik auth middleware
6. Frontend component structure (React) — what's reused from payments-logger, what's new
7. Docker Compose setup
8. Environment variables needed
9. Step-by-step implementation order
10. Verification checklist
Be specific about file paths and code structure. This will be used to write actual implementation code.
Thinking
Thinking
Good plan. Let me ask the one thing I need before writing the final plan.
AskUserQuestion
OUT
User has answered your questions: "What should the new combined app be called? This will be the folder name under /volume2/docker/finance/ and the app branding."="finance-hub". You can now continue with the user's answers in mind.
Thinking
Thinking
Writing the final plan now.
Thinking
Thinking
Write
ets-create-a-new-generic-sun.md
ets-create-a-new-generic-sun.md
326 lines
# finance-hub — Implementation Plan
## Context
Two separate finance apps exist as references and will be retired:
- **payments-logger** (Node.js/React): Ingests Bulgarian bank SMS notifications, stores payments in PostgreSQL, provides a review/tag/notify UI.
- **dsk-uploader** (Python/Flask): Uploads DSK bank CSV exports, parses them, sends to Notion.
The new app **finance-hub** (`/volume2/docker/finance/finance-hub/`) replaces both with a single unified system: all transactions (SMS-ingested and CSV-uploaded) land in one PostgreSQL database and one React UI. Authentication is delegated entirely to Authentik (proxy mode via NPM — no custom JWT).
---
## Tech Stack
| Layer | Choice | Rationale |
|-------|--------|-----------|
| Backend | Node.js 20 + Express + Prisma 5 | Extends payments-logger; no Python needed |
| CSV parsing | `csv-parse` + `iconv-lite` | Handles Bulgarian cp1251 encoding, RFC4180 quoted fields |
| File upload | `multer` (memory storage) | Bank CSVs are small (<500 KB), no disk cleanup needed |
| Database | PostgreSQL 16 | Same as payments-logger |
| Frontend | React 18 + Vite + Tailwind CSS + Lucide React | Same as payments-logger |
| Auth | Authentik proxy via NPM headers | No custom JWT; removes users table entirely |
| Container | Docker Compose | Same pattern as both reference apps |
**Removed vs payments-logger:** `bcryptjs`, `jose`, `users` table, login form, JWT storage.
**Removed vs dsk-uploader:** Python/Flask, Pandas, Notion SDK, all Notion references.
**Added:** `csv-parse`, `iconv-lite`, `multer`, `source` + `currency` fields, `UploadPanel` component.
---
## Folder Structure
```
/volume2/docker/finance/finance-hub/
├── docker-compose.yml
├── .env
├── .env.example
├── .gitignore
├── backend/
│ ├── Dockerfile
│ ├── package.json
│ ├── prisma/
│ │ ├── schema.prisma
│ │ └── migrations/
│ │ ├── migration_lock.toml
│ │ └── 20260508_init/
│ │ └── migration.sql
│ └── src/
│ ├── index.js ← entry point (Authentik middleware wired here)
│ ├── auth.js ← Authentik header middleware (replaces JWT auth)
│ ├── parser.js ← SMS parser (copy verbatim from payments-logger)
│ ├── csvParser.js ← NEW: DSK CSV parser (port of Python dskuploader.py)
│ └── routes/
│ ├── payments.js ← existing routes + source/currency additions
│ └── upload.js ← NEW: POST /api/upload/csv
└── frontend/
├── Dockerfile
├── package.json
├── vite.config.js
├── tailwind.config.js
├── postcss.config.js
├── index.html
└── src/
├── main.jsx ← remove AuthProvider wrapper
├── index.css
├── App.jsx ← remove auth state, add Upload tab toggle
└── components/
├── FilterBar.jsx ← add source filter select
├── PaymentTable.jsx ← add Source badge column + currency display
├── PaymentCard.jsx ← minor source badge addition
├── PaymentList.jsx ← unchanged
└── UploadPanel.jsx ← NEW: drag-and-drop CSV upload UI
```
---
## Database Schema (Prisma)
File: `backend/prisma/schema.prisma`
```prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Payment {
id Int @id @default(autoincrement())
rawMessage String @map("raw_message")
date DateTime?
type String?
card String?
recipient String?
amount Float?
currency String? @default("EUR")
balance Float?
source Source @default(INGEST)
status Status @default(UNPROCESSED)
tags Tag[]
notifiedAt DateTime? @map("notified_at")
notifyPhone String? @map("notify_phone")
debitBgn Float? @map("debit_bgn")
creditBgn Float? @map("credit_bgn")
transactionType String? @map("transaction_type")
payerAccount String? @map("payer_account")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("payments")
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
color String @default("#6b7280")
payments Payment[]
@@map("tags")
}
enum Status { UNPROCESSED SENT SKIPPED }
enum Source { INGEST UPLOAD }
```
**Key decisions:**
- No `User` model — Authentik owns identity.
- `currency`: `EUR` for SMS ingest, `BGN` for CSV uploads.
- `debitBgn`, `creditBgn`, `transactionType`, `payerAccount`: nullable CSV-only columns; INGEST rows store nulls. Avoids a union query for the unified list view.
- `balance` is always null for CSV rows (DSK export does not include running balance).
- Fresh consolidated migration — no data migration from reference apps required.
---
## API Routes
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | /api/health | public | Health check |
| POST | /api/payments/ingest | public | SMS or structured ingest (source=INGEST) |
| GET | /api/payments | required | List with filters/sort/pagination (+ source filter) |
| GET | /api/payments/meta/tags | required | All tags |
| GET | /api/payments/meta/filters | required | Filter options incl. `sources` array |
| GET | /api/payments/:id | required | Single payment |
| PATCH | /api/payments/:id | required | Update status |
| DELETE | /api/payments/:id | required | Delete |
| POST | /api/payments/:id/send | required | Send notification |
| POST | /api/payments/:id/skip | required | Skip |
| POST | /api/payments/:id/tags | required | Add/upsert tag |
| DELETE | /api/payments/:id/tags/:tagId | required | Remove tag |
| POST | /api/upload/csv | required | DSK CSV file upload (source=UPLOAD) |
---
## Key Implementation Details
### auth.js (replaces entire old auth module)
```js
const PUBLIC_PATHS = new Set(['/api/health', '/api/payments/ingest']);
function authentikMiddleware(req, res, next) {
if (PUBLIC_PATHS.has(req.path)) return next();
const username = req.headers['x-authentik-username'];
if (!username) return res.status(401).json({ error: 'Unauthorized' });
req.user = {
username,
email: req.headers['x-authentik-email'] || null,
groups: (req.headers['x-authentik-groups'] || '').split(',').map(g => g.trim()).filter(Boolean),
};
next();
}
module.exports = { authentikMiddleware };
```
### csvParser.js (port of dskuploader.py)
- `iconv-lite` decodes buffer as cp1251 (DSK Bank export encoding), falls back to UTF-8
- `csv-parse` parses the decoded text with `columns: true`
- Columns: `Дата`, `Вид на трансакцията`, `Основание`, `Дебит BGN`, `Кредит BGN`, `Наредител/Получател`, `Номер сметка на наредителя / получателя`
- Card extraction: regex `/^\d{6}x{6}\d{4}$/` on first token of `Основание`
- Skips rows where `Вид на трансакцията === 'ТРАНСФЕР СОБСТВЕНИ СМЕТКИ'`
- Auto-tags via keyword rules (ЗАПЛАТА→Salary, LIDL→Groceries, NETFLIX→Subscriptions, etc.) — same logic as Python `generate_tags()`
- Returns `{ rows: PaymentData[], skipped: number, errors: string[] }`
### payments.js changes from payments-logger
1. Add `source: 'INGEST'` and `currency` to the `/ingest` create call
2. Add `source` to the `GET /` where clause filter
3. Add `sources` to `meta/filters` response
4. Currency-aware amount formatting in notification message
5. Remove all JWT/auth references (no `/auth/register`, `/auth/login`)
### upload.js (new)
- `multer` memory storage, max 10 files × 10 MB
- Calls `parseDskCsv(buffer)` per file
- Upserts tags via `prisma.tag.upsert` then connects
- Returns `{ imported, skipped, errors, payments[] }`
### Frontend changes
- **Delete**: `auth.js`, `AuthProvider.jsx`
- **main.jsx**: Remove `<AuthProvider>` wrapper
- **App.jsx**: Replace `authFetch` with plain `fetch` (Authentik session cookie travels automatically); logout → `window.location.href = '/outpost.goauthentik.io/sign_out'`; add "Payments" / "Upload CSV" tab toggle
- **FilterBar.jsx**: Add source `<select>` (All / SMS Ingest / CSV Upload); widen grid to 5 cols
- **PaymentTable.jsx**: Add `Source` column with `SMS` (indigo) / `CSV` (emerald) badge; show `${amount} ${currency}` in amount cell
- **UploadPanel.jsx** (new): Drag-and-drop zone + file list + import button + result summary with error accordion; calls `POST /api/upload/csv` with `FormData`
---
## Docker Compose
```yaml
services:
db:
image: postgres:16-alpine
environment: { POSTGRES_USER: finance, POSTGRES_PASSWORD: [PASSWORD] POSTGRES_DB: finance_hub }
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck: { test: pg_isready -U finance -d finance_hub, interval: 5s }
backend:
build: ./backend
environment:
DATABASE_URL: [CONNECTION_STRING]
PORT: "3001"
NOTIFIER_URL: ${NOTIFIER_URL}
NOTIFIER_CHANNEL: ${NOTIFIER_CHANNEL:-viber}
NOTIFY_DEFAULT_PHONE: ${NOTIFY_DEFAULT_PHONE}
TZ: ${TZ:-Europe/Sofia}
ports: ["${BACKEND_PORT:-3001}:3001"]
depends_on: { db: { condition: service_healthy } }
frontend:
build: ./frontend
ports: ["${FRONTEND_PORT:-5175}:5173"]
depends_on: [backend]
volumes: { pgdata: }
```
**NPM setup:** Frontend port proxied through Authentik forward auth. Backend port exposed separately (unprotected) so the public `/api/payments/ingest` remains reachable by iOS Shortcuts / SMS gateways without login.
---
## Environment Variables (.env)
```bash
[ENV_SECRET]
NOTIFIER_URL=[URL_WITH_CREDENTIALS] @default(autoincrement())
rawMessage String @map("raw_message")
date DateTime?
type String?
card String?
recipient String?
amount Float?
currency String? @default("EUR")
balance Float?
source Source @default(INGEST)
status Status @default(UNPROCESSED)
tags Tag[]
notifiedAt DateTime? @map("notified_at")
notifyPhone String? @map("notify_phone")
debitBgn Float? @map("debit_bgn")
creditBgn Float? @map("credit_bgn")
transactionType String? @map("transaction_type")
payerAccount String? @map("payer_account")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("payments")
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
color String @default("#6b7280")
payments Payment[]
@@map("tags")
}
enum Status {
UNPROCESSED
SENT
SKIPPED
}
enum Source {
INGEST
UPLOAD
}
Write
migration.sql
migration.sql
55 lines
-- CreateEnum
CREATE TYPE "Status" AS ENUM ('UNPROCESSED', 'SENT', 'SKIPPED');
CREATE TYPE "Source" AS ENUM ('INGEST', 'UPLOAD');
-- CreateTable
CREATE TABLE "payments" (
"id" SERIAL PRIMARY KEY,
"raw_message" TEXT NOT NULL,
"date" TIMESTAMP(3),
"type" TEXT,
"card" TEXT,
"recipient" TEXT,
"amount" DOUBLE PRECISION,
"currency" TEXT DEFAULT 'EUR',
"balance" DOUBLE PRECISION,
"source" "Source" NOT NULL DEFAULT 'INGEST',
"status" "Status" NOT NULL DEFAULT 'UNPROCESSED',
"notified_at" TIMESTAMP(3),
"notify_phone" TEXT,
"debit_bgn" DOUBLE PRECISION,
"credit_bgn" DOUBLE PRECISION,
"transaction_type" TEXT,
"payer_account" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "tags" (
"id" SERIAL PRIMARY KEY,
"name" TEXT NOT NULL,
"color" TEXT NOT NULL DEFAULT '#6b7280'
);
-- CreateUniqueIndex
CREATE UNIQUE INDEX "tags_name_key" ON "tags"("name");
-- CreateTable (M2M join)
CREATE TABLE "_PaymentToTag" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_PaymentToTag_AB_pkey" PRIMARY KEY ("A", "B")
);
CREATE INDEX "_PaymentToTag_B_index" ON "_PaymentToTag"("B");
-- AddForeignKey
ALTER TABLE "_PaymentToTag"
ADD CONSTRAINT "_PaymentToTag_A_fkey"
FOREIGN KEY ("A") REFERENCES "payments"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "_PaymentToTag"
ADD CONSTRAINT "_PaymentToTag_B_fkey"
FOREIGN KEY ("B") REFERENCES "tags"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Write
migration_lock.toml
migration_lock.toml
4 lines
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
Update Todos
Create folder structure and root config files
Write backend Prisma schema and migration
Write backend package.json and Dockerfile
Write backend src files (auth, parser, csvParser, index)
Write backend routes (payments.js, upload.js)
Write frontend config files
Write frontend React components
Build and verify with docker compose
Write
package.json
package.json
26 lines
{
"name": "finance-hub-backend",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate deploy"
},
"dependencies": {
"@prisma/client": "^5.22.0",
"cors": "^2.8.5",
"csv-parse": "^5.5.6",
"express": "^4.21.1",
"express-rate-limit": "^7.4.0",
"iconv-lite": "^0.6.3",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.1"
},
"devDependencies": {
"nodemon": "^3.1.7",
"prisma": "^5.22.0"
}
}
Write
Dockerfile
Dockerfile
18 lines
FROM node:20-alpine
RUN apk add --no-cache openssl
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY prisma ./prisma
RUN npx prisma generate
COPY src ./src
EXPOSE 3001
CMD ["sh", "-c", "npx prisma migrate deploy && node src/index.js"]
Update Todos...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22262
|
966
|
54
|
2026-05-12T06:58:09.089843+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778569089089_m1.jpg...
|
Firefox
|
LLM API Pricing Comparison & Cost Guide (May 2 LLM API Pricing Comparison & Cost Guide (May 2026) — Work...
|
True
|
costgoat.com/compare/llm-api
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
New Tab
New Tab
Jy 20820 es reindex stream model h New Tab
New Tab
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
JY-20361: Add call scores in Panorama by steliyan-g · Pull Request #507 · jiminny/prophet
JY-20361: Add call scores in Panorama by steliyan-g · Pull Request #507 · jiminny/prophet
LLM API Pricing Comparison & Cost Guide (May 2026)
LLM API Pricing Comparison & Cost Guide (May 2026)
Close tab
New Tab
Customize sidebar
Close Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
AI Chat settings
Close
WORK, Google Account: [EMAIL]
Main menu
New Chat
Open menu for conversation actions.
Conversation with Gemini
Conversation with Gemini
Copy prompt
You said I’m on page “<tabTitle>JY-20361: Add call scores in Panorama by steliyan-</tabTitle>” with “<selection>@@ -4,7 +4,7 @@ Today is {date_today}.445System instructions:5System instructions:6- You must answer using markdown. Do not use html tags in your response even if requested by the user's question.6- You must answer using markdown. Do not use html tags in your response even if requested by the user's question.7-- The contexts includes data for call shortlisted through user-applied filters. Note that the calls might not relate to the same deal or not even be by the same sales rep.7+- The context includes data for calls shortlisted through user-applied filters. Note that the calls might not relate to the same deal or not even be by the same sales rep.8 - The full call context includes all calls data available while the short call context contains only an overview of the calls that are analysed. Refer to the short call context for quick reference and to see the overall picture.8 - The full call context includes all calls data available while the short call context contains only an overview of the calls that are analysed. Refer to the short call context for quick reference and to see the overall picture.9- The calls are ordered in chronological order. 9- The calls are ordered in chronological order. 10- Here’s how to use the call context:10- Here’s how to use the call context:@@ -16,6 +16,15 @@ System instructions:16 - Frame answers with awareness of the company’s ICP, deal cycle, and sales motion.16 - Frame answers with awareness of the company’s ICP, deal cycle, and sales motion.17 - Evaluate statements or objections based on how the team operates and what success looks like.17 - Evaluate statements or objections based on how the team operates and what success looks like.18 - Position responses in light of known competitors and market dynamics.18 - Position responses in light of known competitors and market dynamics.19+ - AI call score (`ai_call_score` in the full call JSON): When present, treat it as **pre-computed** output from your team’s AI call-scoring pipeline (the same kind of scoring as the `/call/ai-call-scoring` endpoint). **Do not** invent, override, or recalculate scores; interpret and summarize what is given.20+ - **Short call context:** **AI Scorecard** is the name of the scorecard applied to that call; **AI Score** is the **overall** score for that scorecard (the average of its per-rule scores, possibly shown as a decimal).21+ - **Full call context:** The `ai_call_score` object may include `ai_scorecard_name`, `score` (overall for that scorecard), and `ai_scorecard_rules`. For each rule when listed:22+ - `rule_name`: Title of the criterion.23+ - `rule_prompt`: The criterion text that was evaluated.24+ - `score`: Whole number **1–5** measuring how well the call satisfied that criterion against `rule_prompt` (1 = no evidence / not discussed or contrary; 5 = strong, clear evidence on the call).25+ - `justification`: Short rationale grounded in what happened on the call.26+ - `justification_timestamps`: Up to three entries with speaker **name** and **timestamp** (MM:SS in the recording) highlighting where the justification is supported.27+ - Use scores for coaching summaries, trends across calls, or quick comparisons when relevant. For **what was actually said**, still rely on the **transcript** (you may cross-reference rule timestamps when helpful).19- Use the Message History to:28- Use the Message History to:20 - Maintain continuity if the current question refers to previous exchanges.29 - Maintain continuity if the current question refers to previous exchanges.21 - Clarify or resolve ambiguities if the question depends on prior messages.30 - Clarify or resolve ambiguities if the question depends on prior messages.@@ -28,10 +37,10 @@ System instructions:28 - All factual claims must be supported by one or more references to relevant calls.37 - All factual claims must be supported by one or more references to relevant calls.29 - Use Markdown link syntax ([link text](URL)) and place links inline within sentences. Do not use footnotes, reference sections, or bare URLs. Integrate the reference links directly within the relevant parts of your response, rather than in a separate section.38 - Use Markdown link syntax ([link text](URL)) and place links inline within sentences. Do not use footnotes, reference sections, or bare URLs. Integrate the reference links directly within the relevant parts of your response, rather than in a separate section.30 - Use markdown links in the format [link text](/playback/{{call_id}})39 - Use markdown links in the format [link text](/playback/{{call_id}})31- - Use descriptive link that utlizes the call name. Do not use call time stamps in the link text even if the link itself contains a time stamp, e.g. use 'Call Name' instead of 'Call Name at 10:00'. Never use call ids as link text.\n"40+ - Use descriptive link text that utilizes the call name. Do not use call time stamps in the link text even if the link itself contains a time stamp, e.g. use 'Call Name' instead of 'Call Name at 10:00'. Never use call ids as link text.32- - Good example of link text: [Call Name](/playback/1234567890?apFrom=123)\n"41+ - Good example of link text: [Call Name](/playback/1234567890?apFrom=123)33- - Bad example of link text: [Call Name at 12:34](/playback/1234567890?apFrom=123) (do not use call time stamps in the link text)\n"42+ - Bad example of link text: [Call Name at 12:34](/playback/1234567890?apFrom=123) (do not use call time stamps in the link text)34- - Bad example of links: [Call Name on November 12, 2025](/playback/1234567890?apFrom=123) (do not use dates in the link text)\n\n"43+ - Bad example of links: [Call Name on November 12, 2025](/playback/1234567890?apFrom=123) (do not use dates in the link text)35 - You might back your statements with examples from the provided call transcripts in addition to the reference links.44 - You might back your statements with examples from the provided call transcripts in addition to the reference links.36- Be specific. Use names of accounts, clients and persons involved. Don't just say "one client" or "a client"! If no account is available, use the call title as a reference.45- Be specific. Use names of accounts, clients and persons involved. Don't just say "one client" or "a client"! If no account is available, use the call title as a reference.37- If the question or context is unclear, request clarification or highlight ambiguities.46- If the question or context is unclear, request clarification or highlight ambiguities.</selection>” selected. Please summarize the selection using precise and concise language. Use headers and bulleted lists in the summary, to make it scannable. Maintain the meaning and factual accuracy.
You said
I’m on page “<tabTitle>JY-20361: Add call scores in Panorama by steliyan-</tabTitle>” with “<selection>@@ -4,7 +4,7 @@ Today is {date_today}.445System instructions:5System instructions:6- You must answer using markdown. Do not use html tags in your response even if requested by the user's question.6- You must answer using markdown. Do not use html tags in your response even if requested by the user's question.7-- The contexts includes data for call shortlisted through user-applied filters. Note that the calls might not relate to the same deal or not even be by the same sales rep.7+- The context includes data for calls shortlisted through user-applied filters. Note that the calls might not relate to the same deal or not even be by the same sales rep.8 - The full call context includes all calls data available while the short call context contains only an overview of the calls that are analysed. Refer to the short call context for quick reference and to see the overall picture.8 - The full call context includes all calls data available while the short call context contains only an overview of the calls that are analysed. Refer to the short call context for quick reference and to see the overall picture.9- The calls are ordered in chronological order. 9- The calls are ordered in chronological order. 10- Here’s how to use the call context:10- Here’s how to use the call context:@@ -16,6 +16,15 @@ System instructions:16 - Frame answers with awareness of the company’s ICP, deal cycle, and sales motion.16 - Frame answers with awareness of the company’s ICP, deal cycle, and sales motion.17 - Evaluate statements or objections based on how the team operates and what success looks like.17 - Evaluate statements or objections based on how the team operates and what success looks like.18 - Position responses in light of known competitors and market dynamics.18 - Position responses in light of known competitors and market dynamics.19+ - AI call score (`ai_call_score` in the full call JSON): When present, treat it as **pre-computed** output from your team’s AI call-scoring pipeline (the same kind of scoring as the `/call/ai-call-scoring` endpoint). **Do not** invent, override, or recalculate scores; interpret and summarize what is given.20+ - **Short call context:** **AI Scorecard** is the name of the scorecard applied to that call; **AI Score** is the **overall** score for that scorecard (the average of its per-rule scores, possibly shown as a decimal).21+ - **Full call context:** The `ai_call_score` object may include `ai_scorecard_name`, `score` (overall for that scorecard), and `ai_scorecard_rules`. For each rule when listed:22+ - `rule_name`: Title of the criterion.23+ - `rule_prompt`: The criterion text that was evaluated.24+ - `score`: Whole number **1–5** measuring how well the call satisfied that criterion against `rule_prompt` (1 = no evidence / not discussed or contrary; 5 = strong, clear evidence on the call).25+ - `justification`: Short rationale grounded in what happened on the call.26+ - `justification_timestamps`: Up to three entries with speaker **name** and **timestamp** (MM:SS in the recording) highlighting where the justification is supported.27+ - Use scores for coaching summaries, trends across calls, or quick comparisons when relevant. For **what was actually said**, still rely on the **transcript** (you may cross-reference rule timestamps when helpful).19- Use the Message History to:28- Use the Message History to:20 - Maintain continuity if the current question refers to previous exchanges.29 - Maintain continuity if the current question refers to previous exchanges.21 - Clarify or resolve ambiguities if the question depends on prior messages.30 - Clarify or resolve ambiguities if the question depends on prior messages.@@ -28,10 +37,10 @@ System instructions:28 - All factual claims must be supported by one or more references to relevant calls.37 - All factual claims must be supported by one or more references to relevant calls.29 - Use Markdown link syntax ([link text](URL)) and place links inline within sentences. Do not use footnotes, reference sections, or bare URLs. Integrate the reference links directly within the relevant parts of your response, rather than in a separate section.38 - Use Markdown link syntax ([link text](URL)) and place links inline within sentences. Do not use footnotes, reference sections, or bare URLs. Integrate the reference links directly within the relevant parts of your response, rather than in a separate section.30 - Use markdown links in the format [link text](/playback/{{call_id}})39 - Use markdown links in the format [link text](/playback/{{call_id}})31- - Use descriptive link that utlizes the call name. Do not use call time stamps in the link text even if the link itself contains a time stamp, e.g. use 'Call Name' instead of 'Call Name at 10:00'. Never use call ids as link text.\n"40+ - Use descriptive link text that utilizes the call name. Do not use call time stamps in the link text even if the link itself contains a time stamp, e.g. use 'Call Name' instead of 'Call Name at 10:00'. Never use call ids as link text.32- - Good example of link text: [Call Name](/playback/1234567890?apFrom=123)\n"41+ - Good example of link text: [Call Name](/playback/1234567890?apFrom=123)33- - Bad example of link text: [Call Name at 12:34](/playback/1234567890?apFrom=123) (do not use call time stamps in the link text)\n"42+ - Bad example of link text: [Call Name at 12:34](/playback/1234567890?apFrom=123) (do not use call time stamps in the link text)34- - Bad example of links: [Call Name on November 12, 2025](/playback/1234567890?apFrom=123) (do not use dates in the link text)\n\n"43+ - Bad example of links: [Call Name on November 12, 2025](/playback/1234567890?apFrom=123) (do not use dates in the link text)35 - You might back your statements with examples from the provided call transcripts in addition to the reference links.44 - You might back your statements with examples from the provided call transcripts in addition to the reference links.36- Be specific. Use names of accounts, clients and persons involved. Don't just say "one client" or "a client"! If no account is available, use the call title as a reference.45- Be specific. Use names of accounts, clients and persons involved. Don't just say "one client" or "a client"! If no account is available, use the call title as a reference.37- If the question or context is unclear, request clarification or highlight ambiguities.46- If the question or context is unclear, request clarification or highlight ambiguities.</selection>” selected.
Please summarize the selection using precise and concise language. Use headers and bulleted lists in the summary, to make it scannable. Maintain the meaning and factual accuracy.
Expand...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20361: Add call scores in Panorama by steliyan-g · Pull Request #507 · jiminny/prophet","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20361: Add call scores in Panorama by steliyan-g · Pull Request #507 · jiminny/prophet","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"LLM API Pricing Comparison & Cost Guide (May 2026)","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"LLM API Pricing Comparison & Cost Guide (May 2026)","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Close Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.019097222,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.04236111,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"AI Chat settings","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"WORK, Google Account: lukas.kovalik@jiminny.com","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Main menu","depth":12,"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Chat","depth":12,"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open menu for conversation actions.","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Conversation with Gemini","depth":15,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Conversation with Gemini","depth":16,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Copy prompt","depth":21,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"You said I’m on page “<tabTitle>JY-20361: Add call scores in Panorama by steliyan-</tabTitle>” with “<selection>@@ -4,7 +4,7 @@ Today is {date_today}.445System instructions:5System instructions:6- You must answer using markdown. Do not use html tags in your response even if requested by the user's question.6- You must answer using markdown. Do not use html tags in your response even if requested by the user's question.7-- The contexts includes data for call shortlisted through user-applied filters. Note that the calls might not relate to the same deal or not even be by the same sales rep.7+- The context includes data for calls shortlisted through user-applied filters. Note that the calls might not relate to the same deal or not even be by the same sales rep.8 - The full call context includes all calls data available while the short call context contains only an overview of the calls that are analysed. Refer to the short call context for quick reference and to see the overall picture.8 - The full call context includes all calls data available while the short call context contains only an overview of the calls that are analysed. Refer to the short call context for quick reference and to see the overall picture.9- The calls are ordered in chronological order. 9- The calls are ordered in chronological order. 10- Here’s how to use the call context:10- Here’s how to use the call context:@@ -16,6 +16,15 @@ System instructions:16 - Frame answers with awareness of the company’s ICP, deal cycle, and sales motion.16 - Frame answers with awareness of the company’s ICP, deal cycle, and sales motion.17 - Evaluate statements or objections based on how the team operates and what success looks like.17 - Evaluate statements or objections based on how the team operates and what success looks like.18 - Position responses in light of known competitors and market dynamics.18 - Position responses in light of known competitors and market dynamics.19+ - AI call score (`ai_call_score` in the full call JSON): When present, treat it as **pre-computed** output from your team’s AI call-scoring pipeline (the same kind of scoring as the `/call/ai-call-scoring` endpoint). **Do not** invent, override, or recalculate scores; interpret and summarize what is given.20+ - **Short call context:** **AI Scorecard** is the name of the scorecard applied to that call; **AI Score** is the **overall** score for that scorecard (the average of its per-rule scores, possibly shown as a decimal).21+ - **Full call context:** The `ai_call_score` object may include `ai_scorecard_name`, `score` (overall for that scorecard), and `ai_scorecard_rules`. For each rule when listed:22+ - `rule_name`: Title of the criterion.23+ - `rule_prompt`: The criterion text that was evaluated.24+ - `score`: Whole number **1–5** measuring how well the call satisfied that criterion against `rule_prompt` (1 = no evidence / not discussed or contrary; 5 = strong, clear evidence on the call).25+ - `justification`: Short rationale grounded in what happened on the call.26+ - `justification_timestamps`: Up to three entries with speaker **name** and **timestamp** (MM:SS in the recording) highlighting where the justification is supported.27+ - Use scores for coaching summaries, trends across calls, or quick comparisons when relevant. For **what was actually said**, still rely on the **transcript** (you may cross-reference rule timestamps when helpful).19- Use the Message History to:28- Use the Message History to:20 - Maintain continuity if the current question refers to previous exchanges.29 - Maintain continuity if the current question refers to previous exchanges.21 - Clarify or resolve ambiguities if the question depends on prior messages.30 - Clarify or resolve ambiguities if the question depends on prior messages.@@ -28,10 +37,10 @@ System instructions:28 - All factual claims must be supported by one or more references to relevant calls.37 - All factual claims must be supported by one or more references to relevant calls.29 - Use Markdown link syntax ([link text](URL)) and place links inline within sentences. Do not use footnotes, reference sections, or bare URLs. Integrate the reference links directly within the relevant parts of your response, rather than in a separate section.38 - Use Markdown link syntax ([link text](URL)) and place links inline within sentences. Do not use footnotes, reference sections, or bare URLs. Integrate the reference links directly within the relevant parts of your response, rather than in a separate section.30 - Use markdown links in the format [link text](/playback/{{call_id}})39 - Use markdown links in the format [link text](/playback/{{call_id}})31- - Use descriptive link that utlizes the call name. Do not use call time stamps in the link text even if the link itself contains a time stamp, e.g. use 'Call Name' instead of 'Call Name at 10:00'. Never use call ids as link text.\\n"40+ - Use descriptive link text that utilizes the call name. Do not use call time stamps in the link text even if the link itself contains a time stamp, e.g. use 'Call Name' instead of 'Call Name at 10:00'. Never use call ids as link text.32- - Good example of link text: [Call Name](/playback/1234567890?apFrom=123)\\n"41+ - Good example of link text: [Call Name](/playback/1234567890?apFrom=123)33- - Bad example of link text: [Call Name at 12:34](/playback/1234567890?apFrom=123) (do not use call time stamps in the link text)\\n"42+ - Bad example of link text: [Call Name at 12:34](/playback/1234567890?apFrom=123) (do not use call time stamps in the link text)34- - Bad example of links: [Call Name on November 12, 2025](/playback/1234567890?apFrom=123) (do not use dates in the link text)\\n\\n"43+ - Bad example of links: [Call Name on November 12, 2025](/playback/1234567890?apFrom=123) (do not use dates in the link text)35 - You might back your statements with examples from the provided call transcripts in addition to the reference links.44 - You might back your statements with examples from the provided call transcripts in addition to the reference links.36- Be specific. Use names of accounts, clients and persons involved. Don't just say "one client" or "a client"! If no account is available, use the call title as a reference.45- Be specific. Use names of accounts, clients and persons involved. Don't just say "one client" or "a client"! If no account is available, use the call title as a reference.37- If the question or context is unclear, request clarification or highlight ambiguities.46- If the question or context is unclear, request clarification or highlight ambiguities.</selection>” selected. Please summarize the selection using precise and concise language. Use headers and bulleted lists in the summary, to make it scannable. Maintain the meaning and factual accuracy.","depth":21,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"You said","depth":23,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"I’m on page “<tabTitle>JY-20361: Add call scores in Panorama by steliyan-</tabTitle>” with “<selection>@@ -4,7 +4,7 @@ Today is {date_today}.445System instructions:5System instructions:6- You must answer using markdown. Do not use html tags in your response even if requested by the user's question.6- You must answer using markdown. Do not use html tags in your response even if requested by the user's question.7-- The contexts includes data for call shortlisted through user-applied filters. Note that the calls might not relate to the same deal or not even be by the same sales rep.7+- The context includes data for calls shortlisted through user-applied filters. Note that the calls might not relate to the same deal or not even be by the same sales rep.8 - The full call context includes all calls data available while the short call context contains only an overview of the calls that are analysed. Refer to the short call context for quick reference and to see the overall picture.8 - The full call context includes all calls data available while the short call context contains only an overview of the calls that are analysed. Refer to the short call context for quick reference and to see the overall picture.9- The calls are ordered in chronological order. 9- The calls are ordered in chronological order. 10- Here’s how to use the call context:10- Here’s how to use the call context:@@ -16,6 +16,15 @@ System instructions:16 - Frame answers with awareness of the company’s ICP, deal cycle, and sales motion.16 - Frame answers with awareness of the company’s ICP, deal cycle, and sales motion.17 - Evaluate statements or objections based on how the team operates and what success looks like.17 - Evaluate statements or objections based on how the team operates and what success looks like.18 - Position responses in light of known competitors and market dynamics.18 - Position responses in light of known competitors and market dynamics.19+ - AI call score (`ai_call_score` in the full call JSON): When present, treat it as **pre-computed** output from your team’s AI call-scoring pipeline (the same kind of scoring as the `/call/ai-call-scoring` endpoint). **Do not** invent, override, or recalculate scores; interpret and summarize what is given.20+ - **Short call context:** **AI Scorecard** is the name of the scorecard applied to that call; **AI Score** is the **overall** score for that scorecard (the average of its per-rule scores, possibly shown as a decimal).21+ - **Full call context:** The `ai_call_score` object may include `ai_scorecard_name`, `score` (overall for that scorecard), and `ai_scorecard_rules`. For each rule when listed:22+ - `rule_name`: Title of the criterion.23+ - `rule_prompt`: The criterion text that was evaluated.24+ - `score`: Whole number **1–5** measuring how well the call satisfied that criterion against `rule_prompt` (1 = no evidence / not discussed or contrary; 5 = strong, clear evidence on the call).25+ - `justification`: Short rationale grounded in what happened on the call.26+ - `justification_timestamps`: Up to three entries with speaker **name** and **timestamp** (MM:SS in the recording) highlighting where the justification is supported.27+ - Use scores for coaching summaries, trends across calls, or quick comparisons when relevant. For **what was actually said**, still rely on the **transcript** (you may cross-reference rule timestamps when helpful).19- Use the Message History to:28- Use the Message History to:20 - Maintain continuity if the current question refers to previous exchanges.29 - Maintain continuity if the current question refers to previous exchanges.21 - Clarify or resolve ambiguities if the question depends on prior messages.30 - Clarify or resolve ambiguities if the question depends on prior messages.@@ -28,10 +37,10 @@ System instructions:28 - All factual claims must be supported by one or more references to relevant calls.37 - All factual claims must be supported by one or more references to relevant calls.29 - Use Markdown link syntax ([link text](URL)) and place links inline within sentences. Do not use footnotes, reference sections, or bare URLs. Integrate the reference links directly within the relevant parts of your response, rather than in a separate section.38 - Use Markdown link syntax ([link text](URL)) and place links inline within sentences. Do not use footnotes, reference sections, or bare URLs. Integrate the reference links directly within the relevant parts of your response, rather than in a separate section.30 - Use markdown links in the format [link text](/playback/{{call_id}})39 - Use markdown links in the format [link text](/playback/{{call_id}})31- - Use descriptive link that utlizes the call name. Do not use call time stamps in the link text even if the link itself contains a time stamp, e.g. use 'Call Name' instead of 'Call Name at 10:00'. Never use call ids as link text.\\n"40+ - Use descriptive link text that utilizes the call name. Do not use call time stamps in the link text even if the link itself contains a time stamp, e.g. use 'Call Name' instead of 'Call Name at 10:00'. Never use call ids as link text.32- - Good example of link text: [Call Name](/playback/1234567890?apFrom=123)\\n"41+ - Good example of link text: [Call Name](/playback/1234567890?apFrom=123)33- - Bad example of link text: [Call Name at 12:34](/playback/1234567890?apFrom=123) (do not use call time stamps in the link text)\\n"42+ - Bad example of link text: [Call Name at 12:34](/playback/1234567890?apFrom=123) (do not use call time stamps in the link text)34- - Bad example of links: [Call Name on November 12, 2025](/playback/1234567890?apFrom=123) (do not use dates in the link text)\\n\\n"43+ - Bad example of links: [Call Name on November 12, 2025](/playback/1234567890?apFrom=123) (do not use dates in the link text)35 - You might back your statements with examples from the provided call transcripts in addition to the reference links.44 - You might back your statements with examples from the provided call transcripts in addition to the reference links.36- Be specific. Use names of accounts, clients and persons involved. Don't just say "one client" or "a client"! If no account is available, use the call title as a reference.45- Be specific. Use names of accounts, clients and persons involved. Don't just say "one client" or "a client"! If no account is available, use the call title as a reference.37- If the question or context is unclear, request clarification or highlight ambiguities.46- If the question or context is unclear, request clarification or highlight ambiguities.</selection>” selected.","depth":23,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Please summarize the selection using precise and concise language. Use headers and bulleted lists in the summary, to make it scannable. Maintain the meaning and factual accuracy.","depth":23,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Expand","depth":21,"on_screen":false,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-8895066012401168452
|
-2831912997475075182
|
visual_change
|
accessibility
|
NULL
|
New Tab
New Tab
Jy 20820 es reindex stream model h New Tab
New Tab
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
JY-20361: Add call scores in Panorama by steliyan-g · Pull Request #507 · jiminny/prophet
JY-20361: Add call scores in Panorama by steliyan-g · Pull Request #507 · jiminny/prophet
LLM API Pricing Comparison & Cost Guide (May 2026)
LLM API Pricing Comparison & Cost Guide (May 2026)
Close tab
New Tab
Customize sidebar
Close Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
AI Chat settings
Close
WORK, Google Account: [EMAIL]
Main menu
New Chat
Open menu for conversation actions.
Conversation with Gemini
Conversation with Gemini
Copy prompt
You said I’m on page “<tabTitle>JY-20361: Add call scores in Panorama by steliyan-</tabTitle>” with “<selection>@@ -4,7 +4,7 @@ Today is {date_today}.445System instructions:5System instructions:6- You must answer using markdown. Do not use html tags in your response even if requested by the user's question.6- You must answer using markdown. Do not use html tags in your response even if requested by the user's question.7-- The contexts includes data for call shortlisted through user-applied filters. Note that the calls might not relate to the same deal or not even be by the same sales rep.7+- The context includes data for calls shortlisted through user-applied filters. Note that the calls might not relate to the same deal or not even be by the same sales rep.8 - The full call context includes all calls data available while the short call context contains only an overview of the calls that are analysed. Refer to the short call context for quick reference and to see the overall picture.8 - The full call context includes all calls data available while the short call context contains only an overview of the calls that are analysed. Refer to the short call context for quick reference and to see the overall picture.9- The calls are ordered in chronological order. 9- The calls are ordered in chronological order. 10- Here’s how to use the call context:10- Here’s how to use the call context:@@ -16,6 +16,15 @@ System instructions:16 - Frame answers with awareness of the company’s ICP, deal cycle, and sales motion.16 - Frame answers with awareness of the company’s ICP, deal cycle, and sales motion.17 - Evaluate statements or objections based on how the team operates and what success looks like.17 - Evaluate statements or objections based on how the team operates and what success looks like.18 - Position responses in light of known competitors and market dynamics.18 - Position responses in light of known competitors and market dynamics.19+ - AI call score (`ai_call_score` in the full call JSON): When present, treat it as **pre-computed** output from your team’s AI call-scoring pipeline (the same kind of scoring as the `/call/ai-call-scoring` endpoint). **Do not** invent, override, or recalculate scores; interpret and summarize what is given.20+ - **Short call context:** **AI Scorecard** is the name of the scorecard applied to that call; **AI Score** is the **overall** score for that scorecard (the average of its per-rule scores, possibly shown as a decimal).21+ - **Full call context:** The `ai_call_score` object may include `ai_scorecard_name`, `score` (overall for that scorecard), and `ai_scorecard_rules`. For each rule when listed:22+ - `rule_name`: Title of the criterion.23+ - `rule_prompt`: The criterion text that was evaluated.24+ - `score`: Whole number **1–5** measuring how well the call satisfied that criterion against `rule_prompt` (1 = no evidence / not discussed or contrary; 5 = strong, clear evidence on the call).25+ - `justification`: Short rationale grounded in what happened on the call.26+ - `justification_timestamps`: Up to three entries with speaker **name** and **timestamp** (MM:SS in the recording) highlighting where the justification is supported.27+ - Use scores for coaching summaries, trends across calls, or quick comparisons when relevant. For **what was actually said**, still rely on the **transcript** (you may cross-reference rule timestamps when helpful).19- Use the Message History to:28- Use the Message History to:20 - Maintain continuity if the current question refers to previous exchanges.29 - Maintain continuity if the current question refers to previous exchanges.21 - Clarify or resolve ambiguities if the question depends on prior messages.30 - Clarify or resolve ambiguities if the question depends on prior messages.@@ -28,10 +37,10 @@ System instructions:28 - All factual claims must be supported by one or more references to relevant calls.37 - All factual claims must be supported by one or more references to relevant calls.29 - Use Markdown link syntax ([link text](URL)) and place links inline within sentences. Do not use footnotes, reference sections, or bare URLs. Integrate the reference links directly within the relevant parts of your response, rather than in a separate section.38 - Use Markdown link syntax ([link text](URL)) and place links inline within sentences. Do not use footnotes, reference sections, or bare URLs. Integrate the reference links directly within the relevant parts of your response, rather than in a separate section.30 - Use markdown links in the format [link text](/playback/{{call_id}})39 - Use markdown links in the format [link text](/playback/{{call_id}})31- - Use descriptive link that utlizes the call name. Do not use call time stamps in the link text even if the link itself contains a time stamp, e.g. use 'Call Name' instead of 'Call Name at 10:00'. Never use call ids as link text.\n"40+ - Use descriptive link text that utilizes the call name. Do not use call time stamps in the link text even if the link itself contains a time stamp, e.g. use 'Call Name' instead of 'Call Name at 10:00'. Never use call ids as link text.32- - Good example of link text: [Call Name](/playback/1234567890?apFrom=123)\n"41+ - Good example of link text: [Call Name](/playback/1234567890?apFrom=123)33- - Bad example of link text: [Call Name at 12:34](/playback/1234567890?apFrom=123) (do not use call time stamps in the link text)\n"42+ - Bad example of link text: [Call Name at 12:34](/playback/1234567890?apFrom=123) (do not use call time stamps in the link text)34- - Bad example of links: [Call Name on November 12, 2025](/playback/1234567890?apFrom=123) (do not use dates in the link text)\n\n"43+ - Bad example of links: [Call Name on November 12, 2025](/playback/1234567890?apFrom=123) (do not use dates in the link text)35 - You might back your statements with examples from the provided call transcripts in addition to the reference links.44 - You might back your statements with examples from the provided call transcripts in addition to the reference links.36- Be specific. Use names of accounts, clients and persons involved. Don't just say "one client" or "a client"! If no account is available, use the call title as a reference.45- Be specific. Use names of accounts, clients and persons involved. Don't just say "one client" or "a client"! If no account is available, use the call title as a reference.37- If the question or context is unclear, request clarification or highlight ambiguities.46- If the question or context is unclear, request clarification or highlight ambiguities.</selection>” selected. Please summarize the selection using precise and concise language. Use headers and bulleted lists in the summary, to make it scannable. Maintain the meaning and factual accuracy.
You said
I’m on page “<tabTitle>JY-20361: Add call scores in Panorama by steliyan-</tabTitle>” with “<selection>@@ -4,7 +4,7 @@ Today is {date_today}.445System instructions:5System instructions:6- You must answer using markdown. Do not use html tags in your response even if requested by the user's question.6- You must answer using markdown. Do not use html tags in your response even if requested by the user's question.7-- The contexts includes data for call shortlisted through user-applied filters. Note that the calls might not relate to the same deal or not even be by the same sales rep.7+- The context includes data for calls shortlisted through user-applied filters. Note that the calls might not relate to the same deal or not even be by the same sales rep.8 - The full call context includes all calls data available while the short call context contains only an overview of the calls that are analysed. Refer to the short call context for quick reference and to see the overall picture.8 - The full call context includes all calls data available while the short call context contains only an overview of the calls that are analysed. Refer to the short call context for quick reference and to see the overall picture.9- The calls are ordered in chronological order. 9- The calls are ordered in chronological order. 10- Here’s how to use the call context:10- Here’s how to use the call context:@@ -16,6 +16,15 @@ System instructions:16 - Frame answers with awareness of the company’s ICP, deal cycle, and sales motion.16 - Frame answers with awareness of the company’s ICP, deal cycle, and sales motion.17 - Evaluate statements or objections based on how the team operates and what success looks like.17 - Evaluate statements or objections based on how the team operates and what success looks like.18 - Position responses in light of known competitors and market dynamics.18 - Position responses in light of known competitors and market dynamics.19+ - AI call score (`ai_call_score` in the full call JSON): When present, treat it as **pre-computed** output from your team’s AI call-scoring pipeline (the same kind of scoring as the `/call/ai-call-scoring` endpoint). **Do not** invent, override, or recalculate scores; interpret and summarize what is given.20+ - **Short call context:** **AI Scorecard** is the name of the scorecard applied to that call; **AI Score** is the **overall** score for that scorecard (the average of its per-rule scores, possibly shown as a decimal).21+ - **Full call context:** The `ai_call_score` object may include `ai_scorecard_name`, `score` (overall for that scorecard), and `ai_scorecard_rules`. For each rule when listed:22+ - `rule_name`: Title of the criterion.23+ - `rule_prompt`: The criterion text that was evaluated.24+ - `score`: Whole number **1–5** measuring how well the call satisfied that criterion against `rule_prompt` (1 = no evidence / not discussed or contrary; 5 = strong, clear evidence on the call).25+ - `justification`: Short rationale grounded in what happened on the call.26+ - `justification_timestamps`: Up to three entries with speaker **name** and **timestamp** (MM:SS in the recording) highlighting where the justification is supported.27+ - Use scores for coaching summaries, trends across calls, or quick comparisons when relevant. For **what was actually said**, still rely on the **transcript** (you may cross-reference rule timestamps when helpful).19- Use the Message History to:28- Use the Message History to:20 - Maintain continuity if the current question refers to previous exchanges.29 - Maintain continuity if the current question refers to previous exchanges.21 - Clarify or resolve ambiguities if the question depends on prior messages.30 - Clarify or resolve ambiguities if the question depends on prior messages.@@ -28,10 +37,10 @@ System instructions:28 - All factual claims must be supported by one or more references to relevant calls.37 - All factual claims must be supported by one or more references to relevant calls.29 - Use Markdown link syntax ([link text](URL)) and place links inline within sentences. Do not use footnotes, reference sections, or bare URLs. Integrate the reference links directly within the relevant parts of your response, rather than in a separate section.38 - Use Markdown link syntax ([link text](URL)) and place links inline within sentences. Do not use footnotes, reference sections, or bare URLs. Integrate the reference links directly within the relevant parts of your response, rather than in a separate section.30 - Use markdown links in the format [link text](/playback/{{call_id}})39 - Use markdown links in the format [link text](/playback/{{call_id}})31- - Use descriptive link that utlizes the call name. Do not use call time stamps in the link text even if the link itself contains a time stamp, e.g. use 'Call Name' instead of 'Call Name at 10:00'. Never use call ids as link text.\n"40+ - Use descriptive link text that utilizes the call name. Do not use call time stamps in the link text even if the link itself contains a time stamp, e.g. use 'Call Name' instead of 'Call Name at 10:00'. Never use call ids as link text.32- - Good example of link text: [Call Name](/playback/1234567890?apFrom=123)\n"41+ - Good example of link text: [Call Name](/playback/1234567890?apFrom=123)33- - Bad example of link text: [Call Name at 12:34](/playback/1234567890?apFrom=123) (do not use call time stamps in the link text)\n"42+ - Bad example of link text: [Call Name at 12:34](/playback/1234567890?apFrom=123) (do not use call time stamps in the link text)34- - Bad example of links: [Call Name on November 12, 2025](/playback/1234567890?apFrom=123) (do not use dates in the link text)\n\n"43+ - Bad example of links: [Call Name on November 12, 2025](/playback/1234567890?apFrom=123) (do not use dates in the link text)35 - You might back your statements with examples from the provided call transcripts in addition to the reference links.44 - You might back your statements with examples from the provided call transcripts in addition to the reference links.36- Be specific. Use names of accounts, clients and persons involved. Don't just say "one client" or "a client"! If no account is available, use the call title as a reference.45- Be specific. Use names of accounts, clients and persons involved. Don't just say "one client" or "a client"! If no account is available, use the call title as a reference.37- If the question or context is unclear, request clarification or highlight ambiguities.46- If the question or context is unclear, request clarification or highlight ambiguities.</selection>” selected.
Please summarize the selection using precise and concise language. Use headers and bulleted lists in the summary, to make it scannable. Maintain the meaning and factual accuracy.
Expand...
|
22261
|
NULL
|
NULL
|
NULL
|
|
22706
|
975
|
71
|
2026-05-12T07:18:46.496915+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778570326496_m2.jpg...
|
Firefox
|
Formalize — Work
|
True
|
app-eu3.planhat.com/profile/677e7777f5db4de5da4a9b app-eu3.planhat.com/profile/677e7777f5db4de5da4a9b20?tab=overview...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
New Tab
New Tab
Jy 20820 es reindex stream model h New Tab
New Tab
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
LLM pricing overview with the most actual prices - Google Search
LLM pricing overview with the most actual prices - Google Search
Planhat
Planhat...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.24335106,"top":0.0518755,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.25664893,"top":0.06304868,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":4,"bounds":{"left":0.24335106,"top":0.08459697,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":5,"bounds":{"left":0.25664893,"top":0.09577015,"width":0.16888298,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":4,"bounds":{"left":0.24335106,"top":0.11731844,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":5,"bounds":{"left":0.25664893,"top":0.12849163,"width":0.16140293,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"bounds":{"left":0.24335106,"top":0.15003991,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"bounds":{"left":0.25664893,"top":0.16121309,"width":0.18816489,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"bounds":{"left":0.24335106,"top":0.18276137,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"bounds":{"left":0.25664893,"top":0.19393456,"width":0.039228722,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · jiminny/app","depth":4,"bounds":{"left":0.24335106,"top":0.21548285,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · jiminny/app","depth":5,"bounds":{"left":0.25664893,"top":0.22665602,"width":0.04537899,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"bounds":{"left":0.24335106,"top":0.2482043,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"bounds":{"left":0.25664893,"top":0.25937748,"width":0.1200133,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"bounds":{"left":0.24335106,"top":0.28092578,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"bounds":{"left":0.25664893,"top":0.29209897,"width":0.19331782,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"bounds":{"left":0.24335106,"top":0.31364724,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":5,"bounds":{"left":0.25664893,"top":0.32482043,"width":0.07646277,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"bounds":{"left":0.24335106,"top":0.3463687,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"bounds":{"left":0.25664893,"top":0.3575419,"width":0.40475398,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"bounds":{"left":0.24335106,"top":0.3790902,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"bounds":{"left":0.25664893,"top":0.39026338,"width":0.40475398,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.24335106,"top":0.41181165,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":5,"bounds":{"left":0.25664893,"top":0.42298484,"width":0.10106383,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app","depth":4,"bounds":{"left":0.24335106,"top":0.4445331,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app","depth":5,"bounds":{"left":0.25664893,"top":0.4557063,"width":0.15159574,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"LLM pricing overview with the most actual prices - Google Search","depth":4,"bounds":{"left":0.24335106,"top":0.4772546,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"LLM pricing overview with the most actual prices - Google Search","depth":5,"bounds":{"left":0.25664893,"top":0.4884278,"width":0.1143617,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Planhat","depth":4,"bounds":{"left":0.24335106,"top":0.509976,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Planhat","depth":5,"bounds":{"left":0.25664893,"top":0.5211492,"width":0.012965426,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-8894191516517556486
|
-3020487686398985585
|
click
|
accessibility
|
NULL
|
New Tab
New Tab
Jy 20820 es reindex stream model h New Tab
New Tab
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
JY-20625 | JY-20742 | MCP POC by yalokin-jiminny · Pull Request #12036 · jiminny/app
LLM pricing overview with the most actual prices - Google Search
LLM pricing overview with the most actual prices - Google Search
Planhat
Planhat...
|
22703
|
NULL
|
NULL
|
NULL
|
|
26268
|
1091
|
16
|
2026-05-12T12:16:48.540790+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778588208540_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Collapse sidebar
Search
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
Screenpipe data sync and retention management
More options for Screenpipe data sync and retention management
Screenpipe sync script failing after recent migrations
More options for Screenpipe sync script failing after recent migrations
Hubspot BadRequest headers debugging
More options for Hubspot BadRequest headers debugging
Monthly expense tracking
More options for Monthly expense tracking
Exporting transaction data from Notion to finance hub
More options for Exporting transaction data from Notion to finance hub
💬 How much have I spent for groc…
More options for 💬 How much have I spent for groc…
April 2026 spending by category
More options for April 2026 spending by category
Code diff review
More options for Code diff review
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Lukas Pro
Get apps and extensions
Screenpipe data sync and retention management, rename chat
Screenpipe data sync and retention management
More options for Screenpipe data sync and retention management
Close
Share chat
Claude finished the response
You said: Lets start from the beginning with the fresh mind.
You said: Lets start from the beginning with the fresh mind.
Pasted Text, pasted, 495 lines
#!/bin/bash # screenpipe_sync.sh # Syncs Screenpipe SQLite data to a NAS archive database (append-only, no deletions). # Also copies the day's video/frame data folder to the NAS. # # Schema-drift tolerant: if Screenpipe migrations add new columns to the source DB, # the NAS archive gets ALTER TABLE'
PASTED
Lets start from the beginning with the fresh mind. There is a script for screenpipe to copy data for day from mac to nas. There are two issues I want to work with. Firsts if I reinstall the mac version it starts from fresh id 1. That causes some issues. How to make sure the data are valid. I don't want some overcomplicated flow. I was thinking about making new db every 3 months. Secodn issue is that I wan to implement cleanup (retention policy) on mac original screenpipe db. I want script that will remove data that are 7 days old or older for example. ANd lastly I want to extedn the script to work with audio as well. What od you need
20:19
Retry
Edit
Copy
Claude responded: Good plan to tackle these together — they interact (retention timing depends on sync timing, audio sync depends on schema shape, ID strategy depends on whether…
Claude responded: Good plan to tackle these together — they interact (retention timing depends on sync timing, audio sync depends on schema shape, ID strategy depends on whether…
Analyzed three technical challenges and identified clarifying questions needed
Analyzed three technical challenges and identified clarifying questions needed
Good plan to tackle these together — they interact (retention timing depends on sync timing, audio sync depends on schema shape, ID strategy depends on whether reinstalls have already happened). Before I touch the script, a few things:
Issue 1 — ID collisions / archive rotation
Has a reinstall already happened, or is the current
archive.db
still from one continuous install? If collisions already exist I need to handle that first; if not, we have a clean slate.
Quarterly rotation works, but it doesn't actually solve reinstalls
within
a quarter. A cleaner option that I'd lean toward: add an
install_id
column (UUID generated once and stored in a tiny
_meta
table in the source DB) to every synced table, and make
(install_id, id)
the logical key. FK joins just gain one extra predicate. Reinstall → new install_id → no collisions, ever. You can still rotate the archive DB quarterly for file-size reasons, but it's no longer load-bearing.
Do you ever run cross-period queries (e.g. "every frame mentioning JY-20458 across all time")? That answers whether quarterly sharding is acceptable or whether one growing DB is better.
Issue 2 — Mac-side retention
Your launch command already has
--retention-days 7
. Is that not actually deleting anything, or do you want a separate scripted retention you can trust/observe independently?
Should retention also wipe the matching
~/.screenpipe/data/data/YYYY-MM-DD/
folders, or only DB rows? (CLI flag does both I believe; a script should match.)
Run order matters: sync writes
yesterday
, retention deletes
>7 days old
— no overlap, but I'd still chain them (sync → retention → log) in one cron entry so a failed sync blocks the delete. OK with that?
Issue 3 — Audio
Your current launch flag is
--disable-audio
, so right now there's no audio data to sync. Are you planning to enable it, or is this just to future-proof the script?
I need the audio schema. Could you run this and paste the output:
Copy to clipboard
sqlite3 ~/.screenpipe/db.sqlite ".schema" | grep -iE -A 20 "audio|transcription"
And:
ls ~/.screenpipe/data/data/$(date -v-1d +%Y-%m-%d)/ | head
so I can see what audio files look like on disk alongside the mp4 frames.
Once I have those, I'll restructure the script in one go rather than piecemeal.
Copy
Give positive feedback
Give negative feedback
Retry
You said: ok lets add install_id.
You said: ok lets add install_id.
Pasted Text, pasted, 208 lines
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sqlite3 ~/.screenpipe/db.sqlite ".schema" | grep -iE -A 20 "audio|transcription" CREATE TABLE audio_chunks ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_path TEXT NOT NULL , timestamp TIMESTAMP, sync_id TEXT, machine_id TEXT, synced_at DATETIME, ev
PASTED
ok lets add install_id. Lets add separate script deleting data again from data and it should also remove logs and data. Make it manual script for now. I will remove --disable-audio from alias and run it without. I wan to have audio as well. lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ ls ~/.screenpipe/data/data/$(date -v-1d +%Y-%m-%d)/ | head
compact_monitor_1_1778411210025.mp4
compact_monitor_1_1778413422547.mp4
compact_monitor_1_1778436664801.mp4
compact_monitor_1_1778437267451.mp4
compact_monitor_1_1778437569681.mp4
compact_monitor_1_1778437873507.mp4
compact_monitor_1_1778438175846.mp4
compact_monitor_1_1778438477299.mp4
compact_monitor_1_1778438780686.mp4
compact_monitor_1_1778439082442.mp4 Inside ~/,screenpipe/data there are data like this ... -rw-r--r-- 1 lukas staff 4628 11 May 16:48 System Audio (output)_2026-05-11_13-48-12.mp4
-rw-r--r-- 1 lukas staff 4620 11 May 16:48 System Audio (output)_2026-05-11_13-48-34.mp4
-rw-r--r-- 1 lukas staff 4620 11 May 16:49 System Audio (output)_2026-05-11_13-48-56.mp4
-rw-r--r-- 1 lukas staff 4628 11 May 16:49 System Audio (output)_2026-05-11_13-49-19.mp4
-rw-r--r-- 1 lukas staff 4620 11 May 16:50 System Audio (output)_2026-05-11_13-49-41.mp4
-rw-r--r-- 1 lukas staff 4620 11 May 16:50 System Audio (output)_2026-05-11_13-50-03.mp4
-rw-r--r-- 1 lukas staff 4628 11 May 16:50 System Audio (output)_2026-05-11_13-50-26.mp4
-rw-r--r-- 1 lukas staff 4628 11 May 16:51 System Audio (output)_2026-05-11_13-50-48.mp4
-rw-r--r-- 1 lukas staff 4620 11 May 16:51 System Audio (output)_2026-05-11_13-51-10.mp4
-rw-r--r-- 1 lukas staff 4620 11 May 16:51 System Audio (output)_2026-05-11_13-51-33.mp4
-rw-r--r-- 1 lukas staff 4620 11 May 16:52 System Audio (output)_2026-05-11_13-51-55.mp4
drwxr-xr-x 9 lukas staff 288 11 May 07:54 data
drwxr-xr-x 2 lukas staff 64 11 May 15:48 pending-transcriptions
-rw-r--r-- 1 lukas staff 29419 10 May 14:11 soundcore AeroClip (input)_2026-05-10_11-10-32.mp4
-rw-r--r-- 1 lukas staff 56479 10 May 14:11 soundcore AeroClip (input)_2026-05-10_11-11-05.mp4
-rw-r--r--@ 1 lukas staff 181831 10 May 14:12 soundcore AeroClip (input)_2026-05-10_11-11-35.mp4
-rw-r--r-- 1 lukas staff 149782 10 May 14:43 soundcore AeroClip (input)_2026-05-10_11-42-53.mp4
-rw-r--r-- 1 lukas staff 91059 10 May 14:43 soundcore AeroClip (input)_2026-05-10_11-43-25.mp4
-rw-r--r-- 1 lukas staff 30604 10 May 14:44 soundcore AeroClip (input)_2026-05-10_11-44-25.mp4
-rw-r--r-- 1 lukas staff 93813 10 May 14:45 soundcore AeroClip (input)_2026-05-10_11-44-55.mp4
-rw-r--r-- 1 lukas staff 40444 10 May 21:11 soundcore AeroClip (input)_2026-05-10_18-11-18.mp4
-rw-r--r-- 1 lukas staff 193020 10 May 21:12 soundcore AeroClip (input)_2026-05-10_18-11-48.mp4...
Show more
20:34
Retry
Edit
Copy
Claude responded: Good, that clarifies the audio layout — audio files live flat in ~/.
Claude responded: Good, that clarifies the audio layout — audio files live flat in ~/.
Architected multi-install sync with audio support and retention
Architected multi-install sync with audio support and retention
Good, that clarifies the audio layout — audio files live flat in
~/.screenpipe/data/
with the date in the filename (not in date subdirs like frames), and the schema gives us
audio_chunks
+
audio_transcriptions
+
speakers
+
speaker_embeddings
+
audio_tags
plus the FTS table on transcriptions.
Plan I'll implement:
install_id
: stored as a UUID in
~/.screenpipe/.sync_install_id
— disappears when the directory is wiped (which is what you want on reinstall). Every synced NAS table gets an
install_id TEXT
column and a
UNIQUE(install_id, id)
index. The script auto-migrates the existing NAS archive by adding the column and tagging legacy rows with
legacy-YYYYMMDD
so historical data is still queryable but doesn't conflict with future installs.
Audio sync
: adds
audio_chunks
,
audio_transcriptions
,
speakers
,
speaker_embeddings
,
audio_tags
to the sync pipeline, plus the
audio_transcriptions_fts
index. Audio files go to
$NAS_DATA/<date>/audio/
(sibling to the frames in the same date dir, keeps daily archives self-contained).
Retention
is a separate manual script — dry-run by default,
--apply
to delete. Hits DB rows, frame directories, audio mp4s, and screenpipe logs. Optional
--vacuum
with a warning (screenpipe must be stopped to avoid blocking writes for minutes).
One caveat I'm punting on: the FTS tables use source
id
as
rowid
, so after a reinstall they'd get id-collisions on insert and
INSERT OR IGNORE
would silently skip. Within a single install this is fine; after reinstall you'd fall back to
LIKE
queries on the base tables (still filterable by
install_id
). I noted this in the script. Fixing it properly needs a synthetic rowid scheme which we can do later if you actually hit it.
Let me write both:
Updated screenpipe_sync.sh with install_id support and audio sync
Updated screenpipe_sync.sh with install_id support and audio sync
Updated screenpipe_sync.sh with install_id support and audio sync
Updated screenpipe_sync.sh with install_id support and audio sync
screenpipe_sync.sh
Done
Copy
Give positive feedback
Give negative feedback
Retry
You said: I don't like that.
You said: I don't like that.
I don't like that. It should be solid without id collisions. Wouldn't it be to late if I should fix it later?
14:26
Retry
Edit
Copy
Claude responded: You're right — the FTS tables are the gap I papered over.
Claude responded: You're right — the FTS tables are the gap I papered over.
Architected collision-free FTS schema with contentless tables and install tracking
Architected collision-free FTS schema with contentless tables and install tracking
You're right — the FTS tables are the gap I papered over. Let me explain why and fix it properly.
What I missed:
SQLite FTS tables use an
INTEGER rowid
as their primary key. The previous design used the source
id
as that rowid (e.g.
INSERT INTO frames_fts(rowid, ...) VALUES (NEW.id, ...)
). After a reinstall, source IDs reset to 1, and
INSERT OR IGNORE
silently drops them because rowid=1 is already taken. So FTS would only index your most recent install — searching for old transcriptions or OCR text would miss anything from before the reinstall, and there's no way to detect the loss because it fails silently.
Fix:
switch all four FTS tables to fully contentless mode with
install_id
and
source_id
as
UNINDEXED
columns, and let SQLite auto-assign rowids. No collision is possible because each insert gets a fresh rowid. Queries become:
Copy to clipboard
sql
SELECT
f
.
*
FROM
nas
.
frames f
JOIN
nas
.
frames_fts fts
ON
fts
.
install_id
=
f
.
install_id
AND
fts
.
source_id
=
f
.
id
WHERE
frames_fts
MATCH
'standup'
;
One extra JOIN, but bulletproof.
Migration:
the script detects the old FTS schema on first run, drops + rebuilds the four FTS tables from the base tables. This is the only one-time slow step. After that, every run is normal. The base tables (with all your data) are untouched during the rebuild — only the FTS indexes get regenerated.
I also added
tags
and...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.50232714,"top":0.025538707,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"bounds":{"left":0.50232714,"top":0.026336791,"width":0.030917553,"height":0.013567438},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.50232714,"top":0.027134877,"width":0.0026595744,"height":0.012769354}},{"char_start":1,"char_count":14,"bounds":{"left":0.5049867,"top":0.027134877,"width":0.02825798,"height":0.012769354}}],"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.6023936,"top":0.3216281,"width":0.030917553,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.6023936,"top":0.3216281,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.60538566,"top":0.3216281,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.6349734,"top":0.3216281,"width":0.0066489363,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.6023936,"top":0.33359936,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.6023936,"top":0.33359936,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.60538566,"top":0.33359936,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Collapse sidebar","depth":15,"bounds":{"left":0.5305851,"top":0.02952913,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search","depth":15,"bounds":{"left":0.53856385,"top":0.02952913,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.50598407,"top":0.06304868,"width":0.026263298,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.53291225,"top":0.06304868,"width":0.031914894,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.56515956,"top":0.06304868,"width":0.027260639,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.5053192,"top":0.096568234,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.5152925,"top":0.09976058,"width":0.019614361,"height":0.013567438},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.5152925,"top":0.10055866,"width":0.0033244682,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.51861703,"top":0.10055866,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.5844415,"top":0.10055866,"width":0.0066489363,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.5053192,"top":0.11731844,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.5053192,"top":0.13806863,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.5053192,"top":0.15881884,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.50731385,"top":0.19872306,"width":0.08510638,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.5053192,"top":0.21548285,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.58577126,"top":0.21867518,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.5053192,"top":0.23703113,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.58577126,"top":0.24022347,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.50731385,"top":0.26735833,"width":0.06482713,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.5731383,"top":0.26735833,"width":0.019281914,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe data sync and retention management","depth":18,"bounds":{"left":0.5053192,"top":0.28411812,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe data sync and retention management","depth":19,"bounds":{"left":0.58577126,"top":0.28731045,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync script failing after recent migrations","depth":18,"bounds":{"left":0.5053192,"top":0.3056664,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync script failing after recent migrations","depth":19,"bounds":{"left":0.58577126,"top":0.30885875,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hubspot BadRequest headers debugging","depth":18,"bounds":{"left":0.5053192,"top":0.3272147,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Hubspot BadRequest headers debugging","depth":19,"bounds":{"left":0.58577126,"top":0.33040702,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Monthly expense tracking","depth":18,"bounds":{"left":0.5053192,"top":0.34876296,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Monthly expense tracking","depth":19,"bounds":{"left":0.58577126,"top":0.3519553,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Exporting transaction data from Notion to finance hub","depth":18,"bounds":{"left":0.5053192,"top":0.37031126,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Exporting transaction data from Notion to finance hub","depth":19,"bounds":{"left":0.58577126,"top":0.3735036,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 How much have I spent for groc…","depth":18,"bounds":{"left":0.5053192,"top":0.39185953,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 How much have I spent for groc…","depth":19,"bounds":{"left":0.58577126,"top":0.39505187,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"April 2026 spending by category","depth":18,"bounds":{"left":0.5053192,"top":0.41340783,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for April 2026 spending by category","depth":19,"bounds":{"left":0.58577126,"top":0.41660017,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code diff review","depth":18,"bounds":{"left":0.5053192,"top":0.4349561,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Code diff review","depth":19,"bounds":{"left":0.58577126,"top":0.43814844,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.5053192,"top":0.45650437,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.58577126,"top":0.45969674,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.5053192,"top":0.47805268,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.58577126,"top":0.481245,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.5053192,"top":0.49960095,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.58577126,"top":0.5027933,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.5053192,"top":0.5211492,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.58577126,"top":0.5243416,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.5053192,"top":0.54269755,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.58577126,"top":0.54588985,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.5053192,"top":0.5642458,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.58577126,"top":0.5674381,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.5053192,"top":0.5857941,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.58577126,"top":0.58898646,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.5053192,"top":0.60734236,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.58577126,"top":0.6105347,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.5053192,"top":0.62889063,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.58577126,"top":0.632083,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.5053192,"top":0.65043896,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.58577126,"top":0.65363127,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.5053192,"top":0.67198724,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.58577126,"top":0.67517954,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.5053192,"top":0.6935355,"width":0.087765954,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.58577126,"top":0.6967279,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"bounds":{"left":0.5053192,"top":0.9696728,"width":0.03856383,"height":0.01915403},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"bounds":{"left":0.5851064,"top":0.9696728,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe data sync and retention management, rename chat","depth":19,"bounds":{"left":0.6023936,"top":0.02793296,"width":0.11070479,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Screenpipe data sync and retention management","depth":21,"bounds":{"left":0.6037234,"top":0.031923383,"width":0.10804521,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.6037234,"top":0.031923383,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":44,"bounds":{"left":0.60671544,"top":0.031923383,"width":0.105053194,"height":0.014365523}}],"role_description":"text"},{"role":"AXPopUpButton","text":"More options for Screenpipe data sync and retention management","depth":19,"bounds":{"left":0.7134308,"top":0.02793296,"width":0.006981383,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":21,"bounds":{"left":0.7702792,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":21,"bounds":{"left":0.78224736,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"You said: Lets start from the beginning with the fresh mind.","depth":20,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: Lets start from the beginning with the fresh mind.","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Pasted Text, pasted, 495 lines","depth":21,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"#!/bin/bash # screenpipe_sync.sh # Syncs Screenpipe SQLite data to a NAS archive database (append-only, no deletions). # Also copies the day's video/frame data folder to the NAS. # # Schema-drift tolerant: if Screenpipe migrations add new columns to the source DB, # the NAS archive gets ALTER TABLE'","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PASTED","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Lets start from the beginning with the fresh mind. There is a script for screenpipe to copy data for day from mac to nas. There are two issues I want to work with. Firsts if I reinstall the mac version it starts from fresh id 1. That causes some issues. How to make sure the data are valid. I don't want some overcomplicated flow. I was thinking about making new db every 3 months. Secodn issue is that I wan to implement cleanup (retention policy) on mac original screenpipe db. I want script that will remove data that are 7 days old or older for example. ANd lastly I want to extedn the script to work with audio as well. What od you need","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"20:19","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Good plan to tackle these together — they interact (retention timing depends on sync timing, audio sync depends on schema shape, ID strategy depends on whether…","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Good plan to tackle these together — they interact (retention timing depends on sync timing, audio sync depends on schema shape, ID strategy depends on whether…","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Analyzed three technical challenges and identified clarifying questions needed","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzed three technical challenges and identified clarifying questions needed","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Good plan to tackle these together — they interact (retention timing depends on sync timing, audio sync depends on schema shape, ID strategy depends on whether reinstalls have already happened). Before I touch the script, a few things:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Issue 1 — ID collisions / archive rotation","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Has a reinstall already happened, or is the current","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"archive.db","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"still from one continuous install? If collisions already exist I need to handle that first; if not, we have a clean slate.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Quarterly rotation works, but it doesn't actually solve reinstalls","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"within","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"a quarter. A cleaner option that I'd lean toward: add an","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"install_id","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"column (UUID generated once and stored in a tiny","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"_meta","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"table in the source DB) to every synced table, and make","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(install_id, id)","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"the logical key. FK joins just gain one extra predicate. Reinstall → new install_id → no collisions, ever. You can still rotate the archive DB quarterly for file-size reasons, but it's no longer load-bearing.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Do you ever run cross-period queries (e.g. \"every frame mentioning JY-20458 across all time\")? That answers whether quarterly sharding is acceptable or whether one growing DB is better.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Issue 2 — Mac-side retention","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Your launch command already has","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"--retention-days 7","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". Is that not actually deleting anything, or do you want a separate scripted retention you can trust/observe independently?","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Should retention also wipe the matching","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"~/.screenpipe/data/data/YYYY-MM-DD/","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"folders, or only DB rows? (CLI flag does both I believe; a script should match.)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Run order matters: sync writes","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"yesterday","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", retention deletes","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":">7 days old","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— no overlap, but I'd still chain them (sync → retention → log) in one cron entry so a failed sync blocks the delete. OK with that?","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Issue 3 — Audio","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Your current launch flag is","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"--disable-audio","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", so right now there's no audio data to sync. Are you planning to enable it, or is this just to future-proof the script?","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"I need the audio schema. Could you run this and paste the output:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"sqlite3 ~/.screenpipe/db.sqlite \".schema\" | grep -iE -A 20 \"audio|transcription\"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"And:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ls ~/.screenpipe/data/data/$(date -v-1d +%Y-%m-%d)/ | head","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"so I can see what audio files look like on disk alongside the mp4 frames.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Once I have those, I'll restructure the script in one go rather than piecemeal.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give positive feedback","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give negative feedback","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"You said: ok lets add install_id.","depth":20,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: ok lets add install_id.","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Pasted Text, pasted, 208 lines","depth":21,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sqlite3 ~/.screenpipe/db.sqlite \".schema\" | grep -iE -A 20 \"audio|transcription\" CREATE TABLE audio_chunks ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_path TEXT NOT NULL , timestamp TIMESTAMP, sync_id TEXT, machine_id TEXT, synced_at DATETIME, ev","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PASTED","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ok lets add install_id. Lets add separate script deleting data again from data and it should also remove logs and data. Make it manual script for now. I will remove --disable-audio from alias and run it without. I wan to have audio as well. lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ ls ~/.screenpipe/data/data/$(date -v-1d +%Y-%m-%d)/ | head\ncompact_monitor_1_1778411210025.mp4\ncompact_monitor_1_1778413422547.mp4\ncompact_monitor_1_1778436664801.mp4\ncompact_monitor_1_1778437267451.mp4\ncompact_monitor_1_1778437569681.mp4\ncompact_monitor_1_1778437873507.mp4\ncompact_monitor_1_1778438175846.mp4\ncompact_monitor_1_1778438477299.mp4\ncompact_monitor_1_1778438780686.mp4\ncompact_monitor_1_1778439082442.mp4 Inside ~/,screenpipe/data there are data like this ... -rw-r--r-- 1 lukas staff 4628 11 May 16:48 System Audio (output)_2026-05-11_13-48-12.mp4\n-rw-r--r-- 1 lukas staff 4620 11 May 16:48 System Audio (output)_2026-05-11_13-48-34.mp4\n-rw-r--r-- 1 lukas staff 4620 11 May 16:49 System Audio (output)_2026-05-11_13-48-56.mp4\n-rw-r--r-- 1 lukas staff 4628 11 May 16:49 System Audio (output)_2026-05-11_13-49-19.mp4\n-rw-r--r-- 1 lukas staff 4620 11 May 16:50 System Audio (output)_2026-05-11_13-49-41.mp4\n-rw-r--r-- 1 lukas staff 4620 11 May 16:50 System Audio (output)_2026-05-11_13-50-03.mp4\n-rw-r--r-- 1 lukas staff 4628 11 May 16:50 System Audio (output)_2026-05-11_13-50-26.mp4\n-rw-r--r-- 1 lukas staff 4628 11 May 16:51 System Audio (output)_2026-05-11_13-50-48.mp4\n-rw-r--r-- 1 lukas staff 4620 11 May 16:51 System Audio (output)_2026-05-11_13-51-10.mp4\n-rw-r--r-- 1 lukas staff 4620 11 May 16:51 System Audio (output)_2026-05-11_13-51-33.mp4\n-rw-r--r-- 1 lukas staff 4620 11 May 16:52 System Audio (output)_2026-05-11_13-51-55.mp4\ndrwxr-xr-x 9 lukas staff 288 11 May 07:54 data\ndrwxr-xr-x 2 lukas staff 64 11 May 15:48 pending-transcriptions\n-rw-r--r-- 1 lukas staff 29419 10 May 14:11 soundcore AeroClip (input)_2026-05-10_11-10-32.mp4\n-rw-r--r-- 1 lukas staff 56479 10 May 14:11 soundcore AeroClip (input)_2026-05-10_11-11-05.mp4\n-rw-r--r--@ 1 lukas staff 181831 10 May 14:12 soundcore AeroClip (input)_2026-05-10_11-11-35.mp4\n-rw-r--r-- 1 lukas staff 149782 10 May 14:43 soundcore AeroClip (input)_2026-05-10_11-42-53.mp4\n-rw-r--r-- 1 lukas staff 91059 10 May 14:43 soundcore AeroClip (input)_2026-05-10_11-43-25.mp4\n-rw-r--r-- 1 lukas staff 30604 10 May 14:44 soundcore AeroClip (input)_2026-05-10_11-44-25.mp4\n-rw-r--r-- 1 lukas staff 93813 10 May 14:45 soundcore AeroClip (input)_2026-05-10_11-44-55.mp4\n-rw-r--r-- 1 lukas staff 40444 10 May 21:11 soundcore AeroClip (input)_2026-05-10_18-11-18.mp4\n-rw-r--r-- 1 lukas staff 193020 10 May 21:12 soundcore AeroClip (input)_2026-05-10_18-11-48.mp4...","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"20:34","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Good, that clarifies the audio layout — audio files live flat in ~/.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Good, that clarifies the audio layout — audio files live flat in ~/.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Architected multi-install sync with audio support and retention","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Architected multi-install sync with audio support and retention","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Good, that clarifies the audio layout — audio files live flat in","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"~/.screenpipe/data/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with the date in the filename (not in date subdirs like frames), and the schema gives us","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"audio_chunks","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"audio_transcriptions","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"speakers","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"speaker_embeddings","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"audio_tags","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"plus the FTS table on transcriptions.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Plan I'll implement:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"install_id","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": stored as a UUID in","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"~/.screenpipe/.sync_install_id","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— disappears when the directory is wiped (which is what you want on reinstall). Every synced NAS table gets an","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"install_id TEXT","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"column and a","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UNIQUE(install_id, id)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"index. The script auto-migrates the existing NAS archive by adding the column and tagging legacy rows with","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"legacy-YYYYMMDD","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"so historical data is still queryable but doesn't conflict with future installs.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Audio sync","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": adds","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"audio_chunks","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"audio_transcriptions","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"speakers","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"speaker_embeddings","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"audio_tags","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to the sync pipeline, plus the","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"audio_transcriptions_fts","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"index. Audio files go to","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$NAS_DATA/<date>/audio/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(sibling to the frames in the same date dir, keeps daily archives self-contained).","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retention","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"is a separate manual script — dry-run by default,","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"--apply","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to delete. Hits DB rows, frame directories, audio mp4s, and screenpipe logs. Optional","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"--vacuum","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with a warning (screenpipe must be stopped to avoid blocking writes for minutes).","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"One caveat I'm punting on: the FTS tables use source","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"id","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"as","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"rowid","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", so after a reinstall they'd get id-collisions on insert and","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INSERT OR IGNORE","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"would silently skip. Within a single install this is fine; after reinstall you'd fall back to","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LIKE","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"queries on the base tables (still filterable by","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"install_id","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). I noted this in the script. Fixing it properly needs a synthetic rowid scheme which we can do later if you actually hit it.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Let me write both:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Updated screenpipe_sync.sh with install_id support and audio sync","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXStaticText","text":"Updated screenpipe_sync.sh with install_id support and audio sync","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Updated screenpipe_sync.sh with install_id support and audio sync","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Updated screenpipe_sync.sh with install_id support and audio sync","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"screenpipe_sync.sh","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Done","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give positive feedback","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give negative feedback","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"You said: I don't like that.","depth":20,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: I don't like that.","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"I don't like that. It should be solid without id collisions. Wouldn't it be to late if I should fix it later?","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"14:26","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: You're right — the FTS tables are the gap I papered over.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: You're right — the FTS tables are the gap I papered over.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Architected collision-free FTS schema with contentless tables and install tracking","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Architected collision-free FTS schema with contentless tables and install tracking","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"You're right — the FTS tables are the gap I papered over. Let me explain why and fix it properly.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"What I missed:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SQLite FTS tables use an","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INTEGER rowid","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"as their primary key. The previous design used the source","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"id","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"as that rowid (e.g.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INSERT INTO frames_fts(rowid, ...) VALUES (NEW.id, ...)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). After a reinstall, source IDs reset to 1, and","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INSERT OR IGNORE","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"silently drops them because rowid=1 is already taken. So FTS would only index your most recent install — searching for old transcriptions or OCR text would miss anything from before the reinstall, and there's no way to detect the loss because it fails silently.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fix:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"switch all four FTS tables to fully contentless mode with","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"install_id","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"source_id","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"as","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UNINDEXED","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"columns, and let SQLite auto-assign rowids. No collision is possible because each insert gets a fresh rowid. Queries become:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"sql","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SELECT","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"f","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"*","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FROM","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"nas","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"frames f","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JOIN","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"nas","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"frames_fts fts","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ON","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"fts","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"install_id","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"f","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"install_id","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AND","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"fts","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"source_id","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"f","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"id","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"WHERE","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"frames_fts","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MATCH","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'standup'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"One extra JOIN, but bulletproof.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Migration:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"the script detects the old FTS schema on first run, drops + rebuilds the four FTS tables from the base tables. This is the only one-time slow step. After that, every run is normal. The base tables (with all your data) are untouched during the rebuild — only the FTS indexes get regenerated.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"I also added","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"tags","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":25,"on_screen":false,"role_description":"text"}]...
|
-8893245689988458179
|
-8210416633940836653
|
click
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Collapse sidebar
Search
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
Screenpipe data sync and retention management
More options for Screenpipe data sync and retention management
Screenpipe sync script failing after recent migrations
More options for Screenpipe sync script failing after recent migrations
Hubspot BadRequest headers debugging
More options for Hubspot BadRequest headers debugging
Monthly expense tracking
More options for Monthly expense tracking
Exporting transaction data from Notion to finance hub
More options for Exporting transaction data from Notion to finance hub
💬 How much have I spent for groc…
More options for 💬 How much have I spent for groc…
April 2026 spending by category
More options for April 2026 spending by category
Code diff review
More options for Code diff review
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Lukas Pro
Get apps and extensions
Screenpipe data sync and retention management, rename chat
Screenpipe data sync and retention management
More options for Screenpipe data sync and retention management
Close
Share chat
Claude finished the response
You said: Lets start from the beginning with the fresh mind.
You said: Lets start from the beginning with the fresh mind.
Pasted Text, pasted, 495 lines
#!/bin/bash # screenpipe_sync.sh # Syncs Screenpipe SQLite data to a NAS archive database (append-only, no deletions). # Also copies the day's video/frame data folder to the NAS. # # Schema-drift tolerant: if Screenpipe migrations add new columns to the source DB, # the NAS archive gets ALTER TABLE'
PASTED
Lets start from the beginning with the fresh mind. There is a script for screenpipe to copy data for day from mac to nas. There are two issues I want to work with. Firsts if I reinstall the mac version it starts from fresh id 1. That causes some issues. How to make sure the data are valid. I don't want some overcomplicated flow. I was thinking about making new db every 3 months. Secodn issue is that I wan to implement cleanup (retention policy) on mac original screenpipe db. I want script that will remove data that are 7 days old or older for example. ANd lastly I want to extedn the script to work with audio as well. What od you need
20:19
Retry
Edit
Copy
Claude responded: Good plan to tackle these together — they interact (retention timing depends on sync timing, audio sync depends on schema shape, ID strategy depends on whether…
Claude responded: Good plan to tackle these together — they interact (retention timing depends on sync timing, audio sync depends on schema shape, ID strategy depends on whether…
Analyzed three technical challenges and identified clarifying questions needed
Analyzed three technical challenges and identified clarifying questions needed
Good plan to tackle these together — they interact (retention timing depends on sync timing, audio sync depends on schema shape, ID strategy depends on whether reinstalls have already happened). Before I touch the script, a few things:
Issue 1 — ID collisions / archive rotation
Has a reinstall already happened, or is the current
archive.db
still from one continuous install? If collisions already exist I need to handle that first; if not, we have a clean slate.
Quarterly rotation works, but it doesn't actually solve reinstalls
within
a quarter. A cleaner option that I'd lean toward: add an
install_id
column (UUID generated once and stored in a tiny
_meta
table in the source DB) to every synced table, and make
(install_id, id)
the logical key. FK joins just gain one extra predicate. Reinstall → new install_id → no collisions, ever. You can still rotate the archive DB quarterly for file-size reasons, but it's no longer load-bearing.
Do you ever run cross-period queries (e.g. "every frame mentioning JY-20458 across all time")? That answers whether quarterly sharding is acceptable or whether one growing DB is better.
Issue 2 — Mac-side retention
Your launch command already has
--retention-days 7
. Is that not actually deleting anything, or do you want a separate scripted retention you can trust/observe independently?
Should retention also wipe the matching
~/.screenpipe/data/data/YYYY-MM-DD/
folders, or only DB rows? (CLI flag does both I believe; a script should match.)
Run order matters: sync writes
yesterday
, retention deletes
>7 days old
— no overlap, but I'd still chain them (sync → retention → log) in one cron entry so a failed sync blocks the delete. OK with that?
Issue 3 — Audio
Your current launch flag is
--disable-audio
, so right now there's no audio data to sync. Are you planning to enable it, or is this just to future-proof the script?
I need the audio schema. Could you run this and paste the output:
Copy to clipboard
sqlite3 ~/.screenpipe/db.sqlite ".schema" | grep -iE -A 20 "audio|transcription"
And:
ls ~/.screenpipe/data/data/$(date -v-1d +%Y-%m-%d)/ | head
so I can see what audio files look like on disk alongside the mp4 frames.
Once I have those, I'll restructure the script in one go rather than piecemeal.
Copy
Give positive feedback
Give negative feedback
Retry
You said: ok lets add install_id.
You said: ok lets add install_id.
Pasted Text, pasted, 208 lines
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ sqlite3 ~/.screenpipe/db.sqlite ".schema" | grep -iE -A 20 "audio|transcription" CREATE TABLE audio_chunks ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_path TEXT NOT NULL , timestamp TIMESTAMP, sync_id TEXT, machine_id TEXT, synced_at DATETIME, ev
PASTED
ok lets add install_id. Lets add separate script deleting data again from data and it should also remove logs and data. Make it manual script for now. I will remove --disable-audio from alias and run it without. I wan to have audio as well. lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ ls ~/.screenpipe/data/data/$(date -v-1d +%Y-%m-%d)/ | head
compact_monitor_1_1778411210025.mp4
compact_monitor_1_1778413422547.mp4
compact_monitor_1_1778436664801.mp4
compact_monitor_1_1778437267451.mp4
compact_monitor_1_1778437569681.mp4
compact_monitor_1_1778437873507.mp4
compact_monitor_1_1778438175846.mp4
compact_monitor_1_1778438477299.mp4
compact_monitor_1_1778438780686.mp4
compact_monitor_1_1778439082442.mp4 Inside ~/,screenpipe/data there are data like this ... -rw-r--r-- 1 lukas staff 4628 11 May 16:48 System Audio (output)_2026-05-11_13-48-12.mp4
-rw-r--r-- 1 lukas staff 4620 11 May 16:48 System Audio (output)_2026-05-11_13-48-34.mp4
-rw-r--r-- 1 lukas staff 4620 11 May 16:49 System Audio (output)_2026-05-11_13-48-56.mp4
-rw-r--r-- 1 lukas staff 4628 11 May 16:49 System Audio (output)_2026-05-11_13-49-19.mp4
-rw-r--r-- 1 lukas staff 4620 11 May 16:50 System Audio (output)_2026-05-11_13-49-41.mp4
-rw-r--r-- 1 lukas staff 4620 11 May 16:50 System Audio (output)_2026-05-11_13-50-03.mp4
-rw-r--r-- 1 lukas staff 4628 11 May 16:50 System Audio (output)_2026-05-11_13-50-26.mp4
-rw-r--r-- 1 lukas staff 4628 11 May 16:51 System Audio (output)_2026-05-11_13-50-48.mp4
-rw-r--r-- 1 lukas staff 4620 11 May 16:51 System Audio (output)_2026-05-11_13-51-10.mp4
-rw-r--r-- 1 lukas staff 4620 11 May 16:51 System Audio (output)_2026-05-11_13-51-33.mp4
-rw-r--r-- 1 lukas staff 4620 11 May 16:52 System Audio (output)_2026-05-11_13-51-55.mp4
drwxr-xr-x 9 lukas staff 288 11 May 07:54 data
drwxr-xr-x 2 lukas staff 64 11 May 15:48 pending-transcriptions
-rw-r--r-- 1 lukas staff 29419 10 May 14:11 soundcore AeroClip (input)_2026-05-10_11-10-32.mp4
-rw-r--r-- 1 lukas staff 56479 10 May 14:11 soundcore AeroClip (input)_2026-05-10_11-11-05.mp4
-rw-r--r--@ 1 lukas staff 181831 10 May 14:12 soundcore AeroClip (input)_2026-05-10_11-11-35.mp4
-rw-r--r-- 1 lukas staff 149782 10 May 14:43 soundcore AeroClip (input)_2026-05-10_11-42-53.mp4
-rw-r--r-- 1 lukas staff 91059 10 May 14:43 soundcore AeroClip (input)_2026-05-10_11-43-25.mp4
-rw-r--r-- 1 lukas staff 30604 10 May 14:44 soundcore AeroClip (input)_2026-05-10_11-44-25.mp4
-rw-r--r-- 1 lukas staff 93813 10 May 14:45 soundcore AeroClip (input)_2026-05-10_11-44-55.mp4
-rw-r--r-- 1 lukas staff 40444 10 May 21:11 soundcore AeroClip (input)_2026-05-10_18-11-18.mp4
-rw-r--r-- 1 lukas staff 193020 10 May 21:12 soundcore AeroClip (input)_2026-05-10_18-11-48.mp4...
Show more
20:34
Retry
Edit
Copy
Claude responded: Good, that clarifies the audio layout — audio files live flat in ~/.
Claude responded: Good, that clarifies the audio layout — audio files live flat in ~/.
Architected multi-install sync with audio support and retention
Architected multi-install sync with audio support and retention
Good, that clarifies the audio layout — audio files live flat in
~/.screenpipe/data/
with the date in the filename (not in date subdirs like frames), and the schema gives us
audio_chunks
+
audio_transcriptions
+
speakers
+
speaker_embeddings
+
audio_tags
plus the FTS table on transcriptions.
Plan I'll implement:
install_id
: stored as a UUID in
~/.screenpipe/.sync_install_id
— disappears when the directory is wiped (which is what you want on reinstall). Every synced NAS table gets an
install_id TEXT
column and a
UNIQUE(install_id, id)
index. The script auto-migrates the existing NAS archive by adding the column and tagging legacy rows with
legacy-YYYYMMDD
so historical data is still queryable but doesn't conflict with future installs.
Audio sync
: adds
audio_chunks
,
audio_transcriptions
,
speakers
,
speaker_embeddings
,
audio_tags
to the sync pipeline, plus the
audio_transcriptions_fts
index. Audio files go to
$NAS_DATA/<date>/audio/
(sibling to the frames in the same date dir, keeps daily archives self-contained).
Retention
is a separate manual script — dry-run by default,
--apply
to delete. Hits DB rows, frame directories, audio mp4s, and screenpipe logs. Optional
--vacuum
with a warning (screenpipe must be stopped to avoid blocking writes for minutes).
One caveat I'm punting on: the FTS tables use source
id
as
rowid
, so after a reinstall they'd get id-collisions on insert and
INSERT OR IGNORE
would silently skip. Within a single install this is fine; after reinstall you'd fall back to
LIKE
queries on the base tables (still filterable by
install_id
). I noted this in the script. Fixing it properly needs a synthetic rowid scheme which we can do later if you actually hit it.
Let me write both:
Updated screenpipe_sync.sh with install_id support and audio sync
Updated screenpipe_sync.sh with install_id support and audio sync
Updated screenpipe_sync.sh with install_id support and audio sync
Updated screenpipe_sync.sh with install_id support and audio sync
screenpipe_sync.sh
Done
Copy
Give positive feedback
Give negative feedback
Retry
You said: I don't like that.
You said: I don't like that.
I don't like that. It should be solid without id collisions. Wouldn't it be to late if I should fix it later?
14:26
Retry
Edit
Copy
Claude responded: You're right — the FTS tables are the gap I papered over.
Claude responded: You're right — the FTS tables are the gap I papered over.
Architected collision-free FTS schema with contentless tables and install tracking
Architected collision-free FTS schema with contentless tables and install tracking
You're right — the FTS tables are the gap I papered over. Let me explain why and fix it properly.
What I missed:
SQLite FTS tables use an
INTEGER rowid
as their primary key. The previous design used the source
id
as that rowid (e.g.
INSERT INTO frames_fts(rowid, ...) VALUES (NEW.id, ...)
). After a reinstall, source IDs reset to 1, and
INSERT OR IGNORE
silently drops them because rowid=1 is already taken. So FTS would only index your most recent install — searching for old transcriptions or OCR text would miss anything from before the reinstall, and there's no way to detect the loss because it fails silently.
Fix:
switch all four FTS tables to fully contentless mode with
install_id
and
source_id
as
UNINDEXED
columns, and let SQLite auto-assign rowids. No collision is possible because each insert gets a fresh rowid. Queries become:
Copy to clipboard
sql
SELECT
f
.
*
FROM
nas
.
frames f
JOIN
nas
.
frames_fts fts
ON
fts
.
install_id
=
f
.
install_id
AND
fts
.
source_id
=
f
.
id
WHERE
frames_fts
MATCH
'standup'
;
One extra JOIN, but bulletproof.
Migration:
the script detects the old FTS schema on first run, drops + rebuilds the four FTS tables from the base tables. This is the only one-time slow step. After that, every run is normal. The base tables (with all your data) are untouched during the rebuild — only the FTS indexes get regenerated.
I also added
tags
and...
|
26267
|
NULL
|
NULL
|
NULL
|
|
13300
|
587
|
34
|
2026-05-09T15:18:18.354612+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-09/1778 /Users/lukas/.screenpipe/data/data/2026-05-09/1778339898354_m1.jpg...
|
Code
|
Visual Studio Code
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X)
Claude Code
Open Chat
⌃
⌘
I
Show All Commands
⇧
⌘
P
Open Recent
⌃
R
Open File or Folder
⌘
O
New Untitled Text File
⌘
N
remote
No Problems
0
0
Notifications
Sign In
Sign In
Select an option to open a Remote Window
7 Results
Connect to Host...Remote-SSH
Connect to Host...
Remote-SSH
Connect Current Window to Host...
Connect Current Window to Host...
Continue Working in vscode.devRemote Repositories
Continue Working in vscode.dev
Remote Repositories
TunnelInstall
Tunnel
Install
Dev Container
Dev Container
GitHub Codespace
GitHub Codespace
Remote Repository
Remote Repository...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Explorer (⇧⌘E)","depth":19,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Search (⇧⌘F)","depth":19,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Source Control (⌃⇧G)","depth":19,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Run and Debug (⇧⌘D)","depth":19,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Remote Explorer","depth":19,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Extensions (⇧⌘X)","depth":19,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Claude Code","depth":19,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Open Chat","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌃","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"I","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Show All Commands","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⇧","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"P","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Open Recent","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌃","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"R","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Open File or Folder","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"O","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"New Untitled Text File","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"N","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"remote","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"No Problems","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Notifications","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sign In","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Sign In","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"Select an option to open a Remote Window","depth":16,"on_screen":true,"placeholder":"Select an option to open a Remote Window","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"7 Results","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Connect to Host...Remote-SSH","depth":14,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Connect to Host...","depth":19,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Remote-SSH","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Connect Current Window to Host...","depth":14,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Connect Current Window to Host...","depth":19,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Continue Working in vscode.devRemote Repositories","depth":14,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Continue Working in vscode.dev","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Remote Repositories","depth":18,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TunnelInstall","depth":14,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Tunnel","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Install","depth":18,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Dev Container","depth":14,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Dev Container","depth":19,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"GitHub Codespace","depth":14,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"GitHub Codespace","depth":19,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Remote Repository","depth":14,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Remote Repository","depth":19,"on_screen":true,"role_description":"text"}]...
|
-8891789862553659965
|
-8683641963135187910
|
click
|
accessibility
|
NULL
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X)
Claude Code
Open Chat
⌃
⌘
I
Show All Commands
⇧
⌘
P
Open Recent
⌃
R
Open File or Folder
⌘
O
New Untitled Text File
⌘
N
remote
No Problems
0
0
Notifications
Sign In
Sign In
Select an option to open a Remote Window
7 Results
Connect to Host...Remote-SSH
Connect to Host...
Remote-SSH
Connect Current Window to Host...
Connect Current Window to Host...
Continue Working in vscode.devRemote Repositories
Continue Working in vscode.dev
Remote Repositories
TunnelInstall
Tunnel
Install
Dev Container
Dev Container
GitHub Codespace
GitHub Codespace
Remote Repository
Remote Repository...
|
13297
|
NULL
|
NULL
|
NULL
|
|
13301
|
588
|
26
|
2026-05-09T15:18:19.887197+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-09/1778 /Users/lukas/.screenpipe/data/data/2026-05-09/1778339899887_m2.jpg...
|
Code
|
Visual Studio Code
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X)
Claude Code
Open Chat
⌃
⌘
I
Show All Commands
⇧
⌘
P
Open Recent
⌃
R
Open File or Folder
⌘
O
New Untitled Text File
⌘
N
remote
No Problems
0
0
Notifications
Sign In
Sign In
Select an option to open a Remote Window
7 Results
Connect to Host...Remote-SSH
Connect to Host...
Remote-SSH
Connect Current Window to Host...
Connect Current Window to Host...
Continue Working in vscode.devRemote Repositories
Continue Working in vscode.dev
Remote Repositories
TunnelInstall
Tunnel
Install
Dev Container
Dev Container
GitHub Codespace
GitHub Codespace
Remote Repository
Remote Repository...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Explorer (⇧⌘E)","depth":19,"bounds":{"left":0.0,"top":0.047885075,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.057462092,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Search (⇧⌘F)","depth":19,"bounds":{"left":0.0,"top":0.08619314,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.09577015,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Source Control (⌃⇧G)","depth":19,"bounds":{"left":0.0,"top":0.1245012,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.13407822,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Run and Debug (⇧⌘D)","depth":19,"bounds":{"left":0.0,"top":0.16280925,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.17238627,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Remote Explorer","depth":19,"bounds":{"left":0.0,"top":0.20111732,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.21069433,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Extensions (⇧⌘X)","depth":19,"bounds":{"left":0.0,"top":0.23942538,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.2490024,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Claude Code","depth":19,"bounds":{"left":0.0,"top":0.27773345,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Open Chat","depth":28,"bounds":{"left":0.46276596,"top":0.5786113,"width":0.023271276,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.46276596,"top":0.5786113,"width":0.003656915,"height":0.012769354}},{"char_start":1,"char_count":8,"bounds":{"left":0.4664229,"top":0.5786113,"width":0.019614361,"height":0.012769354}}],"role_description":"text"},{"role":"AXStaticText","text":"⌃","depth":29,"bounds":{"left":0.52892286,"top":0.5794094,"width":0.0033244682,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"bounds":{"left":0.5382314,"top":0.5794094,"width":0.003656915,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"I","depth":29,"bounds":{"left":0.54853725,"top":0.5794094,"width":0.0013297872,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Show All Commands","depth":28,"bounds":{"left":0.46276596,"top":0.59936154,"width":0.044215426,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.46276596,"top":0.60015965,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.46575797,"top":0.60015965,"width":0.041223403,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⇧","depth":29,"bounds":{"left":0.52892286,"top":0.60015965,"width":0.003656915,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"bounds":{"left":0.5382314,"top":0.60015965,"width":0.003656915,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"P","depth":29,"bounds":{"left":0.54787236,"top":0.60015965,"width":0.0026595744,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Open Recent","depth":28,"bounds":{"left":0.46276596,"top":0.6201117,"width":0.02825798,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.46276596,"top":0.6209098,"width":0.003656915,"height":0.011971269}},{"char_start":1,"char_count":10,"bounds":{"left":0.4664229,"top":0.6209098,"width":0.024601065,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌃","depth":29,"bounds":{"left":0.5382314,"top":0.6209098,"width":0.0033244682,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"R","depth":29,"bounds":{"left":0.54787236,"top":0.6209098,"width":0.0026595744,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Open File or Folder","depth":28,"bounds":{"left":0.46276596,"top":0.64166003,"width":0.041888297,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.46276596,"top":0.64166003,"width":0.003656915,"height":0.012769354}},{"char_start":1,"char_count":18,"bounds":{"left":0.4664229,"top":0.64166003,"width":0.037898935,"height":0.012769354}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"bounds":{"left":0.5382314,"top":0.6424581,"width":0.003656915,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"O","depth":29,"bounds":{"left":0.54787236,"top":0.6424581,"width":0.0029920214,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"New Untitled Text File","depth":28,"bounds":{"left":0.46276596,"top":0.6624102,"width":0.047872342,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.46276596,"top":0.6624102,"width":0.0033244682,"height":0.012769354}},{"char_start":1,"char_count":21,"bounds":{"left":0.4660904,"top":0.6624102,"width":0.04454787,"height":0.012769354}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘","depth":29,"bounds":{"left":0.5382314,"top":0.6632083,"width":0.003656915,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"N","depth":29,"bounds":{"left":0.54787236,"top":0.6632083,"width":0.0029920214,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"remote","depth":16,"bounds":{"left":0.0006648936,"top":0.98244214,"width":0.010638298,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"No Problems","depth":16,"bounds":{"left":0.012300532,"top":0.98244214,"width":0.022273935,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.013962766,"top":0.9848364,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"bounds":{"left":0.019281914,"top":0.9856345,"width":0.004654255,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.023936171,"top":0.9848364,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"bounds":{"left":0.02925532,"top":0.9856345,"width":0.003656915,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Notifications","depth":16,"bounds":{"left":0.9886968,"top":0.98244214,"width":0.010638298,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sign In","depth":16,"bounds":{"left":0.9650931,"top":0.98244214,"width":0.022606382,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.96675533,"top":0.9848364,"width":0.0056515955,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Sign In","depth":17,"bounds":{"left":0.97207445,"top":0.9856345,"width":0.013962766,"height":0.011173184},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.97207445,"top":0.9856345,"width":0.0013297872,"height":0.011173184}},{"char_start":1,"char_count":6,"bounds":{"left":0.9734042,"top":0.9856345,"width":0.010638298,"height":0.011173184}}],"role_description":"text"},{"role":"AXTextField","text":"Select an option to open a Remote Window","depth":16,"bounds":{"left":0.40292552,"top":0.0311253,"width":0.19481383,"height":0.018355945},"on_screen":true,"placeholder":"Select an option to open a Remote Window","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"7 Results","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Connect to Host...Remote-SSH","depth":14,"bounds":{"left":0.40259308,"top":0.055067837,"width":0.19547872,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Connect to Host...","depth":19,"bounds":{"left":0.40625,"top":0.057462092,"width":0.036901597,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.40625,"top":0.058260176,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":17,"bounds":{"left":0.40924203,"top":0.058260176,"width":0.033909574,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"Remote-SSH","depth":17,"bounds":{"left":0.57081115,"top":0.058260176,"width":0.022606382,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.57081115,"top":0.05905826,"width":0.0026595744,"height":0.0103751}},{"char_start":1,"char_count":9,"bounds":{"left":0.5731383,"top":0.05905826,"width":0.020279255,"height":0.0103751}}],"role_description":"text"},{"role":"AXStaticText","text":"Connect Current Window to Host...","depth":14,"bounds":{"left":0.40259308,"top":0.0726257,"width":0.19547872,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Connect Current Window to Host...","depth":19,"bounds":{"left":0.40625,"top":0.075019956,"width":0.07047872,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.40625,"top":0.07581804,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":32,"bounds":{"left":0.40924203,"top":0.07581804,"width":0.0674867,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"Continue Working in vscode.devRemote Repositories","depth":14,"bounds":{"left":0.40259308,"top":0.090183556,"width":0.19547872,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Continue Working in vscode.dev","depth":20,"bounds":{"left":0.40625,"top":0.0933759,"width":0.06482713,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.40625,"top":0.09417398,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":29,"bounds":{"left":0.40924203,"top":0.09417398,"width":0.061835106,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"Remote Repositories","depth":18,"bounds":{"left":0.5575133,"top":0.09417398,"width":0.035904255,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.5575133,"top":0.09497207,"width":0.0023271276,"height":0.0103751}},{"char_start":1,"char_count":18,"bounds":{"left":0.55984044,"top":0.09497207,"width":0.033909574,"height":0.0103751}}],"role_description":"text"},{"role":"AXStaticText","text":"TunnelInstall","depth":14,"bounds":{"left":0.40259308,"top":0.10774142,"width":0.19547872,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Tunnel","depth":20,"bounds":{"left":0.40625,"top":0.11093376,"width":0.013630319,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.40625,"top":0.11173184,"width":0.0023271276,"height":0.011971269}},{"char_start":1,"char_count":5,"bounds":{"left":0.4085771,"top":0.11173184,"width":0.011303191,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"Install","depth":18,"bounds":{"left":0.5831117,"top":0.11173184,"width":0.010305851,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.5831117,"top":0.112529926,"width":0.0009973404,"height":0.0103751}},{"char_start":1,"char_count":6,"bounds":{"left":0.58410907,"top":0.112529926,"width":0.00930851,"height":0.0103751}}],"role_description":"text"},{"role":"AXStaticText","text":"Dev Container","depth":14,"bounds":{"left":0.40259308,"top":0.12529927,"width":0.19547872,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Dev Container","depth":19,"bounds":{"left":0.40625,"top":0.12769353,"width":0.028922873,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.40625,"top":0.12849163,"width":0.0033244682,"height":0.011971269}},{"char_start":1,"char_count":12,"bounds":{"left":0.40957448,"top":0.12849163,"width":0.025265958,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"GitHub Codespace","depth":14,"bounds":{"left":0.40259308,"top":0.14285715,"width":0.19547872,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"GitHub Codespace","depth":19,"bounds":{"left":0.40625,"top":0.1452514,"width":0.039228722,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.40625,"top":0.14604948,"width":0.0033244682,"height":0.011971269}},{"char_start":1,"char_count":15,"bounds":{"left":0.40957448,"top":0.14604948,"width":0.034906916,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"Remote Repository","depth":14,"bounds":{"left":0.40259308,"top":0.16041501,"width":0.19547872,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Remote Repository","depth":19,"bounds":{"left":0.40625,"top":0.16280925,"width":0.038231384,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.40625,"top":0.16360734,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.4089096,"top":0.16360734,"width":0.03557181,"height":0.011971269}}],"role_description":"text"}]...
|
-8891789862553659965
|
-8683641963135187910
|
click
|
accessibility
|
NULL
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G)
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X)
Claude Code
Open Chat
⌃
⌘
I
Show All Commands
⇧
⌘
P
Open Recent
⌃
R
Open File or Folder
⌘
O
New Untitled Text File
⌘
N
remote
No Problems
0
0
Notifications
Sign In
Sign In
Select an option to open a Remote Window
7 Results
Connect to Host...Remote-SSH
Connect to Host...
Remote-SSH
Connect Current Window to Host...
Connect Current Window to Host...
Continue Working in vscode.devRemote Repositories
Continue Working in vscode.dev
Remote Repositories
TunnelInstall
Tunnel
Install
Dev Container
Dev Container
GitHub Codespace
GitHub Codespace
Remote Repository
Remote Repository...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
22106
|
964
|
27
|
2026-05-12T06:49:21.931035+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778568561931_m1.jpg...
|
Firefox
|
Platform Sprint 3 Q2 - Platform Team - Scrum Board Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira — Work...
|
True
|
jiminny.atlassian.net/jira/software/c/projects/JY/ jiminny.atlassian.net/jira/software/c/projects/JY/boards/37...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
New Tab
New Tab
Jy 20820 es reindex stream model h New Tab
New Tab
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pipelines - jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pipelines - jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20773] User Pilot not receiving events on report generated - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20776] Automated report - sentry - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20776] Automated report - sentry - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"TypeError: League\\Flysystem\\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.067708336,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.090625,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.11388889,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.13715278,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.16041666,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to:","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Top Bar","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Top Bar","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Sidebar","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Sidebar","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Main Content","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Main Content","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Space navigation","depth":10,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Space navigation","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Collapse sidebar [","depth":9,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Collapse sidebar [","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Switch sites or apps","depth":10,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Switch sites or apps","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Go to your Jira homepage","depth":9,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXComboBox","text":"Search, press enter to navigate to advanced search with your text query","depth":11,"on_screen":true,"help_text":"","placeholder":"Search","role_description":"combo box","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Create","depth":10,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Rovo Ask Rovo","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ask Rovo","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Notifications","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Notifications","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Help","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Help","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Settings","depth":12,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Settings","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"lukas.kovalik@jiminny.com","depth":12,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"lukas.kovalik@jiminny.com","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"For you","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"For you","depth":15,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Recent","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Recent","depth":15,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Starred","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Starred","depth":15,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Apps","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Apps","depth":15,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Apps","depth":13,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Apps","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Spaces","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXStaticText","text":"Spaces","depth":15,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Create space","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create space","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for spaces","depth":13,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for spaces","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Recent","depth":16,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Jiminny (New)","depth":17,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny (New)","depth":20,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Jiminny (New)","depth":18,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXMenuButton","text":"Create board","depth":18,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Create board","depth":20,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Jiminny (New)","depth":18,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Jiminny (New)","depth":20,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Platform Team","depth":19,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Team","depth":22,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":20,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8889008983377807170
|
-3309843206560059250
|
visual_change
|
accessibility
|
NULL
|
New Tab
New Tab
Jy 20820 es reindex stream model h New Tab
New Tab
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
Jy 20820 es reindex stream model hydration by Vasil-Jiminny · Pull Request #12059 · jiminny/app
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
[JY-20725] [HubSpot] Optimise CRM rematching on delete hubspot accounts/contacts - Jira
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
JY-20725 add HS rate limit handling on activities rematching by LakyLak · Pull Request #12066 · jiminny/app
Pipelines - jiminny/app
Pipelines - jiminny/app
Pull requests · jiminny/app
Pull requests · jiminny/app
[JY-20773] User Pilot not receiving events on report generated - Jira
[JY-20773] User Pilot not receiving events on report generated - Jira
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
JY-20773 fix user pilot tracking ofr automated report generated by LakyLak · Pull Request #12024 · jiminny/app
[JY-20776] Automated report - sentry - Jira
[JY-20776] Automated report - sentry - Jira
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
TypeError: League\Flysystem\Filesystem::has(): Argument #1 ($location) must be of type string, null given, called in /home/jiminny/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php on line 218 — jiminny — app
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions...
|
22105
|
NULL
|
NULL
|
NULL
|
|
17496
|
772
|
43
|
2026-05-11T10:24:28.924106+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778495068924_m1.jpg...
|
Firefox
|
Screenpipe — Archive — Personal
|
True
|
app.screenpipe.lakylak.xyz
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
New Tab
New Tab
Screenpipe — Archive
Screenpipe — New Tab
New Tab
Screenpipe — Archive
Screenpipe — Archive
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Screenpipe [archive.db · 12323.6MB]
Screenpipe
[archive.db · 12323.6MB]
Activity
Search
Audio
Work Report
Timetable
AI Summary
Date
07
/
05
/
2026
Calendar
SECONDLY
Search
AND also
second required term — both must appear in same result (optional)
Source
App
Date
dd
/
mm
/
yyyy
Calendar
(blank = all dates)
From
--
:
--
To
--
:
--
Only apps
any app (blank = all)
▾
Skip apps
none skipped
▾
FTS tip: single words work best · "exact phrase" · term1 OR term2 · use AND also for required second term · times are in your local timezone · click any result to expand
49 results
▶ video
7 May 19:25
Screen
PhpStorm
/ faVsco.js – Client.php
⏱ Timeline
...true) ??
:SpolicyName = $body
'policyName'
?? Sbody
'policy'
?? null:Map pollcy names to retry delaysif (SpolicvName === 'TEN
SECONDLY
_ROLLING' |I SpolicvName === 'ten
secondly
rolling') {
… (click to expand)
▶ video
7 May 19:10
Screen
PhpStorm
/ faVsco.js – Client.php
⏱ Timeline
...true) ??
:Snolievlane = Sbod/LUpoltiev/lane:1 22 Shody
LnolieyL1L.22 п11rao nolicy names to retry delav1f SpoLicvName ==='TEN
SECONDLY
ROLLING' |I SpolicvName === 'ten
secondly
rolling') {Le
… (click to expand)
7 May 17:24
Typed
Firefox
text
⏱ Timeline
.
7 May 17:23
Typed
Firefox
text
⏱ Timeline
7 May 17:22
Typed
Firefox
text
⏱ Timeline
.
7 May 17:22
Typed
Firefox
text
⏱ Timeline
'
7 May 17:21
Typed
Firefox
text
⏱ Timeline
7 May 17:20
Typed
Firefox
text
⏱ Timeline
7 May 17:19
Typed
Firefox
text
⏱ Timeline
7 May 17:17
Typed
Firefox
text
⏱ Timeline
7 May 17:17
Typed
Firefox
text
⏱ Timeline
$
7 May 17:17
Typed
Firefox
text
⏱ Timeline
;
7 May 17:17
Typed
Firefox
text
⏱ Timeline
=
7 May 17:16
Typed
Firefox
text
⏱ Timeline
:
7 May 17:16
Typed
Firefox
text
⏱ Timeline
7 May 17:15
Typed
Firefox
text
⏱ Timeline
.
7 May 17:15
Typed
Firefox
text
⏱ Timeline
7 May 17:14
Typed
Firefox
text
⏱ Timeline
7 May 17:14
Typed
Firefox
text
⏱ Timeline
.
7 May 17:10
Typed
Firefox
text
⏱ Timeline
7 May 17:09
Typed
Firefox
text
⏱ Timeline
7 May 17:08
Typed
Firefox
text
⏱ Timeline
7 May 17:07
Typed
Firefox
text
⏱ Timeline
7 May 14:13
Typed
Claude
text
⏱ Timeline
secondly
7 May 10:58
Typed
clipboard
⏱ Timeline
...search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...)
7 May 10:41
Typed
clipboard
⏱ Timeline
...search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...)
7 May 10:41
Typed
Firefox
text
⏱ Timeline
7 May 10:41
Typed
Firefox
text
⏱ Timeline
)
7 May 10:41
Typed
Firefox
text
⏱ Timeline
,
7 May 10:41
Typed
Firefox
text
⏱ Timeline
(
7 May 10:38
Typed
Firefox
text
⏱ Timeline
▶ video
22 Apr 12:58
Screen
Firefox
/ Jiminny — Work
app.staging.jiminny.com/ai-reports/pdf/582d4b50-8cd3-42a9-9819-d676ff8f3b43
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:50
Screen
Firefox
/ Pull requests · jiminny/app — Work
github.com/jiminny/app/pull/11998
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:44
Screen
Firefox
/ Platform Sprint 2 Q2 - Platform Team - Scrum Board
jiminny.atlassian.net/jira/software/c/projects/JY/boards/37
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
22 Apr 12:42
Typed
clipboard
⏱ Timeline
...search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","errorType":"RATE_LIMIT","correlationId":"019db2b6-c (truncated...)
▶ video
22 Apr 12:42
Screen
Firefox
/ SevenShores\Hubspot\Exceptions\BadRequest: Client
jiminny.sentry.io/issues/7007366572/events/38fa421c52fc432b92bfc347df23b251/
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:41
Screen
Firefox
/ SevenShores\Hubspot\Exceptions\BadRequest: Client
jiminny.sentry.io/issues/7007366572/events/?project=82419&referrer=slack
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:40
Screen
Firefox
/ SevenShores\Hubspot\Exceptions\BadRequest: Client
jiminny.sentry.io/issues/7007366572/events/?project=82419&referrer=slack
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:39
Screen
Firefox
/ Pipelines - /app — Work
app.circleci.com/pipelines/github/jiminny/app
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:20
Screen
Firefox
/ Platform Sprint 2 Q2 - Platform Team - Scrum Board
jiminny.atlassian.net/jira/software/c/projects/JY/boards/37
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:11
Screen
Firefox
/ SevenShores\Hubspot\Exceptions\BadRequest: Client
jiminny.sentry.io/issues/7007366572/events/?project=82419&referrer=slack
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:07
Screen
Firefox
/ Meet - CRM issues — Work
meet.google.com/pei-cvuh-fxt?authuser=lukas.kovalik%40jiminny.com
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 11:09
Screen
Firefox
/ Meet - CRM issues — Work
meet.google.com/pei-cvuh-fxt?authuser=lukas.kovalik%40jiminny.com
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 11:08
Screen
Firefox
/ Meet - CRM issues — Work
meet.google.com/pei-cvuh-fxt?authuser=lukas.kovalik%40jiminny.com
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 11:02
Screen
Firefox
/ Meet - CRM issues — Work
meet.google.com/pei-cvuh-fxt?authuser=lukas.kovalik%40jiminny.com
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 11:01
Screen
Firefox
/ Meet - CRM issues — Work
meet.google.com/pei-cvuh-fxt?authuser=lukas.kovalik%40jiminny.com
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
22 Apr 11:00
Typed
clipboard
⏱ Timeline
...search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","errorType":"RATE_LIMIT","correlationId":"019db2b6-c (truncated...)
▶ video
22 Apr 11:00
Screen
Firefox
/ SevenShores\Hubspot\Exceptions\BadRequest: Client
jiminny.sentry.io/issues/7007366572/events/38fa421c52fc432b92bfc347df23b251/
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 10:58
Screen
Firefox
/ SevenShores\Hubspot\Exceptions\BadRequest: Client
jiminny.sentry.io/issues/7007366572/?project=82419&referrer=slack
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
✕
17:17 · Firefox
No screenshot or video available
FULL TEXT
=
⏱ Open in Timetable
METADATA
Time
17:17
Date
2026-05-07...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.043402776,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.06631944,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.08958333,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.112847224,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.13611111,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Screenpipe [archive.db · 12323.6MB]","depth":7,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screenpipe","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[archive.db · 12323.6MB]","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Activity","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Search","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Audio","depth":7,"on_screen":true,"help_text":"No audio data in this database","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Work Report","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Timetable","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"AI Summary","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Date","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"07","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"05","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2026","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Calendar","depth":8,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"SECONDLY","depth":8,"on_screen":true,"value":"SECONDLY","help_text":"","role_description":"text field","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Search","depth":8,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AND also","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXTextField","text":"second required term — both must appear in same result (optional)","depth":8,"on_screen":true,"help_text":"","role_description":"text field","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Source","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"App","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Date","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dd","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"mm","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"yyyy","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Calendar","depth":9,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"(blank = all dates)","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"From","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"--","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"--","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"To","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"--","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"--","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Only apps","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"any app (blank = all)","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▾","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Skip apps","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"none skipped","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▾","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"FTS tip: single words work best · \"exact phrase\" · term1 OR term2 · use AND also for required second term · times are in your local timezone · click any result to expand","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"49 results","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 19:25","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PhpStorm","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ faVsco.js – Client.php","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":true,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...true) ??","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":SpolicyName = $body","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'policyName'","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"?? Sbody","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"'policy'","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"?? null:Map pollcy names to retry delaysif (SpolicvName === 'TEN","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SECONDLY","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"_ROLLING' |I SpolicvName === 'ten","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"rolling') {","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 19:10","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PhpStorm","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ faVsco.js – Client.php","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":true,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...true) ??","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":Snolievlane = Sbod/LUpoltiev/lane:1 22 Shody","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"LnolieyL1L.22 п11rao nolicy names to retry delav1f SpoLicvName ==='TEN","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SECONDLY","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"ROLLING' |I SpolicvName === 'ten","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"rolling') {Le","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 17:24","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":true,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":".","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 17:23","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":true,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 May 17:22","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":true,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":".","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 17:22","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":true,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"'","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 17:21","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":true,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 May 17:20","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":true,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 May 17:19","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":true,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 May 17:17","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":true,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 May 17:17","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":true,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"$","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 17:17","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":true,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":";","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 17:17","depth":11,"bounds":{"left":0.48159721,"top":0.0,"width":0.042013887,"height":0.015},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.53055555,"top":0.0,"width":0.01875,"height":0.012222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"bounds":{"left":0.5548611,"top":0.0,"width":0.02673611,"height":0.015},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.58368057,"top":0.0,"width":0.012847222,"height":0.013888889},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":true,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"=","depth":11,"bounds":{"left":0.48159721,"top":0.0,"width":0.0055555557,"height":0.017777778},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 17:16","depth":11,"bounds":{"left":0.48159721,"top":0.014444444,"width":0.04236111,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.5309028,"top":0.016666668,"width":0.01875,"height":0.012222222},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"bounds":{"left":0.5552083,"top":0.014444444,"width":0.027083334,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.584375,"top":0.015555556,"width":0.012847222,"height":0.013888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":":","depth":11,"bounds":{"left":0.48159721,"top":0.04111111,"width":0.0027777778,"height":0.017777778},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 17:16","depth":11,"bounds":{"left":0.48159721,"top":0.093333334,"width":0.04236111,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.5309028,"top":0.09555556,"width":0.01875,"height":0.012222222},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"bounds":{"left":0.5552083,"top":0.093333334,"width":0.027083334,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.584375,"top":0.094444446,"width":0.012847222,"height":0.013888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 May 17:15","depth":11,"bounds":{"left":0.48159721,"top":0.15055555,"width":0.04236111,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.5309028,"top":0.15277778,"width":0.01875,"height":0.012222222},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"bounds":{"left":0.5552083,"top":0.15055555,"width":0.02673611,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.584375,"top":0.15166667,"width":0.0125,"height":0.013888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":".","depth":11,"bounds":{"left":0.48159721,"top":0.17722222,"width":0.0027777778,"height":0.017777778},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 17:15","depth":11,"bounds":{"left":0.48159721,"top":0.22944444,"width":0.04236111,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.5309028,"top":0.23166667,"width":0.01875,"height":0.012222222},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"bounds":{"left":0.5552083,"top":0.22944444,"width":0.02673611,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.584375,"top":0.23055555,"width":0.0125,"height":0.013888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 May 17:14","depth":11,"bounds":{"left":0.48159721,"top":0.28666666,"width":0.04236111,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.5309028,"top":0.2888889,"width":0.01875,"height":0.012222222},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"bounds":{"left":0.5555556,"top":0.28666666,"width":0.02673611,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.584375,"top":0.28777778,"width":0.012847222,"height":0.013888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 May 17:14","depth":11,"bounds":{"left":0.48159721,"top":0.34388888,"width":0.04236111,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.5309028,"top":0.34611112,"width":0.01875,"height":0.012222222},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"bounds":{"left":0.5555556,"top":0.34388888,"width":0.02673611,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.584375,"top":0.345,"width":0.012847222,"height":0.013888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":".","depth":11,"bounds":{"left":0.48159721,"top":0.37055555,"width":0.0027777778,"height":0.017777778},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 17:10","depth":11,"bounds":{"left":0.48159721,"top":0.42277777,"width":0.04236111,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.5309028,"top":0.425,"width":0.01875,"height":0.012222222},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"bounds":{"left":0.5552083,"top":0.42277777,"width":0.027083334,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.584375,"top":0.4238889,"width":0.012847222,"height":0.013888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 May 17:09","depth":11,"bounds":{"left":0.48159721,"top":0.48,"width":0.04375,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.53229165,"top":0.48222223,"width":0.01875,"height":0.012222222},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"bounds":{"left":0.55659723,"top":0.48,"width":0.02673611,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.5857639,"top":0.4811111,"width":0.0125,"height":0.013888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 May 17:08","depth":11,"bounds":{"left":0.48159721,"top":0.5372222,"width":0.04375,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.53229165,"top":0.53944445,"width":0.01875,"height":0.012222222},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"bounds":{"left":0.55659723,"top":0.5372222,"width":0.02673611,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.5857639,"top":0.53833336,"width":0.012847222,"height":0.013888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 May 17:07","depth":11,"bounds":{"left":0.48159721,"top":0.59444445,"width":0.043055557,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.5315972,"top":0.5966667,"width":0.01875,"height":0.012222222},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"bounds":{"left":0.5559028,"top":0.59444445,"width":0.02673611,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.5850694,"top":0.59555554,"width":0.012847222,"height":0.013888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 May 14:13","depth":11,"bounds":{"left":0.48159721,"top":0.65166664,"width":0.043055557,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.5315972,"top":0.6538889,"width":0.01875,"height":0.012222222},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Claude","depth":12,"bounds":{"left":0.5559028,"top":0.65166664,"width":0.027083334,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.5850694,"top":0.6527778,"width":0.012847222,"height":0.013888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"secondly","depth":12,"bounds":{"left":0.48229167,"top":0.67833334,"width":0.03784722,"height":0.017777778},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 10:58","depth":11,"bounds":{"left":0.48159721,"top":0.73055553,"width":0.044444446,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.5329861,"top":0.7327778,"width":0.01875,"height":0.012222222},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"clipboard","depth":11,"bounds":{"left":0.5572917,"top":0.7316667,"width":0.03125,"height":0.013888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"bounds":{"left":0.48159721,"top":0.75722224,"width":0.47534722,"height":0.017777778},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"bounds":{"left":0.95763886,"top":0.75722224,"width":0.03784722,"height":0.017777778},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019dffc4-4 (truncated...)","depth":11,"bounds":{"left":0.99618053,"top":0.75722224,"width":0.0038194656,"height":0.017777778},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 10:41","depth":11,"bounds":{"left":0.48159721,"top":0.8094444,"width":0.043055557,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.5315972,"top":0.81166667,"width":0.01875,"height":0.012222222},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"clipboard","depth":11,"bounds":{"left":0.5559028,"top":0.8105556,"width":0.03125,"height":0.013888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"bounds":{"left":0.48159721,"top":0.8361111,"width":0.47534722,"height":0.017777778},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"bounds":{"left":0.95763886,"top":0.8361111,"width":0.03784722,"height":0.017777778},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019dffc4-4 (truncated...)","depth":11,"bounds":{"left":0.99618053,"top":0.8361111,"width":0.0038194656,"height":0.017777778},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 10:41","depth":11,"bounds":{"left":0.48159721,"top":0.8883333,"width":0.043055557,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.5315972,"top":0.89055556,"width":0.01875,"height":0.012222222},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"bounds":{"left":0.5559028,"top":0.8883333,"width":0.02673611,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.5847222,"top":0.8894445,"width":0.012847222,"height":0.013888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"7 May 10:41","depth":11,"bounds":{"left":0.48159721,"top":0.94555557,"width":0.043055557,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.5315972,"top":0.94777775,"width":0.01875,"height":0.012222222},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"bounds":{"left":0.5559028,"top":0.94555557,"width":0.02673611,"height":0.015},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.5847222,"top":0.94666666,"width":0.012847222,"height":0.013888889},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":")","depth":11,"bounds":{"left":0.48159721,"top":0.9722222,"width":0.0034722222,"height":0.017777778},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 10:41","depth":11,"bounds":{"left":0.48159721,"top":1.0,"width":0.043055557,"height":-0.02444446},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"bounds":{"left":0.5315972,"top":1.0,"width":0.01875,"height":-0.026666641},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"bounds":{"left":0.5559028,"top":1.0,"width":0.02673611,"height":-0.02444446},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"bounds":{"left":0.5847222,"top":1.0,"width":0.012847222,"height":-0.02555561},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":",","depth":11,"bounds":{"left":0.48159721,"top":1.0,"width":0.0027777778,"height":-0.051111102},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 10:41","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"(","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7 May 10:38","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"text","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 12:58","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ Jiminny — Work","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app.staging.jiminny.com/ai-reports/pdf/582d4b50-8cd3-42a9-9819-d676ff8f3b43","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 12:50","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ Pull requests · jiminny/app — Work","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"github.com/jiminny/app/pull/11998","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 12:44","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ Platform Sprint 2 Q2 - Platform Team - Scrum Board","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"jiminny.atlassian.net/jira/software/c/projects/JY/boards/37","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 12:42","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"clipboard","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019db2b6-c (truncated...)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 12:42","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ SevenShores\\Hubspot\\Exceptions\\BadRequest: Client","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"jiminny.sentry.io/issues/7007366572/events/38fa421c52fc432b92bfc347df23b251/","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 12:41","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ SevenShores\\Hubspot\\Exceptions\\BadRequest: Client","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"jiminny.sentry.io/issues/7007366572/events/?project=82419&referrer=slack","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 12:40","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ SevenShores\\Hubspot\\Exceptions\\BadRequest: Client","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"jiminny.sentry.io/issues/7007366572/events/?project=82419&referrer=slack","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 12:39","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ Pipelines - /app — Work","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"app.circleci.com/pipelines/github/jiminny/app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 12:20","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ Platform Sprint 2 Q2 - Platform Team - Scrum Board","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"jiminny.atlassian.net/jira/software/c/projects/JY/boards/37","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 12:11","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ SevenShores\\Hubspot\\Exceptions\\BadRequest: Client","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"jiminny.sentry.io/issues/7007366572/events/?project=82419&referrer=slack","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 12:07","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ Meet - CRM issues — Work","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"meet.google.com/pei-cvuh-fxt?authuser=lukas.kovalik%40jiminny.com","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 11:09","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ Meet - CRM issues — Work","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"meet.google.com/pei-cvuh-fxt?authuser=lukas.kovalik%40jiminny.com","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 11:08","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ Meet - CRM issues — Work","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"meet.google.com/pei-cvuh-fxt?authuser=lukas.kovalik%40jiminny.com","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 11:02","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ Meet - CRM issues — Work","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"meet.google.com/pei-cvuh-fxt?authuser=lukas.kovalik%40jiminny.com","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 11:01","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ Meet - CRM issues — Work","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"meet.google.com/pei-cvuh-fxt?authuser=lukas.kovalik%40jiminny.com","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 11:00","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Typed","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"clipboard","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019db2b6-c (truncated...)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 11:00","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ SevenShores\\Hubspot\\Exceptions\\BadRequest: Client","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"jiminny.sentry.io/issues/7007366572/events/38fa421c52fc432b92bfc347df23b251/","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"▶ video","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"22 Apr 10:58","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screen","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Firefox","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ SevenShores\\Hubspot\\Exceptions\\BadRequest: Client","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"jiminny.sentry.io/issues/7007366572/?project=82419&referrer=slack","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Timeline","depth":10,"on_screen":false,"help_text":"Open in Timetable","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"secondly","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit.\",\"erro","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"… (click to expand)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"✕","depth":8,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"17:17 · Firefox","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No screenshot or video available","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"FULL TEXT","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"=","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏱ Open in Timetable","depth":9,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"METADATA","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Time","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"17:17","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Date","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2026-05-07","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-8888658461309271959
|
6612526152100808961
|
click
|
accessibility
|
NULL
|
New Tab
New Tab
Screenpipe — Archive
Screenpipe — New Tab
New Tab
Screenpipe — Archive
Screenpipe — Archive
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Screenpipe [archive.db · 12323.6MB]
Screenpipe
[archive.db · 12323.6MB]
Activity
Search
Audio
Work Report
Timetable
AI Summary
Date
07
/
05
/
2026
Calendar
SECONDLY
Search
AND also
second required term — both must appear in same result (optional)
Source
App
Date
dd
/
mm
/
yyyy
Calendar
(blank = all dates)
From
--
:
--
To
--
:
--
Only apps
any app (blank = all)
▾
Skip apps
none skipped
▾
FTS tip: single words work best · "exact phrase" · term1 OR term2 · use AND also for required second term · times are in your local timezone · click any result to expand
49 results
▶ video
7 May 19:25
Screen
PhpStorm
/ faVsco.js – Client.php
⏱ Timeline
...true) ??
:SpolicyName = $body
'policyName'
?? Sbody
'policy'
?? null:Map pollcy names to retry delaysif (SpolicvName === 'TEN
SECONDLY
_ROLLING' |I SpolicvName === 'ten
secondly
rolling') {
… (click to expand)
▶ video
7 May 19:10
Screen
PhpStorm
/ faVsco.js – Client.php
⏱ Timeline
...true) ??
:Snolievlane = Sbod/LUpoltiev/lane:1 22 Shody
LnolieyL1L.22 п11rao nolicy names to retry delav1f SpoLicvName ==='TEN
SECONDLY
ROLLING' |I SpolicvName === 'ten
secondly
rolling') {Le
… (click to expand)
7 May 17:24
Typed
Firefox
text
⏱ Timeline
.
7 May 17:23
Typed
Firefox
text
⏱ Timeline
7 May 17:22
Typed
Firefox
text
⏱ Timeline
.
7 May 17:22
Typed
Firefox
text
⏱ Timeline
'
7 May 17:21
Typed
Firefox
text
⏱ Timeline
7 May 17:20
Typed
Firefox
text
⏱ Timeline
7 May 17:19
Typed
Firefox
text
⏱ Timeline
7 May 17:17
Typed
Firefox
text
⏱ Timeline
7 May 17:17
Typed
Firefox
text
⏱ Timeline
$
7 May 17:17
Typed
Firefox
text
⏱ Timeline
;
7 May 17:17
Typed
Firefox
text
⏱ Timeline
=
7 May 17:16
Typed
Firefox
text
⏱ Timeline
:
7 May 17:16
Typed
Firefox
text
⏱ Timeline
7 May 17:15
Typed
Firefox
text
⏱ Timeline
.
7 May 17:15
Typed
Firefox
text
⏱ Timeline
7 May 17:14
Typed
Firefox
text
⏱ Timeline
7 May 17:14
Typed
Firefox
text
⏱ Timeline
.
7 May 17:10
Typed
Firefox
text
⏱ Timeline
7 May 17:09
Typed
Firefox
text
⏱ Timeline
7 May 17:08
Typed
Firefox
text
⏱ Timeline
7 May 17:07
Typed
Firefox
text
⏱ Timeline
7 May 14:13
Typed
Claude
text
⏱ Timeline
secondly
7 May 10:58
Typed
clipboard
⏱ Timeline
...search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...)
7 May 10:41
Typed
clipboard
⏱ Timeline
...search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...)
7 May 10:41
Typed
Firefox
text
⏱ Timeline
7 May 10:41
Typed
Firefox
text
⏱ Timeline
)
7 May 10:41
Typed
Firefox
text
⏱ Timeline
,
7 May 10:41
Typed
Firefox
text
⏱ Timeline
(
7 May 10:38
Typed
Firefox
text
⏱ Timeline
▶ video
22 Apr 12:58
Screen
Firefox
/ Jiminny — Work
app.staging.jiminny.com/ai-reports/pdf/582d4b50-8cd3-42a9-9819-d676ff8f3b43
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:50
Screen
Firefox
/ Pull requests · jiminny/app — Work
github.com/jiminny/app/pull/11998
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:44
Screen
Firefox
/ Platform Sprint 2 Q2 - Platform Team - Scrum Board
jiminny.atlassian.net/jira/software/c/projects/JY/boards/37
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
22 Apr 12:42
Typed
clipboard
⏱ Timeline
...search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","errorType":"RATE_LIMIT","correlationId":"019db2b6-c (truncated...)
▶ video
22 Apr 12:42
Screen
Firefox
/ SevenShores\Hubspot\Exceptions\BadRequest: Client
jiminny.sentry.io/issues/7007366572/events/38fa421c52fc432b92bfc347df23b251/
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:41
Screen
Firefox
/ SevenShores\Hubspot\Exceptions\BadRequest: Client
jiminny.sentry.io/issues/7007366572/events/?project=82419&referrer=slack
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:40
Screen
Firefox
/ SevenShores\Hubspot\Exceptions\BadRequest: Client
jiminny.sentry.io/issues/7007366572/events/?project=82419&referrer=slack
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:39
Screen
Firefox
/ Pipelines - /app — Work
app.circleci.com/pipelines/github/jiminny/app
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:20
Screen
Firefox
/ Platform Sprint 2 Q2 - Platform Team - Scrum Board
jiminny.atlassian.net/jira/software/c/projects/JY/boards/37
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:11
Screen
Firefox
/ SevenShores\Hubspot\Exceptions\BadRequest: Client
jiminny.sentry.io/issues/7007366572/events/?project=82419&referrer=slack
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 12:07
Screen
Firefox
/ Meet - CRM issues — Work
meet.google.com/pei-cvuh-fxt?authuser=lukas.kovalik%40jiminny.com
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 11:09
Screen
Firefox
/ Meet - CRM issues — Work
meet.google.com/pei-cvuh-fxt?authuser=lukas.kovalik%40jiminny.com
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 11:08
Screen
Firefox
/ Meet - CRM issues — Work
meet.google.com/pei-cvuh-fxt?authuser=lukas.kovalik%40jiminny.com
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 11:02
Screen
Firefox
/ Meet - CRM issues — Work
meet.google.com/pei-cvuh-fxt?authuser=lukas.kovalik%40jiminny.com
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 11:01
Screen
Firefox
/ Meet - CRM issues — Work
meet.google.com/pei-cvuh-fxt?authuser=lukas.kovalik%40jiminny.com
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
22 Apr 11:00
Typed
clipboard
⏱ Timeline
...search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","errorType":"RATE_LIMIT","correlationId":"019db2b6-c (truncated...)
▶ video
22 Apr 11:00
Screen
Firefox
/ SevenShores\Hubspot\Exceptions\BadRequest: Client
jiminny.sentry.io/issues/7007366572/events/38fa421c52fc432b92bfc347df23b251/
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
▶ video
22 Apr 10:58
Screen
Firefox
/ SevenShores\Hubspot\Exceptions\BadRequest: Client
jiminny.sentry.io/issues/7007366572/?project=82419&referrer=slack
⏱ Timeline
...Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your
secondly
limit.","erro
… (click to expand)
✕
17:17 · Firefox
No screenshot or video available
FULL TEXT
=
⏱ Open in Timetable
METADATA
Time
17:17
Date
2026-05-07...
|
17494
|
NULL
|
NULL
|
NULL
|
|
14097
|
628
|
27
|
2026-05-09T16:59:03.143934+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-09/1778 /Users/lukas/.screenpipe/data/data/2026-05-09/1778345943143_m2.jpg...
|
Code
|
Implement Authentik inte… — finance [SSH: nas]
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 12 pending changes
12
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: finance [SSH: nas]
Explorer Section: finance [SSH: nas]
FINANCE [SSH: NAS]
auth
dsk-uploader
finance-hub
backend
frontend
mcp
scripts
.env
.env.example
M
.gitignore
M
docker-compose.yml
M
README.md
sms_export.json
payments-logger
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
docker-compose.yml, Editor Group 1
.env, Editor Group 1
.env.example, preview, Editor Group 1
report(1).csv, Editor Group 1
report(2).csv, Editor Group 1
sms_export.json, Editor Group 1
# ── Database ───────────────────────────────────────────────────────────────────
[ENV_SECRET]
# ── Notifier service ──────────────────────────────────────────────────────────
NOTIFIER_URL=[URL_WITH_CREDENTIALS]
# Secret key — generate with: openssl rand -base64 36
[ENV_SECRET]
# Public URL authentik is reachable at (used for redirects and the outpost)
AUTHENTIK_HOST=[URL_WITH_CREDENTIALS] version 2>/dev/null && node --version
Copy code to clipboard
OUT
Exit code 127
Bash
Verify public path configuration
IN
# Check if the upload endpoints are really public
grep -n 'PUBLIC_PATHS\|upload\|ingest' /volume2/docker/finance/finance-hub/backend/src/auth.js
Copy code to clipboard
OUT
<tool_use_error>Cancelled: parallel tool call Bash(npm view @modelcontextprotocol/sdk versi…) errored</tool_use_error>
Bash
Check node version and MCP SDK latest
IN
node --version 2>/dev/null && python3 -c "import urllib.request; import json; d=json.loads(urllib.request.urlopen('[URL_WITH_CREDENTIALS] "^1.29.0"
},
"engines": {
"node": ">=18"
}
}
Write
server.js
server.js
236 lines
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
const BASE_URL = (process.env.FINANCE_HUB_URL || '[URL_WITH_CREDENTIALS] '^1.29.0'}
Thinking
Thinking
Good. Now let me test the MCP server can at least start and call the health check:
Bash
Smoke test MCP server startup
IN
FINANCE_HUB_URL=http://localhost:3001 [ENV_SECRET] \
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | timeout 5 node server.js 2>/dev/null | head -3 || echo "NODE test (expected timeout on stdin wait)"
Copy code to clipboard
OUT
{"result":{"tools":[{"name":"list_transactions","description":"List finance transactions with optional filters. Returns paginated results including total amount for the filtered set.","inputSchema":{"type":"object","properties":{"page":{"type":"number","description":"Page number (default 1)"},"limit":{"type":"number","description":"Results per page, max 200 (default 50)"},"dateFrom":{"type":"string","description":"Start date YYYY-MM-DD (inclusive)"},"dateTo":{"type":"string","description":"End date YYYY-MM-DD (inclusive)"},"tag":{"type":"string","description":"Filter by tag name"},"recipient":{"type":"string","description":"Substring match on payee name"},"type":{"type":"string","description":"Transaction type: POS | ATM | WALLET"},"source":{"type":"string","description":"Import source: INGEST | UPLOAD"},"search":{"type":"string","description":"Full-text search across rawMessage and recipient"},"hideBalanceAlerts":{"type":"boolean","description":"Exclude balance-notification SMS (default false)"},"sortBy":{"type":"string","description":"Sort field: date | amount | recipient | createdAt"},"sortDir":{"type":"string","description":"asc or desc (default desc)"}}}},{"name":"spending_by_tag","description":"Aggregate spending totals by tag for a date range. Fetches up to 200 transactions and groups them. Good for monthly summaries.","inputSchema":{"type":"object","properties":{"dateFrom":{"type":"string","description":"Start date YYYY-MM-DD"},"dateTo":{"type":"string","description":"End date YYYY-MM-DD"}}}},{"name":"get_transaction","description":"Get a single transaction by its numeric ID.","inputSchema":{"type":"object","required":["id"],"properties":{"id":{"type":"number","description":"Transaction import ID"}}}},{"name":"list_tags","description":"List all available tags with their colors.","inputSchema":{"type":"object","properties":{}}},{"name":"add_tag","description":"Add a tag to a transaction. Creates the tag globally if it does not exist.","inputSchema":{"type":"object","required":["id","name"],"properties":{"id":{"type":"number","description":"Transaction ID"},"name":{"type":"string","description":"Tag name (e.g. \"Groceries\")"},"color":{"type":"string","description":"Hex color (e.g. \"#22c55e\"). Optional — auto-assigned if omitted."}}}},{"name":"remove_tag","description":"Remove a tag from a transaction.","inputSchema":{"type":"object","required":["id","tagId"],"properties":{"id":{"type":"number","description":"Transaction ID"},"tagId":{"type":"number","description":"Tag ID (from list_tags or the transaction object)"}}}},{"name":"health_check","description":"Check Finance Hub backend connectivity and database status.","inputSchema":{"type":"object","properties":{}}}]},"jsonrpc":"2.0","id":1}...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Explorer (⇧⌘E)","depth":19,"bounds":{"left":0.0,"top":0.047885075,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":true},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.057462092,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Search (⇧⌘F)","depth":19,"bounds":{"left":0.0,"top":0.08619314,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.09577015,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Source Control (⌃⇧G) - 12 pending changes","depth":19,"bounds":{"left":0.0,"top":0.1245012,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.13407822,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"12","depth":22,"bounds":{"left":0.00831117,"top":0.1452514,"width":0.003656915,"height":0.008778931},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.008643617,"top":0.14604948,"width":0.0016622341,"height":0.007980846}},{"char_start":1,"char_count":1,"bounds":{"left":0.009973404,"top":0.14604948,"width":0.0023271276,"height":0.007980846}}],"role_description":"text"},{"role":"AXRadioButton","text":"Run and Debug (⇧⌘D)","depth":19,"bounds":{"left":0.0,"top":0.16280925,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.17238627,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Remote Explorer","depth":19,"bounds":{"left":0.0,"top":0.20111732,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.21069433,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Extensions (⇧⌘X) - 2 require update","depth":19,"bounds":{"left":0.0,"top":0.23942538,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":22,"bounds":{"left":0.0039893617,"top":0.2490024,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":22,"bounds":{"left":0.009640957,"top":0.2601756,"width":0.0019946808,"height":0.008778931},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Claude Code","depth":19,"bounds":{"left":0.0,"top":0.27773345,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Containers","depth":19,"bounds":{"left":0.0,"top":0.3160415,"width":0.015957447,"height":0.03830806},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"EXPLORER","depth":17,"bounds":{"left":0.022606382,"top":0.047885075,"width":0.018949468,"height":0.02793296},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"EXPLORER","depth":18,"bounds":{"left":0.022606382,"top":0.056664005,"width":0.018949468,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.022606382,"top":0.056664005,"width":0.0023271276,"height":0.0103751}},{"char_start":1,"char_count":7,"bounds":{"left":0.024933511,"top":0.056664005,"width":0.01662234,"height":0.0103751}}],"role_description":"text"},{"role":"AXButton","text":"Explorer Section: finance [SSH: nas]","depth":21,"bounds":{"left":0.015957447,"top":0.07581804,"width":0.09940159,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXStaticText","text":"","depth":23,"bounds":{"left":0.01662234,"top":0.07821229,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"Explorer Section: finance [SSH: nas]","depth":22,"bounds":{"left":0.022606382,"top":0.07581804,"width":0.039228722,"height":0.017557861},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"FINANCE [SSH: NAS]","depth":23,"bounds":{"left":0.022606382,"top":0.079010375,"width":0.039228722,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.022606382,"top":0.07980846,"width":0.0023271276,"height":0.0103751}},{"char_start":1,"char_count":17,"bounds":{"left":0.024933511,"top":0.07980846,"width":0.036901597,"height":0.0103751}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"bounds":{"left":0.019614361,"top":0.09577015,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"auth","depth":27,"bounds":{"left":0.025930852,"top":0.09577015,"width":0.008976064,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.025930852,"top":0.096568234,"width":0.0023271276,"height":0.011971269}},{"char_start":1,"char_count":3,"bounds":{"left":0.02825798,"top":0.096568234,"width":0.0066489363,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"bounds":{"left":0.019614361,"top":0.11332801,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"dsk-uploader","depth":27,"bounds":{"left":0.025930852,"top":0.11332801,"width":0.026928192,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.025930852,"top":0.11412609,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":11,"bounds":{"left":0.028590426,"top":0.11412609,"width":0.024268618,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"bounds":{"left":0.019614361,"top":0.13088587,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"finance-hub","depth":27,"bounds":{"left":0.025930852,"top":0.13088587,"width":0.024268618,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.025930852,"top":0.13168396,"width":0.0016622341,"height":0.011971269}},{"char_start":1,"char_count":10,"bounds":{"left":0.027593086,"top":0.13168396,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.10605053,"top":0.13168396,"width":0.004654255,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"bounds":{"left":0.022273935,"top":0.14844373,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":27,"bounds":{"left":0.028590426,"top":0.14844373,"width":0.017287234,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.028590426,"top":0.14924182,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":6,"bounds":{"left":0.03125,"top":0.14924182,"width":0.01462766,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.10605053,"top":0.14924182,"width":0.004654255,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"bounds":{"left":0.022273935,"top":0.1660016,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"frontend","depth":27,"bounds":{"left":0.028590426,"top":0.1660016,"width":0.017287234,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.028590426,"top":0.16679968,"width":0.0016622341,"height":0.011971269}},{"char_start":1,"char_count":7,"bounds":{"left":0.03025266,"top":0.16679968,"width":0.015625,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"bounds":{"left":0.022273935,"top":0.18355946,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"mcp","depth":27,"bounds":{"left":0.028590426,"top":0.18355946,"width":0.008643617,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.10605053,"top":0.18435754,"width":0.004654255,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"bounds":{"left":0.022273935,"top":0.20111732,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"scripts","depth":27,"bounds":{"left":0.028590426,"top":0.20111732,"width":0.013630319,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.028590426,"top":0.2019154,"width":0.0023271276,"height":0.011971269}},{"char_start":1,"char_count":6,"bounds":{"left":0.030917553,"top":0.2019154,"width":0.011303191,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.021276595,"top":0.21707901,"width":0.0063164895,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env","depth":27,"bounds":{"left":0.028590426,"top":0.21867518,"width":0.00831117,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.028590426,"top":0.21947326,"width":0.0013297872,"height":0.011971269}},{"char_start":1,"char_count":3,"bounds":{"left":0.029920213,"top":0.21947326,"width":0.006981383,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.021276595,"top":0.23463687,"width":0.0063164895,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.example","depth":27,"bounds":{"left":0.028590426,"top":0.23623304,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.028590426,"top":0.23703113,"width":0.0013297872,"height":0.011971269}},{"char_start":1,"char_count":11,"bounds":{"left":0.029920213,"top":0.23703113,"width":0.024933511,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"M","depth":27,"bounds":{"left":0.10638298,"top":0.23703113,"width":0.003656915,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.021276595,"top":0.25219473,"width":0.0063164895,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".gitignore","depth":27,"bounds":{"left":0.028590426,"top":0.25379092,"width":0.018949468,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.028590426,"top":0.254589,"width":0.0013297872,"height":0.011971269}},{"char_start":1,"char_count":9,"bounds":{"left":0.029920213,"top":0.254589,"width":0.017952127,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"M","depth":27,"bounds":{"left":0.10638298,"top":0.254589,"width":0.003656915,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.021276595,"top":0.2697526,"width":0.0063164895,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"docker-compose.yml","depth":27,"bounds":{"left":0.028590426,"top":0.27134877,"width":0.042220745,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.028590426,"top":0.27214685,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":17,"bounds":{"left":0.03125,"top":0.27214685,"width":0.03956117,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"M","depth":27,"bounds":{"left":0.10638298,"top":0.27214685,"width":0.003656915,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.021276595,"top":0.28731045,"width":0.0063164895,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"README.md","depth":27,"bounds":{"left":0.028590426,"top":0.28890663,"width":0.025265958,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.021276595,"top":0.3048683,"width":0.0063164895,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"sms_export.json","depth":27,"bounds":{"left":0.028590426,"top":0.3064645,"width":0.032912236,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.028590426,"top":0.30726257,"width":0.0023271276,"height":0.011971269}},{"char_start":1,"char_count":14,"bounds":{"left":0.030917553,"top":0.30726257,"width":0.030917553,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":26,"bounds":{"left":0.019614361,"top":0.32402235,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"payments-logger","depth":27,"bounds":{"left":0.025930852,"top":0.32402235,"width":0.034574468,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.025930852,"top":0.32482043,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":14,"bounds":{"left":0.028590426,"top":0.32482043,"width":0.031914894,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Outline Section","depth":21,"bounds":{"left":0.015957447,"top":0.9473264,"width":0.09940159,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"bounds":{"left":0.01662234,"top":0.9497207,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"OUTLINE","depth":22,"bounds":{"left":0.022606382,"top":0.9473264,"width":0.01662234,"height":0.017557861},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"OUTLINE","depth":23,"bounds":{"left":0.022606382,"top":0.95131683,"width":0.01662234,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.022606382,"top":0.95131683,"width":0.0029920214,"height":0.0103751}},{"char_start":1,"char_count":6,"bounds":{"left":0.025598405,"top":0.95131683,"width":0.013630319,"height":0.0103751}}],"role_description":"text"},{"role":"AXButton","text":"Timeline Section","depth":21,"bounds":{"left":0.015957447,"top":0.9648843,"width":0.09940159,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"bounds":{"left":0.01662234,"top":0.96727854,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"TIMELINE","depth":22,"bounds":{"left":0.022606382,"top":0.9648843,"width":0.01761968,"height":0.017557861},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"TIMELINE","depth":23,"bounds":{"left":0.022606382,"top":0.9688747,"width":0.01761968,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.022606382,"top":0.9688747,"width":0.0026595744,"height":0.0103751}},{"char_start":1,"char_count":7,"bounds":{"left":0.025265958,"top":0.9688747,"width":0.015292553,"height":0.0103751}}],"role_description":"text"},{"role":"AXRadioButton","text":"docker-compose.yml, Editor Group 1","depth":28,"bounds":{"left":0.11569149,"top":0.047885075,"width":0.0674867,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":".env, Editor Group 1","depth":28,"bounds":{"left":0.18317819,"top":0.047885075,"width":0.039893616,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXRadioButton","text":".env.example, preview, Editor Group 1","depth":28,"bounds":{"left":0.22307181,"top":0.047885075,"width":0.05219415,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"report(1).csv, Editor Group 1","depth":28,"bounds":{"left":0.27526596,"top":0.047885075,"width":0.045877658,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"report(2).csv, Editor Group 1","depth":28,"bounds":{"left":0.32114363,"top":0.047885075,"width":0.04654255,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"sms_export.json, Editor Group 1","depth":28,"bounds":{"left":0.36768618,"top":0.047885075,"width":0.053523935,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":29,"bounds":{"left":0.14527926,"top":0.07821229,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"# ── Database ───────────────────────────────────────────────────────────────────\nDB_PASSWORD=payments_secret\n\n# ── Notifier service ──────────────────────────────────────────────────────────\nNOTIFIER_URL=https://notifier.lakylak.xyz\nNOTIFIER_CHANNEL=viber\nNOTIFY_DEFAULT_PHONE=+359000000000\n\n# ── Timezone ─────────────────────────────────────────────────────────────────\nTZ=Europe/Sofia\n\n# ── API key auth — for MCP server, scripts, iOS shortcuts on protected endpoints ──\n# Generate with: openssl rand -hex 32\nAPI_KEY=94bb2917b9a1b9dcbaec49e7009c2a6b222fc310d32f530aa07e411e8d4a058c\n\n# ── Dev only — set false and configure NPM+Authentik for production ──────────\n# DEV_BYPASS_AUTH=true","depth":28,"bounds":{"left":0.13763298,"top":0.4094174,"width":0.2017952,"height":0.014365523},"on_screen":true,"value":"# ── Database ───────────────────────────────────────────────────────────────────\nDB_PASSWORD=payments_secret\n\n# ── Notifier service ──────────────────────────────────────────────────────────\nNOTIFIER_URL=https://notifier.lakylak.xyz\nNOTIFIER_CHANNEL=viber\nNOTIFY_DEFAULT_PHONE=+359000000000\n\n# ── Timezone ─────────────────────────────────────────────────────────────────\nTZ=Europe/Sofia\n\n# ── API key auth — for MCP server, scripts, iOS shortcuts on protected endpoints ──\n# Generate with: openssl rand -hex 32\nAPI_KEY=94bb2917b9a1b9dcbaec49e7009c2a6b222fc310d32f530aa07e411e8d4a058c\n\n# ── Dev only — set false and configure NPM+Authentik for production ──────────\n# DEV_BYPASS_AUTH=true","role_description":"editor","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"# ── Database ───────────────────────────────────────────────────────────────────\nDB_PASSWORD=payments_secret\n\n# ── Notifier service ──────────────────────────────────────────────────────────\nNOTIFIER_URL=https://notifier.lakylak.xyz\nNOTIFIER_CHANNEL=viber\nNOTIFY_DEFAULT_PHONE=+359000000000\n\n# ── Timezone ─────────────────────────────────────────────────────────────────\nTZ=Europe/Sofia\n\n# ── API key auth — for MCP server, scripts, iOS shortcuts on protected endpoints ──\n# Generate with: openssl rand -hex 32\nAPI_KEY=94bb2917b9a1b9dcbaec49e7009c2a6b222fc310d32f530aa07e411e8d4a058c\n\n# ── Dev only — set false and configure NPM+Authentik for production ──────────\n# DEV_BYPASS_AUTH=true","depth":29,"bounds":{"left":0.13763298,"top":0.42298484,"width":0.2017952,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Design new payment-logge…, Editor Group 2","depth":28,"bounds":{"left":0.5578458,"top":0.047885075,"width":0.07912234,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Implement Authentik inte…, Editor Group 2","depth":28,"bounds":{"left":0.63663566,"top":0.047885075,"width":0.07446808,"height":0.02793296},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXButton","text":"remote SSH: nas","depth":16,"bounds":{"left":0.0006648936,"top":0.98244214,"width":0.028590426,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.0033244682,"top":0.9848364,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SSH: nas","depth":17,"bounds":{"left":0.008643617,"top":0.9856345,"width":0.017952127,"height":0.011173184},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.008643617,"top":0.9856345,"width":0.0013297872,"height":0.011173184}},{"char_start":1,"char_count":7,"bounds":{"left":0.009973404,"top":0.9856345,"width":0.01462766,"height":0.011173184}}],"role_description":"text"},{"role":"AXButton","text":"finance-hub (Git) - main*, Checkout Branch/Tag...","depth":16,"bounds":{"left":0.030917553,"top":0.98244214,"width":0.019281914,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.031914894,"top":0.9848364,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"main*","depth":17,"bounds":{"left":0.03723404,"top":0.9856345,"width":0.011968086,"height":0.011173184},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.03723404,"top":0.9856345,"width":0.0013297872,"height":0.011173184}},{"char_start":1,"char_count":4,"bounds":{"left":0.03856383,"top":0.9856345,"width":0.008976064,"height":0.011173184}}],"role_description":"text"},{"role":"AXButton","text":"finance-hub (Git) - Synchronize Changes","depth":16,"bounds":{"left":0.050199468,"top":0.98244214,"width":0.00731383,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"No Problems","depth":16,"bounds":{"left":0.06017287,"top":0.98244214,"width":0.022606382,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.061835106,"top":0.9848364,"width":0.005319149,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"bounds":{"left":0.06715426,"top":0.9856345,"width":0.004986702,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.07180851,"top":0.9848364,"width":0.0056515955,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"bounds":{"left":0.07712766,"top":0.9856345,"width":0.0039893617,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"No Ports Forwarded","depth":16,"bounds":{"left":0.08444149,"top":0.98244214,"width":0.012632979,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.08610372,"top":0.9848364,"width":0.0056515955,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":17,"bounds":{"left":0.09142287,"top":0.9856345,"width":0.0039893617,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Notifications","depth":16,"bounds":{"left":0.9886968,"top":0.98244214,"width":0.010638298,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sign In","depth":16,"bounds":{"left":0.9650931,"top":0.98244214,"width":0.022606382,"height":0.01755786},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":17,"bounds":{"left":0.96675533,"top":0.9848364,"width":0.0056515955,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Sign In","depth":17,"bounds":{"left":0.97207445,"top":0.9856345,"width":0.013962766,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Info: Setting up SSH Host nas: Setting up SSH tunnel","depth":12,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Implement Authentik integration with security review","depth":19,"bounds":{"left":0.55984044,"top":0.08060654,"width":0.099734046,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Session history","depth":19,"bounds":{"left":0.9780585,"top":0.08060654,"width":0.00930851,"height":0.022346368},"on_screen":true,"help_text":"Session history","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New session","depth":19,"bounds":{"left":0.9886968,"top":0.08060654,"width":0.00930851,"height":0.022346368},"on_screen":true,"help_text":"New session","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Message actions","depth":24,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"README.md","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"README.md","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lets work on authentication. I want to be able to connect using authentik. REveiw copy of the project in auth folder. Lets test it and establish security. There should be simple way to reauthorize automatically for api and also there should be claude chat compatible mcp with autorization that si stable. Think about it test it and then provide suggestions","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Let me explore the project structure first, particularly the auth folder.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"List finance directory structure","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"find /volume2/docker/finance -type f | head -80 && echo \"---\" && ls /volume2/docker/finance/","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/volume2/docker/finance/payments-logger/.env\n/volume2/docker/finance/payments-logger/.env.example\n/volume2/docker/finance/payments-logger/.gitignore\n/volume2/docker/finance/payments-logger/API.md\n/volume2/docker/finance/payments-logger/README.md\n/volume2/docker/finance/payments-logger/docker-compose.yml\n/volume2/docker/finance/auth/.env\n/volume2/docker/finance/auth/.env.example\n/volume2/docker/finance/auth/README.md\n/volume2/docker/finance/auth/docker-compose.yml\n/volume2/docker/finance/dsk-uploader/.gitignore\n/volume2/docker/finance/dsk-uploader/README.md\n/volume2/docker/finance/dsk-uploader/USAGE.md\n/volume2/docker/finance/dsk-uploader/app.py\n/volume2/docker/finance/dsk-uploader/app_flask.py\n/volume2/docker/finance/dsk-uploader/dskuploader.py\n/volume2/docker/finance/dsk-uploader/requirements.txt\n/volume2/docker/finance/dsk-uploader/.env\n/volume2/docker/finance/dsk-uploader/.env.example\n/volume2/docker/finance/dsk-uploader/Dockerfile\n/volume2/docker/finance/dsk-uploader/docker-compose.yml\n/volume2/docker/finance/dsk-uploader/.dockerignore\n/volume2/docker/finance/finance-hub/.env.example\n/volume2/docker/finance/finance-hub/.gitignore\n/volume2/docker/finance/finance-hub/.env\n/volume2/docker/finance/finance-hub/docker-compose.yml\n/volume2/docker/finance/finance-hub/sms_export.json\n/volume2/docker/finance/finance-hub/README.md\n/volume2/docker/finance/payments-logger/.claude/settings.local.json\n/volume2/docker/finance/payments-logger/backend/.dockerignore\n/volume2/docker/finance/payments-logger/backend/Dockerfile\n/volume2/docker/finance/payments-logger/backend/package.json\n/volume2/docker/finance/finance-hub/backend/package.json\n/volume2/docker/finance/finance-hub/backend/Dockerfile\n/volume2/docker/finance/finance-hub/frontend/package.json\n/volume2/docker/finance/finance-hub/frontend/Dockerfile\n/volume2/docker/finance/finance-hub/frontend/vite.config.js\n/volume2/docker/finance/finance-hub/frontend/tailwind.config.js\n/volume2/docker/finance/finance-hub/frontend/postcss.config.js\n/volume2/docker/finance/finance-hub/frontend/index.html\n/volume2/docker/finance/finance-hub/scripts/reimport_sms.sh\n/volume2/docker/finance/finance-hub/.git/description\n/volume2/docker/finance/finance-hub/.git/HEAD\n/volume2/docker/finance/finance-hub/.git/COMMIT_EDITMSG\n/volume2/docker/finance/finance-hub/.git/index\n/volume2/docker/finance/finance-hub/.git/hooks/post-update.sample\n/volume2/docker/finance/finance-hub/.git/hooks/pre-applypatch.sample\n/volume2/docker/finance/finance-hub/.git/hooks/pre-commit.sample\n/volume2/docker/finance/finance-hub/.git/hooks/applypatch-msg.sample\n/volume2/docker/finance/finance-hub/.git/hooks/pre-rebase.sample\n/volume2/docker/finance/finance-hub/.git/hooks/pre-merge-commit.sample\n/volume2/docker/finance/finance-hub/.git/hooks/update.sample\n/volume2/docker/finance/finance-hub/.git/hooks/pre-push.sample\n/volume2/docker/finance/finance-hub/.git/hooks/push-to-checkout.sample\n/volume2/docker/finance/finance-hub/.git/hooks/prepare-commit-msg.sample\n/volume2/docker/finance/finance-hub/.git/hooks/commit-msg.sample\n/volume2/docker/finance/finance-hub/.git/hooks/pre-receive.sample\n/volume2/docker/finance/finance-hub/.git/hooks/fsmonitor-watchman.sample\n/volume2/docker/finance/finance-hub/.git/logs/HEAD\n/volume2/docker/finance/payments-logger/frontend/.dockerignore\n/volume2/docker/finance/payments-logger/frontend/Dockerfile\n/volume2/docker/finance/payments-logger/frontend/index.html\n/volume2/docker/finance/payments-logger/frontend/package.json\n/volume2/docker/finance/payments-logger/frontend/postcss.config.js\n/volume2/docker/finance/payments-logger/frontend/tailwind.config.js\n/volume2/docker/finance/payments-logger/frontend/vite.config.js\n/volume2/docker/finance/finance-hub/.git/config\n/volume2/docker/finance/payments-logger/auth/.env\n/volume2/docker/finance/payments-logger/auth/.env.example\n/volume2/docker/finance/payments-logger/auth/README.md\n/volume2/docker/finance/auth/auth/.env\n/volume2/docker/finance/auth/auth/.env.example\n/volume2/docker/finance/auth/auth/docker-compose.yml\n/volume2/docker/finance/auth/auth/README.md\n/volume2/docker/finance/dsk-uploader/.git/description\n/volume2/docker/finance/dsk-uploader/.git/packed-refs\n/volume2/docker/finance/dsk-uploader/.git/HEAD\n/volume2/docker/finance/dsk-uploader/.git/FETCH_HEAD\n/volume2/docker/finance/dsk-uploader/.git/index\n/volume2/docker/finance/dsk-uploader/.git/COMMIT_EDITMSG\n---\nauth\ndsk-uploader\nfinance-hub\npayments-logger","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"README.md","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"README.md","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Now let me dive into the auth folder and the existing backend auth setup.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"README.md","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"README.md","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"docker-compose.yml","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"docker-compose.yml","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read auth env examples and nested auth config","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"cat /volume2/docker/finance/auth/.env.example && echo \"===\" && cat /volume2/docker/finance/auth/auth/.env.example 2>/dev/null && echo \"===\" && cat /volume2/docker/finance/auth/auth/docker-compose.yml 2>/dev/null","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"# Copy this file to .env and fill in all values before starting\n\n# ── Image versions ───────────────────────────────────────────────────────────\n# Pinned tag applied to server, worker, AND outpost — they must always match.\n# To upgrade: change this one value and run: docker compose pull && docker compose up -d\n# Check releases: https://github.com/goauthentik/authentik/releases\nAUTHENTIK_TAG=2026.2.1\n\n# ── Database ──────────────────────────────────────────────────────────────────\n# Strong password for the PostgreSQL authentik database\n# Used by the postgres container and all authentik services\nPG_PASS=\n\n# ── Authentik core ────────────────────────────────────────────────────────────\n# Cryptographic secret — NEVER share or commit this\n# Generate with: openssl rand -base64 36\nAUTHENTIK_SECRET_KEY=\n\n# Full public HTTPS URL where authentik is reachable\n# Must match the domain you configure in NPM\n# Used for OAuth redirects and by the outpost to contact the server\nAUTHENTIK_HOST=https://auth.lakylak.xyz\n\n# ── Bootstrap (first-start only) ─────────────────────────────────────────────\n# If set, authentik creates this admin account automatically on first boot.\n# You can leave these empty and use the /if/flow/initial-setup/ URL instead.\nAUTHENTIK_BOOTSTRAP_EMAIL=you@example.com\nAUTHENTIK_BOOTSTRAP_PASSWORD=\n\n# ── Outpost ───────────────────────────────────────────────────────────────────\n# Token for the proxy outpost container.\n# Leave as \"placeholder\" on first deploy — the outpost will fail to connect\n# until you create it in the UI and paste the real token here (see README step 6).\nAUTHENTIK_OUTPOST_TOKEN=placeholder\n===\n# Copy this file to .env and fill in all values before starting\n\n# ── Image versions ───────────────────────────────────────────────────────────\n# Pinned tag applied to server, worker, AND outpost — they must always match.\n# To upgrade: change this one value and run: docker compose pull && docker compose up -d\n# Check releases: https://github.com/goauthentik/authentik/releases\nAUTHENTIK_TAG=2026.2.1\n\n# ── Database ──────────────────────────────────────────────────────────────────\n# Strong password for the PostgreSQL authentik database\n# Used by the postgres container and all authentik services\nPG_PASS=\n\n# ── Authentik core ────────────────────────────────────────────────────────────\n# Cryptographic secret — NEVER share or commit this\n# Generate with: openssl rand -base64 36\nAUTHENTIK_SECRET_KEY=\n\n# Full public HTTPS URL where authentik is reachable\n# Must match the domain you configure in NPM\n# Used for OAuth redirects and by the outpost to contact the server\nAUTHENTIK_HOST=https://auth.lakylak.xyz\n\n# ── Bootstrap (first-start only) ─────────────────────────────────────────────\n# If set, authentik creates this admin account automatically on first boot.\n# You can leave these empty and use the /if/flow/initial-setup/ URL instead.\nAUTHENTIK_BOOTSTRAP_EMAIL=you@example.com\nAUTHENTIK_BOOTSTRAP_PASSWORD=\n\n# ── Outpost ───────────────────────────────────────────────────────────────────\n# Token for the proxy outpost container.\n# Leave as \"placeholder\" on first deploy — the outpost will fail to connect\n# until you create it in the UI and paste the real token here (see README step 6).\nAUTHENTIK_OUTPOST_TOKEN=placeholder\n===\nservices:\n\n postgresql:\n image: docker.io/library/postgres:16-alpine\n container_name: Authentik-DB\n hostname: authentik-db\n restart: unless-stopped\n security_opt:\n - no-new-privileges:true\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -d ${POSTGRES_DB:-authentik} -U ${POSTGRES_USER:-authentik}\"]\n interval: 5s\n timeout: 5s\n retries: 5\n environment:\n POSTGRES_PASSWORD: ${PG_PASS}\n POSTGRES_USER: authentik\n POSTGRES_DB: authentik\n volumes:\n - /volume2/docker/auth/db:/var/lib/postgresql/data\n networks:\n - authentik_internal\n\n redis:\n image: docker.io/library/redis:alpine\n container_name: Authentik-REDIS\n hostname: authentik-redis\n restart: unless-stopped\n security_opt:\n - no-new-privileges:true\n healthcheck:\n test: [\"CMD-SHELL\", \"redis-cli ping | grep PONG\"]\n interval: 5s\n timeout: 3s\n retries: 5\n networks:\n - authentik_internal\n\n server:\n image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG}\n container_name: Authentik-Server\n hostname: authentik-server\n restart: unless-stopped\n command: server\n environment:\n AUTHENTIK_REDIS__HOST: authentik-redis\n AUTHENTIK_POSTGRESQL__HOST: authentik-db\n AUTHENTIK_POSTGRESQL__USER: authentik\n AUTHENTIK_POSTGRESQL__NAME: authentik\n AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}\n AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}\n AUTHENTIK_ERROR_REPORTING__ENABLED: \"false\"\n AUTHENTIK_HOST: ${AUTHENTIK_HOST}\n AUTHENTIK_BOOTSTRAP_EMAIL: ${AUTHENTIK_BOOTSTRAP_EMAIL}\n AUTHENTIK_BOOTSTRAP_PASSWORD: ${AUTHENTIK_BOOTSTRAP_PASSWORD}\n ports:\n - \"9100:9000\"\n volumes:\n - /volume2/docker/auth/media:/media\n - /volume2/docker/auth/templates:/templates\n depends_on:\n postgresql:\n condition: service_healthy\n redis:\n condition: service_healthy\n networks:\n - authentik_internal\n - proxy\n\n worker:\n image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG}\n container_name: Authentik-Worker\n hostname: authentik-worker\n restart: unless-stopped\n command: worker\n user: root\n environment:\n AUTHENTIK_REDIS__HOST: authentik-redis\n AUTHENTIK_POSTGRESQL__HOST: authentik-db\n AUTHENTIK_POSTGRESQL__USER: authentik\n AUTHENTIK_POSTGRESQL__NAME: authentik\n AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}\n AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n - /volume2/docker/auth/media:/media\n - /volume2/docker/auth/certs:/certs\n - /volume2/docker/auth/templates:/templates\n depends_on:\n postgresql:\n condition: service_healthy\n redis:\n condition: service_healthy\n networks:\n - authentik_internal\n\n outpost:\n image: ghcr.io/goauthentik/proxy:${AUTHENTIK_TAG}\n container_name: Authentik-Outpost\n hostname: authentik-outpost\n restart: unless-stopped\n environment:\n AUTHENTIK_HOST: ${AUTHENTIK_HOST}\n AUTHENTIK_INSECURE: \"false\"\n AUTHENTIK_TOKEN: ${AUTHENTIK_OUTPOST_TOKEN}\n ports:\n - \"9101:9000\"\n depends_on:\n - server\n networks:\n - authentik_internal\n - proxy\n\nnetworks:\n authentik_internal:\n internal: true\n proxy:\n external: true","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Now let me look at the backend auth code and the current services.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"List backend and frontend source files","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"find /volume2/docker/finance/finance-hub/backend -type f | sort && echo \"===\" && find /volume2/docker/finance/finance-hub/frontend/src -type f | sort 2>/dev/null || find /volume2/docker/finance/finance-hub/frontend -type f | sort","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/volume2/docker/finance/finance-hub/backend/Dockerfile\n/volume2/docker/finance/finance-hub/backend/package.json\n/volume2/docker/finance/finance-hub/backend/prisma/migrations/20260508_init/migration.sql\n/volume2/docker/finance/finance-hub/backend/prisma/migrations/20260509_refactor/migration.sql\n/volume2/docker/finance/finance-hub/backend/prisma/migrations/migration_lock.toml\n/volume2/docker/finance/finance-hub/backend/prisma/schema.prisma\n/volume2/docker/finance/finance-hub/backend/src/auth.js\n/volume2/docker/finance/finance-hub/backend/src/csvParser.js\n/volume2/docker/finance/finance-hub/backend/src/index.js\n/volume2/docker/finance/finance-hub/backend/src/parser.js\n/volume2/docker/finance/finance-hub/backend/src/routes/payments.js\n/volume2/docker/finance/finance-hub/backend/src/routes/upload.js\n/volume2/docker/finance/finance-hub/backend/src/utils/linkTransaction.js\n/volume2/docker/finance/finance-hub/backend/src/utils/recipientTags.js\n===\n/volume2/docker/finance/finance-hub/frontend/src/App.jsx\n/volume2/docker/finance/finance-hub/frontend/src/components/FilterBar.jsx\n/volume2/docker/finance/finance-hub/frontend/src/components/PaymentCard.jsx\n/volume2/docker/finance/finance-hub/frontend/src/components/PaymentList.jsx\n/volume2/docker/finance/finance-hub/frontend/src/components/PaymentTable.jsx\n/volume2/docker/finance/finance-hub/frontend/src/components/SettingsPanel.jsx\n/volume2/docker/finance/finance-hub/frontend/src/components/UploadPanel.jsx\n/volume2/docker/finance/finance-hub/frontend/src/hooks/useSettings.js\n/volume2/docker/finance/finance-hub/frontend/src/index.css\n/volume2/docker/finance/finance-hub/frontend/src/main.jsx","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Check running Docker containers","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"docker ps --format \"table {{.Names}}\\t{{.Status}}\\t{{.Ports}}\" 2>/dev/null | head -30","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(Bash completed with no output)","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"auth.js","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"auth.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"index.js","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"index.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Check all Docker containers","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"docker ps -a --format \"table {{.Names}}\\t{{.Status}}\\t{{.Ports}}\" 2>/dev/null","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Exit code 1","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":".env","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":".env","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":".env","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":".env","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"docker-compose.yml","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"docker-compose.yml","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"App.jsx","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"App.jsx","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Check status of auth and finance-hub Docker services","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"cd /volume2/docker/finance/auth && docker compose ps 2>/dev/null || echo \"NOT RUNNING\"\necho \"===\"\ncd /volume2/docker/finance/finance-hub && docker compose ps 2>/dev/null || echo \"NOT RUNNING\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"NOT RUNNING\n===\nNOT RUNNING","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Let me check a few more details before forming recommendations.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"README.md","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"README.md","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Check payments-logger auth config","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"cat /volume2/docker/finance/payments-logger/auth/.env.example 2>/dev/null && echo \"===\" && cat /volume2/docker/finance/payments-logger/auth/.env 2>/dev/null","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"# Copy this file to .env and fill in all values before starting\n\n# ── Image versions ───────────────────────────────────────────────────────────\n# Pinned tag applied to server, worker, AND outpost — they must always match.\n# To upgrade: change this one value and run: docker compose pull && docker compose up -d\n# Check releases: https://github.com/goauthentik/authentik/releases\nAUTHENTIK_TAG=2026.2.1\n\n# ── Database ──────────────────────────────────────────────────────────────────\n# Strong password for the PostgreSQL authentik database\n# Used by the postgres container and all authentik services\nPG_PASS=\n\n# ── Authentik core ────────────────────────────────────────────────────────────\n# Cryptographic secret — NEVER share or commit this\n# Generate with: openssl rand -base64 36\nAUTHENTIK_SECRET_KEY=\n\n# Full public HTTPS URL where authentik is reachable\n# Must match the domain you configure in NPM\n# Used for OAuth redirects and by the outpost to contact the server\nAUTHENTIK_HOST=https://auth.lakylak.xyz\n\n# ── Bootstrap (first-start only) ─────────────────────────────────────────────\n# If set, authentik creates this admin account automatically on first boot.\n# You can leave these empty and use the /if/flow/initial-setup/ URL instead.\nAUTHENTIK_BOOTSTRAP_EMAIL=you@example.com\nAUTHENTIK_BOOTSTRAP_PASSWORD=\n\n# ── Outpost ───────────────────────────────────────────────────────────────────\n# Token for the proxy outpost container.\n# Leave as \"placeholder\" on first deploy — the outpost will fail to connect\n# until you create it in the UI and paste the real token here (see README step 6).\nAUTHENTIK_OUTPOST_TOKEN=placeholder\n===\n# Authentik image tag — applies to server, worker, AND outpost (must all match)\n# Check latest: https://github.com/goauthentik/authentik/releases\n# Example: 2024.12.3\nAUTHENTIK_TAG=2026.2.1\n\n# PostgreSQL password — used by both postgres and authentik server/worker\nPG_PASS=Og8L5@a1QA6u\n\n# Secret key — generate with: openssl rand -base64 36\nAUTHENTIK_SECRET_KEY=Uq6Mc2oI6zeWtoZcCy+Dw+IdNJ80yIMZk5ZgsP/tYmopFmhB/RHxbN7TNFZDNR1wrao=\n\n# Public URL authentik is reachable at (used for redirects and the outpost)\nAUTHENTIK_HOST=https://auth.lakylak.xyz\n\n# Bootstrap admin account created on first start (can be left empty to set via UI)\nAUTHENTIK_BOOTSTRAP_EMAIL=kovaliklukas@proton.me\nAUTHENTIK_BOOTSTRAP_PASSWORD=3bAc4Jcs\n\n# Outpost token — leave as placeholder until you create the outpost in the Authentik UI\n# See README.md step 6 for how to get this value\nAUTHENTIK_OUTPOST_TOKEN=H7Ppl2ps6h6e1mJ0CwXXoklQScbFJ1gT5WYXErtaRRQz5ukWfES78OXLVz6S","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Test if Authentik and finance backend are reachable","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"curl -s --max-time 3 http://localhost:9100/api/v3/core/applications/ 2>/dev/null | head -20 || echo \"Authentik not reachable on 9100\"\ncurl -s --max-time 3 http://localhost:3001/api/health 2>/dev/null | head -5 || echo \"Finance backend not reachable on 3001\"","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{\"detail\":\"Authentication credentials were not provided.\"}{\"status\":\"ok\",\"timestamp\":\"2026-05-09T16:02:54.094Z\",\"storage\":{\"type\":\"PostgreSQL\",\"host\":\"db\",\"database\":\"finance_hub\"}}","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Both Authentik (port 9100) and finance backend (port 3001) are running. Let me test the actual auth state and check CORS config.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Test backend auth bypass and Authentik health","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"# Test that backend accepts requests without auth (DEV_BYPASS_AUTH=true)\ncurl -s http://localhost:3001/api/payments?limit=1 | python3 -c \"import sys,json; d=json.load(sys.stdin); print('AUTH BYPASS ACTIVE - got', d.get('total','?'), 'records')\" 2>/dev/null || echo \"Error parsing\"\n\n# Test Authentik health\ncurl -s --max-time 3 http://localhost:9100/-/health/ready/ && echo \"Authentik ready\"","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AUTH BYPASS ACTIVE - got 235 records\nAuthentik ready","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"payments.js","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"payments.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Good picture. Let me now check the latest MCP SDK version before building.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Check MCP SDK latest version and node version","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"npm view @modelcontextprotocol/sdk version 2>/dev/null && node --version","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Exit code 127","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Verify public path configuration","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"# Check if the upload endpoints are really public\ngrep -n 'PUBLIC_PATHS\\|upload\\|ingest' /volume2/docker/finance/finance-hub/backend/src/auth.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"<tool_use_error>Cancelled: parallel tool call Bash(npm view @modelcontextprotocol/sdk versi…) errored</tool_use_error>","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Check node version and MCP SDK latest","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"node --version 2>/dev/null && python3 -c \"import urllib.request; import json; d=json.loads(urllib.request.urlopen('https://registry.npmjs.org/@modelcontextprotocol/sdk/latest').read()); print(d['version'])\" 2>/dev/null || echo \"no npm/network\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"v18.20.4\n1.29.0","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update Todos","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fix auth.js: add API key support + remove upload endpoints from PUBLIC_PATHS","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update index.js: add CORS origin restriction via env var","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update .env and .env.example with API_KEY variable","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Build MCP server (stdio, Claude desktop compatible)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Test: restart backend, verify API key auth and MCP connection","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Edit","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"auth.js","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"auth.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Added 18 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update Todos","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fix auth.js: add API key support + remove upload endpoints from PUBLIC_PATHS","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update index.js: add CORS origin restriction via env var","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update .env and .env.example with API_KEY variable","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Build MCP server (stdio, Claude desktop compatible)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Test: restart backend, verify API key auth and MCP connection","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Edit","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"index.js","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"index.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Added 3 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update Todos","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fix auth.js: add API key support + remove upload endpoints from PUBLIC_PATHS","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update index.js: add CORS origin restriction via env var","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update .env and .env.example with API_KEY variable","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Build MCP server (stdio, Claude desktop compatible)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Test: restart backend, verify API key auth and MCP connection","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Generate a secure API key","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"openssl rand -hex 32","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"94bb2917b9a1b9dcbaec49e7009c2a6b222fc310d32f530aa07e411e8d4a058c","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Edit","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":".env","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":".env","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Added 4 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Read","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":".env.example","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":".env.example","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Edit","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":".env.example","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":".env.example","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Added 12 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Also need to pass","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"API_KEY","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CORS_ORIGIN","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"through docker-compose:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Edit","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"docker-compose.yml","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"docker-compose.yml","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Added 3 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update Todos","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fix auth.js: add API key support + remove upload endpoints from PUBLIC_PATHS","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update index.js: add CORS origin restriction via env var","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Update .env and .env.example with API_KEY variable","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Build MCP server (stdio, Claude desktop compatible)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Test: restart backend, verify API key auth and MCP connection","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Now building the MCP server:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Create MCP directory","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"mkdir -p /volume2/docker/finance/finance-hub/mcp && ls /volume2/docker/finance/finance-hub/mcp","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(Bash completed with no output)","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"package.json","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"package.json","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"17 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{\n \"name\": \"finance-hub-mcp\",\n \"version\": \"1.0.0\",\n \"description\": \"MCP server for Finance Hub — exposes transaction tools to Claude\",\n \"main\": \"server.js\",\n \"type\": \"module\",\n \"scripts\": {\n \"start\": \"node server.js\"\n },\n \"dependencies\": {\n \"@modelcontextprotocol/sdk\": \"^1.29.0\"\n },\n \"engines\": {\n \"node\": \">=18\"\n }\n}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Write","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"server.js","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"server.js","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"236 lines","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"import { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js';\n\nconst BASE_URL = (process.env.FINANCE_HUB_URL || 'http://localhost:3001').replace(/\\/$/, '');\nconst API_KEY = process.env.FINANCE_HUB_API_KEY;\n\nif (!API_KEY) {\n process.stderr.write('Warning: FINANCE_HUB_API_KEY not set — requests will fail if DEV_BYPASS_AUTH is off\\n');\n}\n\n// ── HTTP helper ───────────────────────────────────────────────────────────────\n\nasync function api(path, opts = {}) {\n const headers = { 'Content-Type': 'application/json' };\n if (API_KEY) headers['Authorization'] = `Bearer ${API_KEY}`;\n\n const res = await fetch(`${BASE_URL}${path}`, {\n ...opts,\n headers: { ...headers, ...(opts.headers || {}) },\n });\n\n if (res.status === 204) return null;\n const body = await res.text();\n if (!res.ok) throw new Error(`HTTP ${res.status}: ${body}`);\n return body ? JSON.parse(body) : null;\n}\n\n// ── Tool definitions ──────────────────────────────────────────────────────────\n\nconst TOOLS = [\n {\n name: 'list_transactions',\n description:\n 'List finance transactions with optional filters. Returns paginated results including total amount for the filtered set.',\n inputSchema: {\n type: 'object',\n properties: {\n page: { type: 'number', description: 'Page number (default 1)' },\n limit: { type: 'number', description: 'Results per page, max 200 (default 50)' },\n dateFrom: { type: 'string', description: 'Start date YYYY-MM-DD (inclusive)' },\n dateTo: { type: 'string', description: 'End date YYYY-MM-DD (inclusive)' },\n tag: { type: 'string', description: 'Filter by tag name' },\n recipient: { type: 'string', description: 'Substring match on payee name' },\n type: { type: 'string', description: 'Transaction type: POS | ATM | WALLET' },\n source: { type: 'string', description: 'Import source: INGEST | UPLOAD' },\n search: { type: 'string', description: 'Full-text search across rawMessage and recipient' },\n hideBalanceAlerts: { type: 'boolean', description: 'Exclude balance-notification SMS (default false)' },\n sortBy: { type: 'string', description: 'Sort field: date | amount | recipient | createdAt' },\n sortDir: { type: 'string', description: 'asc or desc (default desc)' },\n },\n },\n },\n {\n name: 'spending_by_tag',\n description:\n 'Aggregate spending totals by tag for a date range. Fetches up to 200 transactions and groups them. Good for monthly summaries.',\n inputSchema: {\n type: 'object',\n properties: {\n dateFrom: { type: 'string', description: 'Start date YYYY-MM-DD' },\n dateTo: { type: 'string', description: 'End date YYYY-MM-DD' },\n },\n },\n },\n {\n name: 'get_transaction',\n description: 'Get a single transaction by its numeric ID.',\n inputSchema: {\n type: 'object',\n required: ['id'],\n properties: {\n id: { type: 'number', description: 'Transaction import ID' },\n },\n },\n },\n {\n name: 'list_tags',\n description: 'List all available tags with their colors.',\n inputSchema: { type: 'object', properties: {} },\n },\n {\n name: 'add_tag',\n description: 'Add a tag to a transaction. Creates the tag globally if it does not exist.',\n inputSchema: {\n type: 'object',\n required: ['id', 'name'],\n properties: {\n id: { type: 'number', description: 'Transaction ID' },\n name: { type: 'string', description: 'Tag name (e.g. \"Groceries\")' },\n color: { type: 'string', description: 'Hex color (e.g. \"#22c55e\"). Optional — auto-assigned if omitted.' },\n },\n },\n },\n {\n name: 'remove_tag',\n description: 'Remove a tag from a transaction.',\n inputSchema: {\n type: 'object',\n required: ['id', 'tagId'],\n properties: {\n id: { type: 'number', description: 'Transaction ID' },\n tagId: { type: 'number', description: 'Tag ID (from list_tags or the transaction object)' },\n },\n },\n },\n {\n name: 'health_check',\n description: 'Check Finance Hub backend connectivity and database status.',\n inputSchema: { type: 'object', properties: {} },\n },\n];\n\n// ── Server setup ──────────────────────────────────────────────────────────────\n\nconst server = new Server(\n { name: 'finance-hub', version: '1.0.0' },\n { capabilities: { tools: {} } },\n);\n\nserver.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));\n\nserver.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args = {} } = request.params;\n\n try {\n switch (name) {\n\n case 'list_transactions': {\n const params = new URLSearchParams();\n for (const [k, v] of Object.entries(args)) {\n if (v !== undefined && v !== null && v !== '') params.set(k, String(v));\n }\n const data = await api(`/api/payments?${params}`);\n return text(JSON.stringify({\n total: data.total,\n page: data.page,\n limit: data.limit,\n totalAmount: data.totalAmount,\n transactions: data.payments,\n }, null, 2));\n }\n\n case 'spending_by_tag': {\n const params = new URLSearchParams({ limit: '200', hideBalanceAlerts: 'true' });\n if (args.dateFrom) params.set('dateFrom', args.dateFrom);\n if (args.dateTo) params.set('dateTo', args.dateTo);\n\n const data = await api(`/api/payments?${params}`);\n\n const byTag = {};\n let untaggedTotal = 0, untaggedCount = 0;\n\n for (const tx of data.payments) {\n const amount = tx.amount ?? 0;\n if (!tx.tags?.length) {\n untaggedTotal += amount;\n untaggedCount++;\n } else {\n for (const tag of tx.tags) {\n if (!byTag[tag.name]) byTag[tag.name] = { total: 0, count: 0 };\n byTag[tag.name].total += amount;\n byTag[tag.name].count++;\n }\n }\n }\n\n const breakdown = Object.entries(byTag)\n .map(([tag, s]) => ({ tag, total: round2(s.total), count: s.count }))\n .sort((a, b) => b.total - a.total);\n\n if (untaggedCount) breakdown.push({ tag: '(untagged)', total: round2(untaggedTotal), count: untaggedCount });\n\n return text(JSON.stringify({\n period: { from: args.dateFrom ?? null, to: args.dateTo ?? null },\n totalTransactions: data.total,\n note: data.total > 200 ? `Only first 200 of ${data.total} transactions included — refine the date range for full accuracy` : undefined,\n breakdown,\n }, null, 2));\n }\n\n case 'get_transaction': {\n const data = await api(`/api/payments/${args.id}`);\n return text(JSON.stringify(data, null, 2));\n }\n\n case 'list_tags': {\n const data = await api('/api/payments/meta/tags');\n return text(JSON.stringify(data, null, 2));\n }\n\n case 'add_tag': {\n const data = await api(`/api/payments/${args.id}/tags`, {\n method: 'POST',\n body: JSON.stringify({ name: args.name, color: args.color }),\n });\n return text(JSON.stringify(data, null, 2));\n }\n\n case 'remove_tag': {\n await api(`/api/payments/${args.id}/tags/${args.tagId}`, { method: 'DELETE' });\n return text('Tag removed successfully');\n }\n\n case 'health_check': {\n const data = await api('/api/health');\n return text(JSON.stringify(data, null, 2));\n }\n\n default:\n throw new Error(`Unknown tool: ${name}`);\n }\n } catch (err) {\n return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };\n }\n});\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nfunction text(str) {\n return { content: [{ type: 'text', text: str }] };\n}\n\nfunction round2(n) {\n return Math.round(n * 100) / 100;\n}\n\n// ── Entry point ───────────────────────────────────────────────────────────────\n\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\nprocess.stderr.write(`Finance Hub MCP server connected to ${BASE_URL}\\n`);","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Now install the MCP SDK and test it:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Install MCP SDK dependencies","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"cd /volume2/docker/finance/finance-hub/mcp && node --version && npm install --save-exact 2>&1 | tail -5","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"v18.20.4\n/bin/bash: line 1: npm: command not found","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Locate npm","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"which npm || find /usr -name npm 2>/dev/null | head -5 || find /usr/local -name npm 2>/dev/null | head -5 || ls /root/.nvm/versions/node/ 2>/dev/null","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(Bash completed with no output)","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Check system info","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ls /proc/1/exe 2>/dev/null && cat /proc/version && uname -a","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Exit code 2","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Find npm via Docker","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"find /volume2 /opt /home -name npm -type f 2>/dev/null | head -5\ndocker run --rm node:18-alpine node --version 2>/dev/null | head -1","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/home/Adm1n/.nvm/versions/node/v24.14.1/lib/node_modules/corepack/shims/npm\n/home/Adm1n/.nvm/versions/node/v24.14.1/lib/node_modules/npm/bin/npm\n/home/Adm1n/.nvm/versions/node/v24.14.1/lib/node_modules/corepack/shims/nodewin/npm","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Install MCP SDK via located npm","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/home/Adm1n/.nvm/versions/node/v24.14.1/lib/node_modules/npm/bin/npm-cli.js install --save-exact 2>&1 | tail -5","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"28 packages are looking for funding\n run `npm fund` for details\n\nfound 0 vulnerabilities","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Verify MCP package installed","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ls && cat package.json | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d['dependencies'])\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"node_modules\npackage.json\npackage-lock.json\nserver.js\n{'@modelcontextprotocol/sdk': '^1.29.0'}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Thinking","depth":23,"on_screen":false,"role_description":"disclosure triangle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thinking","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Good. Now let me test the MCP server can at least start and call the health check:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bash","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Smoke test MCP server startup","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IN","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FINANCE_HUB_URL=http://localhost:3001 FINANCE_HUB_API_KEY=94bb2917b9a1b9dcbaec49e7009c2a6b222fc310d32f530aa07e411e8d4a058c \\\n echo '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}' | timeout 5 node server.js 2>/dev/null | head -3 || echo \"NODE test (expected timeout on stdin wait)\"","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy code to clipboard","depth":24,"on_screen":false,"help_text":"Copy code","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"OUT","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{\"result\":{\"tools\":[{\"name\":\"list_transactions\",\"description\":\"List finance transactions with optional filters. Returns paginated results including total amount for the filtered set.\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"page\":{\"type\":\"number\",\"description\":\"Page number (default 1)\"},\"limit\":{\"type\":\"number\",\"description\":\"Results per page, max 200 (default 50)\"},\"dateFrom\":{\"type\":\"string\",\"description\":\"Start date YYYY-MM-DD (inclusive)\"},\"dateTo\":{\"type\":\"string\",\"description\":\"End date YYYY-MM-DD (inclusive)\"},\"tag\":{\"type\":\"string\",\"description\":\"Filter by tag name\"},\"recipient\":{\"type\":\"string\",\"description\":\"Substring match on payee name\"},\"type\":{\"type\":\"string\",\"description\":\"Transaction type: POS | ATM | WALLET\"},\"source\":{\"type\":\"string\",\"description\":\"Import source: INGEST | UPLOAD\"},\"search\":{\"type\":\"string\",\"description\":\"Full-text search across rawMessage and recipient\"},\"hideBalanceAlerts\":{\"type\":\"boolean\",\"description\":\"Exclude balance-notification SMS (default false)\"},\"sortBy\":{\"type\":\"string\",\"description\":\"Sort field: date | amount | recipient | createdAt\"},\"sortDir\":{\"type\":\"string\",\"description\":\"asc or desc (default desc)\"}}}},{\"name\":\"spending_by_tag\",\"description\":\"Aggregate spending totals by tag for a date range. Fetches up to 200 transactions and groups them. Good for monthly summaries.\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"dateFrom\":{\"type\":\"string\",\"description\":\"Start date YYYY-MM-DD\"},\"dateTo\":{\"type\":\"string\",\"description\":\"End date YYYY-MM-DD\"}}}},{\"name\":\"get_transaction\",\"description\":\"Get a single transaction by its numeric ID.\",\"inputSchema\":{\"type\":\"object\",\"required\":[\"id\"],\"properties\":{\"id\":{\"type\":\"number\",\"description\":\"Transaction import ID\"}}}},{\"name\":\"list_tags\",\"description\":\"List all available tags with their colors.\",\"inputSchema\":{\"type\":\"object\",\"properties\":{}}},{\"name\":\"add_tag\",\"description\":\"Add a tag to a transaction. Creates the tag globally if it does not exist.\",\"inputSchema\":{\"type\":\"object\",\"required\":[\"id\",\"name\"],\"properties\":{\"id\":{\"type\":\"number\",\"description\":\"Transaction ID\"},\"name\":{\"type\":\"string\",\"description\":\"Tag name (e.g. \\\"Groceries\\\")\"},\"color\":{\"type\":\"string\",\"description\":\"Hex color (e.g. \\\"#22c55e\\\"). Optional — auto-assigned if omitted.\"}}}},{\"name\":\"remove_tag\",\"description\":\"Remove a tag from a transaction.\",\"inputSchema\":{\"type\":\"object\",\"required\":[\"id\",\"tagId\"],\"properties\":{\"id\":{\"type\":\"number\",\"description\":\"Transaction ID\"},\"tagId\":{\"type\":\"number\",\"description\":\"Tag ID (from list_tags or the transaction object)\"}}}},{\"name\":\"health_check\",\"description\":\"Check Finance Hub backend connectivity and database status.\",\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}]},\"jsonrpc\":\"2.0\",\"id\":1}","depth":25,"on_screen":false,"role_description":"text"}]...
|
-8886288705134623955
|
-6680934133339813887
|
click
|
accessibility
|
NULL
|
Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧ Explorer (⇧⌘E)
Search (⇧⌘F)
Source Control (⌃⇧G) - 12 pending changes
12
Run and Debug (⇧⌘D)
Remote Explorer
Extensions (⇧⌘X) - 2 require update
2
Claude Code
Containers
EXPLORER
EXPLORER
Explorer Section: finance [SSH: nas]
Explorer Section: finance [SSH: nas]
FINANCE [SSH: NAS]
auth
dsk-uploader
finance-hub
backend
frontend
mcp
scripts
.env
.env.example
M
.gitignore
M
docker-compose.yml
M
README.md
sms_export.json
payments-logger
Outline Section
OUTLINE
OUTLINE
Timeline Section
TIMELINE
TIMELINE
docker-compose.yml, Editor Group 1
.env, Editor Group 1
.env.example, preview, Editor Group 1
report(1).csv, Editor Group 1
report(2).csv, Editor Group 1
sms_export.json, Editor Group 1
# ── Database ───────────────────────────────────────────────────────────────────
[ENV_SECRET]
# ── Notifier service ──────────────────────────────────────────────────────────
NOTIFIER_URL=[URL_WITH_CREDENTIALS]
# Secret key — generate with: openssl rand -base64 36
[ENV_SECRET]
# Public URL authentik is reachable at (used for redirects and the outpost)
AUTHENTIK_HOST=[URL_WITH_CREDENTIALS] version 2>/dev/null && node --version
Copy code to clipboard
OUT
Exit code 127
Bash
Verify public path configuration
IN
# Check if the upload endpoints are really public
grep -n 'PUBLIC_PATHS\|upload\|ingest' /volume2/docker/finance/finance-hub/backend/src/auth.js
Copy code to clipboard
OUT
<tool_use_error>Cancelled: parallel tool call Bash(npm view @modelcontextprotocol/sdk versi…) errored</tool_use_error>
Bash
Check node version and MCP SDK latest
IN
node --version 2>/dev/null && python3 -c "import urllib.request; import json; d=json.loads(urllib.request.urlopen('[URL_WITH_CREDENTIALS] "^1.29.0"
},
"engines": {
"node": ">=18"
}
}
Write
server.js
server.js
236 lines
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
const BASE_URL = (process.env.FINANCE_HUB_URL || '[URL_WITH_CREDENTIALS] '^1.29.0'}
Thinking
Thinking
Good. Now let me test the MCP server can at least start and call the health check:
Bash
Smoke test MCP server startup
IN
FINANCE_HUB_URL=http://localhost:3001 [ENV_SECRET] \
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | timeout 5 node server.js 2>/dev/null | head -3 || echo "NODE test (expected timeout on stdin wait)"
Copy code to clipboard
OUT
{"result":{"tools":[{"name":"list_transactions","description":"List finance transactions with optional filters. Returns paginated results including total amount for the filtered set.","inputSchema":{"type":"object","properties":{"page":{"type":"number","description":"Page number (default 1)"},"limit":{"type":"number","description":"Results per page, max 200 (default 50)"},"dateFrom":{"type":"string","description":"Start date YYYY-MM-DD (inclusive)"},"dateTo":{"type":"string","description":"End date YYYY-MM-DD (inclusive)"},"tag":{"type":"string","description":"Filter by tag name"},"recipient":{"type":"string","description":"Substring match on payee name"},"type":{"type":"string","description":"Transaction type: POS | ATM | WALLET"},"source":{"type":"string","description":"Import source: INGEST | UPLOAD"},"search":{"type":"string","description":"Full-text search across rawMessage and recipient"},"hideBalanceAlerts":{"type":"boolean","description":"Exclude balance-notification SMS (default false)"},"sortBy":{"type":"string","description":"Sort field: date | amount | recipient | createdAt"},"sortDir":{"type":"string","description":"asc or desc (default desc)"}}}},{"name":"spending_by_tag","description":"Aggregate spending totals by tag for a date range. Fetches up to 200 transactions and groups them. Good for monthly summaries.","inputSchema":{"type":"object","properties":{"dateFrom":{"type":"string","description":"Start date YYYY-MM-DD"},"dateTo":{"type":"string","description":"End date YYYY-MM-DD"}}}},{"name":"get_transaction","description":"Get a single transaction by its numeric ID.","inputSchema":{"type":"object","required":["id"],"properties":{"id":{"type":"number","description":"Transaction import ID"}}}},{"name":"list_tags","description":"List all available tags with their colors.","inputSchema":{"type":"object","properties":{}}},{"name":"add_tag","description":"Add a tag to a transaction. Creates the tag globally if it does not exist.","inputSchema":{"type":"object","required":["id","name"],"properties":{"id":{"type":"number","description":"Transaction ID"},"name":{"type":"string","description":"Tag name (e.g. \"Groceries\")"},"color":{"type":"string","description":"Hex color (e.g. \"#22c55e\"). Optional — auto-assigned if omitted."}}}},{"name":"remove_tag","description":"Remove a tag from a transaction.","inputSchema":{"type":"object","required":["id","tagId"],"properties":{"id":{"type":"number","description":"Transaction ID"},"tagId":{"type":"number","description":"Tag ID (from list_tags or the transaction object)"}}}},{"name":"health_check","description":"Check Finance Hub backend connectivity and database status.","inputSchema":{"type":"object","properties":{}}}]},"jsonrpc":"2.0","id":1}...
|
14095
|
NULL
|
NULL
|
NULL
|
|
2833
|
113
|
35
|
2026-05-07T11:42:48.846571+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154168846_m1.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
7
48
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'active' => true,
'ownerId' => $this->profile->crm_provider_id,
'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,
'type' => 'NOTE',
];
// Generate activity transcription.
$transcriptionData = $this->generateTranscription($activity);
// Truncate Notes with max notes length because transcription text could be very long.
$transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);
$metadata = [
'body' => $transcripts,
];
$associations = $this->convertActivityAssociations($activity);
try {
$hsEngagement = $this->client
->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$noteId = $hsEngagement->data->engagement->id;
// Store crm logged id in transcription.
$transcription = $activity->getTranscription();
$transcription->crm_activity_id = $noteId;
$transcription->save();
} catch (Exception $e) {
Sentry::captureException($e);
}
}
/*
* @inheritdoc
*/
public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void
{
$payload = [
'properties' => $data,
];
try {
switch ($objectType) {
case FieldData::OBJECT_OPPORTUNITY:
$this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_CONTACT:
$this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_ACCOUNT:
$this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_TASK:
// Endpoint for Engagements not ready
$engagements = [
'type' => 'TASK',
];
$metadata = $data;
$this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $objectId],
$metadata,
);
break;
default:
throw new UnexpectedValueException('Unsupported object type "' . $objectType . '"');
}
} catch (\HubSpot\Client\Crm\Deals\ApiException $apiException) {
$errorMessage = $apiException->getMessage();
if ($apiException->getResponseBody()) {
$responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);
$errorMessage = $responseBody['message'] ?? $apiException->getMessage();
}
$this->logger->error(
'[HubSpot] Update record failed',
[
'objectType' => $objectType,
'objectId' => $objectId,
'payload' => $payloa...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"7","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"48","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"33","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8884917924411182450
|
-537393330635270041
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
7
48
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'active' => true,
'ownerId' => $this->profile->crm_provider_id,
'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,
'type' => 'NOTE',
];
// Generate activity transcription.
$transcriptionData = $this->generateTranscription($activity);
// Truncate Notes with max notes length because transcription text could be very long.
$transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);
$metadata = [
'body' => $transcripts,
];
$associations = $this->convertActivityAssociations($activity);
try {
$hsEngagement = $this->client
->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$noteId = $hsEngagement->data->engagement->id;
// Store crm logged id in transcription.
$transcription = $activity->getTranscription();
$transcription->crm_activity_id = $noteId;
$transcription->save();
} catch (Exception $e) {
Sentry::captureException($e);
}
}
/*
* @inheritdoc
*/
public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void
{
$payload = [
'properties' => $data,
];
try {
switch ($objectType) {
case FieldData::OBJECT_OPPORTUNITY:
$this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_CONTACT:
$this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_ACCOUNT:
$this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_TASK:
// Endpoint for Engagements not ready
$engagements = [
'type' => 'TASK',
];
$metadata = $data;
$this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $objectId],
$metadata,
);
break;
default:
throw new UnexpectedValueException('Unsupported object type "' . $objectType . '"');
}
} catch (\HubSpot\Client\Crm\Deals\ApiException $apiException) {
$errorMessage = $apiException->getMessage();
if ($apiException->getResponseBody()) {
$responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);
$errorMessage = $responseBody['message'] ?? $apiException->getMessage();
}
$this->logger->error(
'[HubSpot] Update record failed',
[
'objectType' => $objectType,
'objectId' => $objectId,
'payload' => $payloa...
|
2831
|
NULL
|
NULL
|
NULL
|
|
2834
|
114
|
30
|
2026-05-07T11:42:49.540122+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154169540_m2.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
7
48
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'active' => true,
'ownerId' => $this->profile->crm_provider_id,
'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,
'type' => 'NOTE',
];
// Generate activity transcription.
$transcriptionData = $this->generateTranscription($activity);
// Truncate Notes with max notes length because transcription text could be very long.
$transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);
$metadata = [
'body' => $transcripts,
];
$associations = $this->convertActivityAssociations($activity);
try {
$hsEngagement = $this->client
->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$noteId = $hsEngagement->data->engagement->id;
// Store crm logged id in transcription.
$transcription = $activity->getTranscription();
$transcription->crm_activity_id = $noteId;
$transcription->save();
} catch (Exception $e) {
Sentry::captureException($e);
}
}
/*
* @inheritdoc
*/
public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void
{
$payload = [
'properties' => $data,
];
try {
switch ($objectType) {
case FieldData::OBJECT_OPPORTUNITY:
$this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_CONTACT:
$this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_ACCOUNT:
$this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_TASK:
// Endpoint for Engagements not ready
$engagements = [
'type' => 'TASK',
];
$metadata = $data;
$this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $objectId],
$metadata,
);
break;
default:
throw new UnexpectedValueException('Unsupported object type "' . $objectType . '"');
}
} catch (\HubSpot\Client\Crm\Deals\ApiException $apiException) {
$errorMessage = $apiException->getMessage();
if ($apiException->getResponseBody()) {
$responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);
$errorMessage = $responseBody['message'] ?? $apiException->getMessage();
}
$this->logger->error(
'[HubSpot] Update record failed',
[
'objectType' => $objectType,
'objectId' => $objectId,
'payload' => $payloa...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"7","depth":4,"bounds":{"left":0.46143618,"top":0.17478053,"width":0.0076462766,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"48","depth":4,"bounds":{"left":0.4710771,"top":0.17478053,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.48337767,"top":0.17478053,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"33","depth":4,"bounds":{"left":0.49268618,"top":0.17478053,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.5049867,"top":0.17478053,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.51396275,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5212766,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8884917924411182450
|
-537393330635270041
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
7
48
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'active' => true,
'ownerId' => $this->profile->crm_provider_id,
'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,
'type' => 'NOTE',
];
// Generate activity transcription.
$transcriptionData = $this->generateTranscription($activity);
// Truncate Notes with max notes length because transcription text could be very long.
$transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);
$metadata = [
'body' => $transcripts,
];
$associations = $this->convertActivityAssociations($activity);
try {
$hsEngagement = $this->client
->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$noteId = $hsEngagement->data->engagement->id;
// Store crm logged id in transcription.
$transcription = $activity->getTranscription();
$transcription->crm_activity_id = $noteId;
$transcription->save();
} catch (Exception $e) {
Sentry::captureException($e);
}
}
/*
* @inheritdoc
*/
public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void
{
$payload = [
'properties' => $data,
];
try {
switch ($objectType) {
case FieldData::OBJECT_OPPORTUNITY:
$this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_CONTACT:
$this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_ACCOUNT:
$this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_TASK:
// Endpoint for Engagements not ready
$engagements = [
'type' => 'TASK',
];
$metadata = $data;
$this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $objectId],
$metadata,
);
break;
default:
throw new UnexpectedValueException('Unsupported object type "' . $objectType . '"');
}
} catch (\HubSpot\Client\Crm\Deals\ApiException $apiException) {
$errorMessage = $apiException->getMessage();
if ($apiException->getResponseBody()) {
$responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);
$errorMessage = $responseBody['message'] ?? $apiException->getMessage();
}
$this->logger->error(
'[HubSpot] Update record failed',
[
'objectType' => $objectType,
'objectId' => $objectId,
'payload' => $payloa...
|
2832
|
NULL
|
NULL
|
NULL
|
|
2836
|
114
|
32
|
2026-05-07T11:42:54.444590+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154174444_m2.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
7
48
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'active' => true,
'ownerId' => $this->profile->crm_provider_id,
'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,
'type' => 'NOTE',
];
// Generate activity transcription.
$transcriptionData = $this->generateTranscription($activity);
// Truncate Notes with max notes length because transcription text could be very long.
$transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);
$metadata = [
'body' => $transcripts,
];
$associations = $this->convertActivityAssociations($activity);
try {
$hsEngagement = $this->client
->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$noteId = $hsEngagement->data->engagement->id;
// Store crm logged id in transcription.
$transcription = $activity->getTranscription();
$transcription->crm_activity_id = $noteId;
$transcription->save();
} catch (Exception $e) {
Sentry::captureException($e);
}
}
/*
* @inheritdoc
*/
public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void
{
$payload = [
'properties' => $data,
];
try {
switch ($objectType) {
case FieldData::OBJECT_OPPORTUNITY:
$this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_CONTACT:
$this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_ACCOUNT:
$this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_TASK:
// Endpoint for Engagements not ready
$engagements = [
'type' => 'TASK',
];
$metadata = $data;
$this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $objectId],
$metadata,
);
break;
default:
throw new UnexpectedValueException('Unsupported object type "' . $objectType . '"');
}
} catch (\HubSpot\Client\Crm\Deals\ApiException $apiException) {
$errorMessage = $apiException->getMessage();
if ($apiException->getResponseBody()) {
$responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);
$errorMessage = $responseBody['message'] ?? $apiException->getMessage();
}
$this->logger->error(
'[HubSpot] Update record failed',
[
'objectType' => $objectType,
'objectId' => $objectId,
'payload' => $payloa...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"7","depth":4,"bounds":{"left":0.46143618,"top":0.17478053,"width":0.0076462766,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"48","depth":4,"bounds":{"left":0.4710771,"top":0.17478053,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.48337767,"top":0.17478053,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"33","depth":4,"bounds":{"left":0.49268618,"top":0.17478053,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.5049867,"top":0.17478053,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.51396275,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5212766,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8884917924411182450
|
-537393330635270041
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
7
48
1
33
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Hubspot;
use Carbon\Carbon;
use Exception;
use Generator;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ClientInterface;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\HubspotInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SavePlaybackLinkToCrmInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Layout;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\ParticipantRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Hubspot\Actions\SyncArchivedProfilesAction;
use Jiminny\Services\Crm\Hubspot\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\OpportunitySyncTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncCrmEntitiesTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\SyncFieldsTrait;
use Jiminny\Services\Crm\Hubspot\ServiceTraits\WriteCrmTrait;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Utils\PlaybackUrlBuilder;
use Sentry;
use SevenShores\Hubspot\Exceptions\BadRequest;
use Throwable;
use UnexpectedValueException;
/**
* @phpstan-type CrmFieldDefinition array{
* name: string,
* label: string,
* description: string,
* type: string,
* fieldType: string,
* hidden: bool,
* showCurrencySymbol: bool,
* options: array<array{
* id: string,
* label: string,
* value?: string,
* }
*/
class Service extends BaseService implements
HubspotInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SavePlaybackLinkToCrmInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncCrmEntitiesTrait;
use WriteCrmTrait;
use SyncFieldsTrait;
use OpportunitySyncTrait;
private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;
private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';
private const int BATCH_UPDATE_LIMIT = 100;
private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';
private const int TEN_SECONDLY_ROLLING_LIMIT = 10;
private const string CALLS_SEARCH_ENDPOINT = '[URL_WITH_CREDENTIALS] ClientInterface|Client
*/
protected $client;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected ProspectPhotoPathService $prospectPhotoPathService;
private SyncFieldAction $syncFieldAction;
private PayloadBuilder $payloadBuilder;
private SyncRelatedActivityManager $syncRelatedActivityManager;
private SyncArchivedProfilesAction $syncArchivedProfilesAction;
private WebhookSyncBatchProcessor $batchProcessor;
public function __construct(
Client $client,
SyncFieldAction $syncFieldAction,
PayloadBuilder $payloadBuilder,
ProspectPhotoPathService $prospectPhotoPathService,
SyncArchivedProfilesAction $syncArchivedProfilesAction,
WebhookSyncBatchProcessor $batchProcessor,
) {
parent::__construct();
$this->client = $client;
$this->syncFieldAction = $syncFieldAction;
$this->prospectPhotoPathService = $prospectPhotoPathService;
$this->payloadBuilder = $payloadBuilder;
$this->syncArchivedProfilesAction = $syncArchivedProfilesAction;
$this->batchProcessor = $batchProcessor;
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [
'client' => $this->client,
'payloadBuilder' => $this->payloadBuilder,
'logger' => $this->logger,
]);
$this->crmEntityRepository = app(CrmEntityRepository::class);
$this->dealFieldsService = app(DealFieldsService::class);
}
public function getDisplayName(): string
{
return 'HubSpot';
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
$owner = $user->team->owner;
return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);
}
public function getClient(): Client
{
/** @var Client */
return $this->client;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*
* @param bool $internal Direction of the conversion.
* True is pulling from CRM, false normalize before sending to CRM.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize(
fieldType: $fieldType,
fieldValue: $fieldValue,
isInbound: $internal,
);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'activityType',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
/**
* @inheritdoc
*/
public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
// Outcome should always be provided calls/meetings.
$fieldData = [
[
'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',
'object_type' => Field::OBJECT_TASK,
],
];
foreach ($fieldData as $data) {
$field = $this->config->fields()->where($data)->first();
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function syncField(Field $field): void
{
switch ($field->object_type) {
case Field::OBJECT_ACCOUNT:
$crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_CONTACT:
$crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_OPPORTUNITY:
$crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);
break;
case Field::OBJECT_TASK:
$this->syncSingleTaskField($field);
return;
default:
return;
}
$this->syncFieldAction->execute($field, $crmField->toArray());
}
/**
* @param array<array{
* id:string,
* label:string,
* value?:string
* }> $options
*
* @throws CrmException
*
* @return FieldData[]
*
*/
public function importPicklistValues(
Field $field,
array $options = [['id' => '', 'label' => '', 'value' => '']],
): array {
if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {
// We already have the options, no need to fetch them again
return $this->importOptions($field, $options);
}
$options = [];
switch ($field->getObjectType()) {
case Field::OBJECT_ACCOUNT:
$options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());
break;
case Field::OBJECT_CONTACT:
$options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());
break;
case Field::OBJECT_OPPORTUNITY:
// Hubspot has different endpoint for stages
$options = $this->getClient()->fetchOpportunityFieldOptions($field);
break;
case Field::OBJECT_TASK:
if ($field->getCrmProviderId() === 'disposition') {
$options = $this->getClient()->fetchDispositionFieldOptions();
} elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {
$options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);
}
break;
default:
$this->logger->warning('Invalid object type', [
'object_type' => $field->getObjectType(),
'field_id' => $field->getId(),
]);
throw new CrmException('Invalid object type');
}
return $this->importOptions($field, $options);
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$missingStage = null;
try {
// Use the HubSpot API client instead of the SDK crmPipelines() method
$endpoint = self::getDealsPipelinesEndpoint();
$pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);
$pipelines = $pipelinesResponse->data->results;
} catch (RequestException|BadRequest $exception) {
throw $exception;
}
foreach ($pipelines as $pipeline) {
$stages = [];
// We create a business process to contain the pipeline, and store all stages against it.
$p = ResponseNormalize::normalizePipeline($pipeline);
// Create/update business process for this pipeline
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'type' => BusinessProcess::TYPE_OPPORTUNITY,
'is_selectable' => $p['active'],
]);
// A record type is really a clone of the business process, used to store which record uses which pipeline.
// Create/update record type clone
$this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $p['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($p['label'], 0, 150),
'is_selectable' => $p['active'],
'business_process_id' => $businessProcess->id ?? null,
]);
// Stages - fetch all existing stages upfront to avoid N+1 queries
$existingStages = $this->config->stages()
->withTrashed()
->where('type', Stage::TYPE_OPPORTUNITY)
->get()
->keyBy('crm_provider_id');
foreach ($p['stages'] as $dealStage) {
$s = ResponseNormalize::normalizeDealStage($dealStage);
/** @var ?Stage $existingStage */
$existingStage = $existingStages->get($s['id']);
// Restore soft-deleted stages that are now active in HubSpot
if ($existingStage?->trashed() && $s['active']) {
$existingStage->restore();
}
// Upsert stage (updates soft-deleted records without restoring them)
$stage = $this->config->stages()->withTrashed()->updateOrCreate([
'crm_provider_id' => $s['id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($s['label'], 0, 50),
'label' => mb_strimwidth($s['label'], 0, 191),
'type' => Stage::TYPE_OPPORTUNITY,
'sequence' => $s['displayOrder'],
'is_selectable' => $s['active'],
'probability' => $s['probability'] * 100,
]);
if ($missingStageName === $s['id']) {
$missingStage = $stage;
}
$stages[] = $stage->id;
}
$businessProcess->stages()->sync($stages);
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncOrganization(): void
{
try {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
/** @var array<int, array<string, mixed>> */
return Cache::remember(
key: $this->team->getId() . $name . $count . $offset,
ttl: 300,
callback: function () use ($name, $offset, $count): array {
$data = [];
// Use the new V3 API to find contacts based on additional fields.
foreach (['companies', 'contacts'] as $objectType) {
$endpoint = '[URL_WITH_CREDENTIALS]
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
if ($crmAccountId === null) {
return $data;
}
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
$closedStages = $this->getClosedDealStages();
$payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(
$this->config,
$crmAccountId,
$closedStages,
);
$results = $this->client->getPaginatedData($payload, 'deals');
foreach ($results['results'] as $object) {
$properties = $object['properties'];
$amount = null;
if (empty($properties['amount']) === false) {
$currency = $properties['deal_currency_code'] ?? $this->config->default_currency;
// Values can contain commas and any junk so strip them.
$value = (float) preg_replace('/[^\d.]/', '', $properties['amount']);
$amount = formatCurrency($value, $currency);
}
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
if ($businessProcess === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);
$businessProcess = $this->config
->businessProcesses()
->where('crm_provider_id', $properties['pipeline'])
->first();
} else {
$stage = $businessProcess
->stages()
->where('crm_provider_id', $properties['dealstage'])
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages(null, $properties['dealstage']);
}
}
$recordType = null;
if ($businessProcess) {
$recordType = $businessProcess->recordTypes()->first();
}
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$record = [
'crmId' => $object['id'],
'name' => $properties['dealname'] ?? 'Unknown Deal',
'value' => $amount,
'won' => $isWon,
'closed' => $isWon || $isLost,
'stage' => [
'id' => $stage?->getUuid() ?? '',
'name' => $stage?->getName() ?? '',
],
];
if ($recordType) {
$record += [
'recordType' => [
'id' => $recordType->id_string,
'name' => $recordType->name,
],
];
}
if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
switch ($objectType) {
case 'contact':
$hsObject = 'contact';
break;
case 'account':
$hsObject = 'company';
break;
default:
// This is a hack to prioritise and override a contact/company with a deal.
if ($opportunityId) {
$hsObject = 'deal';
$objectId = $opportunityId;
} else {
throw new InvalidArgumentException('Object type not supported.');
}
}
$engagementTypes = ['meetings', 'tasks'];
foreach ($engagementTypes as $engagementType) {
$payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);
$this->logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
$engagements = $this->client->getPaginatedData($payload, $engagementType);
foreach ($engagements['results'] as $engagement) {
if ($engagementType == 'meetings') {
$title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';
} elseif ($engagementType == 'tasks') {
$title = $engagement['properties']['hs_task_subject'];
} else {
$title = 'Scheduled meeting';
}
$data[] = [
'crmId' => $engagement['id'],
'subject' => $title,
'due' => $engagement['properties']['hs_timestamp'],
'type' => $engagement['properties']['hs_activity_type'] ?? null,
];
}
}
usort($data, function ($item1, $item2) {
return $item2['due'] <=> $item1['due'];
});
return $data;
}
/**
* Try to find CRM Objects using email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$contactProperties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
$contact = null;
$account = null;
try {
$hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);
if ($hsContact) {
$contact = $this->importContact($hsContact);
$account = $contact->account;
}
$data = $this->convertCrmData($contact, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
} catch (BadRequest $e) {
$this->logger->warning('[HubSpot] Search failed', [
'team_id' => $this->team->getId(),
'search_identifier' => $email,
'reason' => $e->getMessage(),
]);
}
return null;
}
public function getDomain(string $email): ?string
{
return $this->getDomainFromEmail($email);
}
/**
* Try to find CRM objects using domain name of the email address
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByDomain(string $domain, ?int $userId = null): ?array
{
$companyName = $domain;
// Try to find a company matching their email domain.
$companyProperties = [
'country',
'phone',
'name',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'domain',
];
try {
$hsAccounts = $this->client
->getInstance()
->companies()
->searchByDomain($companyName, $companyProperties);
} catch (Throwable $e) {
$this->logger->info('[HubSpot] Search failed', [
'error' => $e->getMessage(),
'domain' => $domain,
]);
return null;
}
$account = null;
// If there are multiple accounts, don't guess, we'll ask later.
if (\count($hsAccounts->data->results) === 1) {
// Persist this remote object.
$account = $this->syncAccount($hsAccounts->data->results[0]->companyId);
}
$data = $this->convertCrmData(null, $account, $userId);
return ! empty(array_filter($data)) ? $data : null;
}
/**
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array
{
$countryCode = null;
if ($contact && $contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account && $account->country_code) {
$countryCode = $account->country_code;
}
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact ? $contact->crm_provider_id : null,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
// If there are multiple opportunities, don't guess, we'll ask later.
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
if (str_contains($phone, '**')) {
return null;
}
// trim all whitespaces if present so the lookup doesn't fail
$phone = str_replace(' ', '', $phone);
// Check if the user is internal.
if ($this->isPhoneNumberOfTeamMember($phone)) {
return null;
}
$response = $this->searchForPhoneNumber($phone);
if (empty($response)) {
return null;
}
// This would ideally importContact instead but the response type differs.
$contact = $this->findAndSyncContact($response['results'][0]['id']);
if (! $contact instanceof Contact) {
return null;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
try {
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception $e) {
$this->logger->debug('[HubSpot] Opportunity failed to sync.', [
'reason' => $e->getMessage(),
]);
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
private function isPhoneNumberOfTeamMember(string $phone): bool
{
$teamRepository = app(TeamRepository::class);
$user = $teamRepository->findTeamMemberByPhone($this->team, $phone);
if ($user instanceof User) {
return true;
}
return false;
}
private function findAndSyncContact(string $crmId): ?Contact
{
try {
return $this->syncContact($crmId);
} catch (Exception $exception) {
$this->logger->info('[HubSpot] Phone match failed', [
'reason' => $exception->getMessage(),
]);
return null;
}
}
private function hasResults(array $response): bool
{
return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;
}
private function searchForPhoneNumber(string $phone): array
{
// Normalizes the provided phone number for the API search.
$normalizedPhone = $this->normalizePhoneNumber($phone);
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);
$this->logger->info('[HubSpot] Phone match search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);
if (! $this->hasResults($response)) {
$nationalPhone = preg_replace('/\D/', '', phone_national(null, $phone));
$payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);
$this->logger->info('[HubSpot] Phone match national number search triggered', [
'phone' => $phone,
'nationalPhone' => $nationalPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
if (! $this->hasResults($response)) {
$payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);
$this->logger->info('[HubSpot] Phone match alternative search triggered', [
'phone' => $phone,
'normalizedPhone' => $normalizedPhone,
'payload' => $payload,
]);
$response = $this->handlePhoneSearchRequest($phone, $payload);
}
return $this->hasResults($response) ? $response : [];
}
private function handlePhoneSearchRequest(string $phone, array $payload): array
{
$endpoint = '[URL_WITH_CREDENTIALS] null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$payload = $this->payloadBuilder->generateSearchContactsByNamePayload(
$name,
$this->getContactFields()
);
$hsContacts = $this->client->getPaginatedData($payload, 'contact');
if (empty($hsContacts['results'])) {
return false;
}
$contact = $this->importContact($hsContacts['results'][0]);
if ($contact === null) {
return false;
}
$account = $contact->account;
$countryCode = $contact->country_code ?? $account->country_code ?? null;
try {
$hsOpportunities = $this->findOpportunities(
$account ? $account->crm_provider_id : null,
$contact->crm_provider_id,
$userId
);
} catch (Exception $e) {
$hsOpportunities = [];
}
$opportunity = null;
$stage = null;
if (! empty($hsOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
return [
null,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
});
return is_array($result) ? $result : null;
}
private function convertActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],
'ownerIds' => [],
];
}
private function getParticipantsIds(Activity $activity): array
{
$attendees = [];
$participantRepository = app(ParticipantRepository::class);
$participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);
foreach ($participants as $participant) {
if ($participant->user_id || $participant->isCoach()) {
continue;
}
$contact = $participant->contact()->first();
if ($contact && $contact->crm_provider_id) {
$attendees[] = $contact->crm_provider_id;
} else {
if (! empty($participant->name)) {
$attendeeData = $this->fetchMissingAttendeeInfo($participant);
}
if (! empty($attendeeData['id'])) {
$attendees[] = $attendeeData['id'];
}
}
}
if ($activity->hasContact()) {
$attendees[] = $activity->contact->crm_provider_id;
}
return array_unique($attendees);
}
private function fetchMissingAttendeeInfo(Participant $participant): array
{
// Check if we need to look inside an account context.
$activity = $participant->getActivity();
$companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;
// First check the local data.
/** @var Contact[] $contacts */
$contacts = $this->team->contacts()
->with('account')
->where('name', $participant->name)
->whereNotNull('email')
->get();
foreach ($contacts as $contact) {
// If we have a company in scope, check the contact is associated to it.
if (
$companyId !== null
&& ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)
) {
continue;
}
return [
'id' => $contact->crm_provider_id,
'email' => $contact->email,
];
}
$payload = $this->generateNameSearchPayload($participant->name, 0, 20);
try {
$response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);
// TODO add some logic to choose the most suitable contact if multiple
foreach ($response['results'] as $object) {
$properties = $object['properties'];
if (empty($object['properties']) === false) {
// Check the company matches the contact.
// Todo: Move this check inside the API search.
if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {
continue;
}
return [
'id' => $object['id'],
'email' => $properties['email'],
];
}
}
} catch (Exception $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [
'teamId' => $this->team->id_string,
'request' => $payload,
'reason' => $e->getMessage(),
]);
}
return [];
}
/**
* Store transcripts as note engagement.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
// For HS no need to check if Crm profile - Log Notes field is enabled
// We only check if store_transcript toggle is enabled on crm profile.
$engagement = [
'active' => true,
'ownerId' => $this->profile->crm_provider_id,
'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,
'type' => 'NOTE',
];
// Generate activity transcription.
$transcriptionData = $this->generateTranscription($activity);
// Truncate Notes with max notes length because transcription text could be very long.
$transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);
$metadata = [
'body' => $transcripts,
];
$associations = $this->convertActivityAssociations($activity);
try {
$hsEngagement = $this->client
->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$noteId = $hsEngagement->data->engagement->id;
// Store crm logged id in transcription.
$transcription = $activity->getTranscription();
$transcription->crm_activity_id = $noteId;
$transcription->save();
} catch (Exception $e) {
Sentry::captureException($e);
}
}
/*
* @inheritdoc
*/
public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void
{
$payload = [
'properties' => $data,
];
try {
switch ($objectType) {
case FieldData::OBJECT_OPPORTUNITY:
$this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_CONTACT:
$this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_ACCOUNT:
$this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);
break;
case FieldData::OBJECT_TASK:
// Endpoint for Engagements not ready
$engagements = [
'type' => 'TASK',
];
$metadata = $data;
$this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $objectId],
$metadata,
);
break;
default:
throw new UnexpectedValueException('Unsupported object type "' . $objectType . '"');
}
} catch (\HubSpot\Client\Crm\Deals\ApiException $apiException) {
$errorMessage = $apiException->getMessage();
if ($apiException->getResponseBody()) {
$responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);
$errorMessage = $responseBody['message'] ?? $apiException->getMessage();
}
$this->logger->error(
'[HubSpot] Update record failed',
[
'objectType' => $objectType,
'objectId' => $objectId,
'payload' => $payloa...
|
2835
|
NULL
|
NULL
|
NULL
|
|
6283
|
260
|
15
|
2026-05-07T18:08:34.128741+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778177314128_m2.jpg...
|
PhpStorm
|
faVsco.js – HandleHubspotRateLimit.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Middleware;
use Illuminate\Support\Facades\Log;
use Jiminny\Exceptions\RateLimitException;
/**
* Job middleware that catches RateLimitException from HubSpot API calls
* and releases the job back to the queue with the appropriate delay.
*
* This ensures that when HubSpot rate limits are hit (either proactively
* via ProviderRateLimiter or reactively via 429 responses), the job
* will be retried after the correct delay rather than failing immediately.
*/
class HandleHubspotRateLimit
{
private const int MAX_RETRY_DELAY = 600; // 10 minutes max
private const int MIN_RETRY_DELAY = 1; // 1 second min
public function handle(object $job, callable $next): void
{
try {
$next($job);
} catch (RateLimitException $e) {
$retryAfter = $e->getRetryAfter();
// Clamp delay to reasonable bounds
$delay = max(self::MIN_RETRY_DELAY, min($retryAfter, self::MAX_RETRY_DELAY));
Log::info('[HandleHubspotRateLimit] Rate limit caught, releasing job with delay', [
'job_class' => $job::class,
'retry_after_requested' => $retryAfter,
'retry_after_clamped' => $delay,
'rate_limit_message' => $e->getMessage(),
]);
// Release the job back to the queue with the calculated delay
$job->release($delay);
}
}
}
Project...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Middleware;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Exceptions\\RateLimitException;\n\n/**\n * Job middleware that catches RateLimitException from HubSpot API calls\n * and releases the job back to the queue with the appropriate delay.\n *\n * This ensures that when HubSpot rate limits are hit (either proactively\n * via ProviderRateLimiter or reactively via 429 responses), the job\n * will be retried after the correct delay rather than failing immediately.\n */\nclass HandleHubspotRateLimit\n{\n private const int MAX_RETRY_DELAY = 600; // 10 minutes max\n private const int MIN_RETRY_DELAY = 1; // 1 second min\n\n public function handle(object $job, callable $next): void\n {\n try {\n $next($job);\n } catch (RateLimitException $e) {\n $retryAfter = $e->getRetryAfter();\n\n // Clamp delay to reasonable bounds\n $delay = max(self::MIN_RETRY_DELAY, min($retryAfter, self::MAX_RETRY_DELAY));\n\n Log::info('[HandleHubspotRateLimit] Rate limit caught, releasing job with delay', [\n 'job_class' => $job::class,\n 'retry_after_requested' => $retryAfter,\n 'retry_after_clamped' => $delay,\n 'rate_limit_message' => $e->getMessage(),\n ]);\n\n // Release the job back to the queue with the calculated delay\n $job->release($delay);\n }\n }\n}","depth":4,"bounds":{"left":0.11968085,"top":0.22106944,"width":0.30219415,"height":0.77893054},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Middleware;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Exceptions\\RateLimitException;\n\n/**\n * Job middleware that catches RateLimitException from HubSpot API calls\n * and releases the job back to the queue with the appropriate delay.\n *\n * This ensures that when HubSpot rate limits are hit (either proactively\n * via ProviderRateLimiter or reactively via 429 responses), the job\n * will be retried after the correct delay rather than failing immediately.\n */\nclass HandleHubspotRateLimit\n{\n private const int MAX_RETRY_DELAY = 600; // 10 minutes max\n private const int MIN_RETRY_DELAY = 1; // 1 second min\n\n public function handle(object $job, callable $next): void\n {\n try {\n $next($job);\n } catch (RateLimitException $e) {\n $retryAfter = $e->getRetryAfter();\n\n // Clamp delay to reasonable bounds\n $delay = max(self::MIN_RETRY_DELAY, min($retryAfter, self::MAX_RETRY_DELAY));\n\n Log::info('[HandleHubspotRateLimit] Rate limit caught, releasing job with delay', [\n 'job_class' => $job::class,\n 'retry_after_requested' => $retryAfter,\n 'retry_after_clamped' => $delay,\n 'rate_limit_message' => $e->getMessage(),\n ]);\n\n // Release the job back to the queue with the calculated delay\n $job->release($delay);\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"}]...
|
-8884830274773078395
|
-3101287019691831970
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Middleware;
use Illuminate\Support\Facades\Log;
use Jiminny\Exceptions\RateLimitException;
/**
* Job middleware that catches RateLimitException from HubSpot API calls
* and releases the job back to the queue with the appropriate delay.
*
* This ensures that when HubSpot rate limits are hit (either proactively
* via ProviderRateLimiter or reactively via 429 responses), the job
* will be retried after the correct delay rather than failing immediately.
*/
class HandleHubspotRateLimit
{
private const int MAX_RETRY_DELAY = 600; // 10 minutes max
private const int MIN_RETRY_DELAY = 1; // 1 second min
public function handle(object $job, callable $next): void
{
try {
$next($job);
} catch (RateLimitException $e) {
$retryAfter = $e->getRetryAfter();
// Clamp delay to reasonable bounds
$delay = max(self::MIN_RETRY_DELAY, min($retryAfter, self::MAX_RETRY_DELAY));
Log::info('[HandleHubspotRateLimit] Rate limit caught, releasing job with delay', [
'job_class' => $job::class,
'retry_after_requested' => $retryAfter,
'retry_after_clamped' => $delay,
'rate_limit_message' => $e->getMessage(),
]);
// Release the job back to the queue with the calculated delay
$job->release($delay);
}
}
}
Project...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
26184
|
1088
|
10
|
2026-05-12T12:12:46.837211+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-12/1778 /Users/lukas/.screenpipe/data/data/2026-05-12/1778587966837_m1.jpg...
|
Code
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
CodeFileEditSelectionViewGoRunTerminalWindowHelpal CodeFileEditSelectionViewGoRunTerminalWindowHelpallSupport Daily - 3 m left100% <78• Tue 12 May 15:12:46DOCKERDOCKER (-zsh)DEV (-zsh)О 882APP (-zsh)*3STAGE (ssh)ec2-user@ip-10-30-129-.84T2PROD (-zsh)181screenpipe*kibana{"type" : "log""@timestamp": "2026-05-11T19:54:53Z"asticsearch","tags" : ["warning""el,"data"], "pid" :7,'"message" : "Unabletorevive connection: [URL_WITH_CREDENTIALS] : "2026-05-11T19:54:532",asticsearch", "data"],"pid":7,"message": "No living connections"}warning","elkibanaugins"I {"type" : "log", "@timestamp" : "2026-05-11T19:54:53Z""licensing"], "pid" :7,"message" : "License informationasticsearch due to Error: No Livingconnectionskibana1 {"type" : "log".,"@timestamp": "2026-05-11T19:54:54Z","tags" : ["error"ticsearch", "data"], "pid" :7, "message" : "[ConnectionError]: getaddrinfo ENOTFOUND elasticsearch elasticsearch:9200"}kibana1 {"type" : "log","@timestamp": "2026-05-11T19:54:54Z", "tags" : ["warning", "elasticsearch", "data"], "pid" :7,"message":"Unable to revive connection: [URL_WITH_CREDENTIALS] "2026-05-11T19:54:54Z","tags" : ["warning", "elasticsearch", "data"],"pid":7,"message"• "No livingconnections"}kibana1 {"type": "log","@timestamp":"2026-05-11T19:54:54Z""tags" : ["error", "plugins", "taskManager","taskManager"], "pid" :7, "message": "Failed to poll for work: Error: NoLiving connections"}kibana1 {"type": "log", "@timestamp": "2026-05-11T19:54:572", "tags" : ["error", "elasticsearch".,"data"], "pid" :7, "message" :"[ConnectionError]: getaddrinfo ENOTFOUND elasticsearch elasticsearch:9200*}kibana1 {"type" : "Log","@timestamp" : "2026-05-11T19:54:57Z","tags" : ["warning","elasticsearch", "data"], "pid" :7, "message": "Unable to revive connection: [URL_WITH_CREDENTIALS] : "2026-05-11T19:54:57Z", "tags" : ["warning", "elasticsearch", "data"], "pid" :7, "message": "No living connections"}kibanaI {"type" : "log", "@timestamp" : "2026-05-11T19:54:57Z", "tags" : ["error","plugins", "taskManager", "taskManager"], "pid" :7, "message" : "Failed to pollfor work: Error: NoLiving connections"}kibanaI {"type" : "log", "@timestamp" : "2026-05-11T19:54:59Z","tags" : ["error", "elasticsearch", "data"], "pid" :7, "message" : "[ConnectionError]: getaddrinfo ENOTFOUND elasticsearch elasticsearch:9200"}1 {"type" : "log","@timestamp" : "2026-05-11T19:55: 00Z""tags": ["warning"asticsearch", "data"], "pid" :7, "message": "Unable to revive connection: [URL_WITH_CREDENTIALS] : "2026-05-11T19:55 :00Z", "tags" : ["warning", "elasticsearch", "data"], "pid" :7, "message": "No living connections"}1 {"type": "log", "@timestamp" : "2026-05-11T19:55:00Z","tags" : ["error", "plug, "taskManager",, "taskManager"], "pid" :7, "message": "Failed to poll for work: Error: NoLiving connections"}unexpected EOFkas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/infrastructure/dev/docker (develop) $-zshX5O 886-zsh*** System restart required ***Last login: Thu May7 08:01:13 2026 from 212.5.153.87lukas@jiminny-prod-bastion:~$lukas@jiminny-prod-bastion:~$ client_loop: send disconnect: Broken pipeukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ |T3 EU (ssh)New release'24.04.4 LTS' available.Run'do-release-upgrade'to upgrade to it.*** System restart required ***Last login: Wed Apr 22 08:09:46 2026 from 212.5.153.87lukas@jiminny-eu-bastion:~$ |T4STAGE (ssh)Run 'do-release-upgrade' to upgrade to it.System restart required ***Last login: Thu May7 11:01:47 2026 from 212.5.153.87bastion:-$QA (-zsh)Poetry could not find a pyproject.toml file in /Users/lukas or its parentsPoetry could not find a pyproject.toml file in /Users/lukas or its parentsT6FE (-zsh)Poetry could not find a pyproject.toml file in /Users/lukas or its parentsPRODSTAGEFRONTENDPoetry could not find a pyproject.toml file in /Users/lukas or its parentsLukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ ||17 ExT (-zsh)Poetry could not find a pyproject.toml file in /Users/lukas or its parentsPoetry could not find a pyproject.toml file in /Users/lukas or its parentsukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ lEXTENSION...
|
NULL
|
-8883601899878356907
|
NULL
|
click
|
ocr
|
NULL
|
CodeFileEditSelectionViewGoRunTerminalWindowHelpal CodeFileEditSelectionViewGoRunTerminalWindowHelpallSupport Daily - 3 m left100% <78• Tue 12 May 15:12:46DOCKERDOCKER (-zsh)DEV (-zsh)О 882APP (-zsh)*3STAGE (ssh)ec2-user@ip-10-30-129-.84T2PROD (-zsh)181screenpipe*kibana{"type" : "log""@timestamp": "2026-05-11T19:54:53Z"asticsearch","tags" : ["warning""el,"data"], "pid" :7,'"message" : "Unabletorevive connection: [URL_WITH_CREDENTIALS] : "2026-05-11T19:54:532",asticsearch", "data"],"pid":7,"message": "No living connections"}warning","elkibanaugins"I {"type" : "log", "@timestamp" : "2026-05-11T19:54:53Z""licensing"], "pid" :7,"message" : "License informationasticsearch due to Error: No Livingconnectionskibana1 {"type" : "log".,"@timestamp": "2026-05-11T19:54:54Z","tags" : ["error"ticsearch", "data"], "pid" :7, "message" : "[ConnectionError]: getaddrinfo ENOTFOUND elasticsearch elasticsearch:9200"}kibana1 {"type" : "log","@timestamp": "2026-05-11T19:54:54Z", "tags" : ["warning", "elasticsearch", "data"], "pid" :7,"message":"Unable to revive connection: [URL_WITH_CREDENTIALS] "2026-05-11T19:54:54Z","tags" : ["warning", "elasticsearch", "data"],"pid":7,"message"• "No livingconnections"}kibana1 {"type": "log","@timestamp":"2026-05-11T19:54:54Z""tags" : ["error", "plugins", "taskManager","taskManager"], "pid" :7, "message": "Failed to poll for work: Error: NoLiving connections"}kibana1 {"type": "log", "@timestamp": "2026-05-11T19:54:572", "tags" : ["error", "elasticsearch".,"data"], "pid" :7, "message" :"[ConnectionError]: getaddrinfo ENOTFOUND elasticsearch elasticsearch:9200*}kibana1 {"type" : "Log","@timestamp" : "2026-05-11T19:54:57Z","tags" : ["warning","elasticsearch", "data"], "pid" :7, "message": "Unable to revive connection: [URL_WITH_CREDENTIALS] : "2026-05-11T19:54:57Z", "tags" : ["warning", "elasticsearch", "data"], "pid" :7, "message": "No living connections"}kibanaI {"type" : "log", "@timestamp" : "2026-05-11T19:54:57Z", "tags" : ["error","plugins", "taskManager", "taskManager"], "pid" :7, "message" : "Failed to pollfor work: Error: NoLiving connections"}kibanaI {"type" : "log", "@timestamp" : "2026-05-11T19:54:59Z","tags" : ["error", "elasticsearch", "data"], "pid" :7, "message" : "[ConnectionError]: getaddrinfo ENOTFOUND elasticsearch elasticsearch:9200"}1 {"type" : "log","@timestamp" : "2026-05-11T19:55: 00Z""tags": ["warning"asticsearch", "data"], "pid" :7, "message": "Unable to revive connection: [URL_WITH_CREDENTIALS] : "2026-05-11T19:55 :00Z", "tags" : ["warning", "elasticsearch", "data"], "pid" :7, "message": "No living connections"}1 {"type": "log", "@timestamp" : "2026-05-11T19:55:00Z","tags" : ["error", "plug, "taskManager",, "taskManager"], "pid" :7, "message": "Failed to poll for work: Error: NoLiving connections"}unexpected EOFkas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/infrastructure/dev/docker (develop) $-zshX5O 886-zsh*** System restart required ***Last login: Thu May7 08:01:13 2026 from 212.5.153.87lukas@jiminny-prod-bastion:~$lukas@jiminny-prod-bastion:~$ client_loop: send disconnect: Broken pipeukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ |T3 EU (ssh)New release'24.04.4 LTS' available.Run'do-release-upgrade'to upgrade to it.*** System restart required ***Last login: Wed Apr 22 08:09:46 2026 from 212.5.153.87lukas@jiminny-eu-bastion:~$ |T4STAGE (ssh)Run 'do-release-upgrade' to upgrade to it.System restart required ***Last login: Thu May7 11:01:47 2026 from 212.5.153.87bastion:-$QA (-zsh)Poetry could not find a pyproject.toml file in /Users/lukas or its parentsPoetry could not find a pyproject.toml file in /Users/lukas or its parentsT6FE (-zsh)Poetry could not find a pyproject.toml file in /Users/lukas or its parentsPRODSTAGEFRONTENDPoetry could not find a pyproject.toml file in /Users/lukas or its parentsLukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ ||17 ExT (-zsh)Poetry could not find a pyproject.toml file in /Users/lukas or its parentsPoetry could not find a pyproject.toml file in /Users/lukas or its parentsukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ lEXTENSION...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
3459
|
NULL
|
0
|
2026-05-07T12:20:15.720582+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778156415720_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PostmanWindow• • 0HubSpot rate limit implementat PostmanWindow• • 0HubSpot rate limit implementation strategy vOption 1: curl + xargs (built into vour Mac. simplest)TOKEN="pat-nal-..."seq 1 200 | xargs -P 20 -n 1 -I curl -s -o /dev/null-W "%http code n"-H "Authorization: Bearer STOKEN"[URL_WITH_CREDENTIALS] Local VaultStore your API secrets locally in vault.Dwe VauiGiobals Vault Took -- m=m...
|
NULL
|
-8880409726529758374
|
NULL
|
click
|
ocr
|
NULL
|
PostmanWindow• • 0HubSpot rate limit implementat PostmanWindow• • 0HubSpot rate limit implementation strategy vOption 1: curl + xargs (built into vour Mac. simplest)TOKEN="pat-nal-..."seq 1 200 | xargs -P 20 -n 1 -I curl -s -o /dev/null-W "%http code n"-H "Authorization: Bearer STOKEN"[URL_WITH_CREDENTIALS] Local VaultStore your API secrets locally in vault.Dwe VauiGiobals Vault Took -- m=m...
|
3457
|
NULL
|
NULL
|
NULL
|
|
13824
|
619
|
6
|
2026-05-09T16:35:59.210886+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-09/1778 /Users/lukas/.screenpipe/data/data/2026-05-09/1778344559210_m1.jpg...
|
Firefox
|
lakylak/finance-hub - finance-hub - Gitea: Git wit lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea — Personal...
|
True
|
gitea.com/lakylak/finance-hub
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
EU alternatives to github : r/github
reddit.com
Pu EU alternatives to github : r/github
reddit.com
Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Your old PC can run Windows 11 in a VM, but not on bare metal - [EMAIL] - Gmail
Your old PC can run Windows 11 in a VM, but not on bare metal - [EMAIL] - Gmail
(25) Quora
(25) Quora
Location Logger
Location Logger
Finance Hub
Finance Hub
Finance Hub
Finance Hub
Select: transactions - db - Adminer
Select: transactions - db - Adminer
Електронно банкиране ДСК Директ от Банка ДСК
Електронно банкиране ДСК Директ от Банка ДСК
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
VIVACOM
VIVACOM
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
VIVACOM
VIVACOM
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Claude Code | Claude Platform
Claude Code | Claude Platform
Claude
Claude
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
Close tab
EU alternatives to github : r/github
EU alternatives to github : r/github
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Dashboard
Issues
Issues
Pull Requests
Pull Requests
Milestones
Milestones
Explore
Explore
Notifications
lakylak
lakylak
/
finance-hub
finance-hub
Private
RSS Feed
Unwatch
1
1
Star
0
0
Fork
Fork
0
0
Code
Code
Issues
Issues
Pull Requests
Pull Requests
Actions
Actions
Projects
Projects
Releases
Releases
Wiki
Wiki
Activity
Activity
Settings
Settings
2 Commits
2
Commits
1 Branch
1
Branch
0 Tags
0
Tags
main
New Pull Request
Go to file
Code
Code
Lukas Kovalik
9a757fe67e
9a757fe67e
Add README with project description, API reference, and usage guide
Add README with project description, API reference, and usage guide
...
41 minutes ago
backend
backend
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
frontend
frontend
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
scripts
scripts
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
.env.example
.env.example
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
.gitignore
.gitignore
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
docker-compose.yml
docker-compose.yml
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
README.md
README.md
Add README with project description, API reference, and usage guide
Add README with project description, API reference, and usage guide
41 minutes ago
README.md Escape Edit File
README.md
README.md
Escape
Edit File
Finance Hub
Finance Hub
Unified personal finance tracker that consolidates DSK Bank SMS notifications and CSV statement exports into a single PostgreSQL database with a React review UI.
Replaces two separate tools:
payments-logger
— ingested DSK Bank SMS notifications
dsk-uploader
— parsed DSK Bank CSV exports and sent them to Notion
Features
Features
SMS ingest
— receives DSK Bank payment SMS via iOS Shortcuts / HTTP POST; parses amount, recipient, card, date, balance
CSV upload
— drag-and-drop DSK Bank account statement exports (BGN and EUR formats, cp1251 and UTF-8 encodings)
Deduplication
— exact
rawMessage
match prevents duplicate imports from re-uploads or re-runs of the reimport script
Auto-tagging
— new imports for a known recipient are automatically tagged based on tag history for that payee
Rule-based tagging
— CSV rows are auto-tagged by keyword rules (LIDL → Groceries, NETFLIX → Subscriptions, etc.)
Deduplication display
— same-day same-amount records from SMS and CSV are shown as a single "SMS + CSV" row in the UI
Notification forwarding
— sends payment summaries to a configurable notifier service (Viber, etc.)
Tag management
— create, colour-code, and remove tags on individual transactions
Settings
— column visibility, source row colouring, table density, mobile layout, hide balance-alert SMS
Auth
— delegated to Authentik via NPM reverse proxy header injection; no local user accounts
Tech Stack
Tech Stack
Layer
Technology
Backend
Node.js 20, Express, Prisma 5
Database
PostgreSQL 16
CSV parsing
csv-parse
,
iconv-lite
(cp1251 + UTF-8 BOM)
File upload
multer
(memory storage)
Frontend
React 18, Vite, Tailwind CSS, Lucide React
Auth
Authentik forward-auth via
X-authentik-username
header
Container
Docker Compose
Layer
Backend
Database
CSV parsing
File upload
Frontend
Auth
Container
Technology
Node.js 20, Express, Prisma 5
PostgreSQL 16
csv-parse
,
iconv-lite
(cp1251 + UTF-8 BOM)
multer
(memory storage)
React 18, Vite, Tailwind CSS, Lucide React
Authentik forward-auth via
X-authentik-username
header
Docker Compose
Quick Start
Quick Start
1. Clone and configure
1. Clone and configure
git clone [EMAIL]:lakylak/finance-hub.git
cd
finance-hub
cp .env.example .env
# Edit .env — set DB_PASSWORD and notifier settings
2. Start
2. Start
docker compose up -d --build
Services:
Service
Default port
Frontend
5175
Backend API
3001
Adminer (DB UI)
8092
Service
Frontend
Backend API
Adminer (DB UI)
Default port
5175
3001
8092
3. First run
3. First run
Prisma migrations run automatically on backend startup via
prisma migrate deploy
.
Environment variables
Environment variables
Variable
Required
Description
DB_PASSWORD
Yes
PostgreSQL password
NOTIFIER_URL
No
Base URL of the notifier service
NOTIFIER_CHANNEL
No
Channel to use (
viber
, etc.) Default:
viber
NOTIFY_DEFAULT_PHONE
No
Phone number for payment notifications
TZ
No
Timezone for SMS date parsing. Default:
Europe/Sofia
BACKEND_PORT
No
Backend listen port. Default:
3001
FRONTEND_PORT
No
Frontend listen port. Default:
5175
DEV_BYPASS_AUTH
No
Set
true
to skip Authentik header check during local dev
Variable
DB_PASSWORD
NOTIFIER_URL
NOTIFIER_CHANNEL
NOTIFY_DEFAULT_PHONE
TZ
BACKEND_PORT
FRONTEND_PORT
DEV_BYPASS_AUTH
Required
Yes
No
No
No
No
No
No
No
Description
PostgreSQL password
Base URL of the notifier service
Channel to use (
viber
, etc.) Default:
viber
Phone number for payment notifications
Timezone for SMS date parsing. Default:
Europe/Sofia
Backend listen port. Default:
3001
Frontend listen port. Default:
5175
Set
true
to skip Authentik header check during local dev
Authentication
Authentication
In production, route the frontend through
Nginx Proxy Manager
with an
Authentik forward-auth
provider. NPM injects
X-authentik-username
(and optionally
X-authentik-email
,
X-authentik-groups
) into every proxied request.
The backend reads these headers and rejects requests without them (unless
DEV_BYPASS_AUTH=true
).
The public endpoints below are exempt and require no authentication:
GET /api/health
POST /api/payments/ingest
Sign-out link:
/outpost.goauthentik.io/sign_out
API Reference
API Reference
Health
Health
GET /api/health
GET /api/health
Public. Returns service status and database connection info.
{
"status"
:
"ok"
,
"timestamp"
:
"2026-05-09T10:00:00.000Z"
,
"storage"
:
{
"type"
:
"PostgreSQL"
,
"host"
:
"db"
,
"database"
:
"finance_hub"
}
}
Payments (Transaction Imports)
Payments (Transaction Imports)
Base path:
/api/payments
All endpoints require authentication except
/ingest
.
POST /api/payments/ingest
POST /api/payments/ingest
Public.
Ingest a DSK Bank SMS notification or structured (Apple Wallet) payment.
Rate limited to
200 requests/minute
.
Returns
409
if the exact message was already imported.
SMS body:
{
"message"
:
"DSK Bank. Na 08/05/2026 v 19:32 sa plateni 67.81 EUR s karta 400915***4447 na POS s adres: LIDL BALGARIYA EOOD, SOFIYA, BGR. Nalichni: 2011.57 EUR."
}
Structured (Apple Wallet) body:
{
"ingestMode"
:
"apple_wallet"
,
"amount"
:
12.50
,
"recipient"
:
"Starbucks"
,
"type"
:
"WALLET"
,
"card"
:
"4444"
,
"date"
:
"2026-05-09T10:30:00Z"
,
"balance"
:
null
}
Response:
201 Created
— the created import record.
Duplicate:
409 Conflict
—
{ "error": "Already imported", "existing": { ... } }
GET /api/payments
GET /api/payments
List transaction imports with filtering, sorting, and pagination.
Query parameters:
Parameter
Type
Description
page
int
Page number. Default:
1
limit
int
Records per page, max 200. Default:
50
sortBy
string
Field to sort by:
date
,
amount
,
balance
,
recipient
,
type
,
source
,
createdAt
,
status
. Default:
createdAt
sortDir
string
asc
or
desc
. Default:
desc
status
string
Filter by status:
UNPROCESSED
,
SENT
,
SKIPPED
source
string
Filter by source:
INGEST
,
UPLOAD
type
string
Filter by transaction type (e.g.
POS
,
ATM
)
tag
string
Filter by tag name
recipient
string
Case-insensitive substring match on recipient
search
string
Case-insensitive search across
rawMessage
and
recipient
dateFrom
date
ISO date string — inclusive start of date range
dateTo
date
ISO date string — inclusive end of date range (interpreted as end of day)
hideBalanceAlerts
boolean
true
to exclude balance-notification SMS and records with no parsed amount
Parameter
page
limit
sortBy
sortDir
status
source
type
tag
recipient
search
dateFrom
dateTo
hideBalanceAlerts...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"EU alternatives to github : r/github","depth":4,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"reddit.com","depth":4,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DNS / Nameservers | Hostinger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DNS / Nameservers | Hostinger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Nginx Proxy Manager","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"AFFiNE - All In One KnowledgeOS","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AFFiNE - All In One KnowledgeOS","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"All docs · AFFiNE","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"All docs · AFFiNE","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Payments Logger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Payments Logger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Your old PC can run Windows 11 in a VM, but not on bare metal - kovaliklukas@gmail.com - Gmail","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Your old PC can run Windows 11 in a VM, but not on bare metal - kovaliklukas@gmail.com - Gmail","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"(25) Quora","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"(25) Quora","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Location Logger","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Location Logger","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Finance Hub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Finance Hub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Select: transactions - db - Adminer","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Select: transactions - db - Adminer","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Електронно банкиране ДСК Директ от Банка ДСК","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Електронно банкиране ДСК Директ от Банка ДСК","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"VIVACOM","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"VIVACOM","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Смартфони с Unlimited план до 120 € отстъпка | Vivacom","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Смартфони с Unlimited план до 120 € отстъпка | Vivacom","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"VIVACOM","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"VIVACOM","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Claude Code | Claude Platform","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Claude Code | Claude Platform","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Claude","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Claude","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"EU alternatives to github : r/github","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"EU alternatives to github : r/github","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Dashboard","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Issues","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Issues","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull Requests","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull Requests","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Milestones","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Milestones","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Explore","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Explore","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Notifications","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"lakylak","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"lakylak","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"finance-hub","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"finance-hub","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Private","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"RSS Feed","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Unwatch","depth":7,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"1","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Star","depth":7,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"0","depth":7,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Fork","depth":8,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Fork","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Code","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Code","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Issues","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Issues","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Pull Requests","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull Requests","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Actions","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Actions","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Projects","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Projects","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Releases","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Releases","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Wiki","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Wiki","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Activity","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Activity","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Settings","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Settings","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"2 Commits","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"2","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Commits","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"1 Branch","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Branch","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"0 Tags","depth":8,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"0","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Tags","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"main","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Pull Request","depth":7,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXComboBox","text":"Go to file","depth":9,"on_screen":false,"help_text":"","role_description":"combo box","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":7,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Code","depth":9,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"9a757fe67e","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"9a757fe67e","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add README with project description, API reference, and usage guide","depth":11,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add README with project description, API reference, and usage guide","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"...","depth":10,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"41 minutes ago","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"backend","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"backend","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"frontend","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"frontend","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"scripts","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"scripts","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":".env.example","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":".env.example","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":".gitignore","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":".gitignore","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"docker-compose.yml","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"docker-compose.yml","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Initial commit: finance-hub unified finance app","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Initial commit: finance-hub unified finance app","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"44 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"README.md","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"README.md","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Add README with project description, API reference, and usage guide","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add README with project description, API reference, and usage guide","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"41 minutes ago","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"README.md Escape Edit File","depth":8,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXLink","text":"README.md","depth":10,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"README.md","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Escape","depth":9,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXLink","text":"Edit File","depth":9,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Finance Hub","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Finance Hub","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Unified personal finance tracker that consolidates DSK Bank SMS notifications and CSV statement exports into a single PostgreSQL database with a React review UI.","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Replaces two separate tools:","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"payments-logger","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— ingested DSK Bank SMS notifications","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dsk-uploader","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— parsed DSK Bank CSV exports and sent them to Notion","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Features","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Features","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SMS ingest","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— receives DSK Bank payment SMS via iOS Shortcuts / HTTP POST; parses amount, recipient, card, date, balance","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CSV upload","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— drag-and-drop DSK Bank account statement exports (BGN and EUR formats, cp1251 and UTF-8 encodings)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Deduplication","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— exact","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"rawMessage","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"match prevents duplicate imports from re-uploads or re-runs of the reimport script","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auto-tagging","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— new imports for a known recipient are automatically tagged based on tag history for that payee","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Rule-based tagging","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— CSV rows are auto-tagged by keyword rules (LIDL → Groceries, NETFLIX → Subscriptions, etc.)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Deduplication display","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— same-day same-amount records from SMS and CSV are shown as a single \"SMS + CSV\" row in the UI","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Notification forwarding","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— sends payment summaries to a configurable notifier service (Viber, etc.)","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Tag management","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— create, colour-code, and remove tags on individual transactions","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Settings","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— column visibility, source row colouring, table density, mobile layout, hide balance-alert SMS","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auth","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— delegated to Authentik via NPM reverse proxy header injection; no local user accounts","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Tech Stack","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Tech Stack","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Layer","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Technology","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Node.js 20, Express, Prisma 5","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Database","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PostgreSQL 16","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CSV parsing","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"csv-parse","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"iconv-lite","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(cp1251 + UTF-8 BOM)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"File upload","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"multer","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(memory storage)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"React 18, Vite, Tailwind CSS, Lucide React","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auth","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Authentik forward-auth via","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"X-authentik-username","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"header","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Container","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Docker Compose","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Layer","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Database","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CSV parsing","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"File upload","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Auth","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Container","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Technology","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Node.js 20, Express, Prisma 5","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PostgreSQL 16","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"csv-parse","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"iconv-lite","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(cp1251 + UTF-8 BOM)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"multer","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(memory storage)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"React 18, Vite, Tailwind CSS, Lucide React","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Authentik forward-auth via","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"X-authentik-username","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"header","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Docker Compose","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Quick Start","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Quick Start","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"1. Clone and configure","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1. Clone and configure","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"git clone git@gitea.com:lakylak/finance-hub.git","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"cd","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"finance-hub\ncp .env.example .env","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"# Edit .env — set DB_PASSWORD and notifier settings","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"2. Start","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2. Start","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"docker compose up -d --build","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Services:","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Service","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Default port","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5175","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend API","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3001","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Adminer (DB UI)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"8092","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Service","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend API","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Adminer (DB UI)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Default port","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5175","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3001","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"8092","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"3. First run","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3. First run","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Prisma migrations run automatically on backend startup via","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"prisma migrate deploy","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Environment variables","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Environment variables","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Variable","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Required","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Description","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DB_PASSWORD","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Yes","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PostgreSQL password","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFIER_URL","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Base URL of the notifier service","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFIER_CHANNEL","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Channel to use (","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"viber","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", etc.) Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"viber","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFY_DEFAULT_PHONE","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Phone number for payment notifications","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TZ","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Timezone for SMS date parsing. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Europe/Sofia","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"BACKEND_PORT","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend listen port. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3001","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"FRONTEND_PORT","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend listen port. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5175","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEV_BYPASS_AUTH","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Set","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"true","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to skip Authentik header check during local dev","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Variable","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DB_PASSWORD","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFIER_URL","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFIER_CHANNEL","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"NOTIFY_DEFAULT_PHONE","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"TZ","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"BACKEND_PORT","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"FRONTEND_PORT","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEV_BYPASS_AUTH","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Required","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Yes","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"No","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Description","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PostgreSQL password","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Base URL of the notifier service","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Channel to use (","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"viber","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", etc.) Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"viber","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Phone number for payment notifications","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Timezone for SMS date parsing. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Europe/Sofia","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Backend listen port. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"3001","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Frontend listen port. Default:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5175","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Set","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"true","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to skip Authentik header check during local dev","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Authentication","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Authentication","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"In production, route the frontend through","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nginx Proxy Manager","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"with an","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Authentik forward-auth","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"provider. NPM injects","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"X-authentik-username","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"(and optionally","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"X-authentik-email","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"X-authentik-groups","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":") into every proxied request.","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"The backend reads these headers and rejects requests without them (unless","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEV_BYPASS_AUTH=true","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":").","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"The public endpoints below are exempt and require no authentication:","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"GET /api/health","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"POST /api/payments/ingest","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Sign-out link:","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/outpost.goauthentik.io/sign_out","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"API Reference","depth":9,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"API Reference","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Health","depth":9,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Health","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"GET /api/health","depth":9,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"GET /api/health","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Public. Returns service status and database connection info.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"status\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"ok\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"timestamp\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"2026-05-09T10:00:00.000Z\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"storage\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"type\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"PostgreSQL\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"host\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"db\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"database\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"finance_hub\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"}","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"}","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Payments (Transaction Imports)","depth":9,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Payments (Transaction Imports)","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Base path:","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/api/payments","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"All endpoints require authentication except","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/ingest","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"POST /api/payments/ingest","depth":9,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"POST /api/payments/ingest","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Public.","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Ingest a DSK Bank SMS notification or structured (Apple Wallet) payment.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Rate limited to","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"200 requests/minute","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":".","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Returns","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"409","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"if the exact message was already imported.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SMS body:","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"message\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"DSK Bank. Na 08/05/2026 v 19:32 sa plateni 67.81 EUR s karta 400915***4447 na POS s adres: LIDL BALGARIYA EOOD, SOFIYA, BGR. Nalichni: 2011.57 EUR.\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"}","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Structured (Apple Wallet) body:","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"ingestMode\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"apple_wallet\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"amount\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"12.50","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"recipient\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"Starbucks\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"type\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"WALLET\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"card\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"4444\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"date\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"2026-05-09T10:30:00Z\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"\"balance\"","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"null","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"}","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Response:","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"201 Created","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"— the created import record.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Duplicate:","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"409 Conflict","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"—","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"{ \"error\": \"Already imported\", \"existing\": { ... } }","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"GET /api/payments","depth":9,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"GET /api/payments","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"List transaction imports with filtering, sorting, and pagination.","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Query parameters:","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Parameter","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Type","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Description","depth":12,"bounds":{"left":0.034027778,"top":0.0,"width":0.060763888,"height":0.02111111},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"page","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"int","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Page number. Default:","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":13,"bounds":{"left":0.016666668,"top":0.0,"width":0.0055555557,"height":0.018333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"int","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Records per page, max 200. Default:","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"50","depth":13,"bounds":{"left":0.08958333,"top":0.0,"width":0.011458334,"height":0.018333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sortBy","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Field to sort by:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"date","depth":13,"bounds":{"left":0.0,"top":0.037777778,"width":0.022569444,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.009722223,"top":0.035,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"amount","depth":13,"bounds":{"left":0.019791666,"top":0.037777778,"width":0.034027778,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.057638887,"top":0.035,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"balance","depth":13,"bounds":{"left":0.067708336,"top":0.037777778,"width":0.039583333,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.11111111,"top":0.035,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":13,"bounds":{"left":0.12118056,"top":0.037777778,"width":0.051041666,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.17604166,"top":0.035,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"type","depth":13,"bounds":{"left":0.18611111,"top":0.037777778,"width":0.022569444,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.2125,"top":0.035,"width":0.003125,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"source","depth":13,"bounds":{"left":0.0,"top":0.064444445,"width":0.034027778,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.0,"top":0.061666667,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"createdAt","depth":13,"bounds":{"left":0.0,"top":0.064444445,"width":0.05138889,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.004513889,"top":0.061666667,"width":0.005902778,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"status","depth":13,"bounds":{"left":0.014236111,"top":0.064444445,"width":0.034027778,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". Default:","depth":12,"bounds":{"left":0.052083332,"top":0.061666667,"width":0.048611112,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"createdAt","depth":13,"bounds":{"left":0.10451389,"top":0.064444445,"width":0.05138889,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sortDir","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"asc","depth":13,"bounds":{"left":0.0,"top":0.10555556,"width":0.017013889,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"or","depth":12,"bounds":{"left":0.0,"top":0.10277778,"width":0.016319444,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"desc","depth":13,"bounds":{"left":0.0,"top":0.10555556,"width":0.022916667,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":". Default:","depth":12,"bounds":{"left":0.0,"top":0.10277778,"width":0.048611112,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"desc","depth":13,"bounds":{"left":0.021527778,"top":0.10555556,"width":0.022916667,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"status","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Filter by status:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"UNPROCESSED","depth":13,"bounds":{"left":0.0,"top":0.14666666,"width":0.0625,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.048611112,"top":0.14388889,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SENT","depth":13,"bounds":{"left":0.058680557,"top":0.14666666,"width":0.022569444,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.08506945,"top":0.14388889,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"SKIPPED","depth":13,"bounds":{"left":0.09513889,"top":0.14666666,"width":0.039583333,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"source","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Filter by source:","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"INGEST","depth":13,"bounds":{"left":0.0,"top":0.18777777,"width":0.034375,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.023263888,"top":0.185,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"UPLOAD","depth":13,"bounds":{"left":0.033333335,"top":0.18777777,"width":0.034027778,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"type","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Filter by transaction type (e.g.","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"POS","depth":13,"bounds":{"left":0.05590278,"top":0.22888888,"width":0.017013889,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":",","depth":12,"bounds":{"left":0.07673611,"top":0.22611111,"width":0.00625,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"ATM","depth":13,"bounds":{"left":0.08680555,"top":0.22888888,"width":0.017013889,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":")","depth":12,"bounds":{"left":0.10763889,"top":0.22611111,"width":0.0038194444,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"tag","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Filter by tag name","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Case-insensitive substring match on recipient","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"search","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"string","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Case-insensitive search across","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"rawMessage","depth":13,"bounds":{"left":0.061458334,"top":0.35222223,"width":0.05659722,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"and","depth":12,"bounds":{"left":0.121875,"top":0.34944445,"width":0.025,"height":0.02111111},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":13,"bounds":{"left":0.15069444,"top":0.35222223,"width":0.051041666,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dateFrom","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"date","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"ISO date string — inclusive start of date range","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dateTo","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"date","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"ISO date string — inclusive end of date range (interpreted as end of day)","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"hideBalanceAlerts","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"boolean","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"true","depth":13,"bounds":{"left":0.0,"top":0.50222224,"width":0.022569444,"height":0.018333333},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"to exclude balance-notification SMS and records with no parsed amount","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Parameter","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"page","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"limit","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sortBy","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"sortDir","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"status","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"source","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"type","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"tag","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"recipient","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"search","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dateFrom","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"dateTo","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"hideBalanceAlerts","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-8880139889786857011
|
4487437056102435094
|
click
|
accessibility
|
NULL
|
EU alternatives to github : r/github
reddit.com
Pu EU alternatives to github : r/github
reddit.com
Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Your old PC can run Windows 11 in a VM, but not on bare metal - [EMAIL] - Gmail
Your old PC can run Windows 11 in a VM, but not on bare metal - [EMAIL] - Gmail
(25) Quora
(25) Quora
Location Logger
Location Logger
Finance Hub
Finance Hub
Finance Hub
Finance Hub
Select: transactions - db - Adminer
Select: transactions - db - Adminer
Електронно банкиране ДСК Директ от Банка ДСК
Електронно банкиране ДСК Директ от Банка ДСК
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
Stop Losing Notes: Pick A Cross-Device App That Syncs | AFFiNE
VIVACOM
VIVACOM
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
Смартфони с Unlimited план до 120 € отстъпка | Vivacom
VIVACOM
VIVACOM
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Смартфон SAMSUNG GALAXY A57 5G 256GB | Vivacom
Claude Code | Claude Platform
Claude Code | Claude Platform
Claude
Claude
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
Close tab
EU alternatives to github : r/github
EU alternatives to github : r/github
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Dashboard
Issues
Issues
Pull Requests
Pull Requests
Milestones
Milestones
Explore
Explore
Notifications
lakylak
lakylak
/
finance-hub
finance-hub
Private
RSS Feed
Unwatch
1
1
Star
0
0
Fork
Fork
0
0
Code
Code
Issues
Issues
Pull Requests
Pull Requests
Actions
Actions
Projects
Projects
Releases
Releases
Wiki
Wiki
Activity
Activity
Settings
Settings
2 Commits
2
Commits
1 Branch
1
Branch
0 Tags
0
Tags
main
New Pull Request
Go to file
Code
Code
Lukas Kovalik
9a757fe67e
9a757fe67e
Add README with project description, API reference, and usage guide
Add README with project description, API reference, and usage guide
...
41 minutes ago
backend
backend
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
frontend
frontend
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
scripts
scripts
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
.env.example
.env.example
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
.gitignore
.gitignore
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
docker-compose.yml
docker-compose.yml
Initial commit: finance-hub unified finance app
Initial commit: finance-hub unified finance app
44 minutes ago
README.md
README.md
Add README with project description, API reference, and usage guide
Add README with project description, API reference, and usage guide
41 minutes ago
README.md Escape Edit File
README.md
README.md
Escape
Edit File
Finance Hub
Finance Hub
Unified personal finance tracker that consolidates DSK Bank SMS notifications and CSV statement exports into a single PostgreSQL database with a React review UI.
Replaces two separate tools:
payments-logger
— ingested DSK Bank SMS notifications
dsk-uploader
— parsed DSK Bank CSV exports and sent them to Notion
Features
Features
SMS ingest
— receives DSK Bank payment SMS via iOS Shortcuts / HTTP POST; parses amount, recipient, card, date, balance
CSV upload
— drag-and-drop DSK Bank account statement exports (BGN and EUR formats, cp1251 and UTF-8 encodings)
Deduplication
— exact
rawMessage
match prevents duplicate imports from re-uploads or re-runs of the reimport script
Auto-tagging
— new imports for a known recipient are automatically tagged based on tag history for that payee
Rule-based tagging
— CSV rows are auto-tagged by keyword rules (LIDL → Groceries, NETFLIX → Subscriptions, etc.)
Deduplication display
— same-day same-amount records from SMS and CSV are shown as a single "SMS + CSV" row in the UI
Notification forwarding
— sends payment summaries to a configurable notifier service (Viber, etc.)
Tag management
— create, colour-code, and remove tags on individual transactions
Settings
— column visibility, source row colouring, table density, mobile layout, hide balance-alert SMS
Auth
— delegated to Authentik via NPM reverse proxy header injection; no local user accounts
Tech Stack
Tech Stack
Layer
Technology
Backend
Node.js 20, Express, Prisma 5
Database
PostgreSQL 16
CSV parsing
csv-parse
,
iconv-lite
(cp1251 + UTF-8 BOM)
File upload
multer
(memory storage)
Frontend
React 18, Vite, Tailwind CSS, Lucide React
Auth
Authentik forward-auth via
X-authentik-username
header
Container
Docker Compose
Layer
Backend
Database
CSV parsing
File upload
Frontend
Auth
Container
Technology
Node.js 20, Express, Prisma 5
PostgreSQL 16
csv-parse
,
iconv-lite
(cp1251 + UTF-8 BOM)
multer
(memory storage)
React 18, Vite, Tailwind CSS, Lucide React
Authentik forward-auth via
X-authentik-username
header
Docker Compose
Quick Start
Quick Start
1. Clone and configure
1. Clone and configure
git clone [EMAIL]:lakylak/finance-hub.git
cd
finance-hub
cp .env.example .env
# Edit .env — set DB_PASSWORD and notifier settings
2. Start
2. Start
docker compose up -d --build
Services:
Service
Default port
Frontend
5175
Backend API
3001
Adminer (DB UI)
8092
Service
Frontend
Backend API
Adminer (DB UI)
Default port
5175
3001
8092
3. First run
3. First run
Prisma migrations run automatically on backend startup via
prisma migrate deploy
.
Environment variables
Environment variables
Variable
Required
Description
DB_PASSWORD
Yes
PostgreSQL password
NOTIFIER_URL
No
Base URL of the notifier service
NOTIFIER_CHANNEL
No
Channel to use (
viber
, etc.) Default:
viber
NOTIFY_DEFAULT_PHONE
No
Phone number for payment notifications
TZ
No
Timezone for SMS date parsing. Default:
Europe/Sofia
BACKEND_PORT
No
Backend listen port. Default:
3001
FRONTEND_PORT
No
Frontend listen port. Default:
5175
DEV_BYPASS_AUTH
No
Set
true
to skip Authentik header check during local dev
Variable
DB_PASSWORD
NOTIFIER_URL
NOTIFIER_CHANNEL
NOTIFY_DEFAULT_PHONE
TZ
BACKEND_PORT
FRONTEND_PORT
DEV_BYPASS_AUTH
Required
Yes
No
No
No
No
No
No
No
Description
PostgreSQL password
Base URL of the notifier service
Channel to use (
viber
, etc.) Default:
viber
Phone number for payment notifications
Timezone for SMS date parsing. Default:
Europe/Sofia
Backend listen port. Default:
3001
Frontend listen port. Default:
5175
Set
true
to skip Authentik header check during local dev
Authentication
Authentication
In production, route the frontend through
Nginx Proxy Manager
with an
Authentik forward-auth
provider. NPM injects
X-authentik-username
(and optionally
X-authentik-email
,
X-authentik-groups
) into every proxied request.
The backend reads these headers and rejects requests without them (unless
DEV_BYPASS_AUTH=true
).
The public endpoints below are exempt and require no authentication:
GET /api/health
POST /api/payments/ingest
Sign-out link:
/outpost.goauthentik.io/sign_out
API Reference
API Reference
Health
Health
GET /api/health
GET /api/health
Public. Returns service status and database connection info.
{
"status"
:
"ok"
,
"timestamp"
:
"2026-05-09T10:00:00.000Z"
,
"storage"
:
{
"type"
:
"PostgreSQL"
,
"host"
:
"db"
,
"database"
:
"finance_hub"
}
}
Payments (Transaction Imports)
Payments (Transaction Imports)
Base path:
/api/payments
All endpoints require authentication except
/ingest
.
POST /api/payments/ingest
POST /api/payments/ingest
Public.
Ingest a DSK Bank SMS notification or structured (Apple Wallet) payment.
Rate limited to
200 requests/minute
.
Returns
409
if the exact message was already imported.
SMS body:
{
"message"
:
"DSK Bank. Na 08/05/2026 v 19:32 sa plateni 67.81 EUR s karta 400915***4447 na POS s adres: LIDL BALGARIYA EOOD, SOFIYA, BGR. Nalichni: 2011.57 EUR."
}
Structured (Apple Wallet) body:
{
"ingestMode"
:
"apple_wallet"
,
"amount"
:
12.50
,
"recipient"
:
"Starbucks"
,
"type"
:
"WALLET"
,
"card"
:
"4444"
,
"date"
:
"2026-05-09T10:30:00Z"
,
"balance"
:
null
}
Response:
201 Created
— the created import record.
Duplicate:
409 Conflict
—
{ "error": "Already imported", "existing": { ... } }
GET /api/payments
GET /api/payments
List transaction imports with filtering, sorting, and pagination.
Query parameters:
Parameter
Type
Description
page
int
Page number. Default:
1
limit
int
Records per page, max 200. Default:
50
sortBy
string
Field to sort by:
date
,
amount
,
balance
,
recipient
,
type
,
source
,
createdAt
,
status
. Default:
createdAt
sortDir
string
asc
or
desc
. Default:
desc
status
string
Filter by status:
UNPROCESSED
,
SENT
,
SKIPPED
source
string
Filter by source:
INGEST
,
UPLOAD
type
string
Filter by transaction type (e.g.
POS
,
ATM
)
tag
string
Filter by tag name
recipient
string
Case-insensitive substring match on recipient
search
string
Case-insensitive search across
rawMessage
and
recipient
dateFrom
date
ISO date string — inclusive start of date range
dateTo
date
ISO date string — inclusive end of date range (interpreted as end of day)
hideBalanceAlerts
boolean
true
to exclude balance-notification SMS and records with no parsed amount
Parameter
page
limit
sortBy
sortDir
status
source
type
tag
recipient
search
dateFrom
dateTo
hideBalanceAlerts...
|
13821
|
NULL
|
NULL
|
NULL
|
|
14460
|
643
|
22
|
2026-05-09T17:59:05.311349+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-09/1778 /Users/lukas/.screenpipe/data/data/2026-05-09/1778349545311_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
CodeFileEditSelectionView→Nginx Proxy Manager@Scre CodeFileEditSelectionView→Nginx Proxy Manager@Screenpipe - ArchiveSQLite Web: archive.dbSQLite Web: db.sqlitescreenpipe/.claude/skills at main - screenpipe/screenpDXP4800PLUS-B5F8AFFiNE - All In One KnowledgeOSAll docs • AFFiNEPayments LoggerM Your old PC can run Windows 11 in a VM, but not on baLocation LoggerFinance HubFinance HubSelect: transactions - db - AdminerClaude Code | Claude Platform* April 2026 spending by category - Claudelakylak/finance-hub - finance-hub - Gitea: Git with a c• Applications - Admin - authentik+ New TabGoRunTerminalWindow Helpnas.lakylak.xyz/desktop/?os=ugospro#/FilesControl PanelPRI8StorageApp Center/logsLogsSupportQ Search|Connection & AccessUser ManagementFile ServiceW DeviceConnectionDomain/LDAP• TerminalGeneral|Hardware & PowerTime & Language- NetworkSecurityE Indexing ServiceService• About‹ $0lhlLACPUE: Control Panel100% C8Sat 9 May 20:59:06AM F1OZZEI=?TelnetSSHEnablePort 23|Advanced settings• EnablePort22Shut down automatically3h later2026-05-09 21:18 will automatically shut downAdvanced settingsFunction descriptionyaswvord fir tie logi accoum ange nabie aute ok eo enhanice sysfeum secuitis recomended to sel a strongApply...
|
NULL
|
-8879547270676129986
|
NULL
|
visual_change
|
ocr
|
NULL
|
CodeFileEditSelectionView→Nginx Proxy Manager@Scre CodeFileEditSelectionView→Nginx Proxy Manager@Screenpipe - ArchiveSQLite Web: archive.dbSQLite Web: db.sqlitescreenpipe/.claude/skills at main - screenpipe/screenpDXP4800PLUS-B5F8AFFiNE - All In One KnowledgeOSAll docs • AFFiNEPayments LoggerM Your old PC can run Windows 11 in a VM, but not on baLocation LoggerFinance HubFinance HubSelect: transactions - db - AdminerClaude Code | Claude Platform* April 2026 spending by category - Claudelakylak/finance-hub - finance-hub - Gitea: Git with a c• Applications - Admin - authentik+ New TabGoRunTerminalWindow Helpnas.lakylak.xyz/desktop/?os=ugospro#/FilesControl PanelPRI8StorageApp Center/logsLogsSupportQ Search|Connection & AccessUser ManagementFile ServiceW DeviceConnectionDomain/LDAP• TerminalGeneral|Hardware & PowerTime & Language- NetworkSecurityE Indexing ServiceService• About‹ $0lhlLACPUE: Control Panel100% C8Sat 9 May 20:59:06AM F1OZZEI=?TelnetSSHEnablePort 23|Advanced settings• EnablePort22Shut down automatically3h later2026-05-09 21:18 will automatically shut downAdvanced settingsFunction descriptionyaswvord fir tie logi accoum ange nabie aute ok eo enhanice sysfeum secuitis recomended to sel a strongApply...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17371
|
770
|
17
|
2026-05-11T10:19:36.056931+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778494776056_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
67
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Show Replace Field
Search History
Received 429 from API
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/5
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"67","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (429) from HubSpot by translating it\n * into a RateLimitException carrying retry_after.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'policy' => $this->parsePolicy($e),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n// } catch (RateLimitException $e) {\n// throw $e;\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Replace Field","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Received 429 from API","depth":4,"on_screen":true,"value":"Received 429 from API","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1/5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8879074725484226535
|
5225837844252985444
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
67
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
// } catch (RateLimitException $e) {
// throw $e;
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Show Replace Field
Search History
Received 429 from API
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/5
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
1315
|
52
|
6
|
2026-05-07T08:29:25.957737+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778142565957_m2.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
13 matches in 7 files
File mask:
*.p Find in Files
13 matches in 7 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
RateLimitAware
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/tests/Unit/Services/Crm
/Users/lukas/jiminny/app/app/Services/Activity
/Users/lukas/jiminny/app/app/Services/Calendar/Command
/Users/lukas/jiminny/app/.idea/queries
/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm
/Users/lukas/jiminny/app/vendor/hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields
/Users/lukas/jiminny/app/app/Services/Crm/Copper
/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn
/Users/lukas/jiminny/app/app/Notifications/Channels
/Users/lukas/jiminny/app/tests/Unit
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Interactions/Settings/Teams
/Users/lukas/jiminny/app/app/Exceptions/Crm
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/vendor/hubspot/hubspot-php/src/Endpoints
/Users/lukas/jiminny/app/config
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Redis/Connections
/Users/lukas/jiminny/app/app/Http/Controllers/Settings/Teams
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook/Traits
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Broadcasting
/Users/lukas/jiminny/app/app/Component/FeatureFlags
/Users/lukas/jiminny/app/app/Component/Activity
/Users/lukas/jiminny/app/app/Component/ActivitySearch
/Users/lukas/jiminny/app/tests/Unit/Events/Activities/Crm
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Pagination
/Users/lukas/jiminny/app/app/Console/Commands/Dev
/Users/lukas/jiminny/app/front-end
/Users/lukas/jiminny/app/app/Component/Prophet
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Component/AskAnything
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/OnDemandLevel/Events
/Users/lukas/jiminny/app/app/Component/AskAnything/Events
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/DealLevel/Traits
/Users/lukas/jiminny/app/app/Component/ProphetAi
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/d1e2c340-64e9-49c6-aa9a-196201874532
/Users/lukas/jiminny/app/app/Http/Controllers/API/Page
/Users/lukas/jiminny/app/front-end/src/components/ondemand/ActivityList
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/5b1549d5-9876-4d9e-9ce3-025f12a83283
/Users/lukas/jiminny/app/app/Contracts/Repositories
/Users/lukas/jiminny/app/app/Http/Controllers/Kiosk
/Users/lukas/jiminny/app/app/Component/AiAutomation/Actions
/Users/lukas/jiminny/app/app/Services/Activity/HubSpot
/Users/lukas/jiminny/app/app/Services/Crm/Pipedrive
/Users/lukas/jiminny/app/app/Jobs/Activity/Import
/Users/lukas/jiminny/app/app/Events/Import...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"bounds":{"left":0.2992021,"top":0.12609737,"width":0.024601065,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"13 matches in 7 files","depth":1,"bounds":{"left":0.32779256,"top":0.12609737,"width":0.04155585,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"bounds":{"left":0.5315825,"top":0.12290503,"width":0.029587766,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"bounds":{"left":0.5621675,"top":0.11971269,"width":0.027925532,"height":0.027134877},"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"bounds":{"left":0.5661569,"top":0.12609737,"width":0.011635638,"height":0.013567438},"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"bounds":{"left":0.5944149,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"bounds":{"left":0.6037234,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"bounds":{"left":0.2962101,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"RateLimitAware","depth":2,"bounds":{"left":0.30718085,"top":0.15403032,"width":0.26196808,"height":0.017557861},"on_screen":true,"value":"RateLimitAware","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.578125,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"bounds":{"left":0.5880984,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"bounds":{"left":0.59674203,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"bounds":{"left":0.60538566,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"bounds":{"left":0.2992021,"top":0.1867518,"width":0.022938829,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"bounds":{"left":0.32214096,"top":0.1867518,"width":0.019281914,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"bounds":{"left":0.3414229,"top":0.1867518,"width":0.022606382,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"bounds":{"left":0.36402926,"top":0.1867518,"width":0.017287234,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.099734046,"height":0.0},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.1974734,"height":0.0},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Component/Queue/Job","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Delete","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Providers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playbooks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/resources/views/emails/reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Mail/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/routes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/database/migrations","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/V2","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Policies","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Helpers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/storage/logs","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Internal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Transcription","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Observers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Mail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/User","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Activity/Event","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Sidekick","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/MeetingBot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/RingCentral","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/Gmail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Mailbox","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end/src/composables","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Calendars","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Transcription/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Calendar/Command","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/.idea/queries","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Copper","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Notifications/Channels","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Interactions/Settings/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot/hubspot-php/src/Endpoints","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/config","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Redis/Connections","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Settings/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Broadcasting","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/FeatureFlags","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Pagination","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Dev","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Prophet","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskAnything","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/OnDemandLevel/Events","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskAnything/Events","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/DealLevel/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ProphetAi","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/d1e2c340-64e9-49c6-aa9a-196201874532","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/Page","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end/src/components/ondemand/ActivityList","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/5b1549d5-9876-4d9e-9ce3-025f12a83283","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Contracts/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Kiosk","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AiAutomation/Actions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/HubSpot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Pipedrive","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/Import","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Import","depth":6,"on_screen":false,"role_description":"text"}]...
|
-8878498542412598945
|
-3320476524101933117
|
click
|
accessibility
|
NULL
|
Find in Files
13 matches in 7 files
File mask:
*.p Find in Files
13 matches in 7 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
RateLimitAware
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/tests/Unit/Services/Crm
/Users/lukas/jiminny/app/app/Services/Activity
/Users/lukas/jiminny/app/app/Services/Calendar/Command
/Users/lukas/jiminny/app/.idea/queries
/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm
/Users/lukas/jiminny/app/vendor/hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields
/Users/lukas/jiminny/app/app/Services/Crm/Copper
/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn
/Users/lukas/jiminny/app/app/Notifications/Channels
/Users/lukas/jiminny/app/tests/Unit
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Interactions/Settings/Teams
/Users/lukas/jiminny/app/app/Exceptions/Crm
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/vendor/hubspot/hubspot-php/src/Endpoints
/Users/lukas/jiminny/app/config
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Redis/Connections
/Users/lukas/jiminny/app/app/Http/Controllers/Settings/Teams
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook/Traits
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Broadcasting
/Users/lukas/jiminny/app/app/Component/FeatureFlags
/Users/lukas/jiminny/app/app/Component/Activity
/Users/lukas/jiminny/app/app/Component/ActivitySearch
/Users/lukas/jiminny/app/tests/Unit/Events/Activities/Crm
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Pagination
/Users/lukas/jiminny/app/app/Console/Commands/Dev
/Users/lukas/jiminny/app/front-end
/Users/lukas/jiminny/app/app/Component/Prophet
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Component/AskAnything
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/OnDemandLevel/Events
/Users/lukas/jiminny/app/app/Component/AskAnything/Events
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/DealLevel/Traits
/Users/lukas/jiminny/app/app/Component/ProphetAi
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/d1e2c340-64e9-49c6-aa9a-196201874532
/Users/lukas/jiminny/app/app/Http/Controllers/API/Page
/Users/lukas/jiminny/app/front-end/src/components/ondemand/ActivityList
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/5b1549d5-9876-4d9e-9ce3-025f12a83283
/Users/lukas/jiminny/app/app/Contracts/Repositories
/Users/lukas/jiminny/app/app/Http/Controllers/Kiosk
/Users/lukas/jiminny/app/app/Component/AiAutomation/Actions
/Users/lukas/jiminny/app/app/Services/Activity/HubSpot
/Users/lukas/jiminny/app/app/Services/Crm/Pipedrive
/Users/lukas/jiminny/app/app/Jobs/Activity/Import
/Users/lukas/jiminny/app/app/Events/Import...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
1500
|
60
|
0
|
2026-05-07T08:48:07.429366+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778143687429_m2.jpg...
|
PhpStorm
|
faVsco.js – DeleteCrmEntityTrait.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Code changed:
Hide
Sync Changes
Hide This Notification
37
1
35
64
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Crm\Delete;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Jiminny\Enums\CrmObject;
use Jiminny\Events\Crm\DetachActivityObject;
use Jiminny\Models\Activity;
use Psr\Log\LoggerInterface;
use Throwable;
trait DeleteCrmEntityTrait
{
public int $tries = 3;
public function timeout(): int
{
return 300; // 5 minutes
}
public function backoff(): array
{
return [30, 90, 180]; // 30 seconds, 1.5 minutes, 3 minutes
}
protected function handleActivities(
Collection $activities,
Dispatcher $dispatcher,
LoggerInterface $logger,
bool $emitEvent = true,
): void {
if ($activities->isEmpty()) {
return;
}
$crmObject = $this->getEntityType();
$entityIdField = $crmObject->value . '_id';
$activities->each(
function (Activity $activity) use ($dispatcher, $logger, $entityIdField, $crmObject, $emitEvent): void {
$stageId = $activity->getStage()?->getId();
$logData = [
$crmObject->value => $this->id,
'activity' => $activity->getId(),
'emitEvent' => $emitEvent,
];
$updateData = [
$entityIdField => null,
];
// For leads and opportunities, also nullify the stage_id
if ($stageId && in_array($crmObject, [CrmObject::LEAD, CrmObject::OPPORTUNITY], true)) {
$updateData['stage_id'] = null;
$logData['stage_id'] = $stageId;
}
$activity->update($updateData);
if ($emitEvent) {
$dispatcher->dispatch(new DetachActivityObject($activity, $crmObject));
}
$logger->info($this->getLogPrefix() . ' Detach from activity', $logData);
// Dispatch job to verify if CRM task/event still exists
if ($activity->hasCrmProviderId()) {
VerifyActivityCrmTaskJob::dispatch($activity->getId());
}
}
);
}
public function failed(Throwable $exception): void
{
$crmObject = $this->getEntityType();
Log::critical($this->getLogPrefix() . ' Job failed permanently', [
$crmObject->value => $this->id,
'exception' => $exception->getMessage(),
]);
}
// Abstract methods that must be implemented by the using class
abstract protected function getLogPrefix(): string;
abstract protected function getEntityType(): CrmObject;
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app
.circleci
.cursor
.github
.sonarlint
.vscode
.windsurf
app, sources root
Actions
Component
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php, class
HistoryService.php, class
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch, folder
Eloquent, folder
Encoding, folder
Encryption, folder
ES, folder
Faker, folder
FeatureFlags, folder
FFMpeg, folder
FileSystem, folder
Gecko, folder
Gong, folder
GuzzleHttp, folder
KeyPoints, folder
Kiosk, folder
LanguageDetection, folder
LiveFeed, folder
Locks, folder
Math, folder
MediaPipeline, folder...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.38297874,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.39162233,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.40259308,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.4112367,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.41988033,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.43085107,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4418218,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.46841756,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.4793883,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.65359044,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"37","depth":4,"bounds":{"left":0.62333775,"top":0.123703115,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6353058,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"35","depth":4,"bounds":{"left":0.64461434,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"64","depth":4,"bounds":{"left":0.6569149,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.66888297,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.6761968,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;","depth":4,"on_screen":true,"value":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.35738033,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.36702126,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.3743351,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Crm\\Delete;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Events\\Crm\\DetachActivityObject;\nuse Jiminny\\Models\\Activity;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\ntrait DeleteCrmEntityTrait\n{\n public int $tries = 3;\n\n public function timeout(): int\n {\n return 300; // 5 minutes\n }\n\n public function backoff(): array\n {\n return [30, 90, 180]; // 30 seconds, 1.5 minutes, 3 minutes\n }\n\n protected function handleActivities(\n Collection $activities,\n Dispatcher $dispatcher,\n LoggerInterface $logger,\n bool $emitEvent = true,\n ): void {\n if ($activities->isEmpty()) {\n return;\n }\n\n $crmObject = $this->getEntityType();\n $entityIdField = $crmObject->value . '_id';\n\n $activities->each(\n function (Activity $activity) use ($dispatcher, $logger, $entityIdField, $crmObject, $emitEvent): void {\n $stageId = $activity->getStage()?->getId();\n\n $logData = [\n $crmObject->value => $this->id,\n 'activity' => $activity->getId(),\n 'emitEvent' => $emitEvent,\n ];\n\n $updateData = [\n $entityIdField => null,\n ];\n\n // For leads and opportunities, also nullify the stage_id\n if ($stageId && in_array($crmObject, [CrmObject::LEAD, CrmObject::OPPORTUNITY], true)) {\n $updateData['stage_id'] = null;\n $logData['stage_id'] = $stageId;\n }\n\n $activity->update($updateData);\n\n if ($emitEvent) {\n $dispatcher->dispatch(new DetachActivityObject($activity, $crmObject));\n }\n\n $logger->info($this->getLogPrefix() . ' Detach from activity', $logData);\n\n // Dispatch job to verify if CRM task/event still exists\n if ($activity->hasCrmProviderId()) {\n VerifyActivityCrmTaskJob::dispatch($activity->getId());\n }\n }\n );\n }\n\n public function failed(Throwable $exception): void\n {\n $crmObject = $this->getEntityType();\n\n Log::critical($this->getLogPrefix() . ' Job failed permanently', [\n $crmObject->value => $this->id,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n // Abstract methods that must be implemented by the using class\n abstract protected function getLogPrefix(): string;\n abstract protected function getEntityType(): CrmObject;\n}","depth":4,"bounds":{"left":0.11968085,"top":0.17158818,"width":0.3081782,"height":0.8284118},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Crm\\Delete;\n\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Events\\Crm\\DetachActivityObject;\nuse Jiminny\\Models\\Activity;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\ntrait DeleteCrmEntityTrait\n{\n public int $tries = 3;\n\n public function timeout(): int\n {\n return 300; // 5 minutes\n }\n\n public function backoff(): array\n {\n return [30, 90, 180]; // 30 seconds, 1.5 minutes, 3 minutes\n }\n\n protected function handleActivities(\n Collection $activities,\n Dispatcher $dispatcher,\n LoggerInterface $logger,\n bool $emitEvent = true,\n ): void {\n if ($activities->isEmpty()) {\n return;\n }\n\n $crmObject = $this->getEntityType();\n $entityIdField = $crmObject->value . '_id';\n\n $activities->each(\n function (Activity $activity) use ($dispatcher, $logger, $entityIdField, $crmObject, $emitEvent): void {\n $stageId = $activity->getStage()?->getId();\n\n $logData = [\n $crmObject->value => $this->id,\n 'activity' => $activity->getId(),\n 'emitEvent' => $emitEvent,\n ];\n\n $updateData = [\n $entityIdField => null,\n ];\n\n // For leads and opportunities, also nullify the stage_id\n if ($stageId && in_array($crmObject, [CrmObject::LEAD, CrmObject::OPPORTUNITY], true)) {\n $updateData['stage_id'] = null;\n $logData['stage_id'] = $stageId;\n }\n\n $activity->update($updateData);\n\n if ($emitEvent) {\n $dispatcher->dispatch(new DetachActivityObject($activity, $crmObject));\n }\n\n $logger->info($this->getLogPrefix() . ' Detach from activity', $logData);\n\n // Dispatch job to verify if CRM task/event still exists\n if ($activity->hasCrmProviderId()) {\n VerifyActivityCrmTaskJob::dispatch($activity->getId());\n }\n }\n );\n }\n\n public function failed(Throwable $exception): void\n {\n $crmObject = $this->getEntityType();\n\n Log::critical($this->getLogPrefix() . ' Job failed permanently', [\n $crmObject->value => $this->id,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n // Abstract methods that must be implemented by the using class\n abstract protected function getLogPrefix(): string;\n abstract protected function getEntityType(): CrmObject;\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiActivityType, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoring, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnything, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dtos, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Events, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HistoryService.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskJiminnyAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AWS, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BillingManagement, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cache, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedback, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Country, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CustomerApi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Database, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Datadog, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealRisks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ElasticSearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Eloquent, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encoding, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encryption, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ES, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Faker, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlags, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FFMpeg, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FileSystem, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gecko, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gong, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GuzzleHttp, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KeyPoints, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Kiosk, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LanguageDetection, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LiveFeed, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Locks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Math, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MediaPipeline, folder","depth":9,"on_screen":false,"role_description":"text"}]...
|
-8877604480056397566
|
1065713855006914117
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Code changed:
Hide
Sync Changes
Hide This Notification
37
1
35
64
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Crm\Delete;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Jiminny\Enums\CrmObject;
use Jiminny\Events\Crm\DetachActivityObject;
use Jiminny\Models\Activity;
use Psr\Log\LoggerInterface;
use Throwable;
trait DeleteCrmEntityTrait
{
public int $tries = 3;
public function timeout(): int
{
return 300; // 5 minutes
}
public function backoff(): array
{
return [30, 90, 180]; // 30 seconds, 1.5 minutes, 3 minutes
}
protected function handleActivities(
Collection $activities,
Dispatcher $dispatcher,
LoggerInterface $logger,
bool $emitEvent = true,
): void {
if ($activities->isEmpty()) {
return;
}
$crmObject = $this->getEntityType();
$entityIdField = $crmObject->value . '_id';
$activities->each(
function (Activity $activity) use ($dispatcher, $logger, $entityIdField, $crmObject, $emitEvent): void {
$stageId = $activity->getStage()?->getId();
$logData = [
$crmObject->value => $this->id,
'activity' => $activity->getId(),
'emitEvent' => $emitEvent,
];
$updateData = [
$entityIdField => null,
];
// For leads and opportunities, also nullify the stage_id
if ($stageId && in_array($crmObject, [CrmObject::LEAD, CrmObject::OPPORTUNITY], true)) {
$updateData['stage_id'] = null;
$logData['stage_id'] = $stageId;
}
$activity->update($updateData);
if ($emitEvent) {
$dispatcher->dispatch(new DetachActivityObject($activity, $crmObject));
}
$logger->info($this->getLogPrefix() . ' Detach from activity', $logData);
// Dispatch job to verify if CRM task/event still exists
if ($activity->hasCrmProviderId()) {
VerifyActivityCrmTaskJob::dispatch($activity->getId());
}
}
);
}
public function failed(Throwable $exception): void
{
$crmObject = $this->getEntityType();
Log::critical($this->getLogPrefix() . ' Job failed permanently', [
$crmObject->value => $this->id,
'exception' => $exception->getMessage(),
]);
}
// Abstract methods that must be implemented by the using class
abstract protected function getLogPrefix(): string;
abstract protected function getEntityType(): CrmObject;
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app
.circleci
.cursor
.github
.sonarlint
.vscode
.windsurf
app, sources root
Actions
Component
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php, class
HistoryService.php, class
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch, folder
Eloquent, folder
Encoding, folder
Encryption, folder
ES, folder
Faker, folder
FeatureFlags, folder
FFMpeg, folder
FileSystem, folder
Gecko, folder
Gong, folder
GuzzleHttp, folder
KeyPoints, folder
Kiosk, folder
LanguageDetection, folder
LiveFeed, folder
Locks, folder
Math, folder
MediaPipeline, folder...
|
1498
|
NULL
|
NULL
|
NULL
|
|
541
|
21
|
40
|
2026-05-07T07:16:36.954608+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778138196954_m1.jpg...
|
Firefox
|
Screenpipe — Archive — Personal
|
True
|
http://192.168.0.242:8766
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
Hostinger login | Sign in to Hostinger account
Hostinger login | Sign in to Hostinger account
Login – Nginx Proxy Manager
Login – Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
Close tab
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Screenpipe [archive.db · 10616.9MB]
Screenpipe
[archive.db · 10616.9MB]
Activity
Search
Audio
Work Report
Timetable
AI Summary
Date
06
/
05
/
2026
Calendar
Monitor
Jump to
--
:
--
Go
APP TIMELINE · CLICK TO PLAY · DRAG SCROLLBAR TO PAN
−
1×
+
Follow
Follow
20:30
6 May 20:27 · iTerm2 / screenpipe"
⏮ 30s
◀ 10s
▶ Play
10s ▶
30s ⏭
20:27
iTerm2...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Hostinger login | Sign in to Hostinger account","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Hostinger login | Sign in to Hostinger account","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Login – Nginx Proxy Manager","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Login – Nginx Proxy Manager","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Screenpipe [archive.db · 10616.9MB]","depth":7,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screenpipe","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[archive.db · 10616.9MB]","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Activity","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Search","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Audio","depth":7,"on_screen":true,"help_text":"No audio data in this database","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Work Report","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Timetable","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"AI Summary","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Date","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"06","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"05","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2026","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Calendar","depth":8,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Monitor","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Jump to","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"--","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"--","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Go","depth":8,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"APP TIMELINE · CLICK TO PLAY · DRAG SCROLLBAR TO PAN","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"−","depth":9,"on_screen":true,"help_text":"Zoom out","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1×","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"+","depth":9,"on_screen":true,"help_text":"Zoom in","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Follow","depth":10,"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Follow","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"20:30","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"6 May 20:27 · iTerm2 / screenpipe\"","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏮ 30s","depth":9,"on_screen":true,"help_text":"Ctrl+←","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"◀ 10s","depth":9,"on_screen":true,"help_text":"←","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"▶ Play","depth":9,"on_screen":true,"help_text":"Space","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"10s ▶","depth":9,"on_screen":true,"help_text":"→","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"30s ⏭","depth":9,"on_screen":true,"help_text":"Ctrl+→","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20:27","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"iTerm2","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-8875948178524934281
|
-5309587093886106869
|
click
|
accessibility
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
Hostinger login | Sign in to Hostinger account
Hostinger login | Sign in to Hostinger account
Login – Nginx Proxy Manager
Login – Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
Close tab
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Screenpipe [archive.db · 10616.9MB]
Screenpipe
[archive.db · 10616.9MB]
Activity
Search
Audio
Work Report
Timetable
AI Summary
Date
06
/
05
/
2026
Calendar
Monitor
Jump to
--
:
--
Go
APP TIMELINE · CLICK TO PLAY · DRAG SCROLLBAR TO PAN
−
1×
+
Follow
Follow
20:30
6 May 20:27 · iTerm2 / screenpipe"
⏮ 30s
◀ 10s
▶ Play
10s ▶
30s ⏭
20:27
iTerm2...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
542
|
22
|
42
|
2026-05-07T07:16:36.967844+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778138196967_m2.jpg...
|
Firefox
|
Screenpipe — Archive — Personal
|
True
|
http://192.168.0.242:8766
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
Hostinger login | Sign in to Hostinger account
Hostinger login | Sign in to Hostinger account
Login – Nginx Proxy Manager
Login – Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
Close tab
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Screenpipe [archive.db · 10616.9MB]
Screenpipe
[archive.db · 10616.9MB]
Activity
Search
Audio
Work Report
Timetable
AI Summary
Date
06
/
05
/
2026
Calendar
Monitor
Jump to
--
:
--
Go
APP TIMELINE · CLICK TO PLAY · DRAG SCROLLBAR TO PAN
−
1×
+
Follow
Follow
20:30
6 May 20:27 · iTerm2 / screenpipe"
⏮ 30s
◀ 10s
▶ Play
10s ▶
30s ⏭
20:27
iTerm2...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.0518755,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pull requests · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.06304868,"width":0.080784574,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Hostinger login | Sign in to Hostinger account","depth":4,"bounds":{"left":0.0,"top":0.08459697,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Hostinger login | Sign in to Hostinger account","depth":5,"bounds":{"left":0.013297873,"top":0.09577015,"width":0.07895612,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Login – Nginx Proxy Manager","depth":4,"bounds":{"left":0.0,"top":0.11731844,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Login – Nginx Proxy Manager","depth":5,"bounds":{"left":0.013297873,"top":0.12849163,"width":0.05069814,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Screenpipe — Archive","depth":4,"bounds":{"left":0.0,"top":0.15003991,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXStaticText","text":"Screenpipe — Archive","depth":5,"bounds":{"left":0.013297873,"top":0.16121309,"width":0.037898935,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.10139628,"top":0.15722266,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"SQLite Web: archive.db","depth":4,"bounds":{"left":0.0,"top":0.18276137,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: archive.db","depth":5,"bounds":{"left":0.013297873,"top":0.19393456,"width":0.040724736,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SQLite Web: db.sqlite","depth":4,"bounds":{"left":0.0,"top":0.21548285,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SQLite Web: db.sqlite","depth":5,"bounds":{"left":0.013297873,"top":0.22665602,"width":0.03756649,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":4,"bounds":{"left":0.0,"top":0.2482043,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub","depth":5,"bounds":{"left":0.013297873,"top":0.25937748,"width":0.11469415,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"DXP4800PLUS-B5F8","depth":4,"bounds":{"left":0.0,"top":0.28092578,"width":0.113696806,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"DXP4800PLUS-B5F8","depth":5,"bounds":{"left":0.013297873,"top":0.29209897,"width":0.036901597,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.31524342,"width":0.108211435,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Bitwarden","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Screenpipe [archive.db · 10616.9MB]","depth":7,"bounds":{"left":0.12034574,"top":0.061452515,"width":0.06665558,"height":0.017956903},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Screenpipe","depth":8,"bounds":{"left":0.12034574,"top":0.06304868,"width":0.027759308,"height":0.014764565},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[archive.db · 10616.9MB]","depth":9,"bounds":{"left":0.14943483,"top":0.06703911,"width":0.03756649,"height":0.009976057},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Activity","depth":7,"bounds":{"left":0.19165559,"top":0.059856344,"width":0.024933511,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Search","depth":7,"bounds":{"left":0.21725398,"top":0.059856344,"width":0.023603724,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Audio","depth":7,"bounds":{"left":0.24152261,"top":0.059856344,"width":0.020944148,"height":0.0207502},"on_screen":true,"help_text":"No audio data in this database","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Work Report","depth":7,"bounds":{"left":0.26313165,"top":0.059856344,"width":0.03507314,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Timetable","depth":7,"bounds":{"left":0.29886967,"top":0.059856344,"width":0.029753989,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"AI Summary","depth":7,"bounds":{"left":0.32928857,"top":0.059856344,"width":0.034075797,"height":0.0207502},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Date","depth":8,"bounds":{"left":0.93849736,"top":0.0650439,"width":0.008144947,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"06","depth":9,"bounds":{"left":0.9552859,"top":0.06464485,"width":0.0048204786,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":8,"bounds":{"left":0.96110374,"top":0.06464485,"width":0.0023271276,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"05","depth":9,"bounds":{"left":0.9644282,"top":0.06464485,"width":0.0048204786,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":8,"bounds":{"left":0.970246,"top":0.06464485,"width":0.002493351,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2026","depth":9,"bounds":{"left":0.9737367,"top":0.06464485,"width":0.009474734,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Calendar","depth":8,"bounds":{"left":0.98454124,"top":0.0650439,"width":0.0051529254,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Monitor","depth":9,"bounds":{"left":0.34740692,"top":0.10853951,"width":0.013464096,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Jump to","depth":9,"bounds":{"left":0.7059508,"top":0.10853951,"width":0.01412899,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"--","depth":10,"bounds":{"left":0.72606385,"top":0.10814046,"width":0.0048204786,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":":","depth":9,"bounds":{"left":0.7318817,"top":0.10814046,"width":0.0023271276,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"--","depth":10,"bounds":{"left":0.7352061,"top":0.10814046,"width":0.0048204786,"height":0.011572227},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Go","depth":8,"bounds":{"left":0.75398934,"top":0.10454908,"width":0.012300532,"height":0.018754989},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"APP TIMELINE · CLICK TO PLAY · DRAG SCROLLBAR TO PAN","depth":10,"bounds":{"left":0.35239363,"top":0.14964086,"width":0.10571808,"height":0.009976057},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"−","depth":9,"bounds":{"left":0.7087766,"top":0.1452514,"width":0.009807181,"height":0.018754989},"on_screen":true,"help_text":"Zoom out","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"1×","depth":10,"bounds":{"left":0.72273934,"top":0.14924182,"width":0.004155585,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"+","depth":9,"bounds":{"left":0.7312167,"top":0.1452514,"width":0.009640957,"height":0.018754989},"on_screen":true,"help_text":"Zoom in","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Follow","depth":10,"bounds":{"left":0.74418217,"top":0.14924182,"width":0.004654255,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Follow","depth":10,"bounds":{"left":0.75016624,"top":0.14924182,"width":0.011136968,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"20:30","depth":13,"bounds":{"left":0.43085107,"top":0.21947326,"width":0.008477394,"height":0.008778931},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"6 May 20:27 · iTerm2 / screenpipe\"","depth":10,"bounds":{"left":0.35106382,"top":0.2661612,"width":0.06582447,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"⏮ 30s","depth":9,"bounds":{"left":0.35172874,"top":0.8882682,"width":0.023936171,"height":0.02434158},"on_screen":true,"help_text":"Ctrl+←","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"◀ 10s","depth":9,"bounds":{"left":0.37832448,"top":0.8886672,"width":0.02244016,"height":0.023942538},"on_screen":true,"help_text":"←","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"▶ Play","depth":9,"bounds":{"left":0.4034242,"top":0.8886672,"width":0.027925532,"height":0.023942538},"on_screen":true,"help_text":"Space","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"10s ▶","depth":9,"bounds":{"left":0.4340093,"top":0.8886672,"width":0.022273935,"height":0.023942538},"on_screen":true,"help_text":"→","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"30s ⏭","depth":9,"bounds":{"left":0.45894283,"top":0.8882682,"width":0.024102394,"height":0.02434158},"on_screen":true,"help_text":"Ctrl+→","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"20:27","depth":10,"bounds":{"left":0.75199467,"top":0.8950519,"width":0.009973404,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"iTerm2","depth":9,"bounds":{"left":0.35239363,"top":0.93056667,"width":0.011801862,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-8875948178524934281
|
-5309587093886106869
|
click
|
accessibility
|
NULL
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
Hostinger login | Sign in to Hostinger account
Hostinger login | Sign in to Hostinger account
Login – Nginx Proxy Manager
Login – Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
Close tab
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
screenpipe/.claude/skills at main · screenpipe/screenpipe · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Screenpipe [archive.db · 10616.9MB]
Screenpipe
[archive.db · 10616.9MB]
Activity
Search
Audio
Work Report
Timetable
AI Summary
Date
06
/
05
/
2026
Calendar
Monitor
Jump to
--
:
--
Go
APP TIMELINE · CLICK TO PLAY · DRAG SCROLLBAR TO PAN
−
1×
+
Follow
Follow
20:30
6 May 20:27 · iTerm2 / screenpipe"
⏮ 30s
◀ 10s
▶ Play
10s ▶
30s ⏭
20:27
iTerm2...
|
NULL
|
NULL
|
NULL
|
NULL
|