diff --git a/app/Http/RequestHandlers/TelemetryPreviewPage.php b/app/Http/RequestHandlers/TelemetryPreviewPage.php
new file mode 100644
index 0000000000..03dc8826cc
--- /dev/null
+++ b/app/Http/RequestHandlers/TelemetryPreviewPage.php
@@ -0,0 +1,58 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fisharebest\Webtrees\Http\ViewResponseTrait;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Services\TelemetryDataService;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+use function json_encode;
+
+use const JSON_PRETTY_PRINT;
+use const JSON_UNESCAPED_SLASHES;
+use const JSON_UNESCAPED_UNICODE;
+
+final class TelemetryPreviewPage implements RequestHandlerInterface
+{
+ use ViewResponseTrait;
+
+ public function __construct(
+ private readonly TelemetryDataService $telemetry_data_service,
+ ) {
+ }
+
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $this->layout = 'layouts/administration';
+
+ $payload = $this->telemetry_data_service->assemblePayload();
+ $json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+
+ $title = I18N::translate('Telemetry data preview');
+
+ return $this->viewResponse('admin/telemetry-preview', [
+ 'title' => $title,
+ 'payload' => $json,
+ ]);
+ }
+}
diff --git a/app/Http/RequestHandlers/TelemetrySettingsAction.php b/app/Http/RequestHandlers/TelemetrySettingsAction.php
new file mode 100644
index 0000000000..73ad44cb0f
--- /dev/null
+++ b/app/Http/RequestHandlers/TelemetrySettingsAction.php
@@ -0,0 +1,47 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fisharebest\Webtrees\FlashMessages;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Site;
+use Fisharebest\Webtrees\Validator;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+use function redirect;
+use function route;
+
+final class TelemetrySettingsAction implements RequestHandlerInterface
+{
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $endpointUrl = Validator::parsedBody($request)->string('TELEMETRY_ENDPOINT_URL');
+ $publishableKey = Validator::parsedBody($request)->string('TELEMETRY_PUBLISHABLE_KEY');
+
+ Site::setPreference('TELEMETRY_ENDPOINT_URL', $endpointUrl);
+ Site::setPreference('TELEMETRY_PUBLISHABLE_KEY', $publishableKey);
+
+ FlashMessages::addMessage(I18N::translate('The telemetry settings have been updated.'), 'success');
+
+ return redirect(route(ControlPanel::class));
+ }
+}
diff --git a/app/Http/RequestHandlers/TelemetrySettingsPage.php b/app/Http/RequestHandlers/TelemetrySettingsPage.php
new file mode 100644
index 0000000000..a91af3b3f1
--- /dev/null
+++ b/app/Http/RequestHandlers/TelemetrySettingsPage.php
@@ -0,0 +1,45 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fisharebest\Webtrees\Http\ViewResponseTrait;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Site;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+final class TelemetrySettingsPage implements RequestHandlerInterface
+{
+ use ViewResponseTrait;
+
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $this->layout = 'layouts/administration';
+
+ $title = I18N::translate('Telemetry settings');
+
+ return $this->viewResponse('admin/telemetry-settings', [
+ 'title' => $title,
+ 'endpoint_url' => Site::getPreference('TELEMETRY_ENDPOINT_URL') ?: 'https://vjdwtasmykejamkdprro.supabase.co',
+ 'publishable_key' => Site::getPreference('TELEMETRY_PUBLISHABLE_KEY') ?: 'sb_publishable_mzCwYJ8Fs2yjE2gvQgDDeg_Vwpj5uDQ',
+ ]);
+ }
+}
diff --git a/app/Http/RequestHandlers/TelemetrySubmitAction.php b/app/Http/RequestHandlers/TelemetrySubmitAction.php
new file mode 100644
index 0000000000..c7feb3c6d4
--- /dev/null
+++ b/app/Http/RequestHandlers/TelemetrySubmitAction.php
@@ -0,0 +1,86 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fisharebest\Webtrees\FlashMessages;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Services\TelemetryDataService;
+use Fisharebest\Webtrees\Site;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\GuzzleException;
+use GuzzleHttp\Exception\RequestException;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+use function json_encode;
+use function redirect;
+use function route;
+
+use const JSON_UNESCAPED_SLASHES;
+use const JSON_UNESCAPED_UNICODE;
+
+final class TelemetrySubmitAction implements RequestHandlerInterface
+{
+ public function __construct(
+ private readonly TelemetryDataService $telemetry_data_service,
+ ) {
+ }
+
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $payload = $this->telemetry_data_service->assemblePayload();
+ $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+
+ $endpointUrl = Site::getPreference('TELEMETRY_ENDPOINT_URL') ?: 'https://vjdwtasmykejamkdprro.supabase.co';
+ $publishableKey = Site::getPreference('TELEMETRY_PUBLISHABLE_KEY') ?: 'sb_publishable_mzCwYJ8Fs2yjE2gvQgDDeg_Vwpj5uDQ';
+
+ $url = rtrim($endpointUrl, '/') . '/rest/v1/rpc/submit_webtrees_telemetry';
+
+ try {
+ $client = new Client([
+ 'timeout' => 15,
+ ]);
+
+ $client->post($url, [
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ 'apikey' => $publishableKey,
+ 'Authorization' => 'Bearer ' . $publishableKey,
+ 'Prefer' => 'return=minimal',
+ ],
+ 'body' => $json,
+ ]);
+
+ FlashMessages::addMessage(I18N::translate('Telemetry data has been sent successfully. Thank you!'), 'success');
+ } catch (GuzzleException $exception) {
+ $errorDetail = '';
+ if ($exception instanceof RequestException && $exception->getResponse() !== null) {
+ $errorDetail = (string) $exception->getResponse()->getBody();
+ }
+ if ($errorDetail === '') {
+ $errorDetail = $exception->getMessage();
+ }
+ FlashMessages::addMessage(I18N::translate('Failed to send telemetry data.') . ' ' . $errorDetail, 'danger');
+ }
+
+ return redirect(route(ControlPanel::class));
+ }
+}
diff --git a/app/Http/Routes/WebRoutes.php b/app/Http/Routes/WebRoutes.php
index d0d8825958..cde0722fca 100644
--- a/app/Http/Routes/WebRoutes.php
+++ b/app/Http/Routes/WebRoutes.php
@@ -280,6 +280,10 @@
use Fisharebest\Webtrees\Http\RequestHandlers\SubmissionPage;
use Fisharebest\Webtrees\Http\RequestHandlers\SubmitterPage;
use Fisharebest\Webtrees\Http\RequestHandlers\SynchronizeTrees;
+use Fisharebest\Webtrees\Http\RequestHandlers\TelemetryPreviewPage;
+use Fisharebest\Webtrees\Http\RequestHandlers\TelemetrySettingsAction;
+use Fisharebest\Webtrees\Http\RequestHandlers\TelemetrySettingsPage;
+use Fisharebest\Webtrees\Http\RequestHandlers\TelemetrySubmitAction;
use Fisharebest\Webtrees\Http\RequestHandlers\TomSelectFamily;
use Fisharebest\Webtrees\Http\RequestHandlers\TomSelectIndividual;
use Fisharebest\Webtrees\Http\RequestHandlers\TomSelectLocation;
@@ -445,6 +449,10 @@ public function load(Map $router): void
$router->post(SiteRegistrationAction::class, '/site-registration');
$router->get(SiteTagsPage::class, '/tags');
$router->post(SiteTagsAction::class, '/tags');
+ $router->get(TelemetryPreviewPage::class, '/telemetry-preview');
+ $router->post(TelemetrySubmitAction::class, '/telemetry-submit');
+ $router->get(TelemetrySettingsPage::class, '/telemetry-settings');
+ $router->post(TelemetrySettingsAction::class, '/telemetry-settings');
$router->get(TreePageDefaultEdit::class, '/trees/default-blocks');
$router->post(TreePageDefaultUpdate::class, '/trees/default-blocks');
$router->get(MergeTreesPage::class, '/trees/merge');
diff --git a/app/Services/TelemetryDataService.php b/app/Services/TelemetryDataService.php
new file mode 100644
index 0000000000..b23084aab8
--- /dev/null
+++ b/app/Services/TelemetryDataService.php
@@ -0,0 +1,178 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Services;
+
+use Fisharebest\Webtrees\Contracts\UserInterface;
+use Fisharebest\Webtrees\DB;
+use Fisharebest\Webtrees\Module\ModuleCustomInterface;
+use Fisharebest\Webtrees\Module\ModuleInterface;
+use Fisharebest\Webtrees\Site;
+use Fisharebest\Webtrees\Webtrees;
+use Illuminate\Support\Collection;
+use PDO;
+
+use function ini_get;
+use function php_uname;
+
+/**
+ * Assemble telemetry data payload for submission.
+ */
+class TelemetryDataService
+{
+ public function __construct(
+ private readonly ModuleService $module_service,
+ private readonly TreeService $tree_service,
+ private readonly UserService $user_service,
+ ) {
+ }
+
+ /**
+ * Assemble the complete telemetry payload as an associative array.
+ *
+ * @return array
+ = I18N::translate('Please send telemetry data to support development, you will see the submitted data before confirming.') ?> + + = I18N::translate('send data') ?> + +
++ = I18N::translate('Thank you for supporting the development of webtrees! Please review the telemetry data that will be submitted below. Click "Confirm" to send this data, or "Cancel" to return to the control panel.') ?> +
+ + + += e($payload) ?>
+