diff --git a/backend/src/Controller/Api/Admin/StorageLocationsController.php b/backend/src/Controller/Api/Admin/StorageLocationsController.php index df33a231123..ccb6f15fdd6 100644 --- a/backend/src/Controller/Api/Admin/StorageLocationsController.php +++ b/backend/src/Controller/Api/Admin/StorageLocationsController.php @@ -165,9 +165,7 @@ protected function viewRecord(object $record, ServerRequest $request): object { $original = parent::viewRecord($record, $request); - $return = new ApiStorageLocation(); - $return->fromParentObject($original); - + $return = ApiStorageLocation::fromParent($original); $return->storageQuotaBytes = (string)($record->getStorageQuotaBytes() ?? ''); $return->storageUsedBytes = (string)$record->getStorageUsedBytes(); $return->storageUsedPercent = $record->getStorageUsePercentage(); diff --git a/backend/src/Controller/Api/Admin/Updates/GetUpdatesAction.php b/backend/src/Controller/Api/Admin/Updates/GetUpdatesAction.php index 6917454a9ef..109206a68ce 100644 --- a/backend/src/Controller/Api/Admin/Updates/GetUpdatesAction.php +++ b/backend/src/Controller/Api/Admin/Updates/GetUpdatesAction.php @@ -6,16 +6,33 @@ use App\Container\SettingsAwareTrait; use App\Controller\SingleActionInterface; +use App\Entity\Api\Admin\UpdateDetails; use App\Http\Response; use App\Http\ServerRequest; +use App\OpenApi; use App\Service\AzuraCastCentral; use GuzzleHttp\Exception\TransferException; +use OpenApi\Attributes as OA; use Psr\Http\Message\ResponseInterface; use RuntimeException; -/* - * TODO API - */ +#[OA\Get( + path: '/admin/updates', + operationId: 'getUpdateStatus', + description: 'Show information about this installation and its update status.', + tags: ['Administration: General'], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + ref: UpdateDetails::class + ) + ), + new OA\Response(ref: OpenApi::REF_RESPONSE_ACCESS_DENIED, response: 403), + new OA\Response(ref: OpenApi::REF_RESPONSE_GENERIC_ERROR, response: 500), + ] +)] final class GetUpdatesAction implements SingleActionInterface { use SettingsAwareTrait; @@ -40,7 +57,7 @@ public function __invoke( $settings->updateUpdateLastRun(); $this->writeSettings($settings); - return $response->withJson($updates); + return $response->withJson(UpdateDetails::fromParent($updates)); } throw new RuntimeException('Error parsing update data response from AzuraCast central.'); diff --git a/backend/src/Controller/Api/Admin/Updates/PutUpdatesAction.php b/backend/src/Controller/Api/Admin/Updates/PutUpdatesAction.php index c8f93753cfb..1c55df7272c 100644 --- a/backend/src/Controller/Api/Admin/Updates/PutUpdatesAction.php +++ b/backend/src/Controller/Api/Admin/Updates/PutUpdatesAction.php @@ -8,12 +8,22 @@ use App\Entity\Api\Status; use App\Http\Response; use App\Http\ServerRequest; +use App\OpenApi; use App\Service\WebUpdater; +use OpenApi\Attributes as OA; use Psr\Http\Message\ResponseInterface; -/* - * TODO API - */ +#[OA\Put( + path: '/admin/updates', + operationId: 'putWebUpdate', + description: 'Attempts to trigger a web-based update.', + tags: ['Administration: General'], + responses: [ + new OA\Response(ref: OpenApi::REF_RESPONSE_SUCCESS, response: 200), + new OA\Response(ref: OpenApi::REF_RESPONSE_ACCESS_DENIED, response: 403), + new OA\Response(ref: OpenApi::REF_RESPONSE_GENERIC_ERROR, response: 500), + ] +)] final class PutUpdatesAction implements SingleActionInterface { public function __construct( diff --git a/backend/src/Controller/Api/Frontend/Dashboard/NotificationsAction.php b/backend/src/Controller/Api/Frontend/Dashboard/NotificationsAction.php index 63971b9673e..fec28ccd4fd 100644 --- a/backend/src/Controller/Api/Frontend/Dashboard/NotificationsAction.php +++ b/backend/src/Controller/Api/Frontend/Dashboard/NotificationsAction.php @@ -6,14 +6,34 @@ use App\CallableEventDispatcherInterface; use App\Controller\SingleActionInterface; +use App\Entity\Api\Notification; use App\Event; use App\Http\Response; use App\Http\ServerRequest; +use App\OpenApi; +use OpenApi\Attributes as OA; use Psr\Http\Message\ResponseInterface; -/* - * TODO API - */ +#[OA\Get( + path: '/dashboard/notifications', + operationId: 'getNotifications', + description: 'Show all notifications your current account should see.', + tags: ['Miscellaneous'], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items( + ref: Notification::class + ) + ) + ), + new OA\Response(ref: OpenApi::REF_RESPONSE_ACCESS_DENIED, response: 403), + new OA\Response(ref: OpenApi::REF_RESPONSE_GENERIC_ERROR, response: 500), + ] +)] final class NotificationsAction implements SingleActionInterface { public function __construct( diff --git a/backend/src/Controller/Api/Frontend/Dashboard/StationsAction.php b/backend/src/Controller/Api/Frontend/Dashboard/StationsAction.php index fac76d07351..436a99456ba 100644 --- a/backend/src/Controller/Api/Frontend/Dashboard/StationsAction.php +++ b/backend/src/Controller/Api/Frontend/Dashboard/StationsAction.php @@ -83,9 +83,7 @@ static function (Station $station) use ($acl) { $paginator->setPostprocessor( function (NowPlaying $np) use ($router, $listenersEnabled, $acl) { - $row = new Dashboard(); - $row->fromParentObject($np); - + $row = Dashboard::fromParent($np); $row->links = [ 'public' => $router->named('public:index', ['station_id' => $np->station->shortcode]), 'manage' => $router->named('stations:index:index', ['station_id' => $np->station->id]), diff --git a/backend/src/Controller/Api/Stations/QueueController.php b/backend/src/Controller/Api/Stations/QueueController.php index 94bdff1fbd9..272c7626bba 100644 --- a/backend/src/Controller/Api/Stations/QueueController.php +++ b/backend/src/Controller/Api/Stations/QueueController.php @@ -141,9 +141,7 @@ protected function viewRecord(object $record, ServerRequest $request): StationQu $row = $this->queueApiGenerator->__invoke($record); - $apiResponse = new StationQueueDetailed(); - $apiResponse->fromParentObject($row); - + $apiResponse = StationQueueDetailed::fromParent($row); $apiResponse->sent_to_autodj = $record->getSentToAutodj(); $apiResponse->is_played = $record->getIsPlayed(); $apiResponse->autodj_custom_uri = $record->getAutodjCustomUri(); diff --git a/backend/src/Controller/Api/Stations/RemotesController.php b/backend/src/Controller/Api/Stations/RemotesController.php index 94514aa5d83..8da5b98791c 100644 --- a/backend/src/Controller/Api/Stations/RemotesController.php +++ b/backend/src/Controller/Api/Stations/RemotesController.php @@ -181,8 +181,7 @@ protected function viewRecord(object $record, ServerRequest $request): ApiStatio { $returnArray = $this->toArray($record); - $return = new ApiStationRemote(); - $return->fromParentObject($returnArray); + $return = ApiStationRemote::fromParent($returnArray); $isInternal = $request->isInternal(); $router = $request->getRouter(); diff --git a/backend/src/Entity/Api/Admin/UpdateDetails.php b/backend/src/Entity/Api/Admin/UpdateDetails.php new file mode 100644 index 00000000000..f7597535662 --- /dev/null +++ b/backend/src/Entity/Api/Admin/UpdateDetails.php @@ -0,0 +1,47 @@ +fromParentObject($apiSongHistory); + $apiCurrentSong = CurrentSong::fromParent($apiSongHistory); $np->now_playing = $apiCurrentSong; $np->song_history = $this->songHistoryApiGenerator->fromArray( diff --git a/backend/src/Entity/ApiGenerator/SongHistoryApiGenerator.php b/backend/src/Entity/ApiGenerator/SongHistoryApiGenerator.php index bea07574c5f..e97e4e747c3 100644 --- a/backend/src/Entity/ApiGenerator/SongHistoryApiGenerator.php +++ b/backend/src/Entity/ApiGenerator/SongHistoryApiGenerator.php @@ -91,8 +91,7 @@ public function detailed( ?UriInterface $baseUri = null ): DetailedSongHistory { $apiHistory = ($this)($record, $baseUri); - $response = new DetailedSongHistory(); - $response->fromParentObject($apiHistory); + $response = DetailedSongHistory::fromParent($apiHistory); $response->listeners_start = (int)$record->getListenersStart(); $response->listeners_end = (int)$record->getListenersEnd(); $response->delta_total = $record->getDeltaTotal(); diff --git a/backend/src/Session/FlashLevels.php b/backend/src/Enums/FlashLevels.php similarity index 68% rename from backend/src/Session/FlashLevels.php rename to backend/src/Enums/FlashLevels.php index 3bedc8854a9..8722327c77e 100644 --- a/backend/src/Session/FlashLevels.php +++ b/backend/src/Enums/FlashLevels.php @@ -2,8 +2,11 @@ declare(strict_types=1); -namespace App\Session; +namespace App\Enums; +use OpenApi\Attributes as OA; + +#[OA\Schema(type: 'string')] enum FlashLevels: string { case Success = 'success'; diff --git a/backend/src/Event/GetNotifications.php b/backend/src/Event/GetNotifications.php index 82e80f4df2e..8052654f9e0 100644 --- a/backend/src/Event/GetNotifications.php +++ b/backend/src/Event/GetNotifications.php @@ -10,6 +10,7 @@ final class GetNotifications extends Event { + /** @var Notification[] */ private array $notifications = []; public function __construct( diff --git a/backend/src/Notification/Check/BaseUrlCheck.php b/backend/src/Notification/Check/BaseUrlCheck.php index d54712a6e3f..47a3ded2ad9 100644 --- a/backend/src/Notification/Check/BaseUrlCheck.php +++ b/backend/src/Notification/Check/BaseUrlCheck.php @@ -6,9 +6,9 @@ use App\Container\SettingsAwareTrait; use App\Entity\Api\Notification; +use App\Enums\FlashLevels; use App\Enums\GlobalPermissions; use App\Event\GetNotifications; -use App\Session\FlashLevels; final class BaseUrlCheck { @@ -47,18 +47,19 @@ public function __invoke(GetNotifications $event): void ); // phpcs:enable Generic.Files.LineLength - $notification = new Notification(); - $notification->title = sprintf( - __('Your "Base URL" setting (%s) does not match the URL you are currently using (%s).'), - (string)$baseUriWithoutRequest, - (string)$baseUriWithRequest + $event->addNotification( + new Notification( + sprintf( + __('Your "Base URL" setting (%s) does not match the URL you are currently using (%s).'), + (string)$baseUriWithoutRequest, + (string)$baseUriWithRequest + ), + implode(' ', $notificationBodyParts), + FlashLevels::Warning, + __('System Settings'), + $router->named('admin:settings:index') + ) ); - $notification->body = implode(' ', $notificationBodyParts); - $notification->type = FlashLevels::Warning->value; - $notification->actionLabel = __('System Settings'); - $notification->actionUrl = $router->named('admin:settings:index'); - - $event->addNotification($notification); } } } diff --git a/backend/src/Notification/Check/DonateAdvisorCheck.php b/backend/src/Notification/Check/DonateAdvisorCheck.php index abfd7bc8b52..66848a01777 100644 --- a/backend/src/Notification/Check/DonateAdvisorCheck.php +++ b/backend/src/Notification/Check/DonateAdvisorCheck.php @@ -6,9 +6,9 @@ use App\Container\EnvironmentAwareTrait; use App\Entity\Api\Notification; +use App\Enums\FlashLevels; use App\Event\GetNotifications; use App\Exception\Http\RateLimitExceededException; -use App\Session\FlashLevels; final class DonateAdvisorCheck { @@ -29,17 +29,17 @@ public function __invoke(GetNotifications $event): void return; } - $notification = new Notification(); - $notification->title = __('AzuraCast is free and open-source software.'); - $notification->body = __( - 'If you are enjoying AzuraCast, please consider donating to support our work. We depend ' . - 'on donations to build new features, fix bugs, and keep AzuraCast modern, accessible and free.', + $event->addNotification( + new Notification( + __('AzuraCast is free and open-source software.'), + __( + 'If you are enjoying AzuraCast, please consider donating to support our work. We depend ' . + 'on donations to build new features, fix bugs, and keep AzuraCast modern, accessible and free.', + ), + FlashLevels::Info, + __('Donate to AzuraCast'), + 'https://donate.azuracast.com/' + ) ); - $notification->type = FlashLevels::Info->value; - - $notification->actionLabel = __('Donate to AzuraCast'); - $notification->actionUrl = 'https://donate.azuracast.com/'; - - $event->addNotification($notification); } } diff --git a/backend/src/Notification/Check/ProfilerAdvisorCheck.php b/backend/src/Notification/Check/ProfilerAdvisorCheck.php index f789ee573f5..a45cd463919 100644 --- a/backend/src/Notification/Check/ProfilerAdvisorCheck.php +++ b/backend/src/Notification/Check/ProfilerAdvisorCheck.php @@ -6,9 +6,9 @@ use App\Container\EnvironmentAwareTrait; use App\Entity\Api\Notification; +use App\Enums\FlashLevels; use App\Enums\GlobalPermissions; use App\Event\GetNotifications; -use App\Session\FlashLevels; final class ProfilerAdvisorCheck { @@ -30,34 +30,35 @@ public function __invoke(GetNotifications $event): void return; } - $notification = new Notification(); - $notification->title = __('The performance profiling extension is currently enabled on this installation.'); - $notification->body = __( - 'You can track the execution time and memory usage of any AzuraCast page or application ' . - 'from the profiler page.', + $event->addNotification( + new Notification( + __('The performance profiling extension is currently enabled on this installation.'), + __( + 'You can track the execution time and memory usage of any AzuraCast page or application ' . + 'from the profiler page.', + ), + FlashLevels::Info, + __('Profiler Control Panel'), + '/?' . http_build_query( + [ + 'SPX_UI_URI' => '/', + 'SPX_KEY' => $this->environment->getProfilingExtensionHttpKey(), + ] + ), + ) ); - $notification->type = FlashLevels::Info->value; - - $notification->actionLabel = __('Profiler Control Panel'); - $notification->actionUrl = '/?' . http_build_query( - [ - 'SPX_UI_URI' => '/', - 'SPX_KEY' => $this->environment->getProfilingExtensionHttpKey(), - ] - ); - - $event->addNotification($notification); if ($this->environment->isProfilingExtensionAlwaysOn()) { - $notification = new Notification(); - $notification->title = __('Performance profiling is currently enabled for all requests.'); - $notification->body = __( - 'This can have an adverse impact on system performance. ' . - 'You should disable this when possible.' + $event->addNotification( + new Notification( + __('Performance profiling is currently enabled for all requests.'), + __( + 'This can have an adverse impact on system performance. ' . + 'You should disable this when possible.' + ), + FlashLevels::Warning + ) ); - $notification->type = FlashLevels::Warning->value; - - $event->addNotification($notification); } } } diff --git a/backend/src/Notification/Check/RecentBackupCheck.php b/backend/src/Notification/Check/RecentBackupCheck.php index d4952fe9f14..081acf24ac1 100644 --- a/backend/src/Notification/Check/RecentBackupCheck.php +++ b/backend/src/Notification/Check/RecentBackupCheck.php @@ -7,9 +7,9 @@ use App\Container\EnvironmentAwareTrait; use App\Container\SettingsAwareTrait; use App\Entity\Api\Notification; +use App\Enums\FlashLevels; use App\Enums\GlobalPermissions; use App\Event\GetNotifications; -use App\Session\FlashLevels; use Carbon\CarbonImmutable; final class RecentBackupCheck @@ -43,16 +43,16 @@ public function __invoke(GetNotifications $event): void $backupLastRun = $settings->getBackupLastRun(); if ($backupLastRun < $threshold) { - $notification = new Notification(); - $notification->title = __('Installation Not Recently Backed Up'); - $notification->body = __('This installation has not been backed up in the last two weeks.'); - $notification->type = FlashLevels::Info->value; - $router = $request->getRouter(); - $notification->actionLabel = __('Backups'); - $notification->actionUrl = $router->named('admin:backups:index'); - - $event->addNotification($notification); + $event->addNotification( + new Notification( + __('Installation Not Recently Backed Up'), + __('This installation has not been backed up in the last two weeks.'), + FlashLevels::Info, + __('Backups'), + $router->named('admin:backups:index') + ) + ); } } } diff --git a/backend/src/Notification/Check/ServiceCheck.php b/backend/src/Notification/Check/ServiceCheck.php index f48927197d0..37fd51ac3ac 100644 --- a/backend/src/Notification/Check/ServiceCheck.php +++ b/backend/src/Notification/Check/ServiceCheck.php @@ -5,10 +5,10 @@ namespace App\Notification\Check; use App\Entity\Api\Notification; +use App\Enums\FlashLevels; use App\Enums\GlobalPermissions; use App\Event\GetNotifications; use App\Service\ServiceControl; -use App\Session\FlashLevels; final class ServiceCheck { @@ -29,21 +29,21 @@ public function __invoke(GetNotifications $event): void $services = $this->serviceControl->getServices(); foreach ($services as $service) { if (!$service->running) { - // phpcs:disable Generic.Files.LineLength - $notification = new Notification(); - $notification->title = sprintf(__('Service Not Running: %s'), $service->name); - $notification->body = __( - 'One of the essential services on this installation is not currently running. Visit the system administration and check the system logs to find the cause of this issue.' - ); - $notification->type = FlashLevels::Error->value; - $router = $request->getRouter(); - $notification->actionLabel = __('Administration'); - $notification->actionUrl = $router->named('admin:index:index'); + // phpcs:disable Generic.Files.LineLength + $event->addNotification( + new Notification( + sprintf(__('Service Not Running: %s'), $service->name), + __( + 'One of the essential services on this installation is not currently running. Visit the system administration and check the system logs to find the cause of this issue.' + ), + FlashLevels::Error, + __('Administration'), + $router->named('admin:index:index') + ) + ); // phpcs:enable - - $event->addNotification($notification); } } } diff --git a/backend/src/Notification/Check/SyncTaskCheck.php b/backend/src/Notification/Check/SyncTaskCheck.php index dc933545055..0204e0d9164 100644 --- a/backend/src/Notification/Check/SyncTaskCheck.php +++ b/backend/src/Notification/Check/SyncTaskCheck.php @@ -6,9 +6,9 @@ use App\Container\SettingsAwareTrait; use App\Entity\Api\Notification; +use App\Enums\FlashLevels; use App\Enums\GlobalPermissions; use App\Event\GetNotifications; -use App\Session\FlashLevels; final class SyncTaskCheck { @@ -32,35 +32,37 @@ public function __invoke(GetNotifications $event): void if ($settings->getSyncDisabled()) { // phpcs:disable Generic.Files.LineLength - $notification = new Notification(); - $notification->title = __('Synchronization Disabled'); - $notification->body = __( - 'Routine synchronization is currently disabled. Make sure to re-enable it to resume routine maintenance tasks.' + $event->addNotification( + new Notification( + __('Synchronization Disabled'), + __( + 'Routine synchronization is currently disabled. Make sure to re-enable it to resume routine maintenance tasks.' + ), + FlashLevels::Error + ) ); - $notification->type = FlashLevels::Error->value; // phpcs:enable - $event->addNotification($notification); return; } $syncLastRun = $settings->getSyncLastRun(); if ($syncLastRun < (time() - 60 * 5)) { - // phpcs:disable Generic.Files.LineLength - $notification = new Notification(); - $notification->title = __('Synchronization Not Recently Run'); - $notification->body = __( - 'The routine synchronization task has not run recently. This may indicate an error with your installation.' - ); - $notification->type = FlashLevels::Error->value; - $router = $request->getRouter(); - $notification->actionLabel = __('System Debugger'); - $notification->actionUrl = $router->named('admin:debug:index'); + // phpcs:disable Generic.Files.LineLength + $event->addNotification( + new Notification( + __('Synchronization Not Recently Run'), + __( + 'The routine synchronization task has not run recently. This may indicate an error with your installation.' + ), + FlashLevels::Error, + __('System Debugger'), + $router->named('admin:debug:index') + ) + ); // phpcs:enable - - $event->addNotification($notification); } } } diff --git a/backend/src/Notification/Check/UpdateCheck.php b/backend/src/Notification/Check/UpdateCheck.php index b78539af3c0..a1ad32056dd 100644 --- a/backend/src/Notification/Check/UpdateCheck.php +++ b/backend/src/Notification/Check/UpdateCheck.php @@ -5,11 +5,12 @@ namespace App\Notification\Check; use App\Container\SettingsAwareTrait; +use App\Entity\Api\Admin\UpdateDetails; use App\Entity\Api\Notification; +use App\Enums\FlashLevels; use App\Enums\GlobalPermissions; use App\Enums\ReleaseChannel; use App\Event\GetNotifications; -use App\Session\FlashLevels; use App\Version; final class UpdateCheck @@ -34,11 +35,13 @@ public function __invoke(GetNotifications $event): void return; } - $updateData = $settings->getUpdateResults(); - if (empty($updateData)) { + $updateDataRaw = $settings->getUpdateResults(); + if (empty($updateDataRaw)) { return; } + $updateData = UpdateDetails::fromParent($updateDataRaw); + $router = $event->getRequest()->getRouter(); $actionLabel = __('Update AzuraCast'); @@ -48,59 +51,58 @@ public function __invoke(GetNotifications $event): void if ( ReleaseChannel::Stable === $releaseChannel - && ($updateData['needs_release_update'] ?? false) + && ($updateData->needs_release_update) ) { - $notification = new Notification(); - $notification->title = __( - 'New AzuraCast Stable Release Available', - ); - $notification->body = sprintf( - __( - 'Version %s is now available. You are currently running version %s. Updating is recommended.' - ), - $updateData['latest_release'], - $updateData['current_release'] + $event->addNotification( + new Notification( + __( + 'New AzuraCast Stable Release Available', + ), + sprintf( + __( + 'Version %s is now available. You are currently running version %s. Updating is recommended.' + ), + $updateData->latest_release, + $updateData->current_release + ), + FlashLevels::Info, + $actionLabel, + $actionUrl + ) ); - $notification->type = FlashLevels::Info->value; - $notification->actionLabel = $actionLabel; - $notification->actionUrl = $actionUrl; - - $event->addNotification($notification); return; } if (ReleaseChannel::RollingRelease === $releaseChannel) { - if ($updateData['needs_rolling_update'] ?? false) { - $notification = new Notification(); - $notification->title = __( - 'New AzuraCast Rolling Release Available' + if ($updateData->needs_rolling_update) { + $event->addNotification( + new Notification( + __('New AzuraCast Rolling Release Available'), + sprintf( + __( + 'Your installation is currently %d update(s) behind the latest version. Updating is recommended.' + ), + $updateData->rolling_updates_available + ), + FlashLevels::Info, + $actionLabel, + $actionUrl + ) ); - $notification->body = sprintf( - __( - 'Your installation is currently %d update(s) behind the latest version. Updating is recommended.' - ), - $updateData['rolling_updates_available'] - ); - $notification->type = FlashLevels::Info->value; - $notification->actionLabel = $actionLabel; - $notification->actionUrl = $actionUrl; - - $event->addNotification($notification); } - if ($updateData['can_switch_to_stable'] ?? false) { - $notification = new Notification(); - $notification->title = __( - 'Switch to Stable Channel Available' - ); - $notification->body = __( - 'Your Rolling Release installation is currently older than the latest Stable release. This means you can switch releases to the "Stable" release channel if desired.' + if ($updateData->can_switch_to_stable) { + $event->addNotification( + new Notification( + __('Switch to Stable Channel Available'), + __( + 'Your Rolling Release installation is currently older than the latest Stable release. This means you can switch releases to the "Stable" release channel if desired.' + ), + FlashLevels::Info, + __('About Release Channels'), + '/docs/getting-started/updates/release-channels/' + ) ); - $notification->type = FlashLevels::Info->value; - $notification->actionLabel = __('About Release Channels'); - $notification->actionUrl = '/docs/getting-started/updates/release-channels/'; - - $event->addNotification($notification); } } } diff --git a/backend/src/Service/AzuraCastCentral.php b/backend/src/Service/AzuraCastCentral.php index ebd70d233aa..e61b0c0adb0 100644 --- a/backend/src/Service/AzuraCastCentral.php +++ b/backend/src/Service/AzuraCastCentral.php @@ -67,6 +67,7 @@ public function checkForUpdates(): ?array ]); $updateData = json_decode($updateDataRaw, true, 512, JSON_THROW_ON_ERROR); + return $updateData['updates'] ?? null; } catch (Exception $e) { $this->logger->error('Error checking for updates: ' . $e->getMessage()); diff --git a/backend/src/Session/Flash.php b/backend/src/Session/Flash.php index b633880898d..0cc51aa4c31 100644 --- a/backend/src/Session/Flash.php +++ b/backend/src/Session/Flash.php @@ -4,6 +4,7 @@ namespace App\Session; +use App\Enums\FlashLevels; use Mezzio\Session\SessionInterface; /** diff --git a/backend/src/Traits/LoadFromParentObject.php b/backend/src/Traits/LoadFromParentObject.php index 931de0d2173..237665a8934 100644 --- a/backend/src/Traits/LoadFromParentObject.php +++ b/backend/src/Traits/LoadFromParentObject.php @@ -4,25 +4,22 @@ namespace App\Traits; +use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; + trait LoadFromParentObject { /** - * @param object|array $obj + * @param object|array $parent */ - public function fromParentObject(object|array $obj): void + public static function fromParent(array|object $parent): self { - if (is_object($obj)) { - foreach (get_object_vars($obj) as $key => $value) { - if (property_exists($this, $key)) { - $this->$key = $value; - } - } - } elseif (is_array($obj)) { - foreach ($obj as $key => $value) { - if (property_exists($this, $key)) { - $this->$key = $value; - } - } + if (is_object($parent)) { + $parent = get_object_vars($parent); } + + return new PropertyNormalizer()->denormalize( + $parent, + self::class + ); } } diff --git a/frontend/components/Admin/Updates.vue b/frontend/components/Admin/Updates.vue index b381839cd89..7184eb22d02 100644 --- a/frontend/components/Admin/Updates.vue +++ b/frontend/components/Admin/Updates.vue @@ -161,16 +161,12 @@ import CardPage from "~/components/Common/CardPage.vue"; import {getApiUrl} from "~/router"; import {IconInfo, IconSync, IconUpdate, IconUpload} from "~/components/Common/icons"; import {useDialog} from "~/functions/useDialog.ts"; - -interface UpdateInfo { - needs_release_update?: boolean, - needs_rolling_update?: boolean, -} +import {ApiAdminUpdateDetails} from "~/entities/ApiInterfaces.ts"; const props = withDefaults( defineProps<{ releaseChannel: string, - initialUpdateInfo?: UpdateInfo, + initialUpdateInfo?: ApiAdminUpdateDetails, enableWebUpdates: boolean, }>(), { @@ -183,7 +179,7 @@ const props = withDefaults( const updatesApiUrl = getApiUrl('/admin/updates'); -const updateInfo = ref(props.initialUpdateInfo); +const updateInfo = ref(props.initialUpdateInfo); const {$gettext} = useTranslate(); @@ -205,8 +201,8 @@ const {notifySuccess} = useNotify(); const {axios} = useAxios(); const checkForUpdates = () => { - void axios.get(updatesApiUrl.value).then((resp) => { - updateInfo.value = resp.data; + void axios.get(updatesApiUrl.value).then(({data}) => { + updateInfo.value = data; }); }; diff --git a/frontend/components/Dashboard.vue b/frontend/components/Dashboard.vue index 87852fe8dfb..9f9c82f093a 100644 --- a/frontend/components/Dashboard.vue +++ b/frontend/components/Dashboard.vue @@ -262,6 +262,7 @@ import UserInfoPanel from "~/components/Account/UserInfoPanel.vue"; import {getApiUrl} from "~/router.ts"; import DataTable, {DataTableField} from "~/components/Common/DataTable.vue"; import useHasDatatable from "~/functions/useHasDatatable.ts"; +import {ApiNotification} from "~/entities/ApiInterfaces.ts"; defineProps<{ profileUrl: string, @@ -288,8 +289,8 @@ const langShowHideCharts = computed(() => { const {axios} = useAxios(); -const {state: notifications, isLoading: notificationsLoading} = useAsyncState( - () => axios.get(notificationsUrl.value).then((r) => r.data), +const {state: notifications, isLoading: notificationsLoading} = useAsyncState( + async () => (await axios.get(notificationsUrl.value)).data, [] ); diff --git a/frontend/entities/ApiInterfaces.ts b/frontend/entities/ApiInterfaces.ts index b29f6ba5442..4038ee0bf64 100644 --- a/frontend/entities/ApiInterfaces.ts +++ b/frontend/entities/ApiInterfaces.ts @@ -343,6 +343,27 @@ export type ApiAdminStorageLocation = HasLinks & { stations?: string[] | null; }; +export interface ApiAdminUpdateDetails { + /** + * The stable-equivalent branch your installation currently appears to be on. + * @example "0.20.3" + */ + current_release?: string; + /** + * The current latest stable release of the software. + * @example "0.20.4" + */ + latest_release?: string; + /** If you are on the Rolling Release, whether your installation needs to be updated. */ + needs_rolling_update?: boolean; + /** Whether a newer stable release is available than the version you are currently using. */ + needs_release_update?: boolean; + /** If you are on the Rolling Release, the number of updates that have released since your version. */ + rolling_updates_available?: number; + /** Whether you can seamlessly move from the Rolling Release channel to Stable without issues. */ + can_switch_to_stable?: boolean; +} + export type ApiCustomAsset = ApiAbstractStatus & { is_uploaded: boolean; url: string; @@ -547,6 +568,14 @@ export type ApiNewRecord = ApiStatus & { links?: string[]; }; +export interface ApiNotification { + title: string; + body: string; + type: FlashLevels; + actionLabel: string | null; + actionUrl: string | null; +} + export type ApiNowPlayingCurrentSong = ApiNowPlayingSongHistory & { /** * Elapsed time of the song's playback since it started. @@ -1867,6 +1896,13 @@ export type User = HasAutoIncrementId & { roles?: any[]; }; +export enum FlashLevels { + Success = "success", + Warning = "warning", + Error = "danger", + Info = "info", +} + export enum GlobalPermissions { All = "administer all", View = "view administration", diff --git a/web/static/openapi.yml b/web/static/openapi.yml index 2e9cdd40100..f44168cf0a1 100644 --- a/web/static/openapi.yml +++ b/web/static/openapi.yml @@ -938,6 +938,35 @@ paths: $ref: '#/components/responses/RecordNotFound' '500': $ref: '#/components/responses/GenericError' + /admin/updates: + get: + tags: + - 'Administration: General' + description: 'Show information about this installation and its update status.' + operationId: getUpdateStatus + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/Api_Admin_UpdateDetails' + '403': + $ref: '#/components/responses/AccessDenied' + '500': + $ref: '#/components/responses/GenericError' + put: + tags: + - 'Administration: General' + description: 'Attempts to trigger a web-based update.' + operationId: putWebUpdate + responses: + '200': + $ref: '#/components/responses/Success' + '403': + $ref: '#/components/responses/AccessDenied' + '500': + $ref: '#/components/responses/GenericError' /admin/users: get: tags: @@ -1200,6 +1229,25 @@ paths: $ref: '#/components/responses/AccessDenied' '500': $ref: '#/components/responses/GenericError' + /dashboard/notifications: + get: + tags: + - Miscellaneous + description: 'Show all notifications your current account should see.' + operationId: getNotifications + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Api_Notification' + '403': + $ref: '#/components/responses/AccessDenied' + '500': + $ref: '#/components/responses/GenericError' /status: get: tags: @@ -4363,6 +4411,29 @@ components: example: 'AzuraTest Radio' nullable: true type: object + Api_Admin_UpdateDetails: + properties: + current_release: + description: 'The stable-equivalent branch your installation currently appears to be on.' + type: string + example: 0.20.3 + latest_release: + description: 'The current latest stable release of the software.' + type: string + example: 0.20.4 + needs_rolling_update: + description: 'If you are on the Rolling Release, whether your installation needs to be updated.' + type: boolean + needs_release_update: + description: 'Whether a newer stable release is available than the version you are currently using.' + type: boolean + rolling_updates_available: + description: 'If you are on the Rolling Release, the number of updates that have released since your version.' + type: integer + can_switch_to_stable: + description: 'Whether you can seamlessly move from the Rolling Release channel to Stable without issues.' + type: boolean + type: object Api_CustomAsset: required: - is_uploaded @@ -4633,6 +4704,27 @@ components: type: string example: 'http://localhost/api/record/1' type: object + Api_Notification: + required: + - title + - body + - type + - actionLabel + - actionUrl + properties: + title: + type: string + body: + type: string + type: + $ref: '#/components/schemas/FlashLevels' + actionLabel: + type: string + nullable: true + actionUrl: + type: string + nullable: true + type: object Api_NowPlaying_CurrentSong: required: - elapsed @@ -6405,6 +6497,18 @@ components: type: array items: { } type: object + FlashLevels: + type: string + enum: + - success + - warning + - danger + - info + x-enumNames: + - Success + - Warning + - Error + - Info GlobalPermissions: type: string enum: