diff --git a/README.md b/README.md index 47e8c8b..17d5dd5 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,11 @@ EasyMonitor is a full-stack monitoring platform for your websites and APIs. Add a URL, pick an interval, and get alerted when something breaks. Group monitors into projects, share live status with your users via public status pages, and run probes in multiple regions to eliminate false positives. +## Screenshot + + +![EasyMonitor dashboard](public/img/dashboard.png) + --- ## Features diff --git a/app/Enums/CheckType.php b/app/Enums/CheckType.php new file mode 100644 index 0000000..d3baf75 --- /dev/null +++ b/app/Enums/CheckType.php @@ -0,0 +1,33 @@ + 'HTTP / HTTPS', + self::Icmp => 'Ping (ICMP)', + }; + } + + /** + * Short label for compact UI badges. + */ + public function shortLabel(): string + { + return match ($this) { + self::Http => 'HTTP', + self::Icmp => 'ICMP', + }; + } +} diff --git a/app/Livewire/Monitors/Create.php b/app/Livewire/Monitors/Create.php index 116a201..9874623 100644 --- a/app/Livewire/Monitors/Create.php +++ b/app/Livewire/Monitors/Create.php @@ -2,10 +2,12 @@ namespace App\Livewire\Monitors; +use App\Enums\CheckType; use App\Models\Monitor; use App\Models\Project; use App\Models\Team; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Validation\Rule; use Livewire\Attributes\Url; use Livewire\Component; @@ -20,6 +22,8 @@ class Create extends Component public string $name = ''; + public string $checkType = 'http'; + public string $url = ''; public int $checkInterval = 60; @@ -54,11 +58,16 @@ public function mount(): void */ public function rules(): array { + $urlRule = $this->checkType === CheckType::Icmp->value + ? ['required', 'string', 'max:255', 'regex:/^(?!.*:\/\/)[a-zA-Z0-9](?:[a-zA-Z0-9.\-:]*[a-zA-Z0-9])?$/'] + : ['required', 'url', 'max:255']; + return [ 'teamId' => ['nullable', 'exists:teams,id'], 'projectId' => ['nullable', 'exists:projects,id'], 'name' => ['required', 'string', 'max:255'], - 'url' => ['required', 'url', 'max:255'], + 'checkType' => ['required', Rule::in([CheckType::Http->value, CheckType::Icmp->value])], + 'url' => $urlRule, 'checkInterval' => ['required', 'integer', 'min:30', 'max:3600'], 'isActive' => ['boolean'], 'failureThreshold' => ['required', 'integer', 'min:1', 'max:10'], @@ -67,6 +76,18 @@ public function rules(): array ]; } + /** + * Custom validation messages + * + * @return array + */ + public function messages(): array + { + return [ + 'url.regex' => __('Enter a valid hostname or IP address (no scheme, no path).'), + ]; + } + /** * Save the new monitor */ @@ -99,6 +120,7 @@ public function save(): void 'team_id' => $validated['teamId'], 'project_id' => $validated['projectId'], 'name' => $validated['name'], + 'check_type' => $validated['checkType'], 'url' => $validated['url'], 'check_interval' => $validated['checkInterval'], 'is_active' => $validated['isActive'], diff --git a/app/Livewire/Monitors/Edit.php b/app/Livewire/Monitors/Edit.php index 957fc22..8e73493 100644 --- a/app/Livewire/Monitors/Edit.php +++ b/app/Livewire/Monitors/Edit.php @@ -2,9 +2,11 @@ namespace App\Livewire\Monitors; +use App\Enums\CheckType; use App\Models\Monitor; use App\Models\Project; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Validation\Rule; use Livewire\Component; class Edit extends Component @@ -17,6 +19,8 @@ class Edit extends Component public string $name = ''; + public string $checkType = 'http'; + public string $url = ''; public int $checkInterval = 60; @@ -40,6 +44,7 @@ public function mount(Monitor $monitor): void $this->monitor = $monitor; $this->projectId = $monitor->project_id; $this->name = $monitor->name; + $this->checkType = $monitor->check_type->value; $this->url = $monitor->url; $this->checkInterval = $monitor->check_interval; $this->isActive = $monitor->is_active; @@ -56,10 +61,15 @@ public function mount(Monitor $monitor): void */ public function rules(): array { + $urlRule = $this->checkType === CheckType::Icmp->value + ? ['required', 'string', 'max:255', 'regex:/^(?!.*:\/\/)[a-zA-Z0-9](?:[a-zA-Z0-9.\-:]*[a-zA-Z0-9])?$/'] + : ['required', 'url', 'max:255']; + return [ 'projectId' => ['nullable', 'exists:projects,id'], 'name' => ['required', 'string', 'max:255'], - 'url' => ['required', 'url', 'max:255'], + 'checkType' => ['required', Rule::in([CheckType::Http->value, CheckType::Icmp->value])], + 'url' => $urlRule, 'checkInterval' => ['required', 'integer', 'min:30', 'max:3600'], 'isActive' => ['boolean'], 'failureThreshold' => ['required', 'integer', 'min:1', 'max:10'], @@ -68,6 +78,18 @@ public function rules(): array ]; } + /** + * Custom validation messages + * + * @return array + */ + public function messages(): array + { + return [ + 'url.regex' => __('Enter a valid hostname or IP address (no scheme, no path).'), + ]; + } + /** * Save the updated monitor */ @@ -89,6 +111,7 @@ public function save(): void 'project_id' => $validated['projectId'], 'team_id' => $validated['projectId'] ? null : $this->monitor->team_id, 'name' => $validated['name'], + 'check_type' => $validated['checkType'], 'url' => $validated['url'], 'check_interval' => $validated['checkInterval'], 'is_active' => $validated['isActive'], diff --git a/app/Models/Monitor.php b/app/Models/Monitor.php index 9c3151b..8d267cf 100644 --- a/app/Models/Monitor.php +++ b/app/Models/Monitor.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Enums\CheckType; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -25,6 +26,7 @@ class Monitor extends Model 'project_id', 'name', 'url', + 'check_type', 'is_active', 'status', 'check_interval', @@ -47,6 +49,7 @@ protected function casts(): array 'is_active' => 'boolean', 'last_checked_at' => 'datetime', 'next_run_at' => 'datetime', + 'check_type' => CheckType::class, ]; } diff --git a/app/Services/MonitoringEngine/CheckDispatcher.php b/app/Services/MonitoringEngine/CheckDispatcher.php index abe91b8..42e433c 100644 --- a/app/Services/MonitoringEngine/CheckDispatcher.php +++ b/app/Services/MonitoringEngine/CheckDispatcher.php @@ -4,6 +4,7 @@ namespace App\Services\MonitoringEngine; +use App\Enums\CheckType; use App\Models\Monitor; use Illuminate\Support\Facades\Redis; @@ -71,13 +72,19 @@ public function dispatchCheck(Monitor $monitor): string // so ResultConsumer can apply quorum across probes. $roundId = (string) \Illuminate\Support\Str::uuid(); + // For ICMP monitors, the URL column stores a bare host. The probe node + // discriminates check types by URL scheme, so we prefix icmp://. + $url = $monitor->check_type === CheckType::Icmp + ? 'icmp://'.$monitor->url + : $monitor->url; + // XADD checks * check_id=42 url=... timeout=... round_id=... $entryId = Redis::connection('streams')->xadd( self::STREAM_CHECKS, '*', // Auto-generate ID [ 'check_id' => (string) $monitor->id, - 'url' => $monitor->url, + 'url' => $url, 'timeout' => (string) ($monitor->check_interval * 1000), // milliseconds 'round_id' => $roundId, ] diff --git a/database/factories/MonitorFactory.php b/database/factories/MonitorFactory.php index 1754bf4..9430cf2 100644 --- a/database/factories/MonitorFactory.php +++ b/database/factories/MonitorFactory.php @@ -21,6 +21,7 @@ public function definition(): array 'team_id' => null, 'name' => fake()->domainName(), 'url' => fake()->url(), + 'check_type' => \App\Enums\CheckType::Http, 'is_active' => true, 'status' => 'pending', 'check_interval' => 60, @@ -75,4 +76,15 @@ public function inactive(): static 'is_active' => false, ]); } + + /** + * Indicate that the monitor is an ICMP/ping check + */ + public function icmp(): static + { + return $this->state(fn (array $attributes) => [ + 'check_type' => \App\Enums\CheckType::Icmp, + 'url' => fake()->domainName(), + ]); + } } diff --git a/database/migrations/2026_05_14_114700_add_check_type_to_monitors_table.php b/database/migrations/2026_05_14_114700_add_check_type_to_monitors_table.php new file mode 100644 index 0000000..fed0b7b --- /dev/null +++ b/database/migrations/2026_05_14_114700_add_check_type_to_monitors_table.php @@ -0,0 +1,28 @@ +string('check_type')->default('http')->after('url'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('monitors', function (Blueprint $table) { + $table->dropColumn('check_type'); + }); + } +}; diff --git a/public/img/dashboard.png b/public/img/dashboard.png new file mode 100644 index 0000000..4e29874 Binary files /dev/null and b/public/img/dashboard.png differ diff --git a/resources/views/livewire/monitors/create.blade.php b/resources/views/livewire/monitors/create.blade.php index f8c6104..7701965 100644 --- a/resources/views/livewire/monitors/create.blade.php +++ b/resources/views/livewire/monitors/create.blade.php @@ -19,6 +19,28 @@

{{ __('What to monitor') }}

+
+ +
+ + +
+
+