diff --git a/packages/ep_products/Classes/Dashboard/ImportStatusWidget.php b/packages/ep_products/Classes/Dashboard/ImportStatusWidget.php new file mode 100644 index 0000000..bd08d3f --- /dev/null +++ b/packages/ep_products/Classes/Dashboard/ImportStatusWidget.php @@ -0,0 +1,146 @@ + label; the scheduler tasks are found by the command they run, not by uid + */ + private const array COMMANDS = [ + 'ep:import-dates' => 'Reisedaten (MyE&P)', + 'ep:import-snowreport' => 'Schneehöhen', + 'ep:groups-swiss' => 'groups.swiss', + ]; + + /** + * A task whose next run is this far in the past means scheduler:run is not called any more + */ + private const int OVERDUE_SECONDS = 3600; + + private ServerRequestInterface $request; + + public function __construct( + private readonly WidgetConfigurationInterface $configuration, + private readonly BackendViewFactory $backendViewFactory, + private readonly ConnectionPool $connectionPool, + private readonly ApiImportSource $importSource, + private readonly ApiClient $apiClient, + private readonly array $options = [], + ) { + } + + public function setRequest(ServerRequestInterface $request): void + { + $this->request = $request; + } + + public function renderWidgetContent(): string + { + $tasks = $this->findTasks(); + $rows = []; + foreach (self::COMMANDS as $command => $label) { + $rows[$command] = ['label' => $label, 'dataAt' => null, 'task' => $tasks[$command] ?? null]; + } + $rows['ep:import-dates']['dataAt'] = $this->importSource->getImportedTimestamp()?->getTimestamp(); + $rows['ep:import-snowreport']['dataAt'] = $this->findLastSnowreportUpdate(); + + $myEpLastUpdate = null; + $myEpUnreachable = false; + try { + $myEpLastUpdate = $this->apiClient->getLastUpdateAt()?->getTimestamp(); + } catch (\Throwable) { + $myEpUnreachable = true; + } + + $view = $this->backendViewFactory->create($this->request, ['typo3/cms-dashboard', 'ep/ep-products']); + $view->assignMultiple([ + 'rows' => $rows, + 'myEpLastUpdate' => $myEpLastUpdate, + 'myEpUnreachable' => $myEpUnreachable, + 'importPending' => null !== $myEpLastUpdate && $myEpLastUpdate > (int)$rows['ep:import-dates']['dataAt'], + 'configuration' => $this->configuration, + ]); + + return $view->render('Widget/ImportStatusWidget'); + } + + public function getOptions(): array + { + return $this->options; + } + + /** + * @return array + */ + private function findTasks(): array + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_scheduler_task'); + $result = $queryBuilder + ->select('uid', 'disable', 'lastexecution_time', 'lastexecution_failure', 'nextexecution', 'serialized_task_object') + ->from('tx_scheduler_task') + ->where($queryBuilder->expr()->eq('deleted', 0)) + ->orderBy('uid') + ->executeQuery(); + + $tasks = []; + while ($row = $result->fetchAssociative()) { + foreach (array_keys(self::COMMANDS) as $command) { + // The command identifier appears in the serialized ExecuteSchedulableCommandTask exactly as + // serialize() writes the string; matching it avoids unserializing task objects + if (!isset($tasks[$command]) && str_contains((string)$row['serialized_task_object'], serialize($command))) { + $tasks[$command] = [ + 'uid' => (int)$row['uid'], + 'disabled' => (bool)$row['disable'], + 'lastRun' => (int)$row['lastexecution_time'], + 'nextRun' => (int)$row['nextexecution'], + 'overdue' => !$row['disable'] && 0 < (int)$row['nextexecution'] + && (int)$row['nextexecution'] < time() - self::OVERDUE_SECONDS, + 'failure' => $this->failureMessage((string)$row['lastexecution_failure']), + ]; + } + } + } + + return $tasks; + } + + private function failureMessage(string $failure): ?string + { + if ('' === $failure) { + return null; + } + // Written by the scheduler as serialize(['code' => …, 'message' => …, …]) + $data = @unserialize($failure, ['allowed_classes' => false]); + + return is_array($data) && '' !== (string)($data['message'] ?? '') ? (string)$data['message'] : 'Unbekannter Fehler'; + } + + private function findLastSnowreportUpdate(): ?int + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_epproducts_domain_model_snowreport'); + $queryBuilder->getRestrictions()->removeAll(); + $tstamp = $queryBuilder + ->selectLiteral('MAX(tstamp)') + ->from('tx_epproducts_domain_model_snowreport') + ->executeQuery() + ->fetchOne(); + + return $tstamp ? (int)$tstamp : null; + } +} diff --git a/packages/ep_products/Configuration/Backend/DashboardPresets.php b/packages/ep_products/Configuration/Backend/DashboardPresets.php new file mode 100644 index 0000000..ebc21ea --- /dev/null +++ b/packages/ep_products/Configuration/Backend/DashboardPresets.php @@ -0,0 +1,13 @@ + [ + 'title' => 'Importe', + 'description' => 'Import-Status von Reisedaten, Schneehöhen und groups.swiss', + 'iconIdentifier' => 'ep-logo', + 'defaultWidgets' => ['epImportStatus'], + 'showInWizard' => true, + ], +]; diff --git a/packages/ep_products/Configuration/Services.yaml b/packages/ep_products/Configuration/Services.yaml index 85944fa..7330ad4 100644 --- a/packages/ep_products/Configuration/Services.yaml +++ b/packages/ep_products/Configuration/Services.yaml @@ -72,3 +72,14 @@ services: $hotelImageService: '@ep_products.image_service.hotel' $productImageService: '@ep_products.image_service.product' $regionImageService: '@ep_products.image_service.region' + + EP\EpProducts\Dashboard\ImportStatusWidget: + tags: + - name: dashboard.widget + identifier: 'epImportStatus' + groupNames: 'systemInfo' + title: 'Import-Status' + description: 'Wann Reisedaten, Schneehöhen und groups.swiss zuletzt gelaufen sind und welchen Stand die Daten haben' + iconIdentifier: 'ep-logo' + height: 'medium' + width: 'large' diff --git a/packages/ep_products/Configuration/user.tsconfig b/packages/ep_products/Configuration/user.tsconfig new file mode 100644 index 0000000..0fd0b18 --- /dev/null +++ b/packages/ep_products/Configuration/user.tsconfig @@ -0,0 +1 @@ +options.dashboard.dashboardPresetsForNewUsers = default, epImports diff --git a/packages/ep_products/Resources/Private/Templates/Widget/ImportStatusWidget.html b/packages/ep_products/Resources/Private/Templates/Widget/ImportStatusWidget.html new file mode 100644 index 0000000..08f9800 --- /dev/null +++ b/packages/ep_products/Resources/Private/Templates/Widget/ImportStatusWidget.html @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ImportDatenstandLetzter LaufNächster LaufStatus
{row.label} + + + + + + + + deaktiviert + + Fehler +
{row.task.failure}
+
+ + überfällig +
Der Scheduler läuft nicht.
+
+ OK +
+
––kein Scheduler-Task
+ +

+ Letzter Export in MyE&P: + + MyE&P nicht erreichbar + + {myEpLastUpdate -> f:format.date(format: 'd.m.Y H:i')} + + wird mit dem nächsten Lauf importiert + + + – + +

+ +
+