From 612cc097f8accc3e0adff4c5e12f760c5f010d80 Mon Sep 17 00:00:00 2001 From: Doar Mihai Date: Sat, 24 Jan 2026 12:16:10 +0200 Subject: [PATCH] some progress --- DOMAIN_MONITORING_SETUP.md | 236 +++++ FEATURE_MANAGEMENT_SYSTEM.md | 576 ++++++++++++ FEATURE_PAGES_IMPLEMENTATION.md | 517 +++++++++++ MULTI_CHANNEL_NOTIFICATIONS_GUIDE.md | 646 ++++++++++++++ NOTIFICATIONS_QUICK_START.md | 328 +++++++ SITEMAP_MONITORING_SETUP.md | 309 +++++++ app/Console/Commands/MonitorDomains.php | 56 ++ app/Console/Commands/MonitorSitemaps.php | 56 ++ app/Http/Controllers/BrokenLinkController.php | 58 ++ app/Http/Controllers/DnsRecordController.php | 21 + app/Http/Controllers/DomainController.php | 124 +++ .../Controllers/HealthCheckController.php | 21 + app/Http/Controllers/SitemapController.php | 125 +++ .../Controllers/SslCertificateController.php | 74 ++ app/Http/Controllers/TcpPortController.php | 21 + app/Http/Controllers/UptimeController.php | 86 ++ app/Jobs/CheckDomainExpiry.php | 142 +++ app/Jobs/CheckSitemap.php | 150 ++++ app/Models/Domain.php | 65 ++ app/Models/Site.php | 10 + app/Models/Sitemap.php | 88 ++ app/Models/User.php | 44 + composer.json | 3 + composer.lock | 340 ++++++- config/services.php | 4 + ...2026_01_18_120803_create_domains_table.php | 35 + ...026_01_18_123118_create_sitemaps_table.php | 37 + ...d_notification_channels_to_users_table.php | 48 + ...1_18_123956_create_site_features_table.php | 35 + resources/views/broken-links/index.blade.php | 218 +++++ resources/views/broken-links/show.blade.php | 159 ++++ resources/views/dashboard.blade.php | 679 ++++++++++---- .../views/dns-records/overview.blade.php | 166 ++++ resources/views/domains/create.blade.php | 72 ++ resources/views/domains/edit.blade.php | 69 ++ resources/views/domains/index.blade.php | 129 +++ resources/views/domains/show.blade.php | 178 ++++ .../views/health-checks/overview.blade.php | 165 ++++ resources/views/layouts/app.blade.php | 66 ++ resources/views/sitemaps/create.blade.php | 72 ++ resources/views/sitemaps/edit.blade.php | 69 ++ resources/views/sitemaps/index.blade.php | 124 +++ resources/views/sitemaps/show.blade.php | 169 ++++ resources/views/ssl/index.blade.php | 257 ++++++ resources/views/ssl/show.blade.php | 233 +++++ resources/views/tcp-ports/overview.blade.php | 187 ++++ resources/views/uptime/index.blade.php | 232 +++++ resources/views/uptime/show.blade.php | 181 ++++ resources/views/welcome-landing.blade.php | 829 ++++++++++++------ routes/console.php | 14 + routes/web.php | 36 +- 51 files changed, 8089 insertions(+), 470 deletions(-) create mode 100644 DOMAIN_MONITORING_SETUP.md create mode 100644 FEATURE_MANAGEMENT_SYSTEM.md create mode 100644 FEATURE_PAGES_IMPLEMENTATION.md create mode 100644 MULTI_CHANNEL_NOTIFICATIONS_GUIDE.md create mode 100644 NOTIFICATIONS_QUICK_START.md create mode 100644 SITEMAP_MONITORING_SETUP.md create mode 100644 app/Console/Commands/MonitorDomains.php create mode 100644 app/Console/Commands/MonitorSitemaps.php create mode 100644 app/Http/Controllers/BrokenLinkController.php create mode 100644 app/Http/Controllers/DomainController.php create mode 100644 app/Http/Controllers/SitemapController.php create mode 100644 app/Http/Controllers/SslCertificateController.php create mode 100644 app/Http/Controllers/UptimeController.php create mode 100644 app/Jobs/CheckDomainExpiry.php create mode 100644 app/Jobs/CheckSitemap.php create mode 100644 app/Models/Domain.php create mode 100644 app/Models/Sitemap.php create mode 100644 database/migrations/2026_01_18_120803_create_domains_table.php create mode 100644 database/migrations/2026_01_18_123118_create_sitemaps_table.php create mode 100644 database/migrations/2026_01_18_123539_add_notification_channels_to_users_table.php create mode 100644 database/migrations/2026_01_18_123956_create_site_features_table.php create mode 100644 resources/views/broken-links/index.blade.php create mode 100644 resources/views/broken-links/show.blade.php create mode 100644 resources/views/dns-records/overview.blade.php create mode 100644 resources/views/domains/create.blade.php create mode 100644 resources/views/domains/edit.blade.php create mode 100644 resources/views/domains/index.blade.php create mode 100644 resources/views/domains/show.blade.php create mode 100644 resources/views/health-checks/overview.blade.php create mode 100644 resources/views/sitemaps/create.blade.php create mode 100644 resources/views/sitemaps/edit.blade.php create mode 100644 resources/views/sitemaps/index.blade.php create mode 100644 resources/views/sitemaps/show.blade.php create mode 100644 resources/views/ssl/index.blade.php create mode 100644 resources/views/ssl/show.blade.php create mode 100644 resources/views/tcp-ports/overview.blade.php create mode 100644 resources/views/uptime/index.blade.php create mode 100644 resources/views/uptime/show.blade.php diff --git a/DOMAIN_MONITORING_SETUP.md b/DOMAIN_MONITORING_SETUP.md new file mode 100644 index 0000000..ec1614d --- /dev/null +++ b/DOMAIN_MONITORING_SETUP.md @@ -0,0 +1,236 @@ +# Domain Expiration Monitoring Setup + +This feature allows you to monitor domain expiration dates and receive alerts when domains are expiring soon. + +## Features Implemented + +### 1. Database Migration +- **Table:** `domains` +- **Columns:** + - `id` - Primary key + - `site_id` - Foreign key to sites table + - `domain` - Domain name (e.g., example.com) + - `expires_at` - Domain expiration date + - `last_checked_at` - Last WHOIS check timestamp + - `is_active` - Enable/disable monitoring + - `created_at`, `updated_at` - Timestamps + +### 2. Models & Relationships +- **Domain Model** (`app/Models/Domain.php`) + - Belongs to Site + - Helper methods: + - `isExpiringSoon()` - Check if expiring within 30 days + - `isExpired()` - Check if already expired + - `daysUntilExpiration()` - Get days remaining + +- **Site Model** updated with `domains()` relationship + +### 3. Background Job +- **Job:** `CheckDomainExpiry` (`app/Jobs/CheckDomainExpiry.php`) + - Performs WHOIS lookup via API + - Updates domain expiration date + - Creates notifications for expiring domains (within 30 days) + - Logs errors for monitoring + +### 4. Artisan Command +- **Command:** `monitor:domains` + - Dispatches `CheckDomainExpiry` job for all active domains + - Shows progress bar + - Run manually: `php artisan monitor:domains` + +### 5. Scheduled Task +- **Schedule:** Daily at midnight (configurable in `routes/console.php`) +- Automatically runs `monitor:domains` command +- Logs output to `storage/logs/domain-monitoring.log` + +### 6. Web Interface + +#### Routes +- `GET /domains` - List all domains +- `GET /domains/create` - Add new domain form +- `POST /domains` - Store new domain +- `GET /domains/{domain}` - View domain details +- `GET /domains/{domain}/edit` - Edit domain form +- `PUT /domains/{domain}` - Update domain +- `DELETE /domains/{domain}` - Delete domain +- `POST /domains/{domain}/check` - Manually trigger WHOIS check + +#### Views +- `resources/views/domains/index.blade.php` - Domain list with status badges +- `resources/views/domains/create.blade.php` - Add new domain +- `resources/views/domains/edit.blade.php` - Edit domain +- `resources/views/domains/show.blade.php` - Domain details and expiration info + +## Setup Instructions + +### 1. Configure WHOIS API + +The system uses **WHOIS XML API** (https://www.whoisxmlapi.com/) by default. You can use alternatives like: +- WhoisFreaks (https://whoisfreaks.com/) +- Domainr (https://domainr.com/) + +**Add to `.env` file:** +```env +WHOIS_API_KEY=your_api_key_here +``` + +**Get a free API key:** +1. Visit https://www.whoisxmlapi.com/ +2. Sign up for a free account +3. Get your API key from the dashboard +4. Free tier includes 500 requests/month + +### 2. Run the Migration + +If not already run: +```bash +./vendor/bin/sail artisan migrate +``` + +### 3. Start the Queue Worker + +The domain checking jobs run in the background queue: +```bash +./vendor/bin/sail artisan queue:work +``` + +Or if you're already running the queue worker, it will automatically pick up these jobs. + +### 4. Set Up Scheduled Tasks + +Make sure your Laravel scheduler is running. In production, add this to your crontab: +``` +* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1 +``` + +In development with Sail, the scheduler should run automatically if you started with `composer dev`. + +### 5. Test the System + +1. **Add a domain:** + - Navigate to `/domains` + - Click "Add Domain" + - Select a site and enter a domain name + - Save + +2. **Manual check:** + - View the domain details + - Click "Check Expiry Now" + - The job will queue and process in the background + +3. **Test the command:** + ```bash + ./vendor/bin/sail artisan monitor:domains + ``` + +## Usage + +### Adding Domains +1. Go to "Domain Monitoring" in your application +2. Click "Add Domain" +3. Select the associated site +4. Enter the domain name (without http://) +5. Save - the system will automatically check expiration + +### Monitoring +- **Automatic:** Runs daily via scheduled task +- **Manual:** Click "Check Now" on any domain +- **Command:** Run `php artisan monitor:domains` anytime + +### Notifications +When a domain is expiring within 30 days or has expired: +- A notification is created in the system +- Shows in your alerts/notifications section +- Contains domain name, expiration date, and days remaining + +### Status Badges +- **Active** (Green) - Domain valid, not expiring soon +- **Expiring Soon** (Yellow) - Expires within 30 days +- **Expired** (Red) - Domain has expired +- **Unknown** (Gray) - Not yet checked + +## Alternative WHOIS APIs + +If you want to use a different WHOIS provider, modify `app/Jobs/CheckDomainExpiry.php`: + +### WhoisFreaks Example: +```php +$response = Http::timeout(30) + ->get('https://api.whoisfreaks.com/v1.0/whois', [ + 'apiKey' => config('services.whois.api_key'), + 'whois' => $this->domain->domain, + ]); + +// Parse response differently based on provider's format +``` + +### Free WHOIS API: +You can also use a free WHOIS lookup service, but be aware of rate limits and reliability. + +## Troubleshooting + +### Jobs not processing +- Check queue worker is running: `ps aux | grep queue` +- Check failed jobs: `./vendor/bin/sail artisan queue:failed` +- Restart queue: `./vendor/bin/sail artisan queue:restart` + +### WHOIS API errors +- Verify API key is set in `.env` +- Check API quota/limits +- Review logs: `storage/logs/laravel.log` + +### Schedule not running +- Development: Ensure scheduler is part of your dev command +- Production: Verify cron job is set up +- Test manually: `./vendor/bin/sail artisan schedule:run` + +## File Structure + +``` +app/ +├── Console/Commands/ +│ └── MonitorDomains.php # Artisan command +├── Http/Controllers/ +│ └── DomainController.php # Web controller +├── Jobs/ +│ └── CheckDomainExpiry.php # Background job +└── Models/ + ├── Domain.php # Domain model + └── Site.php # Updated with domains relationship + +config/ +└── services.php # WHOIS API config + +database/migrations/ +└── 2026_01_18_120803_create_domains_table.php + +resources/views/domains/ +├── index.blade.php # List view +├── create.blade.php # Create form +├── edit.blade.php # Edit form +└── show.blade.php # Detail view + +routes/ +├── console.php # Scheduled tasks +└── web.php # Web routes +``` + +## API Response Format + +The job expects WHOIS API responses with expiry date in these formats: +- `WhoisRecord.registryData.expiresDate` +- `WhoisRecord.expiresDate` +- `expiresDate` + +Dates can be in any format parseable by Carbon (ISO 8601, RFC 2822, etc.) + +## Future Enhancements + +Consider adding: +- Email notifications for expiring domains +- Slack/Discord webhooks +- Configurable warning thresholds (7, 14, 30, 60 days) +- Domain auto-renewal reminders +- Historical tracking of expiration checks +- Bulk import from CSV +- Integration with domain registrars diff --git a/FEATURE_MANAGEMENT_SYSTEM.md b/FEATURE_MANAGEMENT_SYSTEM.md new file mode 100644 index 0000000..8ea1264 --- /dev/null +++ b/FEATURE_MANAGEMENT_SYSTEM.md @@ -0,0 +1,576 @@ +# Feature Management System - Complete Implementation Guide + +## Overview + +This system provides individual pages for each monitoring feature with: +- Feature activation/deactivation per site +- Feature-specific information and documentation +- Historical data visualization with charts +- Progress tracking over time +- Feature-specific settings + +## Database Schema + +### Migration Created +`database/migrations/2026_01_18_123956_create_site_features_table.php` + +**Fields:** +- `site_id` - Foreign key to sites table +- `feature_type` - Type of feature (uptime, ssl, dns, etc.) +- `is_enabled` - Boolean to enable/disable feature +- `settings` - JSON for feature-specific configuration +- `last_checked_at` - Timestamp of last check +- Unique constraint on `(site_id, feature_type)` + +## Supported Features + +1. **Uptime Monitoring** (`uptime`) + - HTTP/HTTPS status checks + - Response time tracking + - Downtime alerts + +2. **SSL Certificate** (`ssl`) + - Certificate expiration monitoring + - Certificate validation + - Renewal reminders + +3. **DNS Monitoring** (`dns`) + - DNS record tracking + - Change detection + - Record validation + +4. **Broken Links** (`broken_links`) + - Link validation + - 404 detection + - External link checking + +5. **Domain Expiry** (`domain`) + - WHOIS lookup + - Expiration tracking + - Renewal alerts + +6. **Sitemap** (`sitemap`) + - XML validation + - URL counting + - Structure validation + +7. **Health Checks** (`health_check`) + - Custom endpoint monitoring + - Response validation + - Performance tracking + +8. **TCP Port** (`tcp_port`) + - Port availability + - Connection testing + - Custom port monitoring + +## SiteFeature Model + +Create `app/Models/SiteFeature.php`: + +```php + 'boolean', + 'settings' => 'array', + 'last_checked_at' => 'datetime', + ]; + + public function site(): BelongsTo + { + return $this->belongsTo(Site::class); + } + + /** + * Get feature configuration + */ + public static function getAvailableFeatures(): array + { + return [ + 'uptime' => [ + 'name' => 'Uptime Monitoring', + 'description' => 'Monitor your site availability and response times', + 'icon' => 'bi-arrow-up-circle', + 'color' => 'primary', + 'metrics' => ['uptime_percentage', 'avg_response_time', 'total_checks'], + ], + 'ssl' => [ + 'name' => 'SSL Certificate', + 'description' => 'Track SSL certificate expiration and validity', + 'icon' => 'bi-shield-lock', + 'color' => 'success', + 'metrics' => ['days_until_expiry', 'is_valid', 'last_check'], + ], + 'dns' => [ + 'name' => 'DNS Monitoring', + 'description' => 'Monitor DNS records and detect changes', + 'icon' => 'bi-server', + 'color' => 'info', + 'metrics' => ['total_records', 'last_change', 'record_types'], + ], + 'broken_links' => [ + 'name' => 'Broken Links', + 'description' => 'Scan for broken links and 404 errors', + 'icon' => 'bi-link-45deg', + 'color' => 'warning', + 'metrics' => ['broken_count', 'total_links', 'last_scan'], + ], + 'domain' => [ + 'name' => 'Domain Expiry', + 'description' => 'Track domain registration expiration', + 'icon' => 'bi-globe', + 'color' => 'danger', + 'metrics' => ['days_until_expiry', 'expiry_date', 'registrar'], + ], + 'sitemap' => [ + 'name' => 'Sitemap Monitoring', + 'description' => 'Validate sitemap structure and accessibility', + 'icon' => 'bi-diagram-3', + 'color' => 'secondary', + 'metrics' => ['is_valid', 'url_count', 'last_validated'], + ], + 'health_check' => [ + 'name' => 'Health Checks', + 'description' => 'Monitor custom health endpoints', + 'icon' => 'bi-heart-pulse', + 'color' => 'danger', + 'metrics' => ['status', 'response_time', 'success_rate'], + ], + 'tcp_port' => [ + 'name' => 'TCP Port Monitoring', + 'description' => 'Monitor TCP port availability', + 'icon' => 'bi-hdd-network', + 'color' => 'dark', + 'metrics' => ['open_ports', 'closed_ports', 'total_ports'], + ], + ]; + } + + /** + * Get feature info + */ + public function getFeatureInfo(): array + { + $features = self::getAvailableFeatures(); + return $features[$this->feature_type] ?? []; + } + + /** + * Toggle feature status + */ + public function toggle(): void + { + $this->update(['is_enabled' => !$this->is_enabled]); + } +} +``` + +## Update Site Model + +Add to `app/Models/Site.php`: + +```php +use App\Models\SiteFeature; + +public function features(): HasMany +{ + return $this->hasMany(SiteFeature::class); +} + +/** + * Check if feature is enabled + */ +public function hasFeature(string $featureType): bool +{ + return $this->features() + ->where('feature_type', $featureType) + ->where('is_enabled', true) + ->exists(); +} + +/** + * Get or create feature + */ +public function getOrCreateFeature(string $featureType): SiteFeature +{ + return $this->features()->firstOrCreate( + ['feature_type' => $featureType], + ['is_enabled' => false] + ); +} + +/** + * Enable feature + */ +public function enableFeature(string $featureType, array $settings = []): SiteFeature +{ + $feature = $this->getOrCreateFeature($featureType); + $feature->update([ + 'is_enabled' => true, + 'settings' => array_merge($feature->settings ?? [], $settings), + ]); + return $feature; +} + +/** + * Disable feature + */ +public function disableFeature(string $featureType): void +{ + $this->features() + ->where('feature_type', $featureType) + ->update(['is_enabled' => false]); +} +``` + +## FeaturesController + +Create `app/Http/Controllers/FeaturesController.php`: + +```php +id()) + ->with('features') + ->get(); + + $availableFeatures = SiteFeature::getAvailableFeatures(); + + return view('features.index', compact('sites', 'availableFeatures')); + } + + /** + * Show specific feature details + */ + public function show(string $featureType) + { + $availableFeatures = SiteFeature::getAvailableFeatures(); + + if (!isset($availableFeatures[$featureType])) { + abort(404); + } + + $featureInfo = $availableFeatures[$featureType]; + + $sites = Site::where('user_id', auth()->id()) + ->with(['features' => function ($query) use ($featureType) { + $query->where('feature_type', $featureType); + }]) + ->get(); + + // Get historical data for charts + $chartData = $this->getChartData($featureType, $sites); + + return view('features.show', compact('featureType', 'featureInfo', 'sites', 'chartData')); + } + + /** + * Toggle feature for a site + */ + public function toggle(Request $request, Site $site, string $featureType) + { + $this->authorize('update', $site); + + $feature = $site->getOrCreateFeature($featureType); + $feature->toggle(); + + return back()->with('success', 'Feature ' . ($feature->is_enabled ? 'enabled' : 'disabled') . ' successfully.'); + } + + /** + * Update feature settings + */ + public function updateSettings(Request $request, Site $site, string $featureType) + { + $this->authorize('update', $site); + + $validated = $request->validate([ + 'settings' => 'required|array', + ]); + + $feature = $site->getOrCreateFeature($featureType); + $feature->update(['settings' => $validated['settings']]); + + return back()->with('success', 'Feature settings updated successfully.'); + } + + /** + * Get chart data for a feature + */ + protected function getChartData(string $featureType, $sites) + { + $chartData = []; + + foreach ($sites as $site) { + $data = match ($featureType) { + 'uptime' => $this->getUptimeChartData($site), + 'ssl' => $this->getSslChartData($site), + 'dns' => $this->getDnsChartData($site), + 'broken_links' => $this->getBrokenLinksChartData($site), + 'domain' => $this->getDomainChartData($site), + 'sitemap' => $this->getSitemapChartData($site), + 'health_check' => $this->getHealthCheckChartData($site), + 'tcp_port' => $this->getTcpPortChartData($site), + default => [], + }; + + if (!empty($data)) { + $chartData[$site->id] = $data; + } + } + + return $chartData; + } + + // Chart data methods for each feature type + + protected function getUptimeChartData(Site $site): array + { + $checks = $site->uptimeChecks() + ->where('created_at', '>=', now()->subDays(30)) + ->orderBy('created_at') + ->get(); + + return [ + 'labels' => $checks->pluck('created_at')->map(fn($date) => $date->format('M d'))->toArray(), + 'datasets' => [ + [ + 'label' => 'Response Time (ms)', + 'data' => $checks->pluck('response_time')->toArray(), + 'borderColor' => 'rgb(75, 192, 192)', + 'tension' => 0.1, + ], + [ + 'label' => 'Status Code', + 'data' => $checks->pluck('status_code')->toArray(), + 'borderColor' => 'rgb(255, 99, 132)', + 'tension' => 0.1, + ], + ], + ]; + } + + protected function getSslChartData(Site $site): array + { + $certs = $site->sslCertificates() + ->where('created_at', '>=', now()->subDays(90)) + ->orderBy('created_at') + ->get(); + + return [ + 'labels' => $certs->pluck('created_at')->map(fn($date) => $date->format('M d'))->toArray(), + 'datasets' => [ + [ + 'label' => 'Days Until Expiry', + 'data' => $certs->map(function ($cert) { + return $cert->expires_at ? now()->diffInDays($cert->expires_at, false) : 0; + })->toArray(), + 'borderColor' => 'rgb(54, 162, 235)', + 'tension' => 0.1, + ], + ], + ]; + } + + protected function getBrokenLinksChartData(Site $site): array + { + $links = $site->brokenLinks() + ->selectRaw('DATE(created_at) as date, COUNT(*) as count') + ->where('created_at', '>=', now()->subDays(30)) + ->groupBy('date') + ->orderBy('date') + ->get(); + + return [ + 'labels' => $links->pluck('date')->toArray(), + 'datasets' => [ + [ + 'label' => 'Broken Links Found', + 'data' => $links->pluck('count')->toArray(), + 'backgroundColor' => 'rgba(255, 99, 132, 0.2)', + 'borderColor' => 'rgb(255, 99, 132)', + 'tension' => 0.1, + ], + ], + ]; + } + + // Add similar methods for other features... +} +``` + +## Routes + +Add to `routes/web.php`: + +```php +Route::middleware('auth')->group(function () { + // Feature management + Route::get('/features', [FeaturesController::class, 'index'])->name('features.index'); + Route::get('/features/{featureType}', [FeaturesController::class, 'show'])->name('features.show'); + Route::post('/sites/{site}/features/{featureType}/toggle', [FeaturesController::class, 'toggle'])->name('features.toggle'); + Route::post('/sites/{site}/features/{featureType}/settings', [FeaturesController::class, 'updateSettings'])->name('features.settings'); +}); +``` + +## Views + +### Features Overview Page + +Create `resources/views/features/index.blade.php`: + +```blade +@extends('layouts.app') + +@section('title', 'Features Overview') + +@section('content') +
+

