Files

577 lines
18 KiB
Markdown
Raw Permalink Normal View History

2026-01-24 12:16:10 +02:00
# 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
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SiteFeature extends Model
{
protected $fillable = [
'site_id',
'feature_type',
'is_enabled',
'settings',
'last_checked_at',
];
protected $casts = [
'is_enabled' => '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
<?php
namespace App\Http\Controllers;
use App\Models\Site;
use App\Models\SiteFeature;
use Illuminate\Http\Request;
class FeaturesController extends Controller
{
/**
* Display all features overview
*/
public function index()
{
$sites = Site::where('user_id', auth()->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')
<div class="container">
<h1 class="mb-4"><i class="bi bi-puzzle"></i> Monitoring Features</h1>
<p class="lead mb-4">
Manage monitoring features for your sites. Enable or disable features as needed.
</p>
<div class="row g-4">
@foreach($availableFeatures as $type => $feature)
<div class="col-md-6 col-lg-4">
<div class="card h-100 feature-card">
<div class="card-body">
<div class="d-flex align-items-start mb-3">
<div class="feature-icon bg-{{ $feature['color'] }} bg-opacity-10 text-{{ $feature['color'] }} rounded p-3 me-3">
<i class="{{ $feature['icon'] }} fs-2"></i>
</div>
<div class="flex-grow-1">
<h5 class="card-title mb-1">{{ $feature['name'] }}</h5>
<p class="card-text text-muted small">{{ $feature['description'] }}</p>
</div>
</div>
<div class="mb-3">
<div class="row g-2">
@php
$enabledCount = $sites->sum(function($site) use ($type) {
return $site->features->where('feature_type', $type)->where('is_enabled', true)->count();
});
@endphp
<div class="col-6">
<div class="text-center p-2 bg-light rounded">
<div class="h4 mb-0">{{ $enabledCount }}</div>
<small class="text-muted">Active</small>
</div>
</div>
<div class="col-6">
<div class="text-center p-2 bg-light rounded">
<div class="h4 mb-0">{{ $sites->count() }}</div>
<small class="text-muted">Total Sites</small>
</div>
</div>
</div>
</div>
<a href="{{ route('features.show', $type) }}" class="btn btn-outline-{{ $feature['color'] }} w-100">
<i class="bi bi-bar-chart"></i> View Details & Graphs
</a>
</div>
</div>
</div>
@endforeach
</div>
<div class="card mt-4">
<div class="card-header">
<h5 class="mb-0">Sites Overview</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Site</th>
@foreach($availableFeatures as $type => $feature)
<th class="text-center">
<i class="{{ $feature['icon'] }}" title="{{ $feature['name'] }}"></i>
</th>
@endforeach
</tr>
</thead>
<tbody>
@foreach($sites as $site)
<tr>
<td>
<strong>{{ $site->name }}</strong><br>
<small class="text-muted">{{ $site->url }}</small>
</td>
@foreach(array_keys($availableFeatures) as $type)
@php
$feature = $site->features->where('feature_type', $type)->first();
$isEnabled = $feature && $feature->is_enabled;
@endphp
<td class="text-center">
<i class="bi bi-{{ $isEnabled ? 'check-circle-fill text-success' : 'x-circle text-muted' }}"></i>
</td>
@endforeach
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
@endsection
@push('styles')
<style>
.feature-card {
transition: transform 0.2s, box-shadow 0.2s;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.feature-icon {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
@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.