From 6d1b02454e046ab3b1f14c31f4959f43abe88f29 Mon Sep 17 00:00:00 2001 From: Kasim Necdet Percinel Date: Fri, 22 Aug 2025 16:43:14 +0000 Subject: [PATCH 1/6] add a convenience CORS backdoor for dev environments --- src/Config.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Config.php b/src/Config.php index 16e46f457..6fbe26b26 100644 --- a/src/Config.php +++ b/src/Config.php @@ -35,7 +35,15 @@ public function __construct($file) { if ( in_array('acao_url', array_keys($this->config)) ) { - if ( in_array('HTTP_ORIGIN', array_keys($_SERVER)) + // In dev environments, allow CORS from all origins + if ( in_array('app_env', array_keys($this->config)) + && str_starts_with($this->config['app_env'], 'dev') ) { + + header("Access-Control-Allow-Origin: *"); + header("Access-Control-Allow-Methods: ".$this->config['acam']); + header("Access-Control-Allow-Headers: Content-Type"); + + } elseif ( in_array('HTTP_ORIGIN', array_keys($_SERVER)) && in_array($_SERVER['HTTP_ORIGIN'], $this->config['acao_url']) ) { header("Access-Control-Allow-Origin: ".$_SERVER['HTTP_ORIGIN']); From 0e8961eb717a61e58d022f41cd74e5054634e418 Mon Sep 17 00:00:00 2001 From: Kasim Necdet Percinel Date: Fri, 22 Aug 2025 17:41:42 +0000 Subject: [PATCH 2/6] implement eventsapi client and the exceptions, use them for CCMC data in API --- src/Event/EventsApi.php | 91 ++++++++++++++++++++++++++++++++ src/Event/EventsApiException.php | 8 +++ src/Module/SolarEvents.php | 19 +++++++ 3 files changed, 118 insertions(+) create mode 100644 src/Event/EventsApi.php create mode 100644 src/Event/EventsApiException.php diff --git a/src/Event/EventsApi.php b/src/Event/EventsApi.php new file mode 100644 index 000000000..c7a116056 --- /dev/null +++ b/src/Event/EventsApi.php @@ -0,0 +1,91 @@ +client = $client ?? new Client(); + } + + /** + * Get events for a specific source + * + * @param DateTimeInterface $observationTime The observation time + * @param string $source The data source (e.g. "CCMC") + * @return array Array of event data + * @throws EventsApiException on API errors or unexpected responses + */ + public function getEventsForSource(DateTimeInterface $observationTime, string $source): array { + // Build the API URL: /api/v1/events/{source}/observation/{datetime} + $formattedTime = $observationTime->format('Y-m-d H:i:s'); + $encodedTime = urlencode($formattedTime); + + $url = $this->baseUrl . "/api/v1/events/{$source}/observation/{$encodedTime}"; + + Sentry::setContext('EventsApi', [ + 'url' => $url, + 'source' => $source, + 'observation_time' => $observationTime->format('Y-m-d\TH:i:s\Z') + ]); + + $response = $this->client->request('GET', $url, [ + 'timeout' => 30, + 'headers' => [ + 'Accept' => 'application/json', + 'User-Agent' => 'Helioviewer-API/2.0' + ] + ]); + + return $this->parseResponse($response); + } + + /** + * Parse the HTTP response and decode JSON + * + * @param \Psr\Http\Message\ResponseInterface $response + * @return array + * @throws EventsApiException if JSON decoding fails or response format is unexpected + */ + private function parseResponse($response): array + { + $body = (string)$response->getBody(); + $data = json_decode($body, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + Sentry::setContext('EventsApi', [ + 'raw_response' => $body, + 'json_error' => json_last_error_msg(), + 'response_status' => $response->getStatusCode() + ]); + + throw new EventsApiException("Failed to decode JSON response: " . json_last_error_msg()); + } + + if (!is_array($data)) { + Sentry::setContext('EventsApi', [ + 'unexpected_response_type' => gettype($data), + 'raw_response' => $body, + 'response_status' => $response->getStatusCode() + ]); + + throw new EventsApiException("Unexpected response format: expected array, got " . gettype($data)); + } + + return $data; + } +} diff --git a/src/Event/EventsApiException.php b/src/Event/EventsApiException.php new file mode 100644 index 000000000..db5796780 --- /dev/null +++ b/src/Event/EventsApiException.php @@ -0,0 +1,8 @@ +_params['startTime']); + // The query start time is 12 hours earlier. $start = $observationTime->sub(new DateInterval("PT12H")); @@ -238,6 +241,22 @@ public function events() { // at the center. $length = new DateInterval('P1D'); + // Handle CCMC source using new Events API + // This provides direct access to CCMC event data without going through the standard EventInterface + if (array_key_exists('sources', $this->_options) && $this->_options['sources'] === 'CCMC') { + + try { + $eventsApi = new EventsApi(); + $data = $eventsApi->getEventsForSource($observationTime, "CCMC"); + + header("Content-Type: application/json"); + echo json_encode($data); + return; + } catch (EventsApiException $e) { + Sentry::capture($e); + } + } + // Check if any specific datasources were requested if (array_key_exists('sources', $this->_options)) { $sources = explode(',', $this->_options['sources']); From 6242eab08f16426f3f1bb96c8e0b79be3e525984 Mon Sep 17 00:00:00 2001 From: Kasim Necdet Percinel Date: Thu, 9 Oct 2025 16:05:53 +0000 Subject: [PATCH 3/6] use default eventsapi url --- src/Event/EventsApi.php | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Event/EventsApi.php b/src/Event/EventsApi.php index c7a116056..b9d899017 100644 --- a/src/Event/EventsApi.php +++ b/src/Event/EventsApi.php @@ -8,8 +8,7 @@ use Helioviewer\Api\Sentry\Sentry; class EventsApi { - - private string $baseUrl = "http://ec2-44-219-199-246.compute-1.amazonaws.com:8082"; + private ClientInterface $client; /** @@ -19,7 +18,13 @@ class EventsApi { */ public function __construct(ClientInterface $client = null) { - $this->client = $client ?? new Client(); + $this->client = $client ?? new Client([ + 'timeout' => 4, + 'headers' => [ + 'Accept' => 'application/json', + 'User-Agent' => 'Helioviewer-API/2.0' + ] + ]); } /** @@ -30,12 +35,14 @@ public function __construct(ClientInterface $client = null) * @return array Array of event data * @throws EventsApiException on API errors or unexpected responses */ - public function getEventsForSource(DateTimeInterface $observationTime, string $source): array { + public function getEventsForSource(DateTimeInterface $observationTime, string $source): array + { // Build the API URL: /api/v1/events/{source}/observation/{datetime} $formattedTime = $observationTime->format('Y-m-d H:i:s'); $encodedTime = urlencode($formattedTime); - - $url = $this->baseUrl . "/api/v1/events/{$source}/observation/{$encodedTime}"; + + $baseUrl = defined('HV_EVENTS_API_URL') ? HV_EVENTS_API_URL : 'https://events.helioviewer.org'; + $url = $baseUrl . "/api/v1/events/{$source}/observation/{$encodedTime}"; Sentry::setContext('EventsApi', [ 'url' => $url, @@ -43,13 +50,7 @@ public function getEventsForSource(DateTimeInterface $observationTime, string $s 'observation_time' => $observationTime->format('Y-m-d\TH:i:s\Z') ]); - $response = $this->client->request('GET', $url, [ - 'timeout' => 30, - 'headers' => [ - 'Accept' => 'application/json', - 'User-Agent' => 'Helioviewer-API/2.0' - ] - ]); + $response = $this->client->request('GET', $url); return $this->parseResponse($response); } From 0c8a2e40aa868985cd0f943224fdb17cbe0b0b14 Mon Sep 17 00:00:00 2001 From: Kasim Necdet Percinel Date: Fri, 10 Oct 2025 14:48:25 +0000 Subject: [PATCH 4/6] add tests for eventsapi integration --- tests/unit_tests/events/EventsApiTest.php | 86 +++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 tests/unit_tests/events/EventsApiTest.php diff --git a/tests/unit_tests/events/EventsApiTest.php b/tests/unit_tests/events/EventsApiTest.php new file mode 100644 index 000000000..a7b47a40d --- /dev/null +++ b/tests/unit_tests/events/EventsApiTest.php @@ -0,0 +1,86 @@ + + */ + +use PHPUnit\Framework\TestCase; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\Psr7\Response; +use Helioviewer\Api\Event\EventsApi; +use Helioviewer\Api\Event\EventsApiException; + +final class EventsApiTest extends TestCase +{ + private $mockClient; + private $eventsApi; + + protected function setUp(): void + { + $this->mockClient = $this->createMock(ClientInterface::class); + $this->eventsApi = new EventsApi($this->mockClient); + } + + public function testItShouldGetEventsSuccessfully(): void + { + $responseData = [ + ['id' => 1, 'type' => 'event'.rand()], + ['id' => 2, 'type' => 'event'.rand()] + ]; + + $this->mockClient->expects($this->once()) + ->method('request') + ->with('GET', $this->stringContains('/api/v1/events/CCMC/observation/')) + ->willReturn(new Response(200, [], json_encode($responseData))); + + $result = $this->eventsApi->getEventsForSource( + new DateTimeImmutable('2024-01-15 12:00:00'), + 'CCMC' + ); + + $this->assertEquals($responseData, $result); + } + + public function testItShouldUrlEncodeObservationTime(): void + { + $this->mockClient->expects($this->once()) + ->method('request') + ->with('GET', $this->stringContains('2024-01-15+12%3A30%3A45')) + ->willReturn(new Response(200, [], json_encode([]))); + + $this->eventsApi->getEventsForSource( + new DateTimeImmutable('2024-01-15 12:30:45'), + 'CCMC' + ); + } + + public function testItShouldThrowExceptionOnInvalidJson(): void + { + $this->mockClient->expects($this->once()) + ->method('request') + ->willReturn(new Response(200, [], 'invalid json {')); + + $this->expectException(EventsApiException::class); + $this->expectExceptionMessage('Failed to decode JSON response'); + + $this->eventsApi->getEventsForSource( + new DateTimeImmutable('2024-01-15 12:00:00'), + 'CCMC' + ); + } + + public function testItShouldThrowExceptionWhenResponseIsNotArray(): void + { + $this->mockClient->expects($this->once()) + ->method('request') + ->willReturn(new Response(200, [], '"just a string"')); + + $this->expectException(EventsApiException::class); + $this->expectExceptionMessage('Unexpected response format: expected array, got string'); + + $this->eventsApi->getEventsForSource( + new DateTimeImmutable('2024-01-15 12:00:00'), + 'CCMC' + ); + } +} From e83c1614e1fe2f5c4f5825786453dd1531164d59 Mon Sep 17 00:00:00 2001 From: Kasim Necdet Percinel Date: Wed, 22 Oct 2025 16:58:05 +0000 Subject: [PATCH 5/6] default URL for eventsapi in API config --- settings/Config.Example.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/settings/Config.Example.ini b/settings/Config.Example.ini index 414f0869d..e46025af9 100644 --- a/settings/Config.Example.ini +++ b/settings/Config.Example.ini @@ -82,6 +82,9 @@ db_events = true ; Leave blank for no password, otherwise choose a long and sufficiently random string import_events_auth = "" +; This is the URL of the eventsapi, you can set to your development one if needed +events_api_url = "https://events.helioviewer.org" + [movie_params] ; FFmpeg location ffmpeg = ffmpeg From 32727a70bb4b36957449910ec05e5f637c79e81a Mon Sep 17 00:00:00 2001 From: Kasim Necdet Percinel Date: Thu, 30 Oct 2025 18:07:02 +0000 Subject: [PATCH 6/6] use events api for movie generation and screenshots --- .../Composite/HelioviewerCompositeImage.php | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Image/Composite/HelioviewerCompositeImage.php b/src/Image/Composite/HelioviewerCompositeImage.php index 1b5160d8b..4b4f700df 100644 --- a/src/Image/Composite/HelioviewerCompositeImage.php +++ b/src/Image/Composite/HelioviewerCompositeImage.php @@ -19,6 +19,10 @@ require_once HV_ROOT_DIR.'/../src/Database/ImgIndex.php'; require_once HV_ROOT_DIR.'/../src/Module/SolarBodies.php'; +use Helioviewer\Api\Sentry\Sentry; +use Helioviewer\Api\Event\EventsApi; +use Helioviewer\Api\Event\EventsApiException; + class Image_Composite_HelioviewerCompositeImage { private $_composite; @@ -587,16 +591,32 @@ private function _addEventLayer($imagickImage) { require_once HV_ROOT_DIR . "/../src/Helper/EventInterface.php"; // Collect events from all data sources. + // Collect all HEK events $hek = new Event_HEKAdapter(); $event_categories = $hek->getNormalizedEvents($this->date, Array()); + $events_api_sources = ["CCMC", "RHESSI"]; + $observationTime = new DateTimeImmutable($this->date); $startDate = $observationTime->sub(new DateInterval("PT12H")); $length = new DateInterval("P1D"); - $event_categories = array_merge($event_categories, Helper_EventInterface::GetEvents($startDate, $length, $observationTime)); - // Lay down all relevant event REGIONS first + // Collect CCMC events if any + try { + + $eventsApi = new EventsApi(); + $event_categories = array_merge($event_categories, $eventsApi->getEventsForSource($observationTime, "CCMC")); + // if there is no error only left is RHESSI to collect + $events_api_sources = ["RHESSI"]; + } catch (EventsApiException $e) { + Sentry::capture($e); + } + + // Collect RHESSI events + $event_categories = array_merge($event_categories, Helper_EventInterface::GetEvents($startDate, $length, $observationTime, $events_api_sources)); + + // Lay down all relevant event REGIONS first $events_to_render = []; $events_manager = $this->eventsManager; $add_label_visibility_and_concept = function($events_data, $event_cat_pin, $event_group_name) use ($events_manager) {