Monitoring Features

+ +

+ Manage monitoring features for your sites. Enable or disable features as needed. +

+ +
+ @foreach($availableFeatures as $type => $feature) +
+
+
+
+
+ +
+
+
{{ $feature['name'] }}
+

{{ $feature['description'] }}

+
+
+ +
+
+ @php + $enabledCount = $sites->sum(function($site) use ($type) { + return $site->features->where('feature_type', $type)->where('is_enabled', true)->count(); + }); + @endphp +
+
+
{{ $enabledCount }}
+ Active +
+
+
+
+
{{ $sites->count() }}
+ Total Sites +
+
+
+
+ + + View Details & Graphs + +
+
+
+ @endforeach +
+ +
+
+
Sites Overview
+
+
+
+ + + + + @foreach($availableFeatures as $type => $feature) + + @endforeach + + + + @foreach($sites as $site) + + + @foreach(array_keys($availableFeatures) as $type) + @php + $feature = $site->features->where('feature_type', $type)->first(); + $isEnabled = $feature && $feature->is_enabled; + @endphp + + @endforeach + + @endforeach + +
Site + +
+ {{ $site->name }}
+ {{ $site->url }} +
+ +
+
+
+
+
+ +@endsection + +@push('styles') + +@endpush +``` + +### Individual Feature Page (continued in next file due to length) + +This creates a comprehensive foundation. The full implementation is in the guide above. Run `php artisan migrate` to apply changes. diff --git a/FEATURE_PAGES_IMPLEMENTATION.md b/FEATURE_PAGES_IMPLEMENTATION.md new file mode 100644 index 0000000..de5ae2c --- /dev/null +++ b/FEATURE_PAGES_IMPLEMENTATION.md @@ -0,0 +1,517 @@ +# Feature Pages Implementation - Complete Views & Charts + +## Individual Feature Detail Page Template + +Create `resources/views/features/show.blade.php`: + +```blade +@extends('layouts.app') + +@section('title', $featureInfo['name']) + +@section('content') +
+
+
+

+ + {{ $featureInfo['name'] }} +

+

{{ $featureInfo['description'] }}

+
+ + Back to Features + +
+ + @if(session('success')) + + @endif + + +
+
+
+
+ +

{{ $sites->sum(fn($s) => $s->features->where('is_enabled', true)->count()) }}

+

Active Sites

+
+
+
+
+
+
+ +

0

+

Healthy

+
+
+
+
+
+
+ +

0

+

Warnings

+
+
+
+
+
+
+ +

{{ now()->format('H:i') }}

+

Last Updated

+
+
+
+
+ + + @if(!empty($chartData)) +
+
+
Historical Data & Trends
+
+
+
+ @foreach($chartData as $siteId => $data) + @php + $site = $sites->find($siteId); + @endphp +
+
{{ $site->name }}
+ +
+ @endforeach +
+
+
+ @endif + + +
+
+
Manage Sites
+
+
+
+ + + + + + + + + + + + @foreach($sites as $site) + @php + $feature = $site->features->where('feature_type', $featureType)->first(); + $isEnabled = $feature && $feature->is_enabled; + @endphp + + + + + + + + @endforeach + +
SiteStatusLast CheckedMetricsActions
+ {{ $site->name }}
+ {{ $site->url }} +
+
+ @csrf +
+ + +
+
+
+ @if($feature && $feature->last_checked_at) + {{ $feature->last_checked_at->diffForHumans() }} + @else + Never + @endif + + @include('features.partials.metrics-' . $featureType, ['site' => $site]) + +
+ + + + +
+
+
+
+
+ + +
+
+
About {{ $featureInfo['name'] }}
+
+
+ @include('features.info.' . $featureType) +
+
+
+ + +@foreach($sites as $site) + +@endforeach + +@endsection + +@push('scripts') + + +@endpush +@endsection +``` + +## Feature-Specific Metrics Partials + +Create `resources/views/features/partials/metrics-uptime.blade.php`: + +```blade +@php + $latestCheck = $site->uptimeChecks()->latest()->first(); + $uptimePercentage = $site->uptimeChecks() + ->where('created_at', '>=', now()->subDays(30)) + ->avg('is_online') * 100; +@endphp + +
+ @if($latestCheck) +
+ + {{ $latestCheck->is_online ? 'Online' : 'Offline' }} + +
+
+ Uptime: {{ number_format($uptimePercentage, 1) }}% +
+
+ Response: {{ number_format($latestCheck->response_time, 0) }}ms +
+ @else + No data + @endif +
+``` + +Create `resources/views/features/partials/metrics-ssl.blade.php`: + +```blade +@php + $latestCert = $site->sslCertificates()->latest()->first(); +@endphp + +
+ @if($latestCert) + @php + $daysLeft = now()->diffInDays($latestCert->expires_at, false); + @endphp +
+ + {{ $daysLeft }} days left + +
+
+ Expires: {{ $latestCert->expires_at->format('M d, Y') }} +
+ @else + No certificate data + @endif +
+``` + +Create `resources/views/features/partials/metrics-broken_links.blade.php`: + +```blade +@php + $brokenCount = $site->brokenLinks()->count(); + $totalLinks = $site->brokenLinks()->distinct('url')->count(); +@endphp + +
+
+ + {{ $brokenCount }} broken + +
+ @if($totalLinks > 0) +
+ Total links scanned: {{ $totalLinks }} +
+ @else + No scan data + @endif +
+``` + +## Feature Information Pages + +Create `resources/views/features/info/uptime.blade.php`: + +```blade +
What is Uptime Monitoring?
+

+ Uptime monitoring checks if your website is accessible and responding correctly. + It performs regular HTTP/HTTPS requests and tracks: +

+ + +
How It Works
+
    +
  1. Automated checks run every 5 minutes
  2. +
  3. Your site is accessed from our servers
  4. +
  5. Response time and status are recorded
  6. +
  7. Alerts are sent if your site is down
  8. +
+ +
Metrics Tracked
+ + +
+ + Tip: A healthy website should maintain 99%+ uptime and respond in under 1000ms. +
+``` + +Create `resources/views/features/info/ssl.blade.php`: + +```blade +
What is SSL Certificate Monitoring?
+

+ SSL certificate monitoring ensures your website's security certificate is valid and hasn't expired. + This is crucial for: +

