Release v0.21.0 (#1648)

* Develop v0.21.0 (additional notification channels) (#1647)

* Add Placeholder text for notifications (#1570)

first_commit

* Add Gotify Notifications via Webhook (#1561)

* first_commit

* Ready!

* Add_placeholder

* change url

---------

Co-authored-by: Alex Justesen <alexjustesen@users.noreply.github.com>

* Add Support for Slack Notifications via Webhook (#1522)

* First Commit

* Fix lint

* add_url_placeholder

* Fix the liniting

* Add HealthCheck.io Notifications via Webhooks (#1567)

* first_commit

* linting

* add_descrip_for_threshold

* Change name

* add_url_placeholder

* fix_linting

* Add Pushover Notifications via Webhooks (#1574)

* add_pushover

* add_placeholder

* Linting

---------

Co-authored-by: Alex Justesen <alexjustesen@users.noreply.github.com>

* Add ntfy Notifications via Webhooks (#1579)

* first_push

* Fix_json_payload

* Add_auth_option

* fix lint

* fix packet_loss_%

* added eof line

---------

Co-authored-by: Alex Justesen <alexjustesen@users.noreply.github.com>

* Add Ookla URL to the notification (#1615)

* first commit

* added eof line

---------

Co-authored-by: Alex Justesen <alexjustesen@users.noreply.github.com>

* Fix notifications layouts (#1639)

first commit

---------

Co-authored-by: Sven van Ginkel <svenvanginkel@icloud.com>

* updated npm dependencies

---------

Co-authored-by: Sven van Ginkel <svenvanginkel@icloud.com>
This commit is contained in:
Alex Justesen
2024-08-08 13:01:19 -04:00
committed by GitHub
parent 811fbfcc12
commit 362311c896
45 changed files with 1757 additions and 102 deletions
@@ -0,0 +1,37 @@
<?php
namespace App\Actions\Notifications;
use Filament\Notifications\Notification;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\WebhookServer\WebhookCall;
class SendGotifyTestNotification
{
use AsAction;
public function handle(array $webhooks)
{
if (! count($webhooks)) {
Notification::make()
->title('You need to add Gotify urls!')
->warning()
->send();
return;
}
foreach ($webhooks as $webhook) {
WebhookCall::create()
->url($webhook['url'])
->payload(['message' => '👋 Testing the Gotify notification channel.'])
->doNotSign()
->dispatch();
}
Notification::make()
->title('Test Gotify notification sent.')
->success()
->send();
}
}
@@ -0,0 +1,37 @@
<?php
namespace App\Actions\Notifications;
use Filament\Notifications\Notification;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\WebhookServer\WebhookCall;
class SendHealthCheckTestNotification
{
use AsAction;
public function handle(array $webhooks)
{
if (! count($webhooks)) {
Notification::make()
->title('You need to add HealthCheck.io urls!')
->warning()
->send();
return;
}
foreach ($webhooks as $webhook) {
WebhookCall::create()
->url($webhook['url'])
->payload(['message' => '👋 Testing the HealthCheck.io notification channel.'])
->doNotSign()
->dispatch();
}
Notification::make()
->title('Test HealthCheck.io notification sent.')
->success()
->send();
}
}
@@ -0,0 +1,49 @@
<?php
namespace App\Actions\Notifications;
use Filament\Notifications\Notification;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\WebhookServer\WebhookCall;
class SendNtfyTestNotification
{
use AsAction;
public function handle(array $webhooks)
{
if (! count($webhooks)) {
Notification::make()
->title('You need to add ntfy urls!')
->warning()
->send();
return;
}
foreach ($webhooks as $webhook) {
$webhookCall = WebhookCall::create()
->url($webhook['url'])
->payload([
'topic' => $webhook['topic'],
'message' => '👋 Testing the ntfy notification channel.',
])
->doNotSign();
// Only add authentication if username and password are provided
if (! empty($webhook['username']) && ! empty($webhook['password'])) {
$authHeader = 'Basic '.base64_encode($webhook['username'].':'.$webhook['password']);
$webhookCall->withHeaders([
'Authorization' => $authHeader,
]);
}
$webhookCall->dispatch();
}
Notification::make()
->title('Test ntfy notification sent.')
->success()
->send();
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Actions\Notifications;
use Filament\Notifications\Notification;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\WebhookServer\WebhookCall;
class SendPushoverTestNotification
{
use AsAction;
public function handle(array $webhooks)
{
if (! count($webhooks)) {
Notification::make()
->title('You need to add Pushover URLs!')
->warning()
->send();
return;
}
foreach ($webhooks as $webhook) {
WebhookCall::create()
->url($webhook['url'])
->payload([
'token' => $webhook['api_token'],
'user' => $webhook['user_key'],
'message' => '👋 Testing the Pushover notification channel.',
])
->doNotSign()
->dispatch();
}
Notification::make()
->title('Test Pushover notification sent.')
->success()
->send();
}
}
@@ -0,0 +1,37 @@
<?php
namespace App\Actions\Notifications;
use Filament\Notifications\Notification;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\WebhookServer\WebhookCall;
class SendSlackTestNotification
{
use AsAction;
public function handle(array $webhooks)
{
if (! count($webhooks)) {
Notification::make()
->title('You need to add Slack URLs!')
->warning()
->send();
return;
}
foreach ($webhooks as $webhook) {
WebhookCall::create()
->url($webhook['url'])
->payload(['text' => '👋 Testing the Slack notification channel.'])
->doNotSign()
->dispatch();
}
Notification::make()
->title('Test Slack notification sent.')
->success()
->send();
}
}
@@ -4,7 +4,12 @@ namespace App\Filament\Pages\Settings;
use App\Actions\Notifications\SendDatabaseTestNotification;
use App\Actions\Notifications\SendDiscordTestNotification;
use App\Actions\Notifications\SendGotifyTestNotification;
use App\Actions\Notifications\SendHealthCheckTestNotification;
use App\Actions\Notifications\SendMailTestNotification;
use App\Actions\Notifications\SendNtfyTestNotification;
use App\Actions\Notifications\SendPushoverTestNotification;
use App\Actions\Notifications\SendSlackTestNotification;
use App\Actions\Notifications\SendTelegramTestNotification;
use App\Actions\Notifications\SendWebhookTestNotification;
use App\Settings\NotificationSettings;
@@ -84,6 +89,63 @@ class NotificationPage extends SettingsPage
'md' => 2,
]),
Forms\Components\Section::make('Pushover')
->schema([
Forms\Components\Toggle::make('pushover_enabled')
->label('Enable Pushover webhook notifications')
->reactive()
->columnSpanFull(),
Forms\Components\Grid::make([
'default' => 1,
])
->hidden(fn (Forms\Get $get) => $get('pushover_enabled') !== true)
->schema([
Forms\Components\Fieldset::make('Triggers')
->schema([
Forms\Components\Toggle::make('pushover_on_speedtest_run')
->label('Notify on every speedtest run')
->columnSpanFull(),
Forms\Components\Toggle::make('pushover_on_threshold_failure')
->label('Notify on threshold failures')
->columnSpanFull(),
]),
Forms\Components\Repeater::make('pushover_webhooks')
->label('Pushover Webhooks')
->schema([
Forms\Components\TextInput::make('url')
->label('URL')
->placeholder('http://api.pushover.net/1/messages.json')
->maxLength(2000)
->required()
->url(),
Forms\Components\TextInput::make('user_key')
->label('User Key')
->placeholder('Your Pushover User Key')
->maxLength(200)
->required(),
Forms\Components\TextInput::make('api_token')
->label('API Token')
->placeholder('Your Pushover API Token')
->maxLength(200)
->required(),
])
->columnSpanFull(),
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('test pushover')
->label('Test Pushover webhook')
->action(fn (Forms\Get $get) => SendPushoverTestNotification::run(
webhooks: $get('pushover_webhooks')
))
->hidden(fn (Forms\Get $get) => ! count($get('pushover_webhooks'))),
]),
]),
])
->compact()
->columns([
'default' => 1,
'md' => 2,
]),
Forms\Components\Section::make('Discord')
->schema([
Forms\Components\Toggle::make('discord_enabled')
@@ -108,6 +170,7 @@ class NotificationPage extends SettingsPage
->label('Webhooks')
->schema([
Forms\Components\TextInput::make('url')
->placeholder('https://discord.com/api/webhooks/longstringofcharacters')
->maxLength(2000)
->required()
->url(),
@@ -127,6 +190,152 @@ class NotificationPage extends SettingsPage
'md' => 2,
]),
Forms\Components\Section::make('Gotify')
->schema([
Forms\Components\Toggle::make('gotify_enabled')
->label('Enable Gotify webhook notifications')
->reactive()
->columnSpanFull(),
Forms\Components\Grid::make([
'default' => 1,
])
->hidden(fn (Forms\Get $get) => $get('gotify_enabled') !== true)
->schema([
Forms\Components\Fieldset::make('Triggers')
->schema([
Forms\Components\Toggle::make('gotify_on_speedtest_run')
->label('Notify on every speedtest run')
->columnSpanFull(),
Forms\Components\Toggle::make('gotify_on_threshold_failure')
->label('Notify on threshold failures')
->columnSpanFull(),
]),
Forms\Components\Repeater::make('gotify_webhooks')
->label('Webhooks')
->schema([
Forms\Components\TextInput::make('url')
->placeholder('https://example.com/message?token=<apptoken>')
->maxLength(2000)
->required()
->url(),
])
->columnSpanFull(),
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('test gotify')
->label('Test Gotify webhook')
->action(fn (Forms\Get $get) => SendgotifyTestNotification::run(webhooks: $get('gotify_webhooks')))
->hidden(fn (Forms\Get $get) => ! count($get('gotify_webhooks'))),
]),
]),
])
->compact()
->columns([
'default' => 1,
'md' => 2,
]),
Forms\Components\Section::make('Slack')
->schema([
Forms\Components\Toggle::make('slack_enabled')
->label('Enable Slack webhook notifications')
->reactive()
->columnSpanFull(),
Forms\Components\Grid::make([
'default' => 1,
])
->hidden(fn (Forms\Get $get) => $get('slack_enabled') !== true)
->schema([
Forms\Components\Fieldset::make('Triggers')
->schema([
Forms\Components\Toggle::make('slack_on_speedtest_run')
->label('Notify on every speedtest run')
->columnSpanFull(),
Forms\Components\Toggle::make('slack_on_threshold_failure')
->label('Notify on threshold failures')
->columnSpanFull(),
]),
Forms\Components\Repeater::make('slack_webhooks')
->label('Webhooks')
->schema([
Forms\Components\TextInput::make('url')
->placeholder('https://hooks.slack.com/services/abc/xyz')
->maxLength(2000)
->required()
->url(),
])
->columnSpanFull(),
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('test Slack')
->label('Test slack webhook')
->action(fn (Forms\Get $get) => SendSlackTestNotification::run(webhooks: $get('slack_webhooks')))
->hidden(fn (Forms\Get $get) => ! count($get('slack_webhooks'))),
]),
]),
])
->compact()
->columns([
'default' => 1,
'md' => 2,
]),
Forms\Components\Section::make('Ntfy')
->schema([
Forms\Components\Toggle::make('ntfy_enabled')
->label('Enable Ntfy webhook notifications')
->reactive()
->columnSpanFull(),
Forms\Components\Grid::make([
'default' => 1,
])
->hidden(fn (Forms\Get $get) => $get('ntfy_enabled') !== true)
->schema([
Forms\Components\Fieldset::make('Triggers')
->schema([
Forms\Components\Toggle::make('ntfy_on_speedtest_run')
->label('Notify on every speedtest run')
->columnSpanFull(),
Forms\Components\Toggle::make('ntfy_on_threshold_failure')
->label('Notify on threshold failures')
->columnSpanFull(),
]),
Forms\Components\Repeater::make('ntfy_webhooks')
->label('Webhooks')
->schema([
Forms\Components\TextInput::make('url')
->maxLength(2000)
->placeholder('Your ntfy server url')
->required()
->url(),
Forms\Components\TextInput::make('topic')
->label('Topic')
->placeholder('Your ntfy Topic')
->maxLength(200)
->required(),
Forms\Components\TextInput::make('username')
->label('Username')
->placeholder('Username for Basic Auth (optional)')
->maxLength(200),
Forms\Components\TextInput::make('password')
->label('Password')
->placeholder('Password for Basic Auth (optional)')
->password()
->maxLength(200),
])
->columnSpanFull(),
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('test ntfy')
->label('Test Ntfy webhook')
->action(fn (Forms\Get $get) => SendNtfyTestNotification::run(webhooks: $get('ntfy_webhooks')))
->hidden(fn (Forms\Get $get) => ! count($get('ntfy_webhooks'))),
]),
]),
])
->compact()
->columns([
'default' => 1,
'md' => 2,
]),
Forms\Components\Section::make('Mail')
->schema([
Forms\Components\Toggle::make('mail_enabled')
@@ -151,6 +360,7 @@ class NotificationPage extends SettingsPage
->label('Recipients')
->schema([
Forms\Components\TextInput::make('email_address')
->placeholder('your@email.com')
->email()
->required(),
])
@@ -169,6 +379,51 @@ class NotificationPage extends SettingsPage
'md' => 2,
]),
Forms\Components\Section::make('Healthcheck.io')
->schema([
Forms\Components\Toggle::make('healthcheck_enabled')
->label('Enable healthcheck.io webhook notifications')
->reactive()
->columnSpanFull(),
Forms\Components\Grid::make([
'default' => 1,
])
->hidden(fn (Forms\Get $get) => $get('healthcheck_enabled') !== true)
->schema([
Forms\Components\Fieldset::make('Triggers')
->schema([
Forms\Components\Toggle::make('healthcheck_on_speedtest_run')
->label('Notify on every speedtest run')
->columnSpanFull(),
Forms\Components\Toggle::make('healthcheck_on_threshold_failure')
->label('Notify on threshold failures')
->helperText('Threshold notifications will be sent to the /fail path of the URL.')
->columnSpanFull(),
]),
Forms\Components\Repeater::make('healthcheck_webhooks')
->label('webhooks')
->schema([
Forms\Components\TextInput::make('url')
->placeholder('https://hc-ping.com/your-uuid-here')
->maxLength(2000)
->required()
->url(),
])
->columnSpanFull(),
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('test healthcheck')
->label('Test healthcheck.io webhook')
->action(fn (Forms\Get $get) => SendHealthCheckTestNotification::run(webhooks: $get('healthcheck_webhooks')))
->hidden(fn (Forms\Get $get) => ! count($get('healthcheck_webhooks'))),
]),
]),
])
->compact()
->columns([
'default' => 1,
'md' => 2,
]),
Forms\Components\Section::make('Telegram')
->schema([
Forms\Components\Toggle::make('telegram_enabled')
@@ -199,6 +454,7 @@ class NotificationPage extends SettingsPage
->label('Recipients')
->schema([
Forms\Components\TextInput::make('telegram_chat_id')
->placeholder('12345678910')
->label('Telegram Chat ID')
->maxLength(50)
->required(),
@@ -242,6 +498,7 @@ class NotificationPage extends SettingsPage
->label('Recipients')
->schema([
Forms\Components\TextInput::make('url')
->placeholder('https://webhook.site/longstringofcharacters')
->maxLength(2000)
->required()
->url(),
@@ -43,6 +43,7 @@ class SendSpeedtestCompletedNotification
'download' => Number::toBitRate(bits: $event->result->download_bits, precision: 2),
'upload' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2),
'packetLoss' => $event->result->packet_loss,
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])->render(),
];
@@ -69,6 +69,7 @@ class SendSpeedtestThresholdNotification
'serverId' => $event->result->server_id,
'isp' => $event->result->isp,
'metrics' => $failed,
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])->render(),
];
@@ -0,0 +1,59 @@
<?php
namespace App\Listeners\Gotify;
use App\Events\SpeedtestCompleted;
use App\Helpers\Number;
use App\Settings\NotificationSettings;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Spatie\WebhookServer\WebhookCall;
class SendSpeedtestCompletedNotification
{
/**
* Handle the event.
*/
public function handle(SpeedtestCompleted $event): void
{
$notificationSettings = new NotificationSettings();
if (! $notificationSettings->gotify_enabled) {
return;
}
if (! $notificationSettings->gotify_on_speedtest_run) {
return;
}
if (! count($notificationSettings->gotify_webhooks)) {
Log::warning('Gotify urls not found, check Gotify notification channel settings.');
return;
}
$payload = [
'message' => view('gotify.speedtest-completed', [
'id' => $event->result->id,
'service' => Str::title($event->result->service),
'serverName' => $event->result->server_name,
'serverId' => $event->result->server_id,
'isp' => $event->result->isp,
'ping' => round($event->result->ping).' ms',
'download' => Number::toBitRate(bits: $event->result->download_bits, precision: 2),
'upload' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2),
'packetLoss' => $event->result->packet_loss,
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])->render(),
];
foreach ($notificationSettings->gotify_webhooks as $url) {
WebhookCall::create()
->url($url['url'])
->payload($payload)
->doNotSign()
->dispatch();
}
}
}
@@ -0,0 +1,133 @@
<?php
namespace App\Listeners\Gotify;
use App\Events\SpeedtestCompleted;
use App\Helpers\Number;
use App\Settings\NotificationSettings;
use App\Settings\ThresholdSettings;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Spatie\WebhookServer\WebhookCall;
class SendSpeedtestThresholdNotification
{
/**
* Handle the event.
*/
public function handle(SpeedtestCompleted $event): void
{
$notificationSettings = new NotificationSettings();
if (! $notificationSettings->gotify_enabled) {
return;
}
if (! $notificationSettings->gotify_on_threshold_failure) {
return;
}
if (! count($notificationSettings->gotify_webhooks)) {
Log::warning('Gotify urls not found, check gotify notification channel settings.');
return;
}
$thresholdSettings = new ThresholdSettings();
if (! $thresholdSettings->absolute_enabled) {
return;
}
$failed = [];
if ($thresholdSettings->absolute_download > 0) {
array_push($failed, $this->absoluteDownloadThreshold(event: $event, thresholdSettings: $thresholdSettings));
}
if ($thresholdSettings->absolute_upload > 0) {
array_push($failed, $this->absoluteUploadThreshold(event: $event, thresholdSettings: $thresholdSettings));
}
if ($thresholdSettings->absolute_ping > 0) {
array_push($failed, $this->absolutePingThreshold(event: $event, thresholdSettings: $thresholdSettings));
}
$failed = array_filter($failed);
if (! count($failed)) {
Log::warning('Failed Gotify thresholds not found, won\'t send notification.');
return;
}
$payload = [
'message' => view('gotify.speedtest-threshold', [
'id' => $event->result->id,
'service' => Str::title($event->result->service),
'serverName' => $event->result->server_name,
'serverId' => $event->result->server_id,
'isp' => $event->result->isp,
'metrics' => $failed,
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])->render(),
];
foreach ($notificationSettings->gotify_webhooks as $url) {
WebhookCall::create()
->url($url['url'])
->payload($payload)
->doNotSign()
->dispatch();
}
}
/**
* Build gotify notification if absolute download threshold is breached.
*/
protected function absoluteDownloadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
{
if (! absoluteDownloadThresholdFailed($thresholdSettings->absolute_download, $event->result->download)) {
return false;
}
return [
'name' => 'Download',
'threshold' => $thresholdSettings->absolute_download.' Mbps',
'value' => Number::toBitRate(bits: $event->result->download_bits, precision: 2),
];
}
/**
* Build gotify notification if absolute upload threshold is breached.
*/
protected function absoluteUploadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
{
if (! absoluteUploadThresholdFailed($thresholdSettings->absolute_upload, $event->result->upload)) {
return false;
}
return [
'name' => 'Upload',
'threshold' => $thresholdSettings->absolute_upload.' Mbps',
'value' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2),
];
}
/**
* Build gotify notification if absolute ping threshold is breached.
*/
protected function absolutePingThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
{
if (! absolutePingThresholdFailed($thresholdSettings->absolute_ping, $event->result->ping)) {
return false;
}
return [
'name' => 'Ping',
'threshold' => $thresholdSettings->absolute_ping.' ms',
'value' => round($event->result->ping, 2).' ms',
];
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Listeners\HealthCheck;
use App\Events\SpeedtestCompleted;
use App\Settings\NotificationSettings;
use Illuminate\Support\Facades\Log;
use Spatie\WebhookServer\WebhookCall;
class SendSpeedtestCompletedNotification
{
/**
* Handle the event.
*/
public function handle(SpeedtestCompleted $event): void
{
$notificationSettings = new NotificationSettings();
if (! $notificationSettings->healthcheck_enabled) {
return;
}
if (! $notificationSettings->healthcheck_on_speedtest_run) {
return;
}
if (! count($notificationSettings->healthcheck_webhooks)) {
Log::warning('healthcheck urls not found, check healthcheck notification channel settings.');
return;
}
foreach ($notificationSettings->healthcheck_webhooks as $url) {
WebhookCall::create()
->url($url['url'])
->payload([
'result_id' => $event->result->id,
'site_name' => config('app.name'),
'isp' => $event->result->isp,
'ping' => $event->result->ping,
'download' => $event->result->downloadBits,
'upload' => $event->result->uploadBits,
'packetLoss' => $event->result->packet_loss,
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])
->doNotSign()
->dispatch();
}
}
}
@@ -0,0 +1,126 @@
<?php
namespace App\Listeners\HealthCheck;
use App\Events\SpeedtestCompleted;
use App\Helpers\Number;
use App\Settings\NotificationSettings;
use App\Settings\ThresholdSettings;
use Illuminate\Support\Facades\Log;
use Spatie\WebhookServer\WebhookCall;
class SendSpeedtestThresholdNotification
{
/**
* Handle the event.
*/
public function handle(SpeedtestCompleted $event): void
{
$notificationSettings = new NotificationSettings();
if (! $notificationSettings->healthcheck_enabled) {
return;
}
if (! $notificationSettings->healthcheck_on_threshold_failure) {
return;
}
if (! count($notificationSettings->healthcheck_webhooks)) {
Log::warning('HealthCheck urls not found, check healthcheck notification channel settings.');
return;
}
$thresholdSettings = new ThresholdSettings();
if (! $thresholdSettings->absolute_enabled) {
return;
}
$failed = [];
if ($thresholdSettings->absolute_download > 0) {
array_push($failed, $this->absoluteDownloadThreshold(event: $event, thresholdSettings: $thresholdSettings));
}
if ($thresholdSettings->absolute_upload > 0) {
array_push($failed, $this->absoluteUploadThreshold(event: $event, thresholdSettings: $thresholdSettings));
}
if ($thresholdSettings->absolute_ping > 0) {
array_push($failed, $this->absolutePingThreshold(event: $event, thresholdSettings: $thresholdSettings));
}
$failed = array_filter($failed);
if (! count($failed)) {
Log::warning('Failed healthcheck thresholds not found, won\'t send notification.');
return;
}
foreach ($notificationSettings->healthcheck_webhooks as $url) {
WebhookCall::create()
->url($url['url'].'/fail')
->payload([
'result_id' => $event->result->id,
'site_name' => config('app.name'),
'isp' => $event->result->isp,
'metrics' => $failed,
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])
->doNotSign()
->dispatch();
}
}
/**
* Build HealthCheck notification if absolute download threshold is breached.
*/
protected function absoluteDownloadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
{
if (! absoluteDownloadThresholdFailed($thresholdSettings->absolute_download, $event->result->download)) {
return false;
}
return [
'name' => 'Download',
'threshold' => $thresholdSettings->absolute_download.' Mbps',
'value' => Number::toBitRate(bits: $event->result->download_bits, precision: 2),
];
}
/**
* Build Healthcheck notification if absolute upload threshold is breached.
*/
protected function absoluteUploadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
{
if (! absoluteUploadThresholdFailed($thresholdSettings->absolute_upload, $event->result->upload)) {
return false;
}
return [
'name' => 'Upload',
'threshold' => $thresholdSettings->absolute_upload.' Mbps',
'value' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2),
];
}
/**
* Build Healthcheck notification if absolute ping threshold is breached.
*/
protected function absolutePingThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
{
if (! absolutePingThresholdFailed($thresholdSettings->absolute_ping, $event->result->ping)) {
return false;
}
return [
'name' => 'Ping',
'threshold' => $thresholdSettings->absolute_ping.' ms',
'value' => round($event->result->ping, 2).' ms',
];
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Listeners\Ntfy;
use App\Events\SpeedtestCompleted;
use App\Helpers\Number;
use App\Settings\NotificationSettings;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Spatie\WebhookServer\WebhookCall;
class SendSpeedtestCompletedNotification
{
/**
* Handle the event.
*/
public function handle(SpeedtestCompleted $event): void
{
$notificationSettings = new NotificationSettings();
if (! $notificationSettings->ntfy_enabled) {
return;
}
if (! $notificationSettings->ntfy_on_speedtest_run) {
return;
}
if (! count($notificationSettings->ntfy_webhooks)) {
Log::warning('Ntfy urls not found, check Ntfy notification channel settings.');
return;
}
$payload =
view('ntfy.speedtest-completed', [
'id' => $event->result->id,
'service' => Str::title($event->result->service),
'serverName' => $event->result->server_name,
'serverId' => $event->result->server_id,
'isp' => $event->result->isp,
'ping' => round($event->result->ping).' ms',
'download' => Number::toBitRate(bits: $event->result->download_bits, precision: 2),
'upload' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2),
'packetLoss' => $event->result->packet_loss,
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])->render();
foreach ($notificationSettings->ntfy_webhooks as $url) {
$webhookCall = WebhookCall::create()
->url($url['url'])
->payload([
'topic' => $url['topic'],
'message' => $payload,
])
->doNotSign();
// Only add authentication if username and password are provided
if (! empty($url['username']) && ! empty($url['password'])) {
$authHeader = 'Basic '.base64_encode($url['username'].':'.$url['password']);
$webhookCall->withHeaders([
'Authorization' => $authHeader,
]);
}
$webhookCall->dispatch();
}
}
}
@@ -0,0 +1,144 @@
<?php
namespace App\Listeners\Ntfy;
use App\Events\SpeedtestCompleted;
use App\Helpers\Number;
use App\Settings\NotificationSettings;
use App\Settings\ThresholdSettings;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Spatie\WebhookServer\WebhookCall;
class SendSpeedtestThresholdNotification
{
/**
* Handle the event.
*/
public function handle(SpeedtestCompleted $event): void
{
$notificationSettings = new NotificationSettings();
if (! $notificationSettings->ntfy_enabled) {
return;
}
if (! $notificationSettings->ntfy_on_threshold_failure) {
return;
}
if (! count($notificationSettings->ntfy_webhooks)) {
Log::warning('Ntfy urls not found, check Ntfy notification channel settings.');
return;
}
$thresholdSettings = new ThresholdSettings();
if (! $thresholdSettings->absolute_enabled) {
return;
}
$failed = [];
if ($thresholdSettings->absolute_download > 0) {
array_push($failed, $this->absoluteDownloadThreshold(event: $event, thresholdSettings: $thresholdSettings));
}
if ($thresholdSettings->absolute_upload > 0) {
array_push($failed, $this->absoluteUploadThreshold(event: $event, thresholdSettings: $thresholdSettings));
}
if ($thresholdSettings->absolute_ping > 0) {
array_push($failed, $this->absolutePingThreshold(event: $event, thresholdSettings: $thresholdSettings));
}
$failed = array_filter($failed);
if (! count($failed)) {
Log::warning('Failed ntfy thresholds not found, won\'t send notification.');
return;
}
$payload =
view('ntfy.speedtest-threshold', [
'id' => $event->result->id,
'service' => Str::title($event->result->service),
'serverName' => $event->result->server_name,
'serverId' => $event->result->server_id,
'isp' => $event->result->isp,
'metrics' => $failed,
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])->render();
foreach ($notificationSettings->ntfy_webhooks as $url) {
$webhookCall = WebhookCall::create()
->url($url['url'])
->payload([
'topic' => $url['topic'],
'message' => $payload,
])
->doNotSign();
// Only add authentication if username and password are provided
if (! empty($url['username']) && ! empty($url['password'])) {
$authHeader = 'Basic '.base64_encode($url['username'].':'.$url['password']);
$webhookCall->withHeaders([
'Authorization' => $authHeader,
]);
}
$webhookCall->dispatch();
}
}
/**
* Build Ntfy notification if absolute download threshold is breached.
*/
protected function absoluteDownloadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
{
if (! absoluteDownloadThresholdFailed($thresholdSettings->absolute_download, $event->result->download)) {
return false;
}
return [
'name' => 'Download',
'threshold' => $thresholdSettings->absolute_download.' Mbps',
'value' => Number::toBitRate(bits: $event->result->download_bits, precision: 2),
];
}
/**
* Build Ntfy notification if absolute upload threshold is breached.
*/
protected function absoluteUploadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
{
if (! absoluteUploadThresholdFailed($thresholdSettings->absolute_upload, $event->result->upload)) {
return false;
}
return [
'name' => 'Upload',
'threshold' => $thresholdSettings->absolute_upload.' Mbps',
'value' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2),
];
}
/**
* Build Ntfy notification if absolute ping threshold is breached.
*/
protected function absolutePingThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
{
if (! absolutePingThresholdFailed($thresholdSettings->absolute_ping, $event->result->ping)) {
return false;
}
return [
'name' => 'Ping',
'threshold' => $thresholdSettings->absolute_ping.' ms',
'value' => round($event->result->ping, 2).' ms',
];
}
}
@@ -0,0 +1,63 @@
<?php
namespace App\Listeners\Pushover;
use App\Events\SpeedtestCompleted;
use App\Helpers\Number;
use App\Settings\NotificationSettings;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Spatie\WebhookServer\WebhookCall;
class SendSpeedtestCompletedNotification
{
/**
* Handle the event.
*/
public function handle(SpeedtestCompleted $event): void
{
$notificationSettings = new NotificationSettings();
if (! $notificationSettings->pushover_enabled) {
return;
}
if (! $notificationSettings->pushover_on_speedtest_run) {
return;
}
if (! count($notificationSettings->pushover_webhooks)) {
Log::warning('Pushover urls not found, check Pushover notification channel settings.');
return;
}
$payload = [
view('pushover.speedtest-completed', [
'id' => $event->result->id,
'service' => Str::title($event->result->service),
'serverName' => $event->result->server_name,
'serverId' => $event->result->server_id,
'isp' => $event->result->isp,
'ping' => round($event->result->ping).' ms',
'download' => Number::toBitRate(bits: $event->result->download_bits, precision: 2),
'upload' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2),
'packetLoss' => $event->result->packet_loss,
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])->render(),
];
foreach ($notificationSettings->pushover_webhooks as $url) {
WebhookCall::create()
->url($url['url'])
->payload([
'token' => $url['api_token'],
'user' => $url['user_key'],
'message' => $payload,
])
->doNotSign()
->dispatch();
}
}
}
@@ -0,0 +1,137 @@
<?php
namespace App\Listeners\Pushover;
use App\Events\SpeedtestCompleted;
use App\Helpers\Number;
use App\Settings\NotificationSettings;
use App\Settings\ThresholdSettings;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Spatie\WebhookServer\WebhookCall;
class SendSpeedtestThresholdNotification
{
/**
* Handle the event.
*/
public function handle(SpeedtestCompleted $event): void
{
$notificationSettings = new NotificationSettings();
if (! $notificationSettings->pushover_enabled) {
return;
}
if (! $notificationSettings->pushover_on_threshold_failure) {
return;
}
if (! count($notificationSettings->pushover_webhooks)) {
Log::warning('Pushover urls not found, check Pushover notification channel settings.');
return;
}
$thresholdSettings = new ThresholdSettings();
if (! $thresholdSettings->absolute_enabled) {
return;
}
$failed = [];
if ($thresholdSettings->absolute_download > 0) {
array_push($failed, $this->absoluteDownloadThreshold(event: $event, thresholdSettings: $thresholdSettings));
}
if ($thresholdSettings->absolute_upload > 0) {
array_push($failed, $this->absoluteUploadThreshold(event: $event, thresholdSettings: $thresholdSettings));
}
if ($thresholdSettings->absolute_ping > 0) {
array_push($failed, $this->absolutePingThreshold(event: $event, thresholdSettings: $thresholdSettings));
}
$failed = array_filter($failed);
if (! count($failed)) {
Log::warning('Failed Pushover thresholds not found, won\'t send notification.');
return;
}
$payload = [
view('pushover.speedtest-threshold', [
'id' => $event->result->id,
'service' => Str::title($event->result->service),
'serverName' => $event->result->server_name,
'serverId' => $event->result->server_id,
'isp' => $event->result->isp,
'metrics' => $failed,
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])->render(),
];
foreach ($notificationSettings->pushover_webhooks as $url) {
WebhookCall::create()
->url($url['url'])
->payload([
'token' => $url['api_token'],
'user' => $url['user_key'],
'message' => $payload,
])
->doNotSign()
->dispatch();
}
}
/**
* Build Pushover notification if absolute download threshold is breached.
*/
protected function absoluteDownloadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
{
if (! absoluteDownloadThresholdFailed($thresholdSettings->absolute_download, $event->result->download)) {
return false;
}
return [
'name' => 'Download',
'threshold' => $thresholdSettings->absolute_download.' Mbps',
'value' => Number::toBitRate(bits: $event->result->download_bits, precision: 2),
];
}
/**
* Build Pushover notification if absolute upload threshold is breached.
*/
protected function absoluteUploadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
{
if (! absoluteUploadThresholdFailed($thresholdSettings->absolute_upload, $event->result->upload)) {
return false;
}
return [
'name' => 'Upload',
'threshold' => $thresholdSettings->absolute_upload.' Mbps',
'value' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2),
];
}
/**
* Build Pushover notification if absolute ping threshold is breached.
*/
protected function absolutePingThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
{
if (! absolutePingThresholdFailed($thresholdSettings->absolute_ping, $event->result->ping)) {
return false;
}
return [
'name' => 'Ping',
'threshold' => $thresholdSettings->absolute_ping.' ms',
'value' => round($event->result->ping, 2).' ms',
];
}
}
@@ -0,0 +1,59 @@
<?php
namespace App\Listeners\Slack;
use App\Events\SpeedtestCompleted;
use App\Helpers\Number;
use App\Settings\NotificationSettings;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Spatie\WebhookServer\WebhookCall;
class SendSpeedtestCompletedNotification
{
/**
* Handle the event.
*/
public function handle(SpeedtestCompleted $event): void
{
$notificationSettings = new NotificationSettings();
if (! $notificationSettings->slack_enabled) {
return;
}
if (! $notificationSettings->slack_on_speedtest_run) {
return;
}
if (! count($notificationSettings->slack_webhooks)) {
Log::warning('Slack URLs not found, check Slack notification channel settings.');
return;
}
$payload = [
'text' => view('slack.speedtest-completed', [
'id' => $event->result->id,
'service' => Str::title($event->result->service),
'serverName' => $event->result->server_name,
'serverId' => $event->result->server_id,
'isp' => $event->result->isp,
'ping' => round($event->result->ping).' ms',
'download' => Number::toBitRate(bits: $event->result->download_bits, precision: 2),
'upload' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2),
'packetLoss' => $event->result->packet_loss,
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])->render(),
];
foreach ($notificationSettings->slack_webhooks as $url) {
WebhookCall::create()
->url($url['url'])
->payload($payload)
->doNotSign()
->dispatch();
}
}
}
@@ -0,0 +1,133 @@
<?php
namespace App\Listeners\Slack;
use App\Events\SpeedtestCompleted;
use App\Helpers\Number;
use App\Settings\NotificationSettings;
use App\Settings\ThresholdSettings;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Spatie\WebhookServer\WebhookCall;
class SendSpeedtestThresholdNotification
{
/**
* Handle the event.
*/
public function handle(SpeedtestCompleted $event): void
{
$notificationSettings = new NotificationSettings();
if (! $notificationSettings->slack_enabled) {
return;
}
if (! $notificationSettings->slack_on_threshold_failure) {
return;
}
if (! count($notificationSettings->slack_webhooks)) {
Log::warning('Slack urls not found, check Slack notification channel settings.');
return;
}
$thresholdSettings = new ThresholdSettings();
if (! $thresholdSettings->absolute_enabled) {
return;
}
$failed = [];
if ($thresholdSettings->absolute_download > 0) {
array_push($failed, $this->absoluteDownloadThreshold(event: $event, thresholdSettings: $thresholdSettings));
}
if ($thresholdSettings->absolute_upload > 0) {
array_push($failed, $this->absoluteUploadThreshold(event: $event, thresholdSettings: $thresholdSettings));
}
if ($thresholdSettings->absolute_ping > 0) {
array_push($failed, $this->absolutePingThreshold(event: $event, thresholdSettings: $thresholdSettings));
}
$failed = array_filter($failed);
if (! count($failed)) {
Log::warning('Failed Slack thresholds not found, won\'t send notification.');
return;
}
$payload = [
'text' => view('slack.speedtest-threshold', [
'id' => $event->result->id,
'service' => Str::title($event->result->service),
'serverName' => $event->result->server_name,
'serverId' => $event->result->server_id,
'isp' => $event->result->isp,
'metrics' => $failed,
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])->render(),
];
foreach ($notificationSettings->slack_webhooks as $url) {
WebhookCall::create()
->url($url['url'])
->payload($payload)
->doNotSign()
->dispatch();
}
}
/**
* Build Slack notification if absolute download threshold is breached.
*/
protected function absoluteDownloadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
{
if (! absoluteDownloadThresholdFailed($thresholdSettings->absolute_download, $event->result->download)) {
return false;
}
return [
'name' => 'Download',
'threshold' => $thresholdSettings->absolute_download.' Mbps',
'value' => Number::toBitRate(bits: $event->result->download_bits, precision: 2),
];
}
/**
* Build Slack notification if absolute upload threshold is breached.
*/
protected function absoluteUploadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
{
if (! absoluteUploadThresholdFailed($thresholdSettings->absolute_upload, $event->result->upload)) {
return false;
}
return [
'name' => 'Upload',
'threshold' => $thresholdSettings->absolute_upload.' Mbps',
'value' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2),
];
}
/**
* Build Slack notification if absolute ping threshold is breached.
*/
protected function absolutePingThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
{
if (! absolutePingThresholdFailed($thresholdSettings->absolute_ping, $event->result->ping)) {
return false;
}
return [
'name' => 'Ping',
'threshold' => $thresholdSettings->absolute_ping.' ms',
'value' => round($event->result->ping, 2).' ms',
];
}
}
@@ -43,6 +43,7 @@ class SendSpeedtestCompletedNotification
'download' => Number::toBitRate(bits: $event->result->download_bits, precision: 2),
'upload' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2),
'packetLoss' => is_numeric($event->result->packet_loss) ? round($event->result->packet_loss, 2) : 'n/a',
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])->render();
@@ -69,6 +69,7 @@ class SendSpeedtestThresholdNotification
'serverId' => $event->result->server_id,
'isp' => $event->result->isp,
'metrics' => $failed,
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])->render();
@@ -41,6 +41,7 @@ class SendSpeedtestCompletedNotification
'download' => $event->result->downloadBits,
'upload' => $event->result->uploadBits,
'packetLoss' => $event->result->packet_loss,
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])
->doNotSign()
@@ -68,6 +68,7 @@ class SendSpeedtestThresholdNotification
'site_name' => config('app.name'),
'isp' => $event->result->isp,
'metrics' => $failed,
'speedtest_url' => $event->result->result_url,
'url' => url('/admin/results'),
])
->doNotSign()
+1
View File
@@ -52,6 +52,7 @@ class SpeedtestCompletedMail extends Mailable implements ShouldQueue
'download' => Number::toBitRate(bits: $this->result->download_bits, precision: 2),
'upload' => Number::toBitRate(bits: $this->result->upload_bits, precision: 2),
'packetLoss' => is_numeric($this->result->packet_loss) ? $this->result->packet_loss : 'n/a',
'speedtest_url' => $this->result->result_url,
'url' => url('/admin/results'),
],
);
+1
View File
@@ -48,6 +48,7 @@ class SpeedtestThresholdMail extends Mailable implements ShouldQueue
'serverName' => $this->result->server_name,
'serverId' => $this->result->server_id,
'isp' => $this->result->isp,
'speedtest_url' => $this->result->result_url,
'url' => url('/admin/results'),
'metrics' => $this->metrics,
],
+40
View File
@@ -46,6 +46,46 @@ class NotificationSettings extends Settings
public ?array $discord_webhooks;
public bool $ntfy_enabled;
public bool $ntfy_on_speedtest_run;
public bool $ntfy_on_threshold_failure;
public ?array $ntfy_webhooks;
public bool $pushover_enabled;
public bool $pushover_on_speedtest_run;
public bool $pushover_on_threshold_failure;
public ?array $pushover_webhooks;
public bool $healthcheck_enabled;
public bool $healthcheck_on_speedtest_run;
public bool $healthcheck_on_threshold_failure;
public ?array $healthcheck_webhooks;
public bool $slack_enabled;
public bool $slack_on_speedtest_run;
public bool $slack_on_threshold_failure;
public ?array $slack_webhooks;
public bool $gotify_enabled;
public bool $gotify_on_speedtest_run;
public bool $gotify_on_threshold_failure;
public ?array $gotify_webhooks;
public static function group(): string
{
return 'notification';
@@ -0,0 +1,14 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('notification.healthcheck_enabled', false);
$this->migrator->add('notification.healthcheck_on_speedtest_run', false);
$this->migrator->add('notification.healthcheck_on_threshold_failure', false);
$this->migrator->add('notification.healthcheck_webhooks', null);
}
};
@@ -0,0 +1,14 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('notification.ntfy_enabled', false);
$this->migrator->add('notification.ntfy_on_speedtest_run', false);
$this->migrator->add('notification.ntfy_on_threshold_failure', false);
$this->migrator->add('notification.ntfy_webhooks', null);
}
};
@@ -0,0 +1,14 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('notification.slack_enabled', false);
$this->migrator->add('notification.slack_on_speedtest_run', false);
$this->migrator->add('notification.slack_on_threshold_failure', false);
$this->migrator->add('notification.slack_webhooks', null);
}
};
@@ -0,0 +1,14 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('notification.gotify_enabled', false);
$this->migrator->add('notification.gotify_on_speedtest_run', false);
$this->migrator->add('notification.gotify_on_threshold_failure', false);
$this->migrator->add('notification.gotify_webhooks', null);
}
};
@@ -0,0 +1,14 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('notification.pushover_enabled', false);
$this->migrator->add('notification.pushover_on_speedtest_run', false);
$this->migrator->add('notification.pushover_on_threshold_failure', false);
$this->migrator->add('notification.pushover_webhooks', null);
}
};
+105 -101
View File
@@ -539,9 +539,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.1.tgz",
"integrity": "sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz",
"integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==",
"cpu": [
"arm"
],
@@ -553,9 +553,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.1.tgz",
"integrity": "sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz",
"integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==",
"cpu": [
"arm64"
],
@@ -567,9 +567,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.1.tgz",
"integrity": "sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz",
"integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==",
"cpu": [
"arm64"
],
@@ -581,9 +581,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.1.tgz",
"integrity": "sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz",
"integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==",
"cpu": [
"x64"
],
@@ -595,9 +595,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.1.tgz",
"integrity": "sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz",
"integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==",
"cpu": [
"arm"
],
@@ -609,9 +609,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.1.tgz",
"integrity": "sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz",
"integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==",
"cpu": [
"arm"
],
@@ -623,9 +623,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.1.tgz",
"integrity": "sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz",
"integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==",
"cpu": [
"arm64"
],
@@ -637,9 +637,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.1.tgz",
"integrity": "sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz",
"integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==",
"cpu": [
"arm64"
],
@@ -651,9 +651,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.1.tgz",
"integrity": "sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz",
"integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==",
"cpu": [
"ppc64"
],
@@ -665,9 +665,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.1.tgz",
"integrity": "sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz",
"integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==",
"cpu": [
"riscv64"
],
@@ -679,9 +679,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.1.tgz",
"integrity": "sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz",
"integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==",
"cpu": [
"s390x"
],
@@ -693,9 +693,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.1.tgz",
"integrity": "sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz",
"integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==",
"cpu": [
"x64"
],
@@ -707,9 +707,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.1.tgz",
"integrity": "sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz",
"integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==",
"cpu": [
"x64"
],
@@ -721,9 +721,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.1.tgz",
"integrity": "sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz",
"integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==",
"cpu": [
"arm64"
],
@@ -735,9 +735,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.1.tgz",
"integrity": "sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz",
"integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==",
"cpu": [
"ia32"
],
@@ -749,9 +749,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz",
"integrity": "sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz",
"integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==",
"cpu": [
"x64"
],
@@ -776,9 +776,9 @@
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.13.tgz",
"integrity": "sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==",
"version": "0.5.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.14.tgz",
"integrity": "sha512-ZvOCjUbsJBjL9CxQBn+VEnFpouzuKhxh2dH8xMIWHILL+HfOYtlAkWcyoon8LlzE53d2Yo6YO6pahKKNW3q1YQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -853,9 +853,9 @@
"license": "MIT"
},
"node_modules/autoprefixer": {
"version": "10.4.19",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
"integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
"integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
"dev": true,
"funding": [
{
@@ -873,11 +873,11 @@
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0",
"caniuse-lite": "^1.0.30001599",
"browserslist": "^4.23.3",
"caniuse-lite": "^1.0.30001646",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"picocolors": "^1.0.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
@@ -934,9 +934,9 @@
}
},
"node_modules/browserslist": {
"version": "4.23.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz",
"integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==",
"version": "4.23.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
"dev": true,
"funding": [
{
@@ -954,9 +954,9 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001640",
"electron-to-chromium": "^1.4.820",
"node-releases": "^2.0.14",
"caniuse-lite": "^1.0.30001646",
"electron-to-chromium": "^1.5.4",
"node-releases": "^2.0.18",
"update-browserslist-db": "^1.1.0"
},
"bin": {
@@ -977,9 +977,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001643",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz",
"integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==",
"version": "1.0.30001651",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
"dev": true,
"funding": [
{
@@ -1115,9 +1115,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.2.tgz",
"integrity": "sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ==",
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz",
"integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==",
"dev": true,
"license": "ISC"
},
@@ -1231,9 +1231,9 @@
}
},
"node_modules/foreground-child": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz",
"integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==",
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
"integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -1723,9 +1723,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.40",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
"integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
"version": "8.4.41",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
"dev": true,
"funding": [
{
@@ -1973,9 +1973,9 @@
}
},
"node_modules/rollup": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.1.tgz",
"integrity": "sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==",
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz",
"integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1989,22 +1989,22 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.19.1",
"@rollup/rollup-android-arm64": "4.19.1",
"@rollup/rollup-darwin-arm64": "4.19.1",
"@rollup/rollup-darwin-x64": "4.19.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.19.1",
"@rollup/rollup-linux-arm-musleabihf": "4.19.1",
"@rollup/rollup-linux-arm64-gnu": "4.19.1",
"@rollup/rollup-linux-arm64-musl": "4.19.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.19.1",
"@rollup/rollup-linux-riscv64-gnu": "4.19.1",
"@rollup/rollup-linux-s390x-gnu": "4.19.1",
"@rollup/rollup-linux-x64-gnu": "4.19.1",
"@rollup/rollup-linux-x64-musl": "4.19.1",
"@rollup/rollup-win32-arm64-msvc": "4.19.1",
"@rollup/rollup-win32-ia32-msvc": "4.19.1",
"@rollup/rollup-win32-x64-msvc": "4.19.1",
"@rollup/rollup-android-arm-eabi": "4.20.0",
"@rollup/rollup-android-arm64": "4.20.0",
"@rollup/rollup-darwin-arm64": "4.20.0",
"@rollup/rollup-darwin-x64": "4.20.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.20.0",
"@rollup/rollup-linux-arm-musleabihf": "4.20.0",
"@rollup/rollup-linux-arm64-gnu": "4.20.0",
"@rollup/rollup-linux-arm64-musl": "4.20.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.20.0",
"@rollup/rollup-linux-riscv64-gnu": "4.20.0",
"@rollup/rollup-linux-s390x-gnu": "4.20.0",
"@rollup/rollup-linux-x64-gnu": "4.20.0",
"@rollup/rollup-linux-x64-musl": "4.20.0",
"@rollup/rollup-win32-arm64-msvc": "4.20.0",
"@rollup/rollup-win32-ia32-msvc": "4.20.0",
"@rollup/rollup-win32-x64-msvc": "4.20.0",
"fsevents": "~2.3.2"
}
},
@@ -2219,9 +2219,9 @@
}
},
"node_modules/tailwindcss": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
"integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
"version": "3.4.9",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz",
"integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2352,14 +2352,14 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",
"integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz",
"integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.39",
"postcss": "^8.4.40",
"rollup": "^4.13.0"
},
"bin": {
@@ -2379,6 +2379,7 @@
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
@@ -2396,6 +2397,9 @@
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
@@ -9,4 +9,5 @@ A new speedtest was completed using **{{ $service }}**.
- **Download:** {{ $download }}
- **Upload:** {{ $upload }}
- **Packet Loss:** {{ $packetLoss }} **%**
- **Ookla Speedtest:** {{ $speedtest_url }}
- **URL:** {{ $url }}
@@ -5,4 +5,5 @@ A new speedtest was completed using **{{ $service }}** on **{{ $isp }}** but a t
@foreach ($metrics as $item)
- **{{ $item['name'] }}** {{ $item['threshold'] }}: {{ $item['value'] }}
@endforeach
- **Ookla Speedtest:** {{ $speedtest_url }}
- **URL:** {{ $url }}
@@ -12,7 +12,8 @@ A new speedtest was completed using **{{ $service }}**.
| Ping | {{ $ping }} |
| Download | {{ $download }} |
| Upload | {{ $upload }} |
| Packet Loss | {{ $packetLoss }}**%** |
| Packet Loss | {{ $packetLoss }} **%** |
</x-mail::table>
@@ -20,6 +21,10 @@ A new speedtest was completed using **{{ $service }}**.
View Results
</x-mail::button>
<x-mail::button :Ookla Speedtest="$speedtest_url">
View Results on Ookla
</x-mail::button>
Thanks,<br>
{{ config('app.name') }}
</x-mail::message>
@@ -15,6 +15,10 @@ A new speedtest was completed using **{{ $service }}** on **{{ $isp }}** but a t
View Results
</x-mail::button>
<x-mail::button :Ookla Speedtest="$speedtest_url">
View Results on Ookla
</x-mail::button>
Thanks,<br>
{{ config('app.name') }}
</x-mail::message>
@@ -0,0 +1,13 @@
Speedtest Completed - #{{ $id }}
A new speedtest was completed using {{ $service }}.
- Server name: {{ $serverName }}
- Server ID: {{ $serverId }}
- ISP: {{ $isp }}
- Ping: {{ $ping }}
- Download: {{ $download }}
- Upload: {{ $upload }}
- Packet Loss: {{ $packetLoss }} %
- Ookla Speedtest:{{ $speedtest_url }}
- URL: {{ $url }}
@@ -0,0 +1,9 @@
Speedtest Threshold Breached - #{{ $id }}
A new speedtest was completed using {{ $service }} on {{ $isp }} but a threshold was breached.
@foreach ($metrics as $item)
- {{ $item['name'] }} {{ $item['threshold'] }}: {{ $item['value'] }}
@endforeach
- Ookla Speedtest: {{ $speedtest_url }}
- URL: {{ $url }}
@@ -0,0 +1,13 @@
Speedtest Completed - #{{ $id }}
A new speedtest was completed using {{ $service }}.
Server name: {{ $serverName }}
Server ID: {{ $serverId }}
ISP: {{ $isp }}
Ping: {{ $ping }}
Download: {{ $download }}
Upload: {{ $upload }}
Packet Loss: {{ $packetLoss }} %
Ookla Speedtest: {{ $speedtest_url }}
URL: {{ $url }}
@@ -0,0 +1,9 @@
Speedtest Threshold Breached - #{{ $id }}
A new speedtest was completed using {{ $service }} on {{ $isp }} but a threshold was breached.
@foreach ($metrics as $item)
- {{ $item['name'] }} {{ $item['threshold'] }}: {{ $item['value'] }}
@endforeach
- Ookla Speedtest: {{ $speedtest_url }}
- URL: {{ $url }}
@@ -0,0 +1,13 @@
Speedtest Completed - #{{ $id }}
A new speedtest was completed using {{ $service }}.
- Server name: {{ $serverName }}
- Server ID: {{ $serverId }}
- ISP: {{ $isp }}
- Ping: {{ $ping }}
- Download: {{ $download }}
- Upload: {{ $upload }}
- Packet Loss: {{ $packetLoss }} %
- Ookla Speedtest: {{ $speedtest_url }}
- URL: {{ $url }}
@@ -0,0 +1,9 @@
Speedtest Threshold Breached - #{{ $id }}
A new speedtest was completed using {{ $service }} on {{ $isp }} but a threshold was breached.
@foreach ($metrics as $item)
- {{ $item['name'] }} {{ $item['threshold'] }}: {{ $item['value'] }}
@endforeach
- Ookla Speedtest: {{ $speedtest_url }}
- URL: {{ $url }}
@@ -0,0 +1,13 @@
*Speedtest Completed - #{{ $id }}*
A new speedtest was completed using *{{ $service }}*.
- *Server name:* {{ $serverName }}
- *Server ID:* {{ $serverId }}
- *ISP:* {{ $isp }}
- *Ping:* {{ $ping }}
- *Download:* {{ $download }}
- *Upload:* {{ $upload }}
- *Packet Loss:* {{ $packetLoss }} *%*
- *Ookla Speedtest:* {{ $speedtest_url }}
- *URL:* {{ $url }}
@@ -0,0 +1,9 @@
**Speedtest Threshold Breached - #{{ $id }}**
A new speedtest was completed using *{{ $service }}* on *{{ $isp }}* but a threshold was breached.
@foreach ($metrics as $item)
- *{{ $item['name'] }}* {{ $item['threshold'] }}: {{ $item['value'] }}
@endforeach
- *Ookla Speedtest:* {{ $speedtest_url }}
- *URL:* {{ $url }}
@@ -9,4 +9,5 @@ A new speedtest was completed using *{{ $service }}*.
- *Download:* {{ $download }}
- *Upload:* {{ $upload }}
- **Packet Loss:** {{ $packetLoss }}**%**
- **Ookla Speedtest:** {{ $speedtest_url }}
- **URL:** {{ $url }}
@@ -5,4 +5,5 @@ A new speedtest was completed using **{{ $service }}** on **{{ $isp }}** but a t
@foreach ($metrics as $item)
- **{{ $item['name'] }}** {{ $item['threshold'] }}: {{ $item['value'] }}
@endforeach
- **Ookla Speedtest:** {{ $speedtest_url }}
- **URL:** {{ $url }}