2026-01-17 13:46:20 +02:00
|
|
|
@extends('layouts.app')
|
|
|
|
|
|
|
|
|
|
@section('title', $site->name . ' Dashboard')
|
|
|
|
|
|
|
|
|
|
@section('content')
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
|
|
|
<div>
|
|
|
|
|
<h1><i class="bi bi-bar-chart"></i> {{ $site->name }}</h1>
|
|
|
|
|
<p class="text-muted mb-0">
|
|
|
|
|
<i class="bi bi-link-45deg"></i> <a href="{{ $site->url }}" target="_blank">{{ $site->url }}</a>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<a href="{{ route('sites.index') }}" class="btn btn-outline-secondary">
|
|
|
|
|
<i class="bi bi-arrow-left"></i> Back to Sites
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Stats Cards -->
|
|
|
|
|
<div class="row mb-4">
|
|
|
|
|
<div class="col-md-4">
|
|
|
|
|
<div class="card text-white bg-primary">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<h6 class="card-title">Uptime</h6>
|
|
|
|
|
<h2 class="mb-0">{{ $uptimePercentage }}%</h2>
|
|
|
|
|
<small>Last 30 days</small>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-md-4">
|
|
|
|
|
<div class="card text-white bg-success">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<h6 class="card-title">Avg Response Time</h6>
|
|
|
|
|
<h2 class="mb-0">{{ $avgResponseTime ? round($avgResponseTime) : 'N/A' }}</h2>
|
|
|
|
|
<small>{{ $avgResponseTime ? 'milliseconds' : 'No data' }}</small>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-md-4">
|
|
|
|
|
<div class="card text-white {{ $brokenLinks->total() > 0 ? 'bg-danger' : 'bg-success' }}">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<h6 class="card-title">Broken Links</h6>
|
|
|
|
|
<h2 class="mb-0">{{ $brokenLinks->total() }}</h2>
|
|
|
|
|
<small>{{ $brokenLinks->total() == 1 ? 'link' : 'links' }} detected</small>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Uptime Chart -->
|
|
|
|
|
<div class="card mb-4">
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<h5 class="mb-0"><i class="bi bi-graph-up"></i> Response Time History (Last 30 Days)</h5>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
@if($uptimeChecks->isNotEmpty())
|
|
|
|
|
<canvas id="uptimeChart" height="80"></canvas>
|
|
|
|
|
@else
|
|
|
|
|
<div class="alert alert-info">
|
|
|
|
|
<i class="bi bi-info-circle"></i> No uptime data available yet. The site will be checked automatically.
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- SSL Certificate -->
|
|
|
|
|
<div class="card mb-4">
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<h5 class="mb-0"><i class="bi bi-shield-lock"></i> SSL Certificate</h5>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
@if($latestSslCert)
|
|
|
|
|
@php
|
|
|
|
|
$daysUntilExpiry = now()->diffInDays($latestSslCert->expires_at, false);
|
|
|
|
|
$isExpired = $daysUntilExpiry < 0;
|
|
|
|
|
$isExpiringSoon = $daysUntilExpiry < 30 && !$isExpired;
|
|
|
|
|
@endphp
|
|
|
|
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="col-md-3">
|
|
|
|
|
<strong>Issuer:</strong><br>
|
|
|
|
|
{{ $latestSslCert->issuer }}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-md-3">
|
|
|
|
|
<strong>Expires:</strong><br>
|
|
|
|
|
{{ $latestSslCert->expires_at->format('M d, Y') }}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-md-3">
|
|
|
|
|
<strong>Days Remaining:</strong><br>
|
|
|
|
|
<span class="badge bg-{{ $isExpired ? 'danger' : ($isExpiringSoon ? 'warning' : 'success') }}">
|
|
|
|
|
{{ $isExpired ? 'EXPIRED' : $daysUntilExpiry . ' days' }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-md-3">
|
|
|
|
|
<strong>Last Checked:</strong><br>
|
|
|
|
|
{{ $latestSslCert->checked_at->diffForHumans() }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@else
|
|
|
|
|
<div class="alert alert-info mb-0">
|
|
|
|
|
<i class="bi bi-info-circle"></i> No SSL certificate data available yet.
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Broken Links -->
|
|
|
|
|
<div class="card">
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<h5 class="mb-0"><i class="bi bi-link-45deg"></i> Broken Links</h5>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
@if($brokenLinks->isNotEmpty())
|
|
|
|
|
<div class="table-responsive">
|
|
|
|
|
<table class="table table-hover">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>URL</th>
|
|
|
|
|
<th>Status Code</th>
|
|
|
|
|
<th>Last Checked</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
@foreach($brokenLinks as $link)
|
|
|
|
|
<tr>
|
|
|
|
|
<td>
|
|
|
|
|
<a href="{{ $link->url }}" target="_blank" class="text-decoration-none">
|
|
|
|
|
{{ $link->url }}
|
|
|
|
|
</a>
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
<span class="badge bg-danger">{{ $link->status_code ?? 'Failed' }}</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td>{{ $link->last_checked_at->diffForHumans() }}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
@endforeach
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mt-3">
|
|
|
|
|
{{ $brokenLinks->links() }}
|
|
|
|
|
</div>
|
|
|
|
|
@else
|
|
|
|
|
<div class="alert alert-success mb-0">
|
|
|
|
|
<i class="bi bi-check-circle"></i> No broken links detected! Your site is looking good.
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@endsection
|
|
|
|
|
|
|
|
|
|
@push('scripts')
|
|
|
|
|
@if($uptimeChecks->isNotEmpty())
|
|
|
|
|
<script>
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
const ctx = document.getElementById('uptimeChart').getContext('2d');
|
|
|
|
|
|
|
|
|
|
const labels = @json($chartLabels);
|
|
|
|
|
const responseData = @json($chartData);
|
|
|
|
|
const statusCodes = @json($chartStatusCodes);
|
|
|
|
|
|
|
|
|
|
// Create background colors based on status codes
|
|
|
|
|
const backgroundColors = statusCodes.map(code => {
|
|
|
|
|
if (!code) return 'rgba(220, 53, 69, 0.5)'; // Red for null/failed
|
|
|
|
|
if (code >= 200 && code < 400) return 'rgba(25, 135, 84, 0.5)'; // Green for success
|
|
|
|
|
return 'rgba(220, 53, 69, 0.5)'; // Red for errors
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const borderColors = statusCodes.map(code => {
|
|
|
|
|
if (!code) return 'rgb(220, 53, 69)';
|
|
|
|
|
if (code >= 200 && code < 400) return 'rgb(25, 135, 84)';
|
|
|
|
|
return 'rgb(220, 53, 69)';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
new Chart(ctx, {
|
|
|
|
|
type: 'line',
|
|
|
|
|
data: {
|
|
|
|
|
labels: labels,
|
|
|
|
|
datasets: [{
|
|
|
|
|
label: 'Response Time (ms)',
|
|
|
|
|
data: responseData,
|
2026-03-28 22:07:06 +02:00
|
|
|
borderColor: 'rgb(14, 165, 233)',
|
2026-01-17 13:46:20 +02:00
|
|
|
backgroundColor: backgroundColors,
|
|
|
|
|
borderWidth: 2,
|
|
|
|
|
fill: false,
|
|
|
|
|
tension: 0.1,
|
|
|
|
|
pointBackgroundColor: backgroundColors,
|
|
|
|
|
pointBorderColor: borderColors,
|
|
|
|
|
pointRadius: 4,
|
|
|
|
|
pointHoverRadius: 6
|
|
|
|
|
}]
|
|
|
|
|
},
|
|
|
|
|
options: {
|
|
|
|
|
responsive: true,
|
|
|
|
|
maintainAspectRatio: true,
|
|
|
|
|
plugins: {
|
|
|
|
|
legend: {
|
|
|
|
|
display: true,
|
|
|
|
|
position: 'top'
|
|
|
|
|
},
|
|
|
|
|
tooltip: {
|
|
|
|
|
callbacks: {
|
|
|
|
|
afterLabel: function(context) {
|
|
|
|
|
const statusCode = statusCodes[context.dataIndex];
|
|
|
|
|
return statusCode ? 'HTTP ' + statusCode : 'Connection Failed';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
scales: {
|
|
|
|
|
y: {
|
|
|
|
|
beginAtZero: true,
|
|
|
|
|
title: {
|
|
|
|
|
display: true,
|
|
|
|
|
text: 'Response Time (ms)'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
x: {
|
|
|
|
|
title: {
|
|
|
|
|
display: true,
|
|
|
|
|
text: 'Check Time'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
@endif
|
|
|
|
|
@endpush
|