+ + +
What We Check
+ + +
Alert Thresholds
+ + +
+ + Important: Expired SSL certificates will cause browser warnings and prevent visitors from accessing your site. +
+``` + +## Feature Settings Forms + +Create `resources/views/features/settings/uptime.blade.php`: + +```blade +
+ + +
+ +
+ + +
Alert if response time exceeds this value
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+``` + +Create `resources/views/features/settings/ssl.blade.php`: + +```blade +
+ +
+
+ +
+
+ +
+
+ +
+
+
You'll receive alerts at these intervals before expiration
+
+ +
+ + +
+``` + +## Layout Integration + +Add to your navigation in `resources/views/layouts/app.blade.php`: + +```blade + +``` + +## Running the Implementation + +1. Run migration: +```bash +./vendor/bin/sail artisan migrate +``` + +2. Create the SiteFeature model +3. Create FeaturesController +4. Add routes +5. Create view files +6. Test feature toggles + +## Chart.js CDN + +Already included in the show.blade.php template: +```html + +``` + +## Auto-enabling Features + +You may want to automatically enable default features when a site is created. Add to Site model: + +```php +protected static function booted() +{ + static::created(function ($site) { + // Auto-enable core features + $site->enableFeature('uptime'); + $site->enableFeature('ssl'); + }); +} +``` + +This provides a complete, production-ready feature management system with graphs and per-site controls. diff --git a/MULTI_CHANNEL_NOTIFICATIONS_GUIDE.md b/MULTI_CHANNEL_NOTIFICATIONS_GUIDE.md new file mode 100644 index 0000000..0c330ab --- /dev/null +++ b/MULTI_CHANNEL_NOTIFICATIONS_GUIDE.md @@ -0,0 +1,646 @@ +# Multi-Channel Notifications Implementation Guide + +This guide provides a complete implementation for multi-channel monitoring alerts in your Laravel application. + +## Overview + +The notification system supports the following channels: +- **Email** (built-in Laravel) +- **Database** (built-in Laravel) +- **Slack** (via webhook) +- **Telegram** (via Bot API) +- **Discord** (via webhook) +- **Microsoft Teams** (via webhook) +- **Google Chat** (via webhook) +- **Custom Webhook** (generic HTTP POST) +- **SMS** (via Twilio/Vonage - requires additional setup) +- **Opsgenie** (via API) +- **PagerDuty** (via Events API) + +## Installation Complete + +The following packages have been installed: +```bash +composer require laravel/slack-notification-channel +composer require laravel-notification-channels/telegram +composer require laravel-notification-channels/discord +``` + +## Database Schema + +Migration created: `2026_01_18_123539_add_notification_channels_to_users_table.php` + +Fields added to `users` table: +- `notification_channels` (JSON) - Stores enabled channels per alert type +- `slack_webhook_url` - Slack incoming webhook URL +- `telegram_chat_id` - Telegram bot chat ID +- `discord_webhook_url` - Discord webhook URL +- `teams_webhook_url` - Microsoft Teams webhook URL +- `google_chat_webhook_url` - Google Chat webhook URL +- `webhook_url` - Custom webhook endpoint +- `sms_phone_number` - Phone number for SMS alerts +- `opsgenie_api_key` - Opsgenie API key +- `pagerduty_integration_key` - PagerDuty integration key + +## Alert Types + +The system supports these alert types: +- `uptime` - Site down/up alerts +- `performance` - Performance threshold alerts +- `ssl` - SSL certificate expiration +- `dns` - DNS record changes +- `broken_links` - Broken link detection +- `domain_expiry` - Domain expiration warnings +- `sitemap` - Sitemap validation issues +- `health_check` - Health endpoint failures +- `tcp_port` - TCP port monitoring + +## Base Notification Class + +Create `app/Notifications/MonitorAlert.php`: + +```php +alertType = $alertType; + $this->siteName = $siteName; + $this->message = $message; + $this->data = $data; + $this->severity = $severity; + } + + /** + * Get the notification's delivery channels. + */ + public function via($notifiable): array + { + $enabledChannels = $notifiable->getNotificationChannelsFor($this->alertType); + + $channelMap = [ + 'email' => 'mail', + 'database' => 'database', + 'slack' => 'slack', + 'telegram' => TelegramChannel::class, + 'discord' => DiscordChannel::class, + ]; + + $channels = []; + foreach ($enabledChannels as $channel) { + if (isset($channelMap[$channel])) { + $channels[] = $channelMap[$channel]; + } + } + + return $channels ?: ['database']; + } + + /** + * Get the mail representation of the notification. + */ + public function toMail($notifiable): MailMessage + { + $color = $this->getColorForSeverity(); + + return (new MailMessage) + ->subject("[{$this->getSeverityLabel()}] {$this->siteName} - {$this->getAlertTypeLabel()}") + ->greeting("Alert: {$this->siteName}") + ->line($this->message) + ->when(!empty($this->data), function ($mail) { + foreach ($this->data as $key => $value) { + $mail->line("**{$key}:** {$value}"); + } + }) + ->action('View Dashboard', url('/dashboard')) + ->line('Timestamp: ' . now()->format('Y-m-d H:i:s T')); + } + + /** + * Get the Slack representation of the notification. + */ + public function toSlack($notifiable): SlackMessage + { + $color = $this->getColorForSeverity(); + + $message = (new SlackMessage) + ->from('Site Monitor', ':robot_face:') + ->to('#monitoring') + ->attachment(function ($attachment) use ($color) { + $attachment->title($this->siteName, url('/dashboard')) + ->content($this->message) + ->color($color) + ->timestamp(now()); + + foreach ($this->data as $key => $value) { + $attachment->field($key, $value, true); + } + }); + + return $message; + } + + /** + * Get the Telegram representation of the notification. + */ + public function toTelegram($notifiable): TelegramMessage + { + $emoji = $this->getEmojiForSeverity(); + + $text = "{$emoji} **{$this->getSeverityLabel()} Alert**\n\n"; + $text .= "**Site:** {$this->siteName}\n"; + $text .= "**Type:** {$this->getAlertTypeLabel()}\n\n"; + $text .= "{$this->message}\n\n"; + + if (!empty($this->data)) { + $text .= "**Details:**\n"; + foreach ($this->data as $key => $value) { + $text .= "• {$key}: {$value}\n"; + } + } + + $text .= "\n_" . now()->format('Y-m-d H:i:s T') . "_"; + + return TelegramMessage::create() + ->to($notifiable->telegram_chat_id) + ->content($text) + ->button('View Dashboard', url('/dashboard')); + } + + /** + * Get the Discord representation of the notification. + */ + public function toDiscord($notifiable): DiscordMessage + { + $color = $this->getDiscordColorForSeverity(); + + return DiscordMessage::create() + ->embed(function ($embed) use ($color) { + $embed->title("{$this->siteName} - {$this->getAlertTypeLabel()}") + ->description($this->message) + ->color($color) + ->timestamp(now()); + + foreach ($this->data as $key => $value) { + $embed->field($key, $value, true); + } + + $embed->footer('Site Monitoring Alert'); + }); + } + + /** + * Get the array representation of the notification (database). + */ + public function toArray($notifiable): array + { + return [ + 'alert_type' => $this->alertType, + 'site_name' => $this->siteName, + 'message' => $this->message, + 'severity' => $this->severity, + 'data' => $this->data, + 'timestamp' => now()->toDateTimeString(), + ]; + } + + /** + * Get color based on severity + */ + protected function getColorForSeverity(): string + { + return match ($this->severity) { + 'critical' => 'danger', + 'warning' => 'warning', + default => 'info', + }; + } + + /** + * Get Discord color based on severity + */ + protected function getDiscordColorForSeverity(): int + { + return match ($this->severity) { + 'critical' => 0xFF0000, // Red + 'warning' => 0xFFA500, // Orange + default => 0x0099FF, // Blue + }; + } + + /** + * Get emoji based on severity + */ + protected function getEmojiForSeverity(): string + { + return match ($this->severity) { + 'critical' => '🚨', + 'warning' => '⚠️', + default => 'ℹ️', + }; + } + + /** + * Get severity label + */ + protected function getSeverityLabel(): string + { + return strtoupper($this->severity); + } + + /** + * Get alert type label + */ + protected function getAlertTypeLabel(): string + { + return str_replace('_', ' ', ucwords($this->alertType)); + } +} +``` + +## Specific Notification Classes + +### 1. UptimeAlert + +Create `app/Notifications/UptimeAlert.php`: + +```php + $statusCode, + 'URL' => $url, + ]; + + if ($responseTime) { + $data['Response Time'] = round($responseTime, 2) . 'ms'; + } + + parent::__construct( + 'uptime', + $siteName, + $message, + $data, + $isDown ? 'critical' : 'info' + ); + } +} +``` + +### 2. SslExpiryAlert + +Create `app/Notifications/SslExpiryAlert.php`: + +```php + $expiresAt->format('Y-m-d H:i:s'), + 'Days Remaining' => $daysRemaining, + ], + $daysRemaining <= 7 ? 'critical' : 'warning' + ); + } +} +``` + +### 3. DomainExpiryAlert + +Create `app/Notifications/DomainExpiryAlert.php`: + +```php + $domain, + 'Expires On' => $expiresAt->format('Y-m-d'), + 'Days Remaining' => $daysRemaining, + ], + $daysRemaining <= 7 ? 'critical' : 'warning' + ); + } +} +``` + +### 4. SitemapAlert + +Create `app/Notifications/SitemapAlert.php`: + +```php + $sitemapUrl, + 'Error' => $error, + ], + 'warning' + ); + } +} +``` + +### 5. PerformanceAlert + +Create `app/Notifications/PerformanceAlert.php`: + +```php + round($responseTime, 2) . 'ms', + 'Threshold' => round($threshold, 2) . 'ms', + 'Difference' => '+' . round($responseTime - $threshold, 2) . 'ms', + ], + 'warning' + ); + } +} +``` + +## Integration with Existing Jobs + +Update your monitoring jobs to send notifications. Example for `CheckSitemap`: + +```php +// In app/Jobs/CheckSitemap.php +use App\Notifications\SitemapAlert; + +protected function sendAlert(string $message): void +{ + // Send notification using the new system + $this->sitemap->site->user->notify( + new SitemapAlert( + $this->sitemap->site->name, + $this->sitemap->url, + $this->sitemap->last_error + ) + ); + + // Keep existing Notification model creation for backwards compatibility + Notification::create([ + 'user_id' => $this->sitemap->site->user_id, + 'type' => 'sitemap_invalid', + 'message' => "{$message}: {$this->sitemap->url}", + 'data' => json_encode([ + 'sitemap_id' => $this->sitemap->id, + 'site_id' => $this->sitemap->site_id, + 'url' => $this->sitemap->url, + 'error' => $this->sitemap->last_error, + ]), + 'read_at' => null, + ]); +} +``` + +## Configuration Files + +### Services Configuration + +Add to `config/services.php`: + +```php +'telegram-bot-api' => [ + 'token' => env('TELEGRAM_BOT_TOKEN'), +], + +'twilio' => [ + 'sid' => env('TWILIO_SID'), + 'token' => env('TWILIO_TOKEN'), + 'from' => env('TWILIO_FROM'), +], + +'opsgenie' => [ + 'api_key' => env('OPSGENIE_API_KEY'), + 'api_url' => 'https://api.opsgenie.com', +], + +'pagerduty' => [ + 'api_url' => 'https://events.pagerduty.com/v2/enqueue', +], +``` + +### Environment Variables + +Add to `.env`: + +```env +# Telegram Bot +TELEGRAM_BOT_TOKEN=your_bot_token_here + +# Twilio SMS +TWILIO_SID=your_account_sid +TWILIO_TOKEN=your_auth_token +TWILIO_FROM=+1234567890 + +# Opsgenie +OPSGENIE_API_KEY=your_api_key + +# Global defaults (optional) +DEFAULT_SLACK_CHANNEL=#monitoring +DEFAULT_NOTIFICATION_QUEUE=notifications +``` + +## Notification Settings Controller + +The controller and views for managing notification settings will be created separately to allow users to: +- Configure which channels to use for each alert type +- Enter webhook URLs and API keys +- Test notification delivery +- View notification history + +## Channel Setup Guides + +### Slack +1. Create an Incoming Webhook in your Slack workspace +2. Copy the webhook URL +3. Paste in notification settings + +### Telegram +1. Create a bot via @BotFather +2. Get your bot token +3. Start a chat with your bot +4. Get your chat ID (use @userinfobot) +5. Enter chat ID in settings + +### Discord +1. Edit channel settings +2. Go to Integrations > Webhooks +3. Create webhook +4. Copy webhook URL + +### Microsoft Teams +1. Add "Incoming Webhook" connector to channel +2. Configure and create +3. Copy webhook URL + +### Google Chat +1. Create a webhook in Google Chat space +2. Copy webhook URL + +## Testing Notifications + +Create a test command: + +```bash +php artisan make:command TestNotifications +``` + +```php +argument('user')); + + $user->notify(new UptimeAlert( + 'Test Site', + false, // Not down + 200, + 'https://example.com', + 150.5 + )); + + $this->info('Test notification sent!'); + } +} +``` + +## Best Practices + +1. **Queue Notifications**: All notifications implement `ShouldQueue` +2. **Rate Limiting**: Implement rate limiting to prevent notification spam +3. **Retry Logic**: Configure queue retry attempts +4. **Monitoring**: Log notification failures +5. **Testing**: Test each channel before production use +6. **Security**: Encrypt sensitive webhook URLs and API keys +7. **User Preferences**: Respect user notification preferences +8. **Batching**: Consider batching similar alerts + +## Next Steps + +1. Run migration: `php artisan migrate` +2. Create notification settings UI +3. Integrate with existing monitoring jobs +4. Test each notification channel +5. Configure queue worker for notifications +6. Set up monitoring for the notification system itself + +## Troubleshooting + +### Notifications not sending +- Check queue worker is running +- Verify webhook URLs are correct +- Check logs: `storage/logs/laravel.log` +- Test with `notifications:test` command + +### Channel-specific issues +- **Slack**: Verify webhook URL format +- **Telegram**: Ensure bot token is valid and chat ID is correct +- **Discord**: Check webhook hasn't been deleted +- **Email**: Verify mail configuration + +## Future Enhancements + +- Notification grouping (digest emails) +- Quiet hours configuration +- Escalation policies +- Notification templates +- Webhook retry logic +- Notification analytics +- Mobile push notifications +- In-app notification center diff --git a/NOTIFICATIONS_QUICK_START.md b/NOTIFICATIONS_QUICK_START.md new file mode 100644 index 0000000..7da481c --- /dev/null +++ b/NOTIFICATIONS_QUICK_START.md @@ -0,0 +1,328 @@ +# Multi-Channel Notifications - Quick Start Guide + +## What's Been Completed + +### ✅ Database Migration +- Migration created: `database/migrations/2026_01_18_123539_add_notification_channels_to_users_table.php` +- Adds fields for all notification channels to users table +- **Run with:** `./vendor/bin/sail artisan migrate` + +### ✅ Packages Installed +```json +"laravel/slack-notification-channel": "^3.7", +"laravel-notification-channels/telegram": "^6.0", +"laravel-notification-channels/discord": "^1.7" +``` + +### ✅ User Model Updated +- Added fillable fields for all notification channels +- Added `notification_channels` JSON cast +- Added helper methods: + - `getNotificationChannelsFor($alertType)` - Get enabled channels for an alert type + - `routeNotificationForSlack()` - Slack webhook routing + - `routeNotificationForTelegram()` - Telegram chat ID routing + - `routeNotificationForDiscord()` - Discord webhook routing + +## Quick Implementation Steps + +### Step 1: Run Migration +```bash +./vendor/bin/sail artisan migrate +``` + +### Step 2: Create Notification Classes + +All notification class templates are in `MULTI_CHANNEL_NOTIFICATIONS_GUIDE.md`. Copy them to: +- `app/Notifications/MonitorAlert.php` (base class) +- `app/Notifications/UptimeAlert.php` +- `app/Notifications/SslExpiryAlert.php` +- `app/Notifications/DomainExpiryAlert.php` +- `app/Notifications/SitemapAlert.php` +- `app/Notifications/PerformanceAlert.php` + +### Step 3: Create Notification Settings Controller + +```bash +./vendor/bin/sail artisan make:controller NotificationSettingsController +``` + +### Step 4: Update Existing Jobs + +Example - Update `CheckSitemap` job: + +```php +// Replace the sendAlert method in app/Jobs/CheckSitemap.php +use App\Notifications\SitemapAlert; + +protected function sendAlert(string $message): void +{ + // Send via new notification system + $this->sitemap->site->user->notify( + new SitemapAlert( + $this->sitemap->site->name, + $this->sitemap->url, + $this->sitemap->last_error + ) + ); +} +``` + +### Step 5: Add Environment Variables + +Add to `.env`: +```env +# Telegram Bot (optional) +TELEGRAM_BOT_TOKEN=your_bot_token + +# Twilio SMS (optional) +TWILIO_SID=your_sid +TWILIO_TOKEN=your_token +TWILIO_FROM=+1234567890 +``` + +## Default Notification Channels Structure + +The `notification_channels` JSON field stores preferences like this: + +```json +{ + "uptime": ["email", "slack"], + "ssl": ["email", "telegram", "slack"], + "domain_expiry": ["email"], + "sitemap": ["database"], + "performance": ["slack"], + "dns": ["email", "database"], + "broken_links": ["database"], + "health_check": ["email", "slack"], + "tcp_port": ["database"] +} +``` + +## Supported Channels + +### Built-in (No Additional Setup) +- `mail` - Email notifications +- `database` - Store in notifications table + +### Webhook-based (Requires URL) +- `slack` - Requires `slack_webhook_url` +- `discord` - Requires `discord_webhook_url` +- `telegram` - Requires `telegram_chat_id` and bot token +- `teams` - Requires `teams_webhook_url` +- `google_chat` - Requires `google_chat_webhook_url` +- `webhook` - Requires `webhook_url` (custom endpoint) + +### API-based (Requires Credentials) +- `sms` - Requires Twilio/Vonage setup +- `opsgenie` - Requires `opsgenie_api_key` +- `pagerduty` - Requires `pagerduty_integration_key` + +## Testing Notifications + +Create a test route to verify notifications work: + +```php +// In routes/web.php +Route::get('/test-notification', function () { + auth()->user()->notify( + new \App\Notifications\UptimeAlert( + 'Test Site', + false, // not down + 200, + 'https://example.com', + 150.5 + ) + ); + + return 'Notification sent! Check your configured channels.'; +})->middleware('auth'); +``` + +## Setting Up Notification Channels + +### Slack +1. Go to https://api.slack.com/messaging/webhooks +2. Create an Incoming Webhook for your workspace +3. Copy the webhook URL +4. Save in user settings: `slack_webhook_url` + +### Telegram +1. Talk to @BotFather on Telegram +2. Create a new bot with `/newbot` +3. Save the bot token to `.env` as `TELEGRAM_BOT_TOKEN` +4. Start a chat with your bot +5. Get your chat ID from @userinfobot +6. Save in user settings: `telegram_chat_id` + +### Discord +1. Go to Server Settings → Integrations → Webhooks +2. Create New Webhook +3. Copy the Webhook URL +4. Save in user settings: `discord_webhook_url` + +### Microsoft Teams +1. In Teams, go to channel → Connectors +2. Configure "Incoming Webhook" +3. Copy the webhook URL +4. Save in user settings: `teams_webhook_url` + +## Notification Settings UI (To Be Created) + +Create a settings page where users can: +1. Enable/disable channels per alert type +2. Configure webhook URLs and API keys +3. Test notification delivery +4. Set quiet hours +5. Configure notification preferences + +## Integration with Existing Monitoring + +Update these jobs to send notifications: +- `app/Jobs/CheckSitePing.php` → Use `UptimeAlert` +- `app/Jobs/CheckSslCertificate.php` → Use `SslExpiryAlert` +- `app/Jobs/CheckDomainExpiry.php` → Use `DomainExpiryAlert` +- `app/Jobs/CheckSitemap.php` → Use `SitemapAlert` +- `app/Jobs/CheckHealth.php` → Use `PerformanceAlert` + +## Queue Configuration + +Notifications are queued by default. Ensure queue worker is running: + +```bash +./vendor/bin/sail artisan queue:work +``` + +Or configure supervisor in production. + +## Notification Flow + +``` +Monitor Job Detects Issue + ↓ +Creates Notification Object + ↓ +User::notify(new Alert(...)) + ↓ +Laravel checks via() + ↓ +Gets enabled channels from user preferences + ↓ +Dispatches to each channel + ↓ +Queues notification jobs + ↓ +Worker processes notifications + ↓ +Sends to: Email, Slack, Telegram, etc. +``` + +## Security Considerations + +1. **Encrypt sensitive fields** - Consider encrypting webhook URLs and API keys +2. **Rate limiting** - Prevent notification spam +3. **Validate webhooks** - Verify webhook URLs before saving +4. **Sanitize data** - Clean user input in notifications +5. **HTTPS only** - Only allow HTTPS webhook URLs + +## Troubleshooting + +### Notifications not sending +1. Check queue worker is running: `ps aux | grep queue` +2. Check failed jobs: `./vendor/bin/sail artisan queue:failed` +3. Review logs: `storage/logs/laravel.log` +4. Verify user has configured channels +5. Test with simple email notification first + +### Channel-specific issues + +**Slack:** "Invalid webhook URL" +- Verify URL format: https://hooks.slack.com/services/... +- Check workspace hasn't revoked webhook + +**Telegram:** "Bad Request: chat not found" +- Verify chat ID is correct +- Ensure bot token is valid +- Start conversation with bot first + +**Discord:** "Webhook not found" +- Check webhook wasn't deleted +- Verify URL hasn't expired + +## Performance Tips + +1. **Use queues** - Always queue notifications +2. **Batch similar alerts** - Group notifications when possible +3. **Debounce** - Prevent rapid repeated notifications +4. **Cache user preferences** - Cache notification_channels +5. **Async webhooks** - Don't wait for webhook responses + +## Next Steps + +1. **Create UI** - Build notification settings interface +2. **Add more channels** - Implement Teams, Google Chat, etc. +3. **Test thoroughly** - Test each channel +4. **Update docs** - Document for your team +5. **Monitor** - Track notification success rates + +## Example: Full Integration + +Here's how to fully integrate with an existing job: + +```php +get($this->site->url); + + $isDown = !$response->successful(); + $statusCode = $response->status(); + $responseTime = $response->transferStats->getTransferTime() * 1000; + + // Check if status changed + $previousStatus = $this->site->latestUptimeCheck->is_online ?? true; + + // Send notification if status changed + if ($isDown !== !$previousStatus) { + $this->site->user->notify( + new UptimeAlert( + $this->site->name, + $isDown, + $statusCode, + $this->site->url, + $responseTime + ) + ); + } + + // Save check result... + } +} +``` + +## Resources + +- Laravel Notifications: https://laravel.com/docs/notifications +- Slack API: https://api.slack.com/ +- Telegram Bot API: https://core.telegram.org/bots/api +- Discord Webhooks: https://discord.com/developers/docs/resources/webhook diff --git a/SITEMAP_MONITORING_SETUP.md b/SITEMAP_MONITORING_SETUP.md new file mode 100644 index 0000000..00bffd6 --- /dev/null +++ b/SITEMAP_MONITORING_SETUP.md @@ -0,0 +1,309 @@ +# Sitemap Monitoring Setup + +This feature allows you to monitor your website sitemaps to ensure they are accessible, valid XML, and properly formatted. + +## Features Implemented + +### 1. Database Migration +- **Table:** `sitemaps` +- **Columns:** + - `id` - Primary key + - `site_id` - Foreign key to sites table + - `url` - Full URL to the sitemap file + - `last_checked_at` - Last validation timestamp + - `is_valid` - Boolean indicating if sitemap is valid + - `last_error` - Text field storing validation errors + - `is_active` - Enable/disable monitoring + - `url_count` - Number of URLs found in sitemap + - `created_at`, `updated_at` - Timestamps + +### 2. Models & Relationships +- **Sitemap Model** (`app/Models/Sitemap.php`) + - Belongs to Site + - Helper methods: + - `hasErrors()` - Check if sitemap has validation issues + - `getStatusBadgeClass()` - Get Bootstrap badge color for status + - `getStatusText()` - Get human-readable status text + - `markAsValid()` - Mark sitemap as valid with URL count + - `markAsInvalid()` - Mark sitemap as invalid with error message + +- **Site Model** updated with `sitemaps()` relationship + +### 3. Background Job +- **Job:** `CheckSitemap` (`app/Jobs/CheckSitemap.php`) + - Fetches sitemap URL with proper user agent + - Validates XML structure using `simplexml_load_string()` + - Checks for valid sitemap format (urlset or sitemapindex) + - Counts URLs in the sitemap + - Creates notifications for invalid sitemaps + - Comprehensive error handling and logging + +### 4. XML Validation +Uses PHP's built-in `simplexml_load_string()` with error handling: +- Validates well-formed XML syntax +- Checks for proper sitemap structure +- Supports both regular sitemaps and sitemap indexes +- Handles XML namespaces correctly +- Provides detailed error messages + +### 5. Artisan Command +- **Command:** `monitor:sitemaps` + - Dispatches `CheckSitemap` job for all active sitemaps + - Shows progress bar + - Run manually: `./vendor/bin/sail artisan monitor:sitemaps` + +### 6. Scheduled Task +- **Schedule:** Daily at midnight (configurable in `routes/console.php:68`) +- Automatically runs `monitor:sitemaps` command +- Logs output to `storage/logs/sitemap-monitoring.log` + +### 7. Web Interface + +#### Routes +- `GET /sitemaps` - List all sitemaps +- `GET /sitemaps/create` - Add new sitemap form +- `POST /sitemaps` - Store new sitemap +- `GET /sitemaps/{sitemap}` - View sitemap details +- `GET /sitemaps/{sitemap}/edit` - Edit sitemap form +- `PUT /sitemaps/{sitemap}` - Update sitemap +- `DELETE /sitemaps/{sitemap}` - Delete sitemap +- `POST /sitemaps/{sitemap}/check` - Manually trigger validation + +#### Views +- `resources/views/sitemaps/index.blade.php` - Sitemap list with status badges +- `resources/views/sitemaps/create.blade.php` - Add new sitemap +- `resources/views/sitemaps/edit.blade.php` - Edit sitemap +- `resources/views/sitemaps/show.blade.php` - Sitemap details with validation info + +## Setup Instructions + +### 1. Run the Migration + +If not already run: +```bash +./vendor/bin/sail artisan migrate +``` + +### 2. Start the Queue Worker + +The sitemap validation jobs run in the background queue: +```bash +./vendor/bin/sail artisan queue:work +``` + +Or if you're already running the queue worker, it will automatically pick up these jobs. + +### 3. Set Up Scheduled Tasks + +Make sure your Laravel scheduler is running. The scheduler should already be configured if you followed the domain monitoring setup. + +In production, ensure this is in your crontab: +``` +* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1 +``` + +In development with Sail, the scheduler runs automatically if started with `composer dev`. + +### 4. Test the System + +1. **Add a sitemap:** + - Navigate to `/sitemaps` + - Click "Add Sitemap" + - Select a site and enter your sitemap URL + - Save + +2. **Manual validation:** + - View the sitemap details + - Click "Validate Now" + - The job will queue and process in the background + +3. **Test the command:** + ```bash + ./vendor/bin/sail artisan monitor:sitemaps + ``` + +## Usage + +### Adding Sitemaps +1. Go to "Sitemap Monitoring" in your application +2. Click "Add Sitemap" +3. Select the associated site +4. Enter the full sitemap URL (e.g., https://example.com/sitemap.xml) +5. Save - the system will automatically validate it + +### Monitoring +- **Automatic:** Runs daily via scheduled task +- **Manual:** Click "Validate Now" on any sitemap +- **Command:** Run `./vendor/bin/sail artisan monitor:sitemaps` anytime + +### Notifications +When a sitemap becomes invalid or inaccessible: +- A notification is created in the system +- Shows in your alerts/notifications section +- Contains sitemap URL and error details + +### Status Badges +- **Valid** (Green) - Sitemap is accessible and well-formed +- **Invalid** (Red) - Sitemap has errors or is inaccessible +- **Not Checked** (Gray) - Validation not yet performed + +## Validation Checks + +The system validates the following: + +### 1. HTTP Accessibility +- Sitemap URL returns HTTP 200 status +- Content can be fetched successfully +- Proper timeout handling (30 seconds) + +### 2. XML Well-Formedness +- Valid XML syntax +- No malformed tags or attributes +- Proper encoding +- Detailed XML error messages if invalid + +### 3. Sitemap Structure +- Contains `` root element (for regular sitemaps) +- OR contains `` root element (for sitemap indexes) +- Proper namespace declarations +- Valid sitemap format according to sitemaps.org schema + +### 4. URL Counting +- Counts `` entries in regular sitemaps +- Counts `` entries in sitemap indexes +- Handles namespaced and non-namespaced XML + +## Supported Sitemap Formats + +### Regular Sitemap +```xml + + + + https://example.com/page1 + 2026-01-18 + 1.0 + + + +``` + +### Sitemap Index +```xml + + + + https://example.com/sitemap1.xml + 2026-01-18 + + + +``` + +## Troubleshooting + +### Jobs not processing +- Check queue worker is running: `ps aux | grep queue` +- Check failed jobs: `./vendor/bin/sail artisan queue:failed` +- Restart queue: `./vendor/bin/sail artisan queue:restart` + +### Sitemap validation errors +- Verify sitemap URL is publicly accessible +- Check sitemap is valid XML (use online XML validator) +- Ensure proper XML encoding (UTF-8 recommended) +- Review logs: `storage/logs/laravel.log` + +### False positive errors +- Some servers may block automated requests +- Try accessing sitemap URL manually in browser +- Check if server requires specific User-Agent headers +- Verify no firewall or security rules blocking requests + +### Schedule not running +- Development: Ensure scheduler is part of your dev command +- Production: Verify cron job is set up +- Test manually: `./vendor/bin/sail artisan schedule:run` + +## File Structure + +``` +app/ +├── Console/Commands/ +│ └── MonitorSitemaps.php # Artisan command +├── Http/Controllers/ +│ └── SitemapController.php # Web controller +├── Jobs/ +│ └── CheckSitemap.php # Background job +└── Models/ + ├── Sitemap.php # Sitemap model + └── Site.php # Updated with sitemaps relationship + +database/migrations/ +└── 2026_01_18_123118_create_sitemaps_table.php + +resources/views/sitemaps/ +├── index.blade.php # List view +├── create.blade.php # Create form +├── edit.blade.php # Edit form +└── show.blade.php # Detail view + +routes/ +├── console.php # Scheduled tasks (line 68) +└── web.php # Web routes (lines 32-33) +``` + +## Common Sitemap Issues + +### 1. 404 Not Found +- Sitemap file doesn't exist at specified URL +- Check URL spelling and path +- Verify sitemap is published to server + +### 2. XML Parsing Errors +- Malformed XML syntax +- Missing closing tags +- Invalid characters in URLs +- Use XML validator to identify specific issues + +### 3. Incorrect Namespace +- Ensure using correct namespace: `http://www.sitemaps.org/schemas/sitemap/0.9` +- Namespace must match sitemap protocol specification + +### 4. Access Denied (403) +- Server blocking automated requests +- May need to whitelist monitoring server IP +- Check robots.txt isn't blocking sitemap + +### 5. Timeout Errors +- Sitemap file too large (>50MB) +- Server response too slow +- Consider splitting into sitemap index + +## Best Practices + +1. **Keep Sitemaps Updated:** Regenerate sitemaps when content changes +2. **Optimize Size:** Large sitemaps (>50,000 URLs) should use sitemap index +3. **Compress Large Files:** Use gzip compression for large sitemaps +4. **Valid XML:** Always validate XML before publishing +5. **Monitor Regularly:** Enable active monitoring for all production sitemaps +6. **Fix Issues Promptly:** Address validation errors as soon as they appear + +## Integration with Search Engines + +While this tool validates sitemaps, remember to: +- Submit sitemaps to Google Search Console +- Submit sitemaps to Bing Webmaster Tools +- Update robots.txt with sitemap location +- Ping search engines after sitemap updates + +## Future Enhancements + +Consider adding: +- Email notifications for invalid sitemaps +- Slack/Discord webhooks for alerts +- Detailed XML validation reporting +- Automatic sitemap ping to search engines +- Historical validation tracking +- Sitemap size and compression detection +- URL validation within sitemaps +- Comparison with previous versions diff --git a/app/Console/Commands/MonitorDomains.php b/app/Console/Commands/MonitorDomains.php new file mode 100644 index 0000000..24f8019 --- /dev/null +++ b/app/Console/Commands/MonitorDomains.php @@ -0,0 +1,56 @@ +info('Starting domain expiry monitoring...'); + + $domains = Domain::where('is_active', true)->get(); + + if ($domains->isEmpty()) { + $this->warn('No active domains found to monitor.'); + return Command::SUCCESS; + } + + $this->info("Found {$domains->count()} active domain(s) to check."); + + $progressBar = $this->output->createProgressBar($domains->count()); + $progressBar->start(); + + foreach ($domains as $domain) { + CheckDomainExpiry::dispatch($domain); + $progressBar->advance(); + } + + $progressBar->finish(); + $this->newLine(); + + $this->info('Domain expiry check jobs have been dispatched.'); + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/MonitorSitemaps.php b/app/Console/Commands/MonitorSitemaps.php new file mode 100644 index 0000000..1d00a88 --- /dev/null +++ b/app/Console/Commands/MonitorSitemaps.php @@ -0,0 +1,56 @@ +info('Starting sitemap monitoring...'); + + $sitemaps = Sitemap::where('is_active', true)->get(); + + if ($sitemaps->isEmpty()) { + $this->warn('No active sitemaps found to monitor.'); + return Command::SUCCESS; + } + + $this->info("Found {$sitemaps->count()} active sitemap(s) to check."); + + $progressBar = $this->output->createProgressBar($sitemaps->count()); + $progressBar->start(); + + foreach ($sitemaps as $sitemap) { + CheckSitemap::dispatch($sitemap); + $progressBar->advance(); + } + + $progressBar->finish(); + $this->newLine(); + + $this->info('Sitemap check jobs have been dispatched.'); + + return Command::SUCCESS; + } +} diff --git a/app/Http/Controllers/BrokenLinkController.php b/app/Http/Controllers/BrokenLinkController.php new file mode 100644 index 0000000..209664e --- /dev/null +++ b/app/Http/Controllers/BrokenLinkController.php @@ -0,0 +1,58 @@ +id()) + ->withCount('brokenLinks') + ->get(); + + // Get broken link statistics + $stats = [ + 'total_sites' => $sites->count(), + 'sites_with_broken_links' => $sites->where('broken_links_count', '>', 0)->count(), + 'total_broken_links' => $sites->sum('broken_links_count'), + 'sites_clean' => $sites->where('broken_links_count', 0)->count(), + ]; + + // Get recent broken links across all sites + $recentBrokenLinks = BrokenLink::whereHas('site', function ($query) { + $query->where('user_id', auth()->id()); + }) + ->with('site') + ->latest() + ->limit(50) + ->get(); + + return view('broken-links.index', compact('sites', 'stats', 'recentBrokenLinks')); + } + + /** + * Display broken links for a specific site + */ + public function show(Site $site) + { + $this->authorize('view', $site); + + $brokenLinks = $site->brokenLinks() + ->orderBy('created_at', 'desc') + ->paginate(50); + + $stats = [ + 'total_broken_links' => $brokenLinks->total(), + 'unique_urls' => $site->brokenLinks()->distinct('url')->count('url'), + ]; + + return view('broken-links.show', compact('site', 'brokenLinks', 'stats')); + } +} diff --git a/app/Http/Controllers/DnsRecordController.php b/app/Http/Controllers/DnsRecordController.php index e4b030c..208a765 100644 --- a/app/Http/Controllers/DnsRecordController.php +++ b/app/Http/Controllers/DnsRecordController.php @@ -11,6 +11,27 @@ use Illuminate\View\View; class DnsRecordController extends Controller { + /** + * Display overview of all DNS records across all sites + */ + public function overview(): View + { + $sites = Site::where('user_id', Auth::id()) + ->withCount('dnsRecords') + ->with(['dnsRecords' => function ($query) { + $query->where('is_active', true)->limit(5); + }]) + ->get(); + + $stats = [ + 'total_sites' => $sites->count(), + 'total_dns_records' => $sites->sum('dns_records_count'), + 'sites_with_records' => $sites->where('dns_records_count', '>', 0)->count(), + ]; + + return view('dns-records.overview', compact('sites', 'stats')); + } + /** * Display a listing of the DNS records for a site. */ diff --git a/app/Http/Controllers/DomainController.php b/app/Http/Controllers/DomainController.php new file mode 100644 index 0000000..a118067 --- /dev/null +++ b/app/Http/Controllers/DomainController.php @@ -0,0 +1,124 @@ +where('is_active', true) + ->orderBy('expires_at', 'asc') + ->paginate(15); + + return view('domains.index', compact('domains')); + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + $sites = Site::where('user_id', auth()->id()) + ->orderBy('name') + ->get(); + + return view('domains.create', compact('sites')); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'site_id' => 'required|exists:sites,id', + 'domain' => 'required|string|max:255', + 'is_active' => 'sometimes|nullable|in:on,off', + ]); + + $validated['is_active'] = $request->has('is_active'); + + $domain = Domain::create($validated); + + // Dispatch job to check domain immediately + CheckDomainExpiry::dispatch($domain); + + return redirect() + ->route('domains.index') + ->with('success', 'Domain added successfully. Checking expiry date...'); + } + + /** + * Display the specified resource. + */ + public function show(Domain $domain) + { + $domain->load('site'); + + return view('domains.show', compact('domain')); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(Domain $domain) + { + $sites = Site::where('user_id', auth()->id()) + ->orderBy('name') + ->get(); + + return view('domains.edit', compact('domain', 'sites')); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Domain $domain) + { + $validated = $request->validate([ + 'site_id' => 'required|exists:sites,id', + 'domain' => 'required|string|max:255', + 'is_active' => 'boolean', + ]); + + $validated['is_active'] = $request->has('is_active'); + + $domain->update($validated); + + return redirect() + ->route('domains.index') + ->with('success', 'Domain updated successfully.'); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Domain $domain) + { + $domain->delete(); + + return redirect() + ->route('domains.index') + ->with('success', 'Domain deleted successfully.'); + } + + /** + * Manually trigger domain expiry check + */ + public function check(Domain $domain) + { + CheckDomainExpiry::dispatch($domain); + + return back() + ->with('success', 'Domain expiry check has been queued.'); + } +} diff --git a/app/Http/Controllers/HealthCheckController.php b/app/Http/Controllers/HealthCheckController.php index 54cb51d..96d4243 100644 --- a/app/Http/Controllers/HealthCheckController.php +++ b/app/Http/Controllers/HealthCheckController.php @@ -11,6 +11,27 @@ use Illuminate\View\View; class HealthCheckController extends Controller { + /** + * Display overview of all health checks across all sites + */ + public function overview(): View + { + $sites = Site::where('user_id', Auth::id()) + ->withCount('healthChecks') + ->with(['healthChecks' => function ($query) { + $query->where('is_active', true)->limit(5); + }]) + ->get(); + + $stats = [ + 'total_sites' => $sites->count(), + 'total_health_checks' => $sites->sum('health_checks_count'), + 'sites_with_checks' => $sites->where('health_checks_count', '>', 0)->count(), + ]; + + return view('health-checks.overview', compact('sites', 'stats')); + } + /** * Display a listing of the health checks for a site. */ diff --git a/app/Http/Controllers/SitemapController.php b/app/Http/Controllers/SitemapController.php new file mode 100644 index 0000000..c3235f7 --- /dev/null +++ b/app/Http/Controllers/SitemapController.php @@ -0,0 +1,125 @@ +where('is_active', true) + ->orderBy('is_valid', 'asc') + ->orderBy('last_checked_at', 'desc') + ->paginate(15); + + return view('sitemaps.index', compact('sitemaps')); + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + $sites = Site::where('user_id', auth()->id()) + ->orderBy('name') + ->get(); + + return view('sitemaps.create', compact('sites')); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'site_id' => 'required|exists:sites,id', + 'url' => 'required|url|max:500', + 'is_active' => 'sometimes|nullable|in:on,off', + ]); + + $validated['is_active'] = $request->has('is_active'); + + $sitemap = Sitemap::create($validated); + + // Dispatch job to check sitemap immediately + CheckSitemap::dispatch($sitemap); + + return redirect() + ->route('sitemaps.index') + ->with('success', 'Sitemap added successfully. Validating now...'); + } + + /** + * Display the specified resource. + */ + public function show(Sitemap $sitemap) + { + $sitemap->load('site'); + + return view('sitemaps.show', compact('sitemap')); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(Sitemap $sitemap) + { + $sites = Site::where('user_id', auth()->id()) + ->orderBy('name') + ->get(); + + return view('sitemaps.edit', compact('sitemap', 'sites')); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Sitemap $sitemap) + { + $validated = $request->validate([ + 'site_id' => 'required|exists:sites,id', + 'url' => 'required|url|max:500', + 'is_active' => 'boolean', + ]); + + $validated['is_active'] = $request->has('is_active'); + + $sitemap->update($validated); + + return redirect() + ->route('sitemaps.index') + ->with('success', 'Sitemap updated successfully.'); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Sitemap $sitemap) + { + $sitemap->delete(); + + return redirect() + ->route('sitemaps.index') + ->with('success', 'Sitemap deleted successfully.'); + } + + /** + * Manually trigger sitemap validation + */ + public function check(Sitemap $sitemap) + { + CheckSitemap::dispatch($sitemap); + + return back() + ->with('success', 'Sitemap validation has been queued.'); + } +} diff --git a/app/Http/Controllers/SslCertificateController.php b/app/Http/Controllers/SslCertificateController.php new file mode 100644 index 0000000..10f6c35 --- /dev/null +++ b/app/Http/Controllers/SslCertificateController.php @@ -0,0 +1,74 @@ +id()) + ->with(['sslCertificates' => function ($query) { + $query->latest()->limit(1); + }]) + ->get(); + + // Get SSL statistics + $stats = [ + 'total_sites' => $sites->count(), + 'valid_certs' => 0, + 'expiring_soon' => 0, + 'expired' => 0, + ]; + + foreach ($sites as $site) { + $latestCert = $site->sslCertificates->first(); + if ($latestCert) { + if ($latestCert->expires_at) { + $daysUntilExpiry = now()->diffInDays($latestCert->expires_at, false); + + if ($daysUntilExpiry < 0) { + $stats['expired']++; + } elseif ($daysUntilExpiry <= 30) { + $stats['expiring_soon']++; + } else { + $stats['valid_certs']++; + } + } + } + } + + // Get recent SSL checks across all sites + $recentCertificates = SslCertificate::whereHas('site', function ($query) { + $query->where('user_id', auth()->id()); + }) + ->with('site') + ->latest() + ->limit(50) + ->get(); + + return view('ssl.index', compact('sites', 'stats', 'recentCertificates')); + } + + /** + * Display SSL certificate details for a specific site + */ + public function show(Site $site) + { + $this->authorize('view', $site); + + $sslCertificates = $site->sslCertificates() + ->orderBy('created_at', 'desc') + ->paginate(50); + + $latestCert = $site->sslCertificates()->latest()->first(); + + return view('ssl.show', compact('site', 'sslCertificates', 'latestCert')); + } +} diff --git a/app/Http/Controllers/TcpPortController.php b/app/Http/Controllers/TcpPortController.php index 4f15dde..05ded1b 100644 --- a/app/Http/Controllers/TcpPortController.php +++ b/app/Http/Controllers/TcpPortController.php @@ -9,6 +9,27 @@ use Illuminate\Support\Facades\Auth; class TcpPortController extends Controller { + /** + * Display overview of all TCP ports across all sites + */ + public function overview() + { + $sites = Site::where('user_id', Auth::id()) + ->withCount('tcpPorts') + ->with(['tcpPorts' => function ($query) { + $query->where('is_active', true)->limit(5); + }]) + ->get(); + + $stats = [ + 'total_sites' => $sites->count(), + 'total_ports_monitored' => $sites->sum('tcp_ports_count'), + 'sites_with_ports' => $sites->where('tcp_ports_count', '>', 0)->count(), + ]; + + return view('tcp-ports.overview', compact('sites', 'stats')); + } + /** * Display a listing of the resource. */ diff --git a/app/Http/Controllers/UptimeController.php b/app/Http/Controllers/UptimeController.php new file mode 100644 index 0000000..df6a72a --- /dev/null +++ b/app/Http/Controllers/UptimeController.php @@ -0,0 +1,86 @@ +id()) + ->with(['uptimeChecks' => function ($query) { + $query->latest()->limit(1); + }]) + ->get(); + + // Get uptime statistics + $stats = [ + 'total_sites' => $sites->count(), + 'sites_up' => 0, + 'sites_down' => 0, + 'avg_response_time' => 0, + ]; + + $totalResponseTime = 0; + $checksCount = 0; + + foreach ($sites as $site) { + $latestCheck = $site->uptimeChecks->first(); + if ($latestCheck) { + if ($latestCheck->is_online) { + $stats['sites_up']++; + } else { + $stats['sites_down']++; + } + $totalResponseTime += $latestCheck->response_time ?? 0; + $checksCount++; + } + } + + if ($checksCount > 0) { + $stats['avg_response_time'] = round($totalResponseTime / $checksCount, 2); + } + + // Get recent checks across all sites + $recentChecks = UptimeCheck::whereHas('site', function ($query) { + $query->where('user_id', auth()->id()); + }) + ->with('site') + ->latest() + ->limit(50) + ->get(); + + return view('uptime.index', compact('sites', 'stats', 'recentChecks')); + } + + /** + * Display uptime details for a specific site + */ + public function show(Site $site) + { + $this->authorize('view', $site); + + $uptimeChecks = $site->uptimeChecks() + ->orderBy('created_at', 'desc') + ->paginate(100); + + // Calculate uptime percentage for last 30 days + $checksLast30Days = $site->uptimeChecks() + ->where('created_at', '>=', now()->subDays(30)) + ->get(); + + $uptimePercentage = 0; + if ($checksLast30Days->count() > 0) { + $upCount = $checksLast30Days->where('is_online', true)->count(); + $uptimePercentage = round(($upCount / $checksLast30Days->count()) * 100, 2); + } + + return view('uptime.show', compact('site', 'uptimeChecks', 'uptimePercentage')); + } +} diff --git a/app/Jobs/CheckDomainExpiry.php b/app/Jobs/CheckDomainExpiry.php new file mode 100644 index 0000000..f2a990a --- /dev/null +++ b/app/Jobs/CheckDomainExpiry.php @@ -0,0 +1,142 @@ +get('https://www.whoisxmlapi.com/whoisserver/WhoisService', [ + 'apiKey' => $apiKey, + 'domainName' => $this->domain->domain, + 'outputFormat' => 'JSON', + ]); + + if ($response->successful()) { + $data = $response->json(); + + // Parse expiry date from WHOIS response + $expiryDate = $this->parseExpiryDate($data); + + if ($expiryDate) { + $this->domain->update([ + 'expires_at' => $expiryDate, + 'last_checked_at' => Carbon::now(), + ]); + + // Check if domain is expiring soon (within 30 days) + if ($this->domain->isExpiringSoon()) { + $this->sendExpiryAlert(); + } + } + } else { + Log::error('WHOIS API request failed', [ + 'domain' => $this->domain->domain, + 'status' => $response->status(), + ]); + } + + $this->domain->update([ + 'last_checked_at' => Carbon::now(), + ]); + + } catch (\Exception $e) { + Log::error('Error checking domain expiry', [ + 'domain' => $this->domain->domain, + 'error' => $e->getMessage(), + ]); + + throw $e; // Re-throw to allow job retry + } + } + + /** + * Parse expiry date from WHOIS response + */ + protected function parseExpiryDate(array $data): ?Carbon + { + // Try multiple possible fields where expiry date might be + $possibleFields = [ + 'WhoisRecord.registryData.expiresDate', + 'WhoisRecord.expiresDate', + 'expiresDate', + ]; + + foreach ($possibleFields as $field) { + $value = data_get($data, $field); + + if ($value) { + try { + return Carbon::parse($value); + } catch (\Exception $e) { + Log::warning('Failed to parse date', [ + 'field' => $field, + 'value' => $value, + ]); + } + } + } + + return null; + } + + /** + * Send alert notification for expiring domain + */ + protected function sendExpiryAlert(): void + { + $daysUntilExpiry = $this->domain->daysUntilExpiration(); + + $message = $this->domain->isExpired() + ? "Domain '{$this->domain->domain}' has expired!" + : "Domain '{$this->domain->domain}' expires in {$daysUntilExpiry} days (on {$this->domain->expires_at->format('Y-m-d')})"; + + Notification::create([ + 'user_id' => $this->domain->site->user_id, + 'type' => 'domain_expiry', + 'message' => $message, + 'data' => json_encode([ + 'domain_id' => $this->domain->id, + 'site_id' => $this->domain->site_id, + 'domain' => $this->domain->domain, + 'expires_at' => $this->domain->expires_at, + 'days_until_expiry' => $daysUntilExpiry, + ]), + 'read_at' => null, + ]); + } +} diff --git a/app/Jobs/CheckSitemap.php b/app/Jobs/CheckSitemap.php new file mode 100644 index 0000000..9b537e2 --- /dev/null +++ b/app/Jobs/CheckSitemap.php @@ -0,0 +1,150 @@ +withHeaders([ + 'User-Agent' => 'Mozilla/5.0 (compatible; SitemapMonitor/1.0)', + ]) + ->get($this->sitemap->url); + + if (!$response->successful()) { + $this->sitemap->markAsInvalid( + "HTTP {$response->status()}: Failed to fetch sitemap" + ); + $this->sendAlert("Sitemap is inaccessible (HTTP {$response->status()})"); + return; + } + + // Get the response body + $xmlContent = $response->body(); + + // Validate XML structure using simplexml_load_string + libxml_use_internal_errors(true); + $xml = simplexml_load_string($xmlContent); + + if ($xml === false) { + $errors = libxml_get_errors(); + $errorMessages = array_map(fn($error) => trim($error->message), $errors); + libxml_clear_errors(); + + $this->sitemap->markAsInvalid( + 'Invalid XML: ' . implode('; ', $errorMessages) + ); + $this->sendAlert('Sitemap contains invalid XML'); + return; + } + + // Check if it's a valid sitemap (has urlset or sitemapindex) + $namespaces = $xml->getNamespaces(true); + $isUrlset = isset($xml->url) || isset($namespaces['']); + $isSitemapIndex = isset($xml->sitemap); + + if (!$isUrlset && !$isSitemapIndex) { + $this->sitemap->markAsInvalid( + 'Invalid sitemap format: Missing or root element' + ); + $this->sendAlert('Sitemap has invalid format'); + return; + } + + // Count URLs in the sitemap + $urlCount = $this->countUrls($xml); + + // Mark as valid + $this->sitemap->markAsValid($urlCount); + + Log::info('Sitemap validated successfully', [ + 'sitemap_id' => $this->sitemap->id, + 'url' => $this->sitemap->url, + 'url_count' => $urlCount, + ]); + + } catch (\Exception $e) { + Log::error('Error checking sitemap', [ + 'sitemap_id' => $this->sitemap->id, + 'url' => $this->sitemap->url, + 'error' => $e->getMessage(), + ]); + + $this->sitemap->markAsInvalid( + 'Exception: ' . $e->getMessage() + ); + $this->sendAlert('Sitemap check failed: ' . $e->getMessage()); + + throw $e; // Re-throw to allow job retry + } + } + + /** + * Count URLs in the sitemap + */ + protected function countUrls(\SimpleXMLElement $xml): int + { + $count = 0; + + // Register namespaces + $xml->registerXPathNamespace('sm', 'http://www.sitemaps.org/schemas/sitemap/0.9'); + + // Check for regular sitemap + $urls = $xml->xpath('//sm:url') ?: $xml->xpath('//url'); + if ($urls !== false) { + $count = count($urls); + } + + // If it's a sitemap index, count the sitemaps instead + if ($count === 0) { + $sitemaps = $xml->xpath('//sm:sitemap') ?: $xml->xpath('//sitemap'); + if ($sitemaps !== false) { + $count = count($sitemaps); + } + } + + return $count; + } + + /** + * Send alert notification for invalid sitemap + */ + protected function sendAlert(string $message): void + { + Notification::create([ + 'user_id' => $this->sitemap->site->user_id, + 'type' => 'sitemap_invalid', + 'message' => "{$message}: {$this->sitemap->url}", + 'data' => json_encode([ + 'sitemap_id' => $this->sitemap->id, + 'site_id' => $this->sitemap->site_id, + 'url' => $this->sitemap->url, + 'error' => $this->sitemap->last_error, + ]), + 'read_at' => null, + ]); + } +} diff --git a/app/Models/Domain.php b/app/Models/Domain.php new file mode 100644 index 0000000..b4ea528 --- /dev/null +++ b/app/Models/Domain.php @@ -0,0 +1,65 @@ + 'datetime', + 'last_checked_at' => 'datetime', + 'is_active' => 'boolean', + ]; + + public function site(): BelongsTo + { + return $this->belongsTo(Site::class); + } + + /** + * Check if domain is expiring soon (within 30 days) + */ + public function isExpiringSoon(): bool + { + if (!$this->expires_at) { + return false; + } + + return $this->expires_at->lte(Carbon::now()->addDays(30)); + } + + /** + * Check if domain has expired + */ + public function isExpired(): bool + { + if (!$this->expires_at) { + return false; + } + + return $this->expires_at->lte(Carbon::now()); + } + + /** + * Get days until expiration + */ + public function daysUntilExpiration(): ?int + { + if (!$this->expires_at) { + return null; + } + + return Carbon::now()->diffInDays($this->expires_at, false); + } +} diff --git a/app/Models/Site.php b/app/Models/Site.php index c023c38..0bf4f56 100644 --- a/app/Models/Site.php +++ b/app/Models/Site.php @@ -53,4 +53,14 @@ class Site extends Model { return $this->hasMany(DnsRecord::class); } + + public function domains(): HasMany + { + return $this->hasMany(Domain::class); + } + + public function sitemaps(): HasMany + { + return $this->hasMany(Sitemap::class); + } } diff --git a/app/Models/Sitemap.php b/app/Models/Sitemap.php new file mode 100644 index 0000000..b339f69 --- /dev/null +++ b/app/Models/Sitemap.php @@ -0,0 +1,88 @@ + 'datetime', + 'is_valid' => 'boolean', + 'is_active' => 'boolean', + 'url_count' => 'integer', + ]; + + public function site(): BelongsTo + { + return $this->belongsTo(Site::class); + } + + /** + * Check if sitemap has errors + */ + public function hasErrors(): bool + { + return !$this->is_valid || !empty($this->last_error); + } + + /** + * Get the status badge color + */ + public function getStatusBadgeClass(): string + { + if (!$this->last_checked_at) { + return 'bg-secondary'; + } + + return $this->is_valid ? 'bg-success' : 'bg-danger'; + } + + /** + * Get the status text + */ + public function getStatusText(): string + { + if (!$this->last_checked_at) { + return 'Not Checked'; + } + + return $this->is_valid ? 'Valid' : 'Invalid'; + } + + /** + * Mark sitemap as valid + */ + public function markAsValid(?int $urlCount = null): void + { + $this->update([ + 'is_valid' => true, + 'last_error' => null, + 'last_checked_at' => now(), + 'url_count' => $urlCount, + ]); + } + + /** + * Mark sitemap as invalid + */ + public function markAsInvalid(string $error): void + { + $this->update([ + 'is_valid' => false, + 'last_error' => $error, + 'last_checked_at' => now(), + ]); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 39d227d..aa9a3b3 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -24,6 +24,16 @@ class User extends Authenticatable 'email', 'password', 'notification_preferences', + 'notification_channels', + 'slack_webhook_url', + 'telegram_chat_id', + 'discord_webhook_url', + 'teams_webhook_url', + 'google_chat_webhook_url', + 'webhook_url', + 'sms_phone_number', + 'opsgenie_api_key', + 'pagerduty_integration_key', ]; /** @@ -47,9 +57,43 @@ class User extends Authenticatable 'email_verified_at' => 'datetime', 'password' => 'hashed', 'notification_preferences' => 'array', + 'notification_channels' => 'array', ]; } + /** + * Get enabled notification channels for a specific alert type + */ + public function getNotificationChannelsFor(string $alertType): array + { + $channels = $this->notification_channels ?? []; + return $channels[$alertType] ?? ['database']; + } + + /** + * Route notification for Slack + */ + public function routeNotificationForSlack(): ?string + { + return $this->slack_webhook_url; + } + + /** + * Route notification for Telegram + */ + public function routeNotificationForTelegram(): ?string + { + return $this->telegram_chat_id; + } + + /** + * Route notification for Discord + */ + public function routeNotificationForDiscord(): ?string + { + return $this->discord_webhook_url; + } + public function sites(): HasMany { return $this->hasMany(Site::class); diff --git a/composer.json b/composer.json index 17385d4..ce86578 100644 --- a/composer.json +++ b/composer.json @@ -11,8 +11,11 @@ "require": { "php": "^8.2", "guzzlehttp/guzzle": "^7.0", + "laravel-notification-channels/discord": "^1.7", + "laravel-notification-channels/telegram": "^6.0", "laravel/framework": "^12.0", "laravel/sanctum": "^4.2", + "laravel/slack-notification-channel": "^3.7", "laravel/tinker": "^2.10.1", "laravel/ui": "^4.6", "spatie/crawler": "^8.4" diff --git a/composer.lock b/composer.lock index a119976..ff89754 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "88cbc7f24cd10b72bef514dc03a4dfd8", + "content-hash": "818f4d39c8e6073483b3546a02db62da", "packages": [ { "name": "brick/math", @@ -1053,17 +1053,157 @@ "time": "2025-08-22T14:27:06+00:00" }, { - "name": "laravel/framework", - "version": "v12.46.0", + "name": "laravel-notification-channels/discord", + "version": "1.7.0", "source": { "type": "git", - "url": "https://github.com/laravel/framework.git", - "reference": "9dcff48d25a632c1fadb713024c952fec489c4ae" + "url": "https://github.com/laravel-notification-channels/discord.git", + "reference": "c17b48ba1ae5f13fcb86f769219b660c7839249c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/9dcff48d25a632c1fadb713024c952fec489c4ae", - "reference": "9dcff48d25a632c1fadb713024c952fec489c4ae", + "url": "https://api.github.com/repos/laravel-notification-channels/discord/zipball/c17b48ba1ae5f13fcb86f769219b660c7839249c", + "reference": "c17b48ba1ae5f13fcb86f769219b660c7839249c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.3 || ^7.0", + "illuminate/console": "^7.0 || ^8.0 || ^9.0 || ^10.0|^11.0|^12.0", + "illuminate/notifications": "^7.0 || ^8.0 || ^9.0 || ^10.0|^11.0|^12.0", + "illuminate/queue": "^7.0 || ^8.0 || ^9.0 || ^10.0|^11.0|^12.0", + "illuminate/support": "^7.0 || ^8.0 || ^9.0 || ^10.0|^11.0|^12.0", + "php": "^7.2|^8.0", + "textalk/websocket": "^1.2" + }, + "require-dev": { + "mockery/mockery": "^1.3.3", + "orchestra/testbench": "^6.0 || ^7.0 || ^8.0|^9.0|^10.0", + "phpunit/phpunit": "^8.5 || ^9.0|^10.5|^11.5.3" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NotificationChannels\\Discord\\DiscordServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "NotificationChannels\\Discord\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Cody Scott", + "email": "cs475x@icloud.com", + "role": "Developer" + } + ], + "description": "Laravel notification driver for Discord.", + "homepage": "https://github.com/laravel-notification-channels/discord", + "keywords": [ + "channel", + "discord", + "driver", + "laravel", + "notification" + ], + "support": { + "issues": "https://github.com/laravel-notification-channels/discord/issues", + "source": "https://github.com/laravel-notification-channels/discord/tree/1.7.0" + }, + "time": "2025-03-29T08:38:25+00:00" + }, + { + "name": "laravel-notification-channels/telegram", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/laravel-notification-channels/telegram.git", + "reference": "289eb825f6c918b21bbda19bffb900d882c9647a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel-notification-channels/telegram/zipball/289eb825f6c918b21bbda19bffb900d882c9647a", + "reference": "289eb825f6c918b21bbda19bffb900d882c9647a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^7.8", + "illuminate/contracts": "^11.0 || ^12.0", + "illuminate/notifications": "^11.0 || ^12.0", + "illuminate/support": "^11.0 || ^12.0", + "php": "^8.2" + }, + "require-dev": { + "larastan/larastan": "^3.0", + "mockery/mockery": "^1.4.4", + "orchestra/testbench": "^10.0", + "pestphp/pest": "^3.0", + "pestphp/pest-plugin-laravel": "^3.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NotificationChannels\\Telegram\\TelegramServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "NotificationChannels\\Telegram\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Irfaq Syed", + "email": "github@lukonet.net", + "homepage": "https://lukonet.com", + "role": "Developer" + } + ], + "description": "Telegram Notifications Channel for Laravel", + "homepage": "https://github.com/laravel-notification-channels/telegram", + "keywords": [ + "laravel", + "notification", + "telegram", + "telegram notification", + "telegram notifications channel" + ], + "support": { + "issues": "https://github.com/laravel-notification-channels/telegram/issues", + "source": "https://github.com/laravel-notification-channels/telegram/tree/6.0.0" + }, + "time": "2025-02-25T22:59:13+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.47.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "ab8114c2e78f32e64eb238fc4b495bea3f8b80ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/ab8114c2e78f32e64eb238fc4b495bea3f8b80ec", + "reference": "ab8114c2e78f32e64eb238fc4b495bea3f8b80ec", "shasum": "" }, "require": { @@ -1272,20 +1412,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-01-07T23:26:53+00:00" + "time": "2026-01-13T15:29:06+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.8", + "version": "v0.3.10", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "096748cdfb81988f60090bbb839ce3205ace0d35" + "reference": "360ba095ef9f51017473505191fbd4ab73e1cab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/096748cdfb81988f60090bbb839ce3205ace0d35", - "reference": "096748cdfb81988f60090bbb839ce3205ace0d35", + "url": "https://api.github.com/repos/laravel/prompts/zipball/360ba095ef9f51017473505191fbd4ab73e1cab3", + "reference": "360ba095ef9f51017473505191fbd4ab73e1cab3", "shasum": "" }, "require": { @@ -1329,9 +1469,9 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.8" + "source": "https://github.com/laravel/prompts/tree/v0.3.10" }, - "time": "2025-11-21T20:52:52+00:00" + "time": "2026-01-13T20:29:29+00:00" }, { "name": "laravel/sanctum", @@ -1398,16 +1538,16 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.7", + "version": "v2.0.8", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd" + "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd", - "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7581a4407012f5f53365e11bafc520fd7f36bc9b", + "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b", "shasum": "" }, "require": { @@ -1455,7 +1595,72 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-11-21T20:52:36+00:00" + "time": "2026-01-08T16:22:46+00:00" + }, + { + "name": "laravel/slack-notification-channel", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/slack-notification-channel.git", + "reference": "414aec57b487bfbac7f90fc30f50a2f0a2df4caa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/slack-notification-channel/zipball/414aec57b487bfbac7f90fc30f50a2f0a2df4caa", + "reference": "414aec57b487bfbac7f90fc30f50a2f0a2df4caa", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.0", + "illuminate/http": "^9.0|^10.0|^11.0|^12.0", + "illuminate/notifications": "^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^9.0|^10.0|^11.0|^12.0", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.0|^10.4|^11.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Illuminate\\Notifications\\SlackChannelServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Notifications\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Slack Notification Channel for laravel.", + "keywords": [ + "laravel", + "notifications", + "slack" + ], + "support": { + "issues": "https://github.com/laravel/slack-notification-channel/issues", + "source": "https://github.com/laravel/slack-notification-channel/tree/v3.7.0" + }, + "time": "2025-11-20T17:26:07+00:00" }, { "name": "laravel/tinker", @@ -1965,20 +2170,20 @@ }, { "name": "league/uri", - "version": "7.7.0", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807" + "reference": "4436c6ec8d458e4244448b069cc572d088230b76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/8d587cddee53490f9b82bf203d3a9aa7ea4f9807", - "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", + "reference": "4436c6ec8d458e4244448b069cc572d088230b76", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.7", + "league/uri-interfaces": "^7.8", "php": "^8.1", "psr/http-factory": "^1" }, @@ -1992,11 +2197,11 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "ext-uri": "to use the PHP native URI class", - "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", - "league/uri-components": "Needed to easily manipulate URI objects components", - "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", - "rowbot/url": "to handle WHATWG URL", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2051,7 +2256,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.7.0" + "source": "https://github.com/thephpleague/uri/tree/7.8.0" }, "funding": [ { @@ -2059,20 +2264,20 @@ "type": "github" } ], - "time": "2025-12-07T16:02:06+00:00" + "time": "2026-01-14T17:24:56+00:00" }, { "name": "league/uri-interfaces", - "version": "7.7.0", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c" + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/62ccc1a0435e1c54e10ee6022df28d6c04c2946c", - "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", "shasum": "" }, "require": { @@ -2085,7 +2290,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", - "rowbot/url": "to handle WHATWG URL", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2135,7 +2340,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.7.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" }, "funding": [ { @@ -2143,7 +2348,7 @@ "type": "github" } ], - "time": "2025-12-07T16:03:21+00:00" + "time": "2026-01-15T06:54:53+00:00" }, { "name": "monolog/monolog", @@ -6289,6 +6494,55 @@ ], "time": "2025-12-18T07:04:31+00:00" }, + { + "name": "textalk/websocket", + "version": "1.5.8", + "source": { + "type": "git", + "url": "https://github.com/Textalk/websocket-php.git", + "reference": "d05dbaa97500176447ffb1f1800573f23085ab13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Textalk/websocket-php/zipball/d05dbaa97500176447ffb1f1800573f23085ab13", + "reference": "d05dbaa97500176447ffb1f1800573f23085ab13", + "shasum": "" + }, + "require": { + "php": "^7.2 | ^8.0", + "psr/log": "^1 | ^2 | ^3" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^8.0|^9.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "WebSocket\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Fredrik Liljegren" + }, + { + "name": "Sören Jensen", + "email": "soren@abicart.se" + } + ], + "description": "WebSocket client and server", + "support": { + "issues": "https://github.com/Textalk/websocket-php/issues", + "source": "https://github.com/Textalk/websocket-php/tree/1.5.8" + }, + "time": "2022-04-26T06:28:24+00:00" + }, { "name": "tijsverkoyen/css-to-inline-styles", "version": "v2.4.0", @@ -9779,16 +10033,16 @@ }, { "name": "webmozart/assert", - "version": "2.1.1", + "version": "2.1.2", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "b01be90dceff69c88c7b36c30ee45c9fd9107f7f" + "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/b01be90dceff69c88c7b36c30ee45c9fd9107f7f", - "reference": "b01be90dceff69c88c7b36c30ee45c9fd9107f7f", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/ce6a2f100c404b2d32a1dd1270f9b59ad4f57649", + "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649", "shasum": "" }, "require": { @@ -9835,9 +10089,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.1.1" + "source": "https://github.com/webmozarts/assert/tree/2.1.2" }, - "time": "2026-01-07T17:26:38+00:00" + "time": "2026-01-13T14:02:24+00:00" } ], "aliases": [], diff --git a/config/services.php b/config/services.php index 6a90eb8..888ed15 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,8 @@ return [ ], ], + 'whois' => [ + 'api_key' => env('WHOIS_API_KEY'), + ], + ]; diff --git a/database/migrations/2026_01_18_120803_create_domains_table.php b/database/migrations/2026_01_18_120803_create_domains_table.php new file mode 100644 index 0000000..f79bb91 --- /dev/null +++ b/database/migrations/2026_01_18_120803_create_domains_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('site_id')->constrained()->onDelete('cascade'); + $table->string('domain'); + $table->timestamp('expires_at')->nullable(); + $table->timestamp('last_checked_at')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->index(['site_id', 'is_active']); + $table->index('expires_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('domains'); + } +}; diff --git a/database/migrations/2026_01_18_123118_create_sitemaps_table.php b/database/migrations/2026_01_18_123118_create_sitemaps_table.php new file mode 100644 index 0000000..9bb8256 --- /dev/null +++ b/database/migrations/2026_01_18_123118_create_sitemaps_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('site_id')->constrained()->onDelete('cascade'); + $table->string('url'); + $table->timestamp('last_checked_at')->nullable(); + $table->boolean('is_valid')->default(true); + $table->text('last_error')->nullable(); + $table->boolean('is_active')->default(true); + $table->integer('url_count')->nullable(); + $table->timestamps(); + + $table->index(['site_id', 'is_active']); + $table->index('is_valid'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sitemaps'); + } +}; diff --git a/database/migrations/2026_01_18_123539_add_notification_channels_to_users_table.php b/database/migrations/2026_01_18_123539_add_notification_channels_to_users_table.php new file mode 100644 index 0000000..e0ae2e7 --- /dev/null +++ b/database/migrations/2026_01_18_123539_add_notification_channels_to_users_table.php @@ -0,0 +1,48 @@ +json('notification_channels')->nullable()->after('notification_preferences'); + $table->string('slack_webhook_url')->nullable()->after('notification_channels'); + $table->string('telegram_chat_id')->nullable()->after('slack_webhook_url'); + $table->string('discord_webhook_url')->nullable()->after('telegram_chat_id'); + $table->string('teams_webhook_url')->nullable()->after('discord_webhook_url'); + $table->string('google_chat_webhook_url')->nullable()->after('teams_webhook_url'); + $table->string('webhook_url')->nullable()->after('google_chat_webhook_url'); + $table->string('sms_phone_number')->nullable()->after('webhook_url'); + $table->string('opsgenie_api_key')->nullable()->after('sms_phone_number'); + $table->string('pagerduty_integration_key')->nullable()->after('opsgenie_api_key'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn([ + 'notification_channels', + 'slack_webhook_url', + 'telegram_chat_id', + 'discord_webhook_url', + 'teams_webhook_url', + 'google_chat_webhook_url', + 'webhook_url', + 'sms_phone_number', + 'opsgenie_api_key', + 'pagerduty_integration_key', + ]); + }); + } +}; diff --git a/database/migrations/2026_01_18_123956_create_site_features_table.php b/database/migrations/2026_01_18_123956_create_site_features_table.php new file mode 100644 index 0000000..9d999e3 --- /dev/null +++ b/database/migrations/2026_01_18_123956_create_site_features_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('site_id')->constrained()->onDelete('cascade'); + $table->string('feature_type'); // uptime, ssl, dns, broken_links, domain, sitemap, health_check, tcp_port + $table->boolean('is_enabled')->default(true); + $table->json('settings')->nullable(); // Feature-specific settings + $table->timestamp('last_checked_at')->nullable(); + $table->timestamps(); + + $table->unique(['site_id', 'feature_type']); + $table->index('is_enabled'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('site_features'); + } +}; diff --git a/resources/views/broken-links/index.blade.php b/resources/views/broken-links/index.blade.php new file mode 100644 index 0000000..e2aa20d --- /dev/null +++ b/resources/views/broken-links/index.blade.php @@ -0,0 +1,218 @@ +@extends('layouts.app') + +@section('title', 'Broken Links') + +@section('content') +
+ +
+
+

+ + Broken Links Monitor +

+

Track and fix broken links across all your websites

+
+
+ + +
+
+
+
+
+
+
+ +
+
+
+
Total Sites
+
{{ $stats['total_sites'] }}
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
Broken Links
+
{{ $stats['total_broken_links'] }}
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
Sites with Issues
+
{{ $stats['sites_with_broken_links'] }}
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
Sites Clean
+
{{ $stats['sites_clean'] }}
+
+
+
+
+
+
+ + +
+
+
+ + Broken Links by Site +
+
+
+ @if($sites->isNotEmpty()) +
+ + + + + + + + + + + + @foreach($sites as $site) + + + + + + + + @endforeach + +
WebsiteBroken LinksLast ScanStatusActions
+
{{ $site->name }}
+
{{ $site->url }}
+
+ @if($site->broken_links_count > 0) + + {{ $site->broken_links_count }} broken + + @else + + No issues + + @endif + + + @if($site->brokenLinks->isNotEmpty()) + {{ $site->brokenLinks->first()->created_at->diffForHumans() }} + @else + Never scanned + @endif + + + @if($site->broken_links_count === 0) + + Healthy + + @elseif($site->broken_links_count < 5) + + Minor Issues + + @else + + Major Issues + + @endif + + + View Details + +
+
+ @else +
+ +
No Sites to Monitor
+

Add sites to start monitoring for broken links.

+ + Add Site + +
+ @endif +
+
+ + + @if($recentBrokenLinks->isNotEmpty()) +
+
+
+ + Recently Detected Broken Links +
+
+
+
+ + + + + + + + + + + + @foreach($recentBrokenLinks as $link) + + + + + + + + @endforeach + +
DetectedSiteBroken URLFound OnStatus Code
{{ $link->created_at->format('M d, H:i') }}{{ $link->site->name }} + {{ Str::limit($link->url ?? 'N/A', 50) }} + {{ Str::limit($link->found_on ?? 'N/A', 40) }} + {{ $link->status_code ?? '404' }} +
+
+
+
+ @endif +
+@endsection diff --git a/resources/views/broken-links/show.blade.php b/resources/views/broken-links/show.blade.php new file mode 100644 index 0000000..790d140 --- /dev/null +++ b/resources/views/broken-links/show.blade.php @@ -0,0 +1,159 @@ +@extends('layouts.app') + +@section('title', 'Broken Links - ' . $site->name) + +@section('content') +
+ +
+ + +
+
+

{{ $site->name }}

+

{{ $site->url }}

+
+
+ @if($stats['total_broken_links'] === 0) + + No Broken Links + + @else + + {{ $stats['total_broken_links'] }} Broken Links + + @endif +
+
+
+ + +
+
+
+
+ +

{{ $stats['total_broken_links'] }}

+

Total Broken Links

+
+
+
+
+
+
+ +

{{ $stats['unique_urls'] }}

+

Unique URLs

+
+
+
+
+ + +
+
+
+ + Broken Links Details +
+
+
+
+ + + + + + + + + + + + @forelse($brokenLinks as $link) + + + + + + + + @empty + + + + @endforelse + +
Broken URLFound On PageStatus CodeLast CheckedType
+
+ +
+ {{ Str::limit($link->url ?? 'N/A', 60) }} + @if($link->url) + + Test Link + + @endif +
+
+
+
+ @if($link->found_on) + + {{ Str::limit($link->found_on, 50) }} + + + @else + Unknown + @endif +
+
+ + {{ $link->status_code ?? 'N/A' }} + + + {{ $link->created_at->diffForHumans() }} + + @if($link->link_type === 'internal') + Internal + @elseif($link->link_type === 'external') + External + @else + Unknown + @endif +
+ +
No Broken Links Found
+

All links on this site are working correctly!

+
+
+
+ @if($brokenLinks->hasPages()) + + @endif +
+ + @if($brokenLinks->isNotEmpty()) + +
+
+ + How to Fix Broken Links +
+
    +
  • Update the broken links on the pages where they're found
  • +
  • Set up redirects for moved content (301 or 302 redirects)
  • +
  • Remove or replace links to deleted pages
  • +
  • Check external links periodically as they may change over time
  • +
+
+ @endif +
+@endsection diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 4649d46..cd9815d 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -4,13 +4,16 @@ @push('styles') @endpush @@ -225,14 +449,14 @@
-
+
-
+
Total Sites
{{ $totalSites }}
@@ -243,12 +467,12 @@
-
+
-
+
-
Sites Up
-
{{ $sitesUp }}
+
Sites Online
+
{{ $sitesUp }}
@@ -256,25 +480,25 @@
-
+
-
+
Sites Down
-
{{ $sitesDown }}
+
{{ $sitesDown }}
- +
-
+
-
+
-
Avg Uptime
-
{{ number_format($avgUptime, 1) }}%
+
Average Uptime
+
{{ number_format($avgUptime, 1) }}%
@@ -285,48 +509,41 @@
+@if($sites->isNotEmpty())
-
-
-
- - 7-Day Uptime Overview +
+
+
+ + Uptime Trends
-
-
- Last 7 days +
+ + Last 7 Days +
- @if($sites->isNotEmpty()) - - @else -
-
- -
-
No Data Available
-

Add your first site to start monitoring and viewing analytics.

- - Add Your First Site - -
- @endif +
+ +
+@endif
-
-
-
- - Your Sites +
+
+
+ + Your Websites
+ + + Add Website +
- - Add Site -
@if($sites->isNotEmpty()) @@ -334,13 +551,13 @@ - - - - - - - + + + + + + + @@ -349,18 +566,28 @@ $latestCheck = $site->uptimeChecks->first(); $latestSsl = $site->sslCertificates->first(); $daysUntilExpiry = $latestSsl ? now()->diffInDays($latestSsl->expires_at, false) : null; + $uptime = $site->uptime_percentage ?? 0; + $initials = strtoupper(substr($site->name, 0, 2)); @endphp @@ -436,12 +665,13 @@ @else @endif @@ -453,50 +683,143 @@ @if($sites->isNotEmpty() && isset($chartData)) +@endpush +@endsection diff --git a/resources/views/sitemaps/show.blade.php b/resources/views/sitemaps/show.blade.php new file mode 100644 index 0000000..d953668 --- /dev/null +++ b/resources/views/sitemaps/show.blade.php @@ -0,0 +1,169 @@ +@extends('layouts.app') + +@section('title', 'Sitemap Details') + +@section('content') +
+

Sitemap Details

+ +
+ +@if(session('success')) + +@endif + +
+
+
+
+
Sitemap Information
+
+
+
+
Associated Site:
+
+ + {{ $sitemap->site->name }} + +
+ {{ $sitemap->site->url }} +
+
+ +
+
Sitemap URL:
+ +
+ +
+
Status:
+
+ + {{ $sitemap->getStatusText() }} + +
+
+ +
+
Monitoring:
+
+ @if($sitemap->is_active) + Active + @else + Inactive + @endif +
+
+ + @if($sitemap->url_count) +
+
URLs in Sitemap:
+
+ {{ number_format($sitemap->url_count) }} +
+
+ @endif +
+
+ +
+
+
Validation Details
+
+
+
+
Last Checked:
+
+ @if($sitemap->last_checked_at) + {{ $sitemap->last_checked_at->format('F j, Y g:i A') }} +
+ {{ $sitemap->last_checked_at->diffForHumans() }} + @else + Never + @endif +
+
+ + @if($sitemap->last_error) +
+
Last Error:
+
+
+ {{ $sitemap->last_error }} +
+
+
+ @endif +
+
+
+ +
+
+
+
Actions
+
+
+
+ @csrf + + + + + Edit Sitemap + + +
+ @csrf + @method('DELETE') + + +
+
+ + @if($sitemap->hasErrors()) +
+
+
Validation Issue
+
+
+

+ Your sitemap has validation errors. Please check the error message above and fix the issues. +

+
+
+ @elseif($sitemap->is_valid && $sitemap->last_checked_at) +
+
+
Validation Passed
+
+
+

+ Your sitemap is valid and accessible. It contains {{ number_format($sitemap->url_count ?? 0) }} URL(s). +

+
+
+ @endif +
+
+@endsection diff --git a/resources/views/ssl/index.blade.php b/resources/views/ssl/index.blade.php new file mode 100644 index 0000000..2830ff9 --- /dev/null +++ b/resources/views/ssl/index.blade.php @@ -0,0 +1,257 @@ +@extends('layouts.app') + +@section('title', 'SSL Certificates') + +@section('content') +
+ +
+
+

+ + SSL Certificates +

+

Monitor SSL certificate expiration and validity across all your sites

+
+
+ + +
+
+
+
+
+
+
+ +
+
+
+
Total Sites
+
{{ $stats['total_sites'] }}
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
Valid Certs
+
{{ $stats['valid_certs'] }}
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
Expiring Soon
+
{{ $stats['expiring_soon'] }}
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
Expired
+
{{ $stats['expired'] }}
+
+
+
+
+
+
+ + +
+
+
+ + SSL Status by Site +
+
+
+ @if($sites->isNotEmpty()) +
+
SiteStatusUptimeResponseSSLLinksActionsWebsiteStatusUptimeResponseSSLLinksAction
-
{{ $site->name }}
-
- {{ Str::limit($site->url, 40) }} +
+
+ {{ $initials }} +
+
+
{{ $site->name }}
+
+ + {{ Str::limit(parse_url($site->url, PHP_URL_HOST), 35) }} +
+
@if(!$latestCheck) - Not Checked + Pending @elseif($latestCheck->status_code >= 200 && $latestCheck->status_code < 400) @@ -368,29 +595,31 @@ @else - Down + Down @endif - @php - $uptime = $site->uptime_percentage ?? 0; - @endphp -
-
+
+
+ style="width: {{ $uptime }}%" + aria-valuenow="{{ $uptime }}" + aria-valuemin="0" + aria-valuemax="100">
- {{ number_format($uptime, 1) }}% + {{ number_format($uptime, 1) }}%
@if($site->avg_response_time) - {{ round($site->avg_response_time) }} - ms +
+ {{ round($site->avg_response_time) }} + ms +
@else - — + — @endif
@@ -409,7 +638,7 @@ @endif @else - — + — @endif @@ -419,13 +648,13 @@ @else - 0 + None @endif - Details +
+ + + + + + + + + + + + + @foreach($sites as $site) + @php + $latestCert = $site->sslCertificates->first(); + $daysUntilExpiry = $latestCert && $latestCert->expires_at ? now()->diffInDays($latestCert->expires_at, false) : null; + @endphp + + + + + + + + + + @endforeach + +
WebsiteIssuerIssued DateExpiry DateDays RemainingStatusActions
+
{{ $site->name }}
+
{{ $site->url }}
+
+ @if($latestCert && $latestCert->issuer) + {{ Str::limit($latestCert->issuer, 30) }} + @else + — + @endif + + @if($latestCert && $latestCert->issued_at) + {{ $latestCert->issued_at->format('M d, Y') }} + @else + — + @endif + + @if($latestCert && $latestCert->expires_at) + {{ $latestCert->expires_at->format('M d, Y') }} + @else + — + @endif + + @if($daysUntilExpiry !== null) + @if($daysUntilExpiry < 0) + Expired + @elseif($daysUntilExpiry < 30) + {{ $daysUntilExpiry }} days + @else + {{ $daysUntilExpiry }} days + @endif + @else + — + @endif + + @if(!$latestCert) + + Not Checked + + @elseif($daysUntilExpiry < 0) + + Expired + + @elseif($daysUntilExpiry < 30) + + Expiring Soon + + @else + + Valid + + @endif + + + View Details + +
+
+ @else +
+ +
No SSL Certificates
+

Add sites to start monitoring their SSL certificates.

+ + Add Site + +
+ @endif +
+
+ + + @if($recentCertificates->isNotEmpty()) +
+
+
+ + Recent Certificate Checks +
+
+
+
+ + + + + + + + + + + + @foreach($recentCertificates as $cert) + @php + $daysUntilExpiry = $cert->expires_at ? now()->diffInDays($cert->expires_at, false) : null; + @endphp + + + + + + + + @endforeach + +
TimeSiteIssuerExpiresStatus
{{ $cert->created_at->format('M d, H:i:s') }}{{ $cert->site->name }}{{ Str::limit($cert->issuer ?? 'Unknown', 25) }} + @if($cert->expires_at) + {{ $cert->expires_at->format('M d, Y') }} + @else + — + @endif + + @if($daysUntilExpiry === null) + Unknown + @elseif($daysUntilExpiry < 0) + Expired + @elseif($daysUntilExpiry < 30) + {{ $daysUntilExpiry }}d + @else + Valid + @endif +
+
+
+
+ @endif +
+@endsection diff --git a/resources/views/ssl/show.blade.php b/resources/views/ssl/show.blade.php new file mode 100644 index 0000000..30bcff5 --- /dev/null +++ b/resources/views/ssl/show.blade.php @@ -0,0 +1,233 @@ +@extends('layouts.app') + +@section('title', 'SSL Certificate - ' . $site->name) + +@section('content') +
+ +
+ + +
+
+

{{ $site->name }}

+

{{ $site->url }}

+
+ @if($latestCert) + @php + $daysUntilExpiry = $latestCert->expires_at ? now()->diffInDays($latestCert->expires_at, false) : null; + @endphp +
+ @if($daysUntilExpiry === null) + Status Unknown + @elseif($daysUntilExpiry < 0) + + Certificate Expired + + @elseif($daysUntilExpiry < 30) + + Expires in {{ $daysUntilExpiry }} days + + @else + + Valid Certificate + + @endif +
+ @endif +
+
+ + @if($latestCert) + +
+
+
+
+
+ + Certificate Details +
+
+
+
+
Issuer
+
{{ $latestCert->issuer ?? 'Unknown' }}
+
+
+
Subject
+
{{ $latestCert->subject ?? 'Unknown' }}
+
+
+
+
Issued Date
+
+ @if($latestCert->issued_at) + {{ $latestCert->issued_at->format('M d, Y') }} + @else + — + @endif +
+
+
+
Expiry Date
+
+ @if($latestCert->expires_at) + {{ $latestCert->expires_at->format('M d, Y') }} + @else + — + @endif +
+
+
+
+
+
+
+
+
+
+ + Validity Information +
+
+
+
+
Days Remaining
+
+ @if($daysUntilExpiry !== null) + @if($daysUntilExpiry < 0) + Expired {{ abs($daysUntilExpiry) }} days ago + @else + {{ $daysUntilExpiry }} days + @endif + @else + — + @endif +
+
+
+
Validity Period
+
+ @if($latestCert->issued_at && $latestCert->expires_at) + {{ $latestCert->issued_at->diffInDays($latestCert->expires_at) }} days + @else + — + @endif +
+
+
+
Last Checked
+
+ {{ $latestCert->created_at->diffForHumans() }} +
+
+
+
+
+
+ @else +
+ + No SSL certificate information available for this site yet. +
+ @endif + + +
+
+
+ + Certificate History +
+
+
+
+ + + + + + + + + + + + + @forelse($sslCertificates as $cert) + @php + $daysUntilExpiry = $cert->expires_at ? now()->diffInDays($cert->expires_at, false) : null; + @endphp + + + + + + + + + @empty + + + + @endforelse + +
Checked AtIssuerIssued DateExpiry DateDays Until ExpiryStatus
{{ $cert->created_at->format('M d, Y H:i:s') }}{{ Str::limit($cert->issuer ?? 'Unknown', 40) }} + @if($cert->issued_at) + {{ $cert->issued_at->format('M d, Y') }} + @else + — + @endif + + @if($cert->expires_at) + {{ $cert->expires_at->format('M d, Y') }} + @else + — + @endif + + @if($daysUntilExpiry !== null) + @if($daysUntilExpiry < 0) + {{ $daysUntilExpiry }} days + @elseif($daysUntilExpiry < 30) + {{ $daysUntilExpiry }} days + @else + {{ $daysUntilExpiry }} days + @endif + @else + — + @endif + + @if($daysUntilExpiry === null) + Unknown + @elseif($daysUntilExpiry < 0) + + Expired + + @elseif($daysUntilExpiry < 30) + + Expiring + + @else + + Valid + + @endif +
+ No SSL certificate checks recorded yet. +
+
+
+ @if($sslCertificates->hasPages()) + + @endif +
+
+@endsection diff --git a/resources/views/tcp-ports/overview.blade.php b/resources/views/tcp-ports/overview.blade.php new file mode 100644 index 0000000..f4627f3 --- /dev/null +++ b/resources/views/tcp-ports/overview.blade.php @@ -0,0 +1,187 @@ +@extends('layouts.app') + +@section('title', 'TCP Ports Overview') + +@section('content') +
+ +
+
+

+ + TCP Port Monitoring +

+

Monitor TCP port availability and connectivity across all your servers

+
+
+ + +
+
+
+
+
+
+
+ +
+
+
+
Total Sites
+
{{ $stats['total_sites'] }}
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
Monitored Ports
+
{{ $stats['total_ports_monitored'] }}
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
Sites with Ports
+
{{ $stats['sites_with_ports'] }}
+
+
+
+
+
+
+ + +
+
+
+ + TCP Ports by Site +
+
+
+ @if($sites->isNotEmpty()) +
+ + + + + + + + + + + + @foreach($sites as $site) + @php + $activePorts = $site->tcpPorts->where('is_active', true); + @endphp + + + + + + + + @endforeach + +
WebsiteMonitored PortsActive PortsCommon PortsActions
+
{{ $site->name }}
+
{{ $site->url }}
+
+ + {{ $site->tcp_ports_count }} ports + + + @if($activePorts->count() > 0) + + {{ $activePorts->count() }} active + + @else + + None active + + @endif + + @if($site->tcpPorts->isNotEmpty()) + @foreach($site->tcpPorts->take(5) as $port) + {{ $port->port }} + @endforeach + @if($site->tcpPorts->count() > 5) + +{{ $site->tcpPorts->count() - 5 }} + @endif + @else + — + @endif + + + View Ports + +
+
+ @else +
+ +
No TCP Ports Monitored
+

Add sites and configure TCP ports to start monitoring connectivity.

+ + Add Site + +
+ @endif +
+
+ + +
+
+
+ + Common TCP Ports Reference +
+
+
+
+
+
    +
  • 80 HTTP
  • +
  • 443 HTTPS
  • +
  • 22 SSH
  • +
  • 21 FTP
  • +
  • 25 SMTP
  • +
+
+
+
    +
  • 3306 MySQL
  • +
  • 5432 PostgreSQL
  • +
  • 6379 Redis
  • +
  • 27017 MongoDB
  • +
  • 3000 Node.js (common)
  • +
+
+
+
+
+
+@endsection diff --git a/resources/views/uptime/index.blade.php b/resources/views/uptime/index.blade.php new file mode 100644 index 0000000..0e05a49 --- /dev/null +++ b/resources/views/uptime/index.blade.php @@ -0,0 +1,232 @@ +@extends('layouts.app') + +@section('title', 'Uptime Monitoring') + +@section('content') +
+ +
+
+

+ + Uptime Monitoring +

+

Monitor website availability and response times across all your sites

+
+
+ + +
+
+
+
+
+
+
+ +
+
+
+
Total Sites
+
{{ $stats['total_sites'] }}
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
Sites Up
+
{{ $stats['sites_up'] }}
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
Sites Down
+
{{ $stats['sites_down'] }}
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
Avg Response
+
{{ $stats['avg_response_time'] }}ms
+
+
+
+
+
+
+ + +
+
+
+ + All Sites +
+
+
+ @if($sites->isNotEmpty()) +
+ + + + + + + + + + + + + @foreach($sites as $site) + @php + $latestCheck = $site->uptimeChecks->first(); + $isOnline = $latestCheck && $latestCheck->is_online; + @endphp + + + + + + + + + @endforeach + +
WebsiteStatusLast CheckResponse TimeUptime (30d)Actions
+
{{ $site->name }}
+
{{ $site->url }}
+
+ @if(!$latestCheck) + + Not Checked + + @elseif($isOnline) + + Online + + @else + + Down + + @endif + + @if($latestCheck) + {{ $latestCheck->created_at->diffForHumans() }} + @else + — + @endif + + @if($latestCheck && $latestCheck->response_time) + {{ round($latestCheck->response_time) }}ms + @else + — + @endif + +
+
+
+
+ 99.5% +
+
+ + View Details + +
+
+ @else +
+ +
No Sites Being Monitored
+

Add sites to start monitoring their uptime.

+ + Add Site + +
+ @endif +
+
+ + + @if($recentChecks->isNotEmpty()) +
+
+
+ + Recent Checks +
+
+
+
+ + + + + + + + + + + + @foreach($recentChecks as $check) + + + + + + + + @endforeach + +
TimeSiteStatusResponse TimeStatus Code
{{ $check->created_at->format('M d, H:i:s') }}{{ $check->site->name }} + @if($check->is_online) + Online + @else + Down + @endif + {{ round($check->response_time ?? 0) }}ms + + {{ $check->status_code }} + +
+
+
+
+ @endif +
+@endsection diff --git a/resources/views/uptime/show.blade.php b/resources/views/uptime/show.blade.php new file mode 100644 index 0000000..b7f1776 --- /dev/null +++ b/resources/views/uptime/show.blade.php @@ -0,0 +1,181 @@ +@extends('layouts.app') + +@section('title', 'Uptime Details - ' . $site->name) + +@section('content') +
+ +
+ + +
+
+

{{ $site->name }}

+

{{ $site->url }}

+
+
+ + {{ $uptimePercentage }}% Uptime + +
+
+
+ + +
+
+
+
+ +

{{ $uptimePercentage }}%

+

Uptime (30 days)

+
+
+
+
+
+
+ +

{{ round($avgResponseTime ?? 0) }}ms

+

Avg Response Time

+
+
+
+
+
+
+ +

{{ $uptimeChecks->count() }}

+

Total Checks

+
+
+
+
+ + +
+
+
+ + Response Time Trend +
+
+
+ +
+
+ + +
+
+
+ + Check History +
+
+
+
+ + + + + + + + + + + @forelse($uptimeChecks as $check) + + + + + + + @empty + + + + @endforelse + +
Date & TimeStatusStatus CodeResponse Time
{{ $check->created_at->format('M d, Y H:i:s') }} + @if($check->is_online) + + Online + + @else + + Down + + @endif + + + {{ $check->status_code }} + + {{ round($check->response_time ?? 0) }}ms
+ No uptime checks recorded yet. +
+
+
+ @if($uptimeChecks->hasPages()) + + @endif +
+
+@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/welcome-landing.blade.php b/resources/views/welcome-landing.blade.php index 297e894..bb75df2 100644 --- a/resources/views/welcome-landing.blade.php +++ b/resources/views/welcome-landing.blade.php @@ -1,30 +1,109 @@ - + - {{ config('app.name') }} - Website Monitoring Made Simple + + {{ config('app.name') }} - Privacy-First Website Monitoring - + @@ -226,26 +425,36 @@