Files
ismysiteup.online/FEATURE_MANAGEMENT_SYSTEM.md
T
2026-01-24 12:16:10 +02:00

18 KiB

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

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:

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

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:

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:

@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.