some progress
This commit is contained in:
@@ -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
|
||||
<?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.
|
||||
Reference in New Issue
Block a user