some progress
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Broken Links')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h2 mb-2">
|
||||
<i class="bi bi-link-45deg text-warning"></i>
|
||||
Broken Links Monitor
|
||||
</h1>
|
||||
<p class="text-muted">Track and fix broken links across all your websites</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Overview -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-primary bg-opacity-10 text-primary rounded p-3">
|
||||
<i class="bi bi-globe fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Total Sites</div>
|
||||
<div class="h4 mb-0">{{ $stats['total_sites'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-danger bg-opacity-10 text-danger rounded p-3">
|
||||
<i class="bi bi-link-45deg fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Broken Links</div>
|
||||
<div class="h4 mb-0 text-danger">{{ $stats['total_broken_links'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-warning bg-opacity-10 text-warning rounded p-3">
|
||||
<i class="bi bi-exclamation-triangle fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Sites with Issues</div>
|
||||
<div class="h4 mb-0 text-warning">{{ $stats['sites_with_broken_links'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-success bg-opacity-10 text-success rounded p-3">
|
||||
<i class="bi bi-check-circle fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Sites Clean</div>
|
||||
<div class="h4 mb-0 text-success">{{ $stats['sites_clean'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sites List -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-list-check me-2"></i>
|
||||
Broken Links by Site
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($sites->isNotEmpty())
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Website</th>
|
||||
<th>Broken Links</th>
|
||||
<th>Last Scan</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($sites as $site)
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold">{{ $site->name }}</div>
|
||||
<div class="small text-muted">{{ $site->url }}</div>
|
||||
</td>
|
||||
<td>
|
||||
@if($site->broken_links_count > 0)
|
||||
<span class="badge bg-danger fs-6 px-3">
|
||||
{{ $site->broken_links_count }} broken
|
||||
</span>
|
||||
@else
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-check-circle"></i> No issues
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-muted">
|
||||
@if($site->brokenLinks->isNotEmpty())
|
||||
{{ $site->brokenLinks->first()->created_at->diffForHumans() }}
|
||||
@else
|
||||
Never scanned
|
||||
@endif
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@if($site->broken_links_count === 0)
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-check-circle-fill"></i> Healthy
|
||||
</span>
|
||||
@elseif($site->broken_links_count < 5)
|
||||
<span class="badge bg-warning text-dark">
|
||||
<i class="bi bi-exclamation-triangle"></i> Minor Issues
|
||||
</span>
|
||||
@else
|
||||
<span class="badge bg-danger">
|
||||
<i class="bi bi-x-circle-fill"></i> Major Issues
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('broken-links.show', $site) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-eye"></i> View Details
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-link-45deg text-muted" style="font-size: 4rem; opacity: 0.3;"></i>
|
||||
<h5 class="mt-3">No Sites to Monitor</h5>
|
||||
<p class="text-muted">Add sites to start monitoring for broken links.</p>
|
||||
<a href="{{ route('sites.create') }}" class="btn btn-primary mt-3">
|
||||
<i class="bi bi-plus-circle"></i> Add Site
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Broken Links -->
|
||||
@if($recentBrokenLinks->isNotEmpty())
|
||||
<div class="card border-0 shadow-sm mt-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-clock-history me-2"></i>
|
||||
Recently Detected Broken Links
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Detected</th>
|
||||
<th>Site</th>
|
||||
<th>Broken URL</th>
|
||||
<th>Found On</th>
|
||||
<th>Status Code</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($recentBrokenLinks as $link)
|
||||
<tr>
|
||||
<td class="text-muted small">{{ $link->created_at->format('M d, H:i') }}</td>
|
||||
<td>{{ $link->site->name }}</td>
|
||||
<td>
|
||||
<code class="small">{{ Str::limit($link->url ?? 'N/A', 50) }}</code>
|
||||
</td>
|
||||
<td class="small">{{ Str::limit($link->found_on ?? 'N/A', 40) }}</td>
|
||||
<td>
|
||||
<span class="badge bg-danger">{{ $link->status_code ?? '404' }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,159 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Broken Links - ' . $site->name)
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-4">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('broken-links.index') }}">Broken Links</a></li>
|
||||
<li class="breadcrumb-item active">{{ $site->name }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="h2 mb-2">{{ $site->name }}</h1>
|
||||
<p class="text-muted">{{ $site->url }}</p>
|
||||
</div>
|
||||
<div>
|
||||
@if($stats['total_broken_links'] === 0)
|
||||
<span class="badge bg-success fs-6 px-3 py-2">
|
||||
<i class="bi bi-check-circle-fill"></i> No Broken Links
|
||||
</span>
|
||||
@else
|
||||
<span class="badge bg-danger fs-6 px-3 py-2">
|
||||
<i class="bi bi-exclamation-circle-fill"></i> {{ $stats['total_broken_links'] }} Broken Links
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-link-45deg text-danger fs-1 mb-3"></i>
|
||||
<h3 class="mb-1">{{ $stats['total_broken_links'] }}</h3>
|
||||
<p class="text-muted mb-0">Total Broken Links</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-file-text text-primary fs-1 mb-3"></i>
|
||||
<h3 class="mb-1">{{ $stats['unique_urls'] }}</h3>
|
||||
<p class="text-muted mb-0">Unique URLs</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Broken Links List -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-list-ul me-2"></i>
|
||||
Broken Links Details
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Broken URL</th>
|
||||
<th>Found On Page</th>
|
||||
<th>Status Code</th>
|
||||
<th>Last Checked</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($brokenLinks as $link)
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-link-broken text-danger me-2 mt-1"></i>
|
||||
<div class="flex-grow-1">
|
||||
<code class="small d-block mb-1">{{ Str::limit($link->url ?? 'N/A', 60) }}</code>
|
||||
@if($link->url)
|
||||
<a href="{{ $link->url }}" target="_blank" class="small text-muted" rel="noopener">
|
||||
<i class="bi bi-box-arrow-up-right"></i> Test Link
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="small">
|
||||
@if($link->found_on)
|
||||
<a href="{{ $link->found_on }}" target="_blank" class="text-decoration-none" rel="noopener">
|
||||
{{ Str::limit($link->found_on, 50) }}
|
||||
<i class="bi bi-box-arrow-up-right small"></i>
|
||||
</a>
|
||||
@else
|
||||
<span class="text-muted">Unknown</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-{{ $link->status_code == 404 ? 'danger' : ($link->status_code >= 500 ? 'warning' : 'secondary') }}">
|
||||
{{ $link->status_code ?? 'N/A' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-muted small">
|
||||
{{ $link->created_at->diffForHumans() }}
|
||||
</td>
|
||||
<td>
|
||||
@if($link->link_type === 'internal')
|
||||
<span class="badge bg-info">Internal</span>
|
||||
@elseif($link->link_type === 'external')
|
||||
<span class="badge bg-secondary">External</span>
|
||||
@else
|
||||
<span class="badge bg-light text-dark">Unknown</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5">
|
||||
<i class="bi bi-check-circle text-success" style="font-size: 3rem;"></i>
|
||||
<h5 class="mt-3">No Broken Links Found</h5>
|
||||
<p class="text-muted mb-0">All links on this site are working correctly!</p>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@if($brokenLinks->hasPages())
|
||||
<div class="card-footer bg-white">
|
||||
{{ $brokenLinks->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($brokenLinks->isNotEmpty())
|
||||
<!-- Help Section -->
|
||||
<div class="alert alert-info mt-4">
|
||||
<h6 class="alert-heading">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
How to Fix Broken Links
|
||||
</h6>
|
||||
<ul class="mb-0">
|
||||
<li>Update the broken links on the pages where they're found</li>
|
||||
<li>Set up redirects for moved content (301 or 302 redirects)</li>
|
||||
<li>Remove or replace links to deleted pages</li>
|
||||
<li>Check external links periodically as they may change over time</li>
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
+501
-178
@@ -4,13 +4,16 @@
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
/* Modern Stats Cards */
|
||||
.stat-card {
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 20px;
|
||||
padding: 2rem 1.75rem;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.stat-card::before {
|
||||
@@ -19,204 +22,425 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--card-gradient-start), var(--card-gradient-end));
|
||||
height: 3px;
|
||||
background: var(--card-accent);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-card.primary {
|
||||
--card-gradient-start: #667eea;
|
||||
--card-gradient-end: #764ba2;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05), rgba(118, 75, 162, 0.05));
|
||||
--card-accent: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.stat-card.success {
|
||||
--card-gradient-start: #10b981;
|
||||
--card-gradient-end: #059669;
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.05), rgba(5, 150, 105, 0.05));
|
||||
--card-accent: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
}
|
||||
|
||||
.stat-card.danger {
|
||||
--card-gradient-start: #ef4444;
|
||||
--card-gradient-end: #dc2626;
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.05), rgba(220, 38, 38, 0.05));
|
||||
--card-accent: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
}
|
||||
|
||||
.stat-card.info {
|
||||
--card-gradient-start: #3b82f6;
|
||||
--card-gradient-end: #2563eb;
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.05), rgba(37, 99, 235, 0.05));
|
||||
--card-accent: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
}
|
||||
|
||||
.dark .stat-card.primary {
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
|
||||
.dark .stat-card {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dark .stat-card.success {
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1), rgba(5, 150, 105, 0.1));
|
||||
}
|
||||
|
||||
.dark .stat-card.danger {
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(220, 38, 38, 0.1));
|
||||
}
|
||||
|
||||
.dark .stat-card.info {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(37, 99, 235, 0.1));
|
||||
.dark .stat-card:hover {
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.75rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-icon::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--icon-gradient);
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.stat-icon i {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stat-icon.primary {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
--icon-gradient: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.stat-icon.success {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
--icon-gradient: linear-gradient(135deg, #10b981, #059669);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.stat-icon.danger {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
color: white;
|
||||
--icon-gradient: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.stat-icon.info {
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
color: white;
|
||||
--icon-gradient: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.dark .stat-icon {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
opacity: 0.7;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
opacity: 0.6;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
letter-spacing: 0.8px;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-trend.up {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.stat-trend.down {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Chart Card */
|
||||
.chart-card {
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
background: var(--bg-primary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .chart-card {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.chart-card .card-header {
|
||||
background: transparent;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 1.5rem;
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 1.75rem 2rem;
|
||||
}
|
||||
|
||||
.chart-card .card-body {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.chart-card .card-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.chart-card .card-title i {
|
||||
color: var(--indigo-600);
|
||||
}
|
||||
|
||||
/* Modern Table */
|
||||
.table-modern {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-modern thead th {
|
||||
background: transparent;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
background: var(--bg-secondary);
|
||||
border: none;
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 1rem;
|
||||
letter-spacing: 1px;
|
||||
padding: 1.25rem 1.5rem;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table-modern thead th:first-child {
|
||||
border-top-left-radius: 12px;
|
||||
}
|
||||
|
||||
.table-modern thead th:last-child {
|
||||
border-top-right-radius: 12px;
|
||||
}
|
||||
|
||||
.table-modern tbody tr {
|
||||
transition: all 0.2s ease;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.table-modern tbody tr:hover {
|
||||
background: var(--bg-secondary);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.table-modern tbody tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table-modern tbody td {
|
||||
padding: 1.25rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 1.5rem 1.5rem;
|
||||
vertical-align: middle;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.site-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.site-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.site-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.site-name {
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.95rem;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.site-url {
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.site-url i {
|
||||
opacity: 0.5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.badge-modern {
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.3px;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 10px;
|
||||
font-weight: 700;
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
.badge-modern i {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Metric Display */
|
||||
.metric-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-weight: 700;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-modern {
|
||||
height: 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--bg-secondary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-modern .progress-bar {
|
||||
border-radius: 10px;
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-action {
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||||
box-shadow: 0 8px 20px rgba(79, 70, 229, 0.25);
|
||||
}
|
||||
|
||||
.btn-action.btn-outline-primary:hover {
|
||||
background: var(--indigo-600);
|
||||
color: white;
|
||||
border-color: var(--indigo-600);
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
padding: 4rem 2rem;
|
||||
padding: 5rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 4rem;
|
||||
opacity: 0.3;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 5rem;
|
||||
opacity: 0.15;
|
||||
margin-bottom: 2rem;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.empty-state h5 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 1rem;
|
||||
color: var(--text-secondary);
|
||||
max-width: 400px;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
/* Page Header */
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
font-size: 2.25rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: -0.02em;
|
||||
background: linear-gradient(135deg, var(--text-primary) 0%, var(--text-secondary) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: 1rem;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Time Badge */
|
||||
.time-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 10px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Chart Container */
|
||||
.chart-container {
|
||||
position: relative;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.stat-card {
|
||||
padding: 1.5rem 1.25rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.table-modern thead th,
|
||||
.table-modern tbody td {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.site-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
@@ -225,14 +449,14 @@
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Dashboard</h1>
|
||||
<p class="page-subtitle">Welcome back! Here's an overview of all your monitored sites.</p>
|
||||
<p class="page-subtitle">Monitor and manage all your websites from one place</p>
|
||||
</div>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="stat-card primary">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="flex-grow-1">
|
||||
<div class="stat-label">Total Sites</div>
|
||||
<div class="stat-value">{{ $totalSites }}</div>
|
||||
@@ -243,12 +467,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="stat-card success">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="flex-grow-1">
|
||||
<div class="stat-label">Sites Up</div>
|
||||
<div class="stat-value text-success">{{ $sitesUp }}</div>
|
||||
<div class="stat-label">Sites Online</div>
|
||||
<div class="stat-value" style="color: #10b981;">{{ $sitesUp }}</div>
|
||||
</div>
|
||||
<div class="stat-icon success">
|
||||
<i class="bi bi-check-circle-fill"></i>
|
||||
@@ -256,25 +480,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="stat-card danger">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="flex-grow-1">
|
||||
<div class="stat-label">Sites Down</div>
|
||||
<div class="stat-value text-danger">{{ $sitesDown }}</div>
|
||||
<div class="stat-value" style="color: #ef4444;">{{ $sitesDown }}</div>
|
||||
</div>
|
||||
<div class="stat-icon danger">
|
||||
<i class="bi bi-exclamation-circle-fill"></i>
|
||||
<i class="bi bi-x-circle-fill"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="stat-card info">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="flex-grow-1">
|
||||
<div class="stat-label">Avg Uptime</div>
|
||||
<div class="stat-value text-info">{{ number_format($avgUptime, 1) }}%</div>
|
||||
<div class="stat-label">Average Uptime</div>
|
||||
<div class="stat-value" style="color: #3b82f6;">{{ number_format($avgUptime, 1) }}%</div>
|
||||
</div>
|
||||
<div class="stat-icon info">
|
||||
<i class="bi bi-graph-up-arrow"></i>
|
||||
@@ -285,48 +509,41 @@
|
||||
</div>
|
||||
|
||||
<!-- 7-Day Uptime Chart -->
|
||||
@if($sites->isNotEmpty())
|
||||
<div class="card chart-card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h5 class="mb-0 d-flex align-items-center">
|
||||
<i class="bi bi-graph-up me-2"></i>
|
||||
7-Day Uptime Overview
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-activity"></i>
|
||||
Uptime Trends
|
||||
</h5>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
<i class="bi bi-clock"></i> Last 7 days
|
||||
<div class="time-badge">
|
||||
<i class="bi bi-calendar3"></i>
|
||||
Last 7 Days
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if($sites->isNotEmpty())
|
||||
<canvas id="weeklyUptimeChart" height="80"></canvas>
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">
|
||||
<i class="bi bi-graph-up"></i>
|
||||
</div>
|
||||
<h5>No Data Available</h5>
|
||||
<p class="text-muted">Add your first site to start monitoring and viewing analytics.</p>
|
||||
<a href="{{ route('sites.create') }}" class="btn btn-primary mt-3">
|
||||
<i class="bi bi-plus-circle"></i> Add Your First Site
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
<div class="chart-container">
|
||||
<canvas id="weeklyUptimeChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Sites Table -->
|
||||
<div class="card chart-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h5 class="mb-0 d-flex align-items-center">
|
||||
<i class="bi bi-globe me-2"></i>
|
||||
Your Sites
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-list-check"></i>
|
||||
Your Websites
|
||||
</h5>
|
||||
<a href="{{ route('sites.create') }}" class="btn btn-primary btn-action">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
Add Website
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ route('sites.create') }}" class="btn btn-primary btn-sm btn-action">
|
||||
<i class="bi bi-plus-circle"></i> Add Site
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($sites->isNotEmpty())
|
||||
@@ -334,13 +551,13 @@
|
||||
<table class="table table-modern mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Site</th>
|
||||
<th>Status</th>
|
||||
<th>Uptime</th>
|
||||
<th>Response</th>
|
||||
<th>SSL</th>
|
||||
<th>Links</th>
|
||||
<th class="text-end">Actions</th>
|
||||
<th style="width: 30%;">Website</th>
|
||||
<th style="width: 12%;">Status</th>
|
||||
<th style="width: 15%;">Uptime</th>
|
||||
<th style="width: 12%;">Response</th>
|
||||
<th style="width: 12%;">SSL</th>
|
||||
<th style="width: 10%;">Links</th>
|
||||
<th style="width: 9%;" class="text-end">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -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
|
||||
<tr>
|
||||
<td>
|
||||
<div class="site-name">{{ $site->name }}</div>
|
||||
<div class="site-url">
|
||||
<i class="bi bi-link-45deg"></i> {{ Str::limit($site->url, 40) }}
|
||||
<div class="site-info">
|
||||
<div class="site-avatar" style="background: linear-gradient(135deg, {{ '#' . substr(md5($site->id), 0, 6) }}, {{ '#' . substr(md5($site->id . 'salt'), 0, 6) }});">
|
||||
{{ $initials }}
|
||||
</div>
|
||||
<div class="site-details">
|
||||
<div class="site-name" title="{{ $site->name }}">{{ $site->name }}</div>
|
||||
<div class="site-url" title="{{ $site->url }}">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
<span>{{ Str::limit(parse_url($site->url, PHP_URL_HOST), 35) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@if(!$latestCheck)
|
||||
<span class="badge badge-modern bg-secondary">
|
||||
<i class="bi bi-question-circle"></i> Not Checked
|
||||
<i class="bi bi-dash-circle"></i> Pending
|
||||
</span>
|
||||
@elseif($latestCheck->status_code >= 200 && $latestCheck->status_code < 400)
|
||||
<span class="badge badge-modern bg-success">
|
||||
@@ -368,29 +595,31 @@
|
||||
</span>
|
||||
@else
|
||||
<span class="badge badge-modern bg-danger">
|
||||
<i class="bi bi-x-circle-fill"></i> Down
|
||||
<i class="bi bi-exclamation-circle-fill"></i> Down
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@php
|
||||
$uptime = $site->uptime_percentage ?? 0;
|
||||
@endphp
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="progress" style="width: 60px; height: 6px;">
|
||||
<div class="metric-display">
|
||||
<div class="progress-modern" style="width: 70px;">
|
||||
<div class="progress-bar bg-{{ $uptime >= 99 ? 'success' : ($uptime >= 95 ? 'warning' : 'danger') }}"
|
||||
role="progressbar"
|
||||
style="width: {{ $uptime }}%"></div>
|
||||
style="width: {{ $uptime }}%"
|
||||
aria-valuenow="{{ $uptime }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"></div>
|
||||
</div>
|
||||
<span class="small fw-semibold">{{ number_format($uptime, 1) }}%</span>
|
||||
<span class="metric-value">{{ number_format($uptime, 1) }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@if($site->avg_response_time)
|
||||
<span class="fw-semibold">{{ round($site->avg_response_time) }}</span>
|
||||
<span class="text-muted small">ms</span>
|
||||
<div class="metric-display">
|
||||
<span class="metric-value">{{ round($site->avg_response_time) }}</span>
|
||||
<span class="metric-label">ms</span>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
<span class="text-muted fw-semibold">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@@ -409,7 +638,7 @@
|
||||
</span>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
<span class="text-muted fw-semibold">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@@ -419,13 +648,13 @@
|
||||
</span>
|
||||
@else
|
||||
<span class="badge badge-modern bg-success">
|
||||
<i class="bi bi-check-circle"></i> 0
|
||||
<i class="bi bi-check2"></i> None
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('dashboard.show', $site) }}" class="btn btn-sm btn-outline-primary btn-action">
|
||||
<i class="bi bi-arrow-right-circle"></i> Details
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -436,12 +665,13 @@
|
||||
@else
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">
|
||||
<i class="bi bi-globe"></i>
|
||||
<i class="bi bi-globe2"></i>
|
||||
</div>
|
||||
<h5>No Sites Yet</h5>
|
||||
<p class="text-muted">Start monitoring your websites by adding your first site.</p>
|
||||
<a href="{{ route('sites.create') }}" class="btn btn-primary mt-3">
|
||||
<i class="bi bi-plus-circle"></i> Add Your First Site
|
||||
<h5>No Websites Yet</h5>
|
||||
<p>Get started by adding your first website to monitor. It only takes a few seconds!</p>
|
||||
<a href="{{ route('sites.create') }}" class="btn btn-primary btn-lg btn-action">
|
||||
<i class="bi bi-plus-circle-fill"></i>
|
||||
Add Your First Website
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
@@ -453,50 +683,143 @@
|
||||
@if($sites->isNotEmpty() && isset($chartData))
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const ctx = document.getElementById('weeklyUptimeChart').getContext('2d');
|
||||
const ctx = document.getElementById('weeklyUptimeChart');
|
||||
if (!ctx) return;
|
||||
|
||||
const chartData = @json($chartData);
|
||||
|
||||
// Check if user prefers dark mode
|
||||
const isDarkMode = document.documentElement.classList.contains('dark') ||
|
||||
localStorage.getItem('darkMode') === 'true';
|
||||
|
||||
const gridColor = isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.06)';
|
||||
const textColor = isDarkMode ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)';
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
datasets: chartData.datasets.map((dataset, index) => ({
|
||||
label: dataset.label,
|
||||
data: dataset.data,
|
||||
borderColor: dataset.color,
|
||||
backgroundColor: dataset.color + '33',
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
tension: 0.1
|
||||
}))
|
||||
datasets: chartData.datasets.map((dataset, index) => {
|
||||
const colors = [
|
||||
{ border: '#667eea', bg: 'rgba(102, 126, 234, 0.1)' },
|
||||
{ border: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
||||
{ border: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
||||
{ border: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
||||
{ border: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
||||
];
|
||||
const color = colors[index % colors.length];
|
||||
|
||||
return {
|
||||
label: dataset.label,
|
||||
data: dataset.data,
|
||||
borderColor: color.border,
|
||||
backgroundColor: color.bg,
|
||||
borderWidth: 3,
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
pointBackgroundColor: color.border,
|
||||
pointBorderColor: '#fff',
|
||||
pointBorderWidth: 2,
|
||||
pointHoverBackgroundColor: color.border,
|
||||
pointHoverBorderColor: '#fff',
|
||||
pointHoverBorderWidth: 3,
|
||||
};
|
||||
})
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
aspectRatio: 2.5,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
pointStyle: 'circle',
|
||||
padding: 20,
|
||||
font: {
|
||||
size: 13,
|
||||
weight: '600',
|
||||
family: 'Inter, sans-serif'
|
||||
},
|
||||
color: textColor
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
enabled: true,
|
||||
backgroundColor: isDarkMode ? 'rgba(30, 30, 30, 0.95)' : 'rgba(255, 255, 255, 0.95)',
|
||||
titleColor: isDarkMode ? '#fff' : '#000',
|
||||
bodyColor: isDarkMode ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.8)',
|
||||
borderColor: gridColor,
|
||||
borderWidth: 1,
|
||||
padding: 12,
|
||||
boxPadding: 6,
|
||||
usePointStyle: true,
|
||||
titleFont: {
|
||||
size: 14,
|
||||
weight: '700',
|
||||
family: 'Inter, sans-serif'
|
||||
},
|
||||
bodyFont: {
|
||||
size: 13,
|
||||
weight: '600',
|
||||
family: 'Inter, sans-serif'
|
||||
},
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += context.parsed.y.toFixed(1) + '%';
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Uptime %'
|
||||
grid: {
|
||||
color: gridColor,
|
||||
drawBorder: false,
|
||||
},
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return value + '%';
|
||||
},
|
||||
font: {
|
||||
size: 12,
|
||||
weight: '600',
|
||||
family: 'Inter, sans-serif'
|
||||
},
|
||||
color: textColor,
|
||||
padding: 8
|
||||
}
|
||||
},
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Date'
|
||||
grid: {
|
||||
display: false,
|
||||
drawBorder: false,
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 12,
|
||||
weight: '600',
|
||||
family: 'Inter, sans-serif'
|
||||
},
|
||||
color: textColor,
|
||||
padding: 8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'DNS Records Overview')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h2 mb-2">
|
||||
<i class="bi bi-server text-info"></i>
|
||||
DNS Records Monitoring
|
||||
</h1>
|
||||
<p class="text-muted">Monitor and track DNS records across all your websites</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Overview -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-primary bg-opacity-10 text-primary rounded p-3">
|
||||
<i class="bi bi-globe fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Total Sites</div>
|
||||
<div class="h4 mb-0">{{ $stats['total_sites'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-info bg-opacity-10 text-info rounded p-3">
|
||||
<i class="bi bi-server fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">DNS Records</div>
|
||||
<div class="h4 mb-0">{{ $stats['total_dns_records'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-success bg-opacity-10 text-success rounded p-3">
|
||||
<i class="bi bi-check-circle fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Sites Monitored</div>
|
||||
<div class="h4 mb-0">{{ $stats['sites_with_records'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sites List -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-list-check me-2"></i>
|
||||
DNS Records by Site
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($sites->isNotEmpty())
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Website</th>
|
||||
<th>DNS Records</th>
|
||||
<th>Record Types</th>
|
||||
<th>Last Updated</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($sites as $site)
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold">{{ $site->name }}</div>
|
||||
<div class="small text-muted">{{ $site->url }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-primary fs-6">
|
||||
{{ $site->dns_records_count }} records
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@if($site->dnsRecords->isNotEmpty())
|
||||
@php
|
||||
$types = $site->dnsRecords->pluck('record_type')->unique();
|
||||
@endphp
|
||||
@foreach($types->take(3) as $type)
|
||||
<span class="badge bg-secondary me-1">{{ $type }}</span>
|
||||
@endforeach
|
||||
@if($types->count() > 3)
|
||||
<span class="badge bg-light text-dark">+{{ $types->count() - 3 }}</span>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($site->dnsRecords->isNotEmpty())
|
||||
<span class="text-muted">
|
||||
{{ $site->dnsRecords->first()->updated_at->diffForHumans() }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('dns-records.index', $site) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-eye"></i> View Records
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-server text-muted" style="font-size: 4rem; opacity: 0.3;"></i>
|
||||
<h5 class="mt-3">No DNS Records</h5>
|
||||
<p class="text-muted">Add sites and configure DNS records to start monitoring.</p>
|
||||
<a href="{{ route('sites.create') }}" class="btn btn-primary mt-3">
|
||||
<i class="bi bi-plus-circle"></i> Add Site
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Card -->
|
||||
<div class="alert alert-info mt-4">
|
||||
<h6 class="alert-heading">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
About DNS Monitoring
|
||||
</h6>
|
||||
<p class="mb-0">
|
||||
DNS record monitoring helps you track changes to your domain's DNS configuration.
|
||||
You can monitor A, AAAA, CNAME, MX, TXT, NS, SOA, PTR, and SRV records.
|
||||
Add DNS records for each site to get notified of any unexpected changes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,72 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Add Domain')
|
||||
|
||||
@section('content')
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0"><i class="bi bi-plus-circle"></i> Add Domain for Monitoring</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('domains.store') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="site_id" class="form-label">Associated Site <span class="text-danger">*</span></label>
|
||||
<select class="form-select @error('site_id') is-invalid @enderror"
|
||||
id="site_id" name="site_id" required>
|
||||
<option value="">Select a site...</option>
|
||||
@foreach($sites as $site)
|
||||
<option value="{{ $site->id }}" {{ old('site_id') == $site->id ? 'selected' : '' }}>
|
||||
{{ $site->name }} ({{ $site->url }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('site_id')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
<div class="form-text">Select the site this domain belongs to</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="domain" class="form-label">Domain Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control @error('domain') is-invalid @enderror"
|
||||
id="domain" name="domain" value="{{ old('domain') }}" required
|
||||
placeholder="example.com">
|
||||
@error('domain')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
<div class="form-text">Enter the domain name without protocol (e.g., example.com)</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="is_active"
|
||||
name="is_active" {{ old('is_active', true) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="is_active">
|
||||
Active Monitoring
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">Monitor this domain's expiration date</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> <strong>Note:</strong> After adding, we'll automatically check the domain's expiration date using WHOIS lookup.
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> Add Domain
|
||||
</button>
|
||||
<a href="{{ route('domains.index') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,69 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Edit Domain')
|
||||
|
||||
@section('content')
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0"><i class="bi bi-pencil"></i> Edit Domain</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('domains.update', $domain) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="site_id" class="form-label">Associated Site <span class="text-danger">*</span></label>
|
||||
<select class="form-select @error('site_id') is-invalid @enderror"
|
||||
id="site_id" name="site_id" required>
|
||||
<option value="">Select a site...</option>
|
||||
@foreach($sites as $site)
|
||||
<option value="{{ $site->id }}" {{ old('site_id', $domain->site_id) == $site->id ? 'selected' : '' }}>
|
||||
{{ $site->name }} ({{ $site->url }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('site_id')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
<div class="form-text">Select the site this domain belongs to</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="domain" class="form-label">Domain Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control @error('domain') is-invalid @enderror"
|
||||
id="domain" name="domain" value="{{ old('domain', $domain->domain) }}" required
|
||||
placeholder="example.com">
|
||||
@error('domain')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
<div class="form-text">Enter the domain name without protocol (e.g., example.com)</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="is_active"
|
||||
name="is_active" {{ old('is_active', $domain->is_active) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="is_active">
|
||||
Active Monitoring
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">Monitor this domain's expiration date</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> Update Domain
|
||||
</button>
|
||||
<a href="{{ route('domains.index') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,129 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Domain Monitoring')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1><i class="bi bi-globe"></i> Domain Monitoring</h1>
|
||||
<a href="{{ route('domains.create') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Add Domain
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle"></i> {{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($domains->isEmpty())
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> No domains are being monitored yet. Click "Add Domain" to get started!
|
||||
</div>
|
||||
@else
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Site</th>
|
||||
<th>Status</th>
|
||||
<th>Expires At</th>
|
||||
<th>Days Left</th>
|
||||
<th>Last Checked</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($domains as $domain)
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ $domain->domain }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ route('dashboard.show', $domain->site) }}">
|
||||
{{ $domain->site->name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
@if($domain->isExpired())
|
||||
<span class="badge bg-danger">Expired</span>
|
||||
@elseif($domain->isExpiringSoon())
|
||||
<span class="badge bg-warning text-dark">Expiring Soon</span>
|
||||
@elseif($domain->expires_at)
|
||||
<span class="badge bg-success">Active</span>
|
||||
@else
|
||||
<span class="badge bg-secondary">Unknown</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($domain->expires_at)
|
||||
{{ $domain->expires_at->format('Y-m-d') }}
|
||||
@else
|
||||
<span class="text-muted">Not checked</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@php
|
||||
$days = $domain->daysUntilExpiration();
|
||||
@endphp
|
||||
@if($days !== null)
|
||||
@if($days < 0)
|
||||
<span class="text-danger fw-bold">Expired {{ abs($days) }} days ago</span>
|
||||
@elseif($days < 30)
|
||||
<span class="text-warning fw-bold">{{ $days }} days</span>
|
||||
@else
|
||||
<span class="text-success">{{ $days }} days</span>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-muted">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($domain->last_checked_at)
|
||||
<small>{{ $domain->last_checked_at->diffForHumans() }}</small>
|
||||
@else
|
||||
<span class="text-muted">Never</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{{ route('domains.show', $domain) }}" class="btn btn-outline-primary" title="View">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a href="{{ route('domains.edit', $domain) }}" class="btn btn-outline-secondary" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form action="{{ route('domains.check', $domain) }}" method="POST" class="d-inline">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-outline-info" title="Check Now">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ route('domains.destroy', $domain) }}" method="POST" class="d-inline"
|
||||
onsubmit="return confirm('Are you sure you want to delete this domain?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-outline-danger" title="Delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@if($domains->hasPages())
|
||||
<div class="card-footer">
|
||||
{{ $domains->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
@@ -0,0 +1,178 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Domain Details')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1><i class="bi bi-globe"></i> Domain Details</h1>
|
||||
<div>
|
||||
<a href="{{ route('domains.edit', $domain) }}" class="btn btn-secondary">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
<a href="{{ route('domains.index') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to List
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle"></i> {{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Domain Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Domain Name:</div>
|
||||
<div class="col-md-8">{{ $domain->domain }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Associated Site:</div>
|
||||
<div class="col-md-8">
|
||||
<a href="{{ route('dashboard.show', $domain->site) }}">
|
||||
{{ $domain->site->name }}
|
||||
</a>
|
||||
<br>
|
||||
<small class="text-muted">{{ $domain->site->url }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Status:</div>
|
||||
<div class="col-md-8">
|
||||
@if($domain->isExpired())
|
||||
<span class="badge bg-danger fs-6">Expired</span>
|
||||
@elseif($domain->isExpiringSoon())
|
||||
<span class="badge bg-warning text-dark fs-6">Expiring Soon</span>
|
||||
@elseif($domain->expires_at)
|
||||
<span class="badge bg-success fs-6">Active</span>
|
||||
@else
|
||||
<span class="badge bg-secondary fs-6">Unknown</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Monitoring:</div>
|
||||
<div class="col-md-8">
|
||||
@if($domain->is_active)
|
||||
<span class="badge bg-success">Active</span>
|
||||
@else
|
||||
<span class="badge bg-secondary">Inactive</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Expiration Details</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Expires At:</div>
|
||||
<div class="col-md-8">
|
||||
@if($domain->expires_at)
|
||||
{{ $domain->expires_at->format('F j, Y') }}
|
||||
<br>
|
||||
<small class="text-muted">{{ $domain->expires_at->format('g:i A T') }}</small>
|
||||
@else
|
||||
<span class="text-muted">Not yet checked</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Days Until Expiration:</div>
|
||||
<div class="col-md-8">
|
||||
@php
|
||||
$days = $domain->daysUntilExpiration();
|
||||
@endphp
|
||||
@if($days !== null)
|
||||
@if($days < 0)
|
||||
<span class="text-danger fs-4 fw-bold">Expired {{ abs($days) }} days ago</span>
|
||||
@elseif($days < 30)
|
||||
<span class="text-warning fs-4 fw-bold">{{ $days }} days</span>
|
||||
@else
|
||||
<span class="text-success fs-4 fw-bold">{{ $days }} days</span>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-muted">Unknown</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Last Checked:</div>
|
||||
<div class="col-md-8">
|
||||
@if($domain->last_checked_at)
|
||||
{{ $domain->last_checked_at->format('F j, Y g:i A') }}
|
||||
<br>
|
||||
<small class="text-muted">{{ $domain->last_checked_at->diffForHumans() }}</small>
|
||||
@else
|
||||
<span class="text-muted">Never</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Actions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('domains.check', $domain) }}" method="POST" class="mb-3">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-arrow-clockwise"></i> Check Expiry Now
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<a href="{{ route('domains.edit', $domain) }}" class="btn btn-secondary w-100 mb-3">
|
||||
<i class="bi bi-pencil"></i> Edit Domain
|
||||
</a>
|
||||
|
||||
<form action="{{ route('domains.destroy', $domain) }}" method="POST"
|
||||
onsubmit="return confirm('Are you sure you want to delete this domain?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-danger w-100">
|
||||
<i class="bi bi-trash"></i> Delete Domain
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($domain->isExpiringSoon() || $domain->isExpired())
|
||||
<div class="card border-{{ $domain->isExpired() ? 'danger' : 'warning' }}">
|
||||
<div class="card-header bg-{{ $domain->isExpired() ? 'danger' : 'warning' }} text-{{ $domain->isExpired() ? 'white' : 'dark' }}">
|
||||
<h5 class="mb-0"><i class="bi bi-exclamation-triangle"></i> Alert</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if($domain->isExpired())
|
||||
<p class="mb-0">
|
||||
<strong>This domain has expired!</strong> Please renew it immediately to avoid losing ownership.
|
||||
</p>
|
||||
@else
|
||||
<p class="mb-0">
|
||||
<strong>This domain is expiring soon!</strong> Consider renewing it to prevent any interruptions.
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,165 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Health Checks Overview')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h2 mb-2">
|
||||
<i class="bi bi-heart-pulse text-danger"></i>
|
||||
Health Checks Monitoring
|
||||
</h1>
|
||||
<p class="text-muted">Monitor custom health endpoints and API status across all your applications</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Overview -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-primary bg-opacity-10 text-primary rounded p-3">
|
||||
<i class="bi bi-globe fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Total Sites</div>
|
||||
<div class="h4 mb-0">{{ $stats['total_sites'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-danger bg-opacity-10 text-danger rounded p-3">
|
||||
<i class="bi bi-heart-pulse fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Health Checks</div>
|
||||
<div class="h4 mb-0">{{ $stats['total_health_checks'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-success bg-opacity-10 text-success rounded p-3">
|
||||
<i class="bi bi-check-circle fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Active Monitors</div>
|
||||
<div class="h4 mb-0">{{ $stats['sites_with_checks'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sites List -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-list-check me-2"></i>
|
||||
Health Checks by Site
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($sites->isNotEmpty())
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Website</th>
|
||||
<th>Health Endpoints</th>
|
||||
<th>Active Checks</th>
|
||||
<th>Last Check</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($sites as $site)
|
||||
@php
|
||||
$activeChecks = $site->healthChecks->where('is_active', true);
|
||||
@endphp
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold">{{ $site->name }}</div>
|
||||
<div class="small text-muted">{{ $site->url }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-primary fs-6">
|
||||
{{ $site->health_checks_count }} endpoints
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@if($activeChecks->count() > 0)
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-check-circle"></i> {{ $activeChecks->count() }} active
|
||||
</span>
|
||||
@else
|
||||
<span class="badge bg-secondary">
|
||||
<i class="bi bi-dash-circle"></i> None active
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($site->healthChecks->isNotEmpty())
|
||||
<span class="text-muted">
|
||||
{{ $site->healthChecks->first()->updated_at->diffForHumans() }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('health-checks.index', $site) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-eye"></i> View Checks
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-heart-pulse text-muted" style="font-size: 4rem; opacity: 0.3;"></i>
|
||||
<h5 class="mt-3">No Health Checks</h5>
|
||||
<p class="text-muted">Add sites and configure health check endpoints to start monitoring.</p>
|
||||
<a href="{{ route('sites.create') }}" class="btn btn-primary mt-3">
|
||||
<i class="bi bi-plus-circle"></i> Add Site
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Card -->
|
||||
<div class="alert alert-info mt-4">
|
||||
<h6 class="alert-heading">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
About Health Check Monitoring
|
||||
</h6>
|
||||
<p class="mb-0">
|
||||
Health checks monitor custom endpoints in your applications (like /health or /api/status).
|
||||
You can validate JSON responses, check for specific keys, and set performance thresholds.
|
||||
This helps ensure your APIs and services are functioning correctly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -172,6 +172,72 @@
|
||||
Sites
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Monitoring Features Section -->
|
||||
<li class="sidebar-nav-item mt-3">
|
||||
<div class="px-4 py-2">
|
||||
<small class="text-uppercase text-muted fw-semibold" style="font-size: 0.75rem; letter-spacing: 0.5px;">
|
||||
Monitoring Features
|
||||
</small>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="{{ route('uptime.index') }}" class="sidebar-nav-link {{ request()->routeIs('uptime.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-arrow-up-circle"></i>
|
||||
Uptime Monitoring
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="{{ route('ssl.index') }}" class="sidebar-nav-link {{ request()->routeIs('ssl.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-shield-lock"></i>
|
||||
SSL Certificates
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="{{ route('dns-records.overview') }}" class="sidebar-nav-link {{ request()->routeIs('dns-records.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-server"></i>
|
||||
DNS Records
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="{{ route('broken-links.index') }}" class="sidebar-nav-link {{ request()->routeIs('broken-links.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
Broken Links
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="{{ route('domains.index') }}" class="sidebar-nav-link {{ request()->routeIs('domains.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-globe"></i>
|
||||
Domain Expiry
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="{{ route('sitemaps.index') }}" class="sidebar-nav-link {{ request()->routeIs('sitemaps.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-diagram-3"></i>
|
||||
Sitemaps
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="{{ route('health-checks.overview') }}" class="sidebar-nav-link {{ request()->routeIs('health-checks.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-heart-pulse"></i>
|
||||
Health Checks
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="{{ route('tcp-ports.overview') }}" class="sidebar-nav-link {{ request()->routeIs('tcp-ports.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-hdd-network"></i>
|
||||
TCP Ports
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- General Section -->
|
||||
<li class="sidebar-nav-item mt-3">
|
||||
<div class="px-4 py-2">
|
||||
<small class="text-uppercase text-muted fw-semibold" style="font-size: 0.75rem; letter-spacing: 0.5px;">
|
||||
General
|
||||
</small>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="{{ route('alerts.index') }}" class="sidebar-nav-link {{ request()->routeIs('alerts.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-bell"></i>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Add Sitemap')
|
||||
|
||||
@section('content')
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0"><i class="bi bi-plus-circle"></i> Add Sitemap for Monitoring</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('sitemaps.store') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="site_id" class="form-label">Associated Site <span class="text-danger">*</span></label>
|
||||
<select class="form-select @error('site_id') is-invalid @enderror"
|
||||
id="site_id" name="site_id" required>
|
||||
<option value="">Select a site...</option>
|
||||
@foreach($sites as $site)
|
||||
<option value="{{ $site->id }}" {{ old('site_id') == $site->id ? 'selected' : '' }}>
|
||||
{{ $site->name }} ({{ $site->url }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('site_id')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
<div class="form-text">Select the site this sitemap belongs to</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="url" class="form-label">Sitemap URL <span class="text-danger">*</span></label>
|
||||
<input type="url" class="form-control @error('url') is-invalid @enderror"
|
||||
id="url" name="url" value="{{ old('url') }}" required
|
||||
placeholder="https://example.com/sitemap.xml">
|
||||
@error('url')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
<div class="form-text">The full URL to your sitemap file (e.g., https://example.com/sitemap.xml)</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="is_active"
|
||||
name="is_active" {{ old('is_active', true) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="is_active">
|
||||
Active Monitoring
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">Automatically validate this sitemap</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> <strong>Note:</strong> After adding, we'll automatically validate your sitemap to ensure it's accessible and well-formed XML.
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> Add Sitemap
|
||||
</button>
|
||||
<a href="{{ route('sitemaps.index') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,69 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Edit Sitemap')
|
||||
|
||||
@section('content')
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0"><i class="bi bi-pencil"></i> Edit Sitemap</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('sitemaps.update', $sitemap) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="site_id" class="form-label">Associated Site <span class="text-danger">*</span></label>
|
||||
<select class="form-select @error('site_id') is-invalid @enderror"
|
||||
id="site_id" name="site_id" required>
|
||||
<option value="">Select a site...</option>
|
||||
@foreach($sites as $site)
|
||||
<option value="{{ $site->id }}" {{ old('site_id', $sitemap->site_id) == $site->id ? 'selected' : '' }}>
|
||||
{{ $site->name }} ({{ $site->url }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('site_id')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
<div class="form-text">Select the site this sitemap belongs to</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="url" class="form-label">Sitemap URL <span class="text-danger">*</span></label>
|
||||
<input type="url" class="form-control @error('url') is-invalid @enderror"
|
||||
id="url" name="url" value="{{ old('url', $sitemap->url) }}" required
|
||||
placeholder="https://example.com/sitemap.xml">
|
||||
@error('url')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
<div class="form-text">The full URL to your sitemap file</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="is_active"
|
||||
name="is_active" {{ old('is_active', $sitemap->is_active) ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="is_active">
|
||||
Active Monitoring
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">Automatically validate this sitemap</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> Update Sitemap
|
||||
</button>
|
||||
<a href="{{ route('sitemaps.index') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,124 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Sitemap Monitoring')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1><i class="bi bi-diagram-3"></i> Sitemap Monitoring</h1>
|
||||
<a href="{{ route('sitemaps.create') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Add Sitemap
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle"></i> {{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($sitemaps->isEmpty())
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> No sitemaps are being monitored yet. Click "Add Sitemap" to get started!
|
||||
</div>
|
||||
@else
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Site</th>
|
||||
<th>Sitemap URL</th>
|
||||
<th>Status</th>
|
||||
<th>URL Count</th>
|
||||
<th>Last Checked</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($sitemaps as $sitemap)
|
||||
<tr class="{{ $sitemap->hasErrors() ? 'table-danger' : '' }}">
|
||||
<td>
|
||||
<a href="{{ route('dashboard.show', $sitemap->site) }}">
|
||||
{{ $sitemap->site->name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ $sitemap->url }}" target="_blank" rel="noopener" class="text-break">
|
||||
{{ Str::limit($sitemap->url, 60) }}
|
||||
<i class="bi bi-box-arrow-up-right small"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {{ $sitemap->getStatusBadgeClass() }}">
|
||||
{{ $sitemap->getStatusText() }}
|
||||
</span>
|
||||
@if($sitemap->last_error)
|
||||
<i class="bi bi-exclamation-triangle text-danger"
|
||||
data-bs-toggle="tooltip"
|
||||
title="{{ $sitemap->last_error }}"></i>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($sitemap->url_count)
|
||||
<span class="badge bg-info">{{ number_format($sitemap->url_count) }} URLs</span>
|
||||
@else
|
||||
<span class="text-muted">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($sitemap->last_checked_at)
|
||||
<small>{{ $sitemap->last_checked_at->diffForHumans() }}</small>
|
||||
@else
|
||||
<span class="text-muted">Never</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{{ route('sitemaps.show', $sitemap) }}" class="btn btn-outline-primary" title="View">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a href="{{ route('sitemaps.edit', $sitemap) }}" class="btn btn-outline-secondary" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form action="{{ route('sitemaps.check', $sitemap) }}" method="POST" class="d-inline">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-outline-info" title="Validate Now">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ route('sitemaps.destroy', $sitemap) }}" method="POST" class="d-inline"
|
||||
onsubmit="return confirm('Are you sure you want to delete this sitemap?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-outline-danger" title="Delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@if($sitemaps->hasPages())
|
||||
<div class="card-footer">
|
||||
{{ $sitemaps->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Initialize Bootstrap tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||
})
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
@@ -0,0 +1,169 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Sitemap Details')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1><i class="bi bi-diagram-3"></i> Sitemap Details</h1>
|
||||
<div>
|
||||
<a href="{{ route('sitemaps.edit', $sitemap) }}" class="btn btn-secondary">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
<a href="{{ route('sitemaps.index') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to List
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle"></i> {{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Sitemap Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Associated Site:</div>
|
||||
<div class="col-md-8">
|
||||
<a href="{{ route('dashboard.show', $sitemap->site) }}">
|
||||
{{ $sitemap->site->name }}
|
||||
</a>
|
||||
<br>
|
||||
<small class="text-muted">{{ $sitemap->site->url }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Sitemap URL:</div>
|
||||
<div class="col-md-8">
|
||||
<a href="{{ $sitemap->url }}" target="_blank" rel="noopener" class="text-break">
|
||||
{{ $sitemap->url }}
|
||||
<i class="bi bi-box-arrow-up-right small"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Status:</div>
|
||||
<div class="col-md-8">
|
||||
<span class="badge {{ $sitemap->getStatusBadgeClass() }} fs-6">
|
||||
{{ $sitemap->getStatusText() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Monitoring:</div>
|
||||
<div class="col-md-8">
|
||||
@if($sitemap->is_active)
|
||||
<span class="badge bg-success">Active</span>
|
||||
@else
|
||||
<span class="badge bg-secondary">Inactive</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($sitemap->url_count)
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">URLs in Sitemap:</div>
|
||||
<div class="col-md-8">
|
||||
<span class="fs-4 fw-bold text-primary">{{ number_format($sitemap->url_count) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Validation Details</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Last Checked:</div>
|
||||
<div class="col-md-8">
|
||||
@if($sitemap->last_checked_at)
|
||||
{{ $sitemap->last_checked_at->format('F j, Y g:i A') }}
|
||||
<br>
|
||||
<small class="text-muted">{{ $sitemap->last_checked_at->diffForHumans() }}</small>
|
||||
@else
|
||||
<span class="text-muted">Never</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($sitemap->last_error)
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 fw-bold">Last Error:</div>
|
||||
<div class="col-md-8">
|
||||
<div class="alert alert-danger mb-0">
|
||||
<i class="bi bi-exclamation-triangle"></i> {{ $sitemap->last_error }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Actions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('sitemaps.check', $sitemap) }}" method="POST" class="mb-3">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-arrow-clockwise"></i> Validate Now
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<a href="{{ route('sitemaps.edit', $sitemap) }}" class="btn btn-secondary w-100 mb-3">
|
||||
<i class="bi bi-pencil"></i> Edit Sitemap
|
||||
</a>
|
||||
|
||||
<form action="{{ route('sitemaps.destroy', $sitemap) }}" method="POST"
|
||||
onsubmit="return confirm('Are you sure you want to delete this sitemap?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-danger w-100">
|
||||
<i class="bi bi-trash"></i> Delete Sitemap
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($sitemap->hasErrors())
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-exclamation-triangle"></i> Validation Issue</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0">
|
||||
<strong>Your sitemap has validation errors.</strong> Please check the error message above and fix the issues.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@elseif($sitemap->is_valid && $sitemap->last_checked_at)
|
||||
<div class="card border-success">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0"><i class="bi bi-check-circle"></i> Validation Passed</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0">
|
||||
Your sitemap is valid and accessible. It contains {{ number_format($sitemap->url_count ?? 0) }} URL(s).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,257 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'SSL Certificates')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h2 mb-2">
|
||||
<i class="bi bi-shield-lock text-success"></i>
|
||||
SSL Certificates
|
||||
</h1>
|
||||
<p class="text-muted">Monitor SSL certificate expiration and validity across all your sites</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Overview -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-primary bg-opacity-10 text-primary rounded p-3">
|
||||
<i class="bi bi-globe fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Total Sites</div>
|
||||
<div class="h4 mb-0">{{ $stats['total_sites'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-success bg-opacity-10 text-success rounded p-3">
|
||||
<i class="bi bi-shield-check fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Valid Certs</div>
|
||||
<div class="h4 mb-0 text-success">{{ $stats['valid_certs'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-warning bg-opacity-10 text-warning rounded p-3">
|
||||
<i class="bi bi-clock-history fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Expiring Soon</div>
|
||||
<div class="h4 mb-0 text-warning">{{ $stats['expiring_soon'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-danger bg-opacity-10 text-danger rounded p-3">
|
||||
<i class="bi bi-shield-x fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Expired</div>
|
||||
<div class="h4 mb-0 text-danger">{{ $stats['expired'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sites List -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-list-check me-2"></i>
|
||||
SSL Status by Site
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($sites->isNotEmpty())
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Website</th>
|
||||
<th>Issuer</th>
|
||||
<th>Issued Date</th>
|
||||
<th>Expiry Date</th>
|
||||
<th>Days Remaining</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($sites as $site)
|
||||
@php
|
||||
$latestCert = $site->sslCertificates->first();
|
||||
$daysUntilExpiry = $latestCert && $latestCert->expires_at ? now()->diffInDays($latestCert->expires_at, false) : null;
|
||||
@endphp
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold">{{ $site->name }}</div>
|
||||
<div class="small text-muted">{{ $site->url }}</div>
|
||||
</td>
|
||||
<td>
|
||||
@if($latestCert && $latestCert->issuer)
|
||||
<span class="text-muted">{{ Str::limit($latestCert->issuer, 30) }}</span>
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($latestCert && $latestCert->issued_at)
|
||||
{{ $latestCert->issued_at->format('M d, Y') }}
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($latestCert && $latestCert->expires_at)
|
||||
{{ $latestCert->expires_at->format('M d, Y') }}
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($daysUntilExpiry !== null)
|
||||
@if($daysUntilExpiry < 0)
|
||||
<span class="text-danger fw-bold">Expired</span>
|
||||
@elseif($daysUntilExpiry < 30)
|
||||
<span class="text-warning fw-bold">{{ $daysUntilExpiry }} days</span>
|
||||
@else
|
||||
<span class="text-success">{{ $daysUntilExpiry }} days</span>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if(!$latestCert)
|
||||
<span class="badge bg-secondary">
|
||||
<i class="bi bi-dash-circle"></i> Not Checked
|
||||
</span>
|
||||
@elseif($daysUntilExpiry < 0)
|
||||
<span class="badge bg-danger">
|
||||
<i class="bi bi-shield-x"></i> Expired
|
||||
</span>
|
||||
@elseif($daysUntilExpiry < 30)
|
||||
<span class="badge bg-warning text-dark">
|
||||
<i class="bi bi-clock-history"></i> Expiring Soon
|
||||
</span>
|
||||
@else
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-shield-check"></i> Valid
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('ssl.show', $site) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-eye"></i> View Details
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-shield-lock text-muted" style="font-size: 4rem; opacity: 0.3;"></i>
|
||||
<h5 class="mt-3">No SSL Certificates</h5>
|
||||
<p class="text-muted">Add sites to start monitoring their SSL certificates.</p>
|
||||
<a href="{{ route('sites.create') }}" class="btn btn-primary mt-3">
|
||||
<i class="bi bi-plus-circle"></i> Add Site
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Certificates -->
|
||||
@if($recentCertificates->isNotEmpty())
|
||||
<div class="card border-0 shadow-sm mt-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-clock-history me-2"></i>
|
||||
Recent Certificate Checks
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Site</th>
|
||||
<th>Issuer</th>
|
||||
<th>Expires</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($recentCertificates as $cert)
|
||||
@php
|
||||
$daysUntilExpiry = $cert->expires_at ? now()->diffInDays($cert->expires_at, false) : null;
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="text-muted small">{{ $cert->created_at->format('M d, H:i:s') }}</td>
|
||||
<td>{{ $cert->site->name }}</td>
|
||||
<td class="small">{{ Str::limit($cert->issuer ?? 'Unknown', 25) }}</td>
|
||||
<td>
|
||||
@if($cert->expires_at)
|
||||
{{ $cert->expires_at->format('M d, Y') }}
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($daysUntilExpiry === null)
|
||||
<span class="badge bg-secondary">Unknown</span>
|
||||
@elseif($daysUntilExpiry < 0)
|
||||
<span class="badge bg-danger">Expired</span>
|
||||
@elseif($daysUntilExpiry < 30)
|
||||
<span class="badge bg-warning text-dark">{{ $daysUntilExpiry }}d</span>
|
||||
@else
|
||||
<span class="badge bg-success">Valid</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,233 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'SSL Certificate - ' . $site->name)
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-4">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('ssl.index') }}">SSL Certificates</a></li>
|
||||
<li class="breadcrumb-item active">{{ $site->name }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="h2 mb-2">{{ $site->name }}</h1>
|
||||
<p class="text-muted">{{ $site->url }}</p>
|
||||
</div>
|
||||
@if($latestCert)
|
||||
@php
|
||||
$daysUntilExpiry = $latestCert->expires_at ? now()->diffInDays($latestCert->expires_at, false) : null;
|
||||
@endphp
|
||||
<div>
|
||||
@if($daysUntilExpiry === null)
|
||||
<span class="badge bg-secondary fs-6 px-3 py-2">Status Unknown</span>
|
||||
@elseif($daysUntilExpiry < 0)
|
||||
<span class="badge bg-danger fs-6 px-3 py-2">
|
||||
<i class="bi bi-shield-x"></i> Certificate Expired
|
||||
</span>
|
||||
@elseif($daysUntilExpiry < 30)
|
||||
<span class="badge bg-warning text-dark fs-6 px-3 py-2">
|
||||
<i class="bi bi-clock-history"></i> Expires in {{ $daysUntilExpiry }} days
|
||||
</span>
|
||||
@else
|
||||
<span class="badge bg-success fs-6 px-3 py-2">
|
||||
<i class="bi bi-shield-check"></i> Valid Certificate
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($latestCert)
|
||||
<!-- Current Certificate Info -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
Certificate Details
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small mb-1">Issuer</div>
|
||||
<div class="fw-semibold">{{ $latestCert->issuer ?? 'Unknown' }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small mb-1">Subject</div>
|
||||
<div class="fw-semibold">{{ $latestCert->subject ?? 'Unknown' }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="text-muted small mb-1">Issued Date</div>
|
||||
<div class="fw-semibold">
|
||||
@if($latestCert->issued_at)
|
||||
{{ $latestCert->issued_at->format('M d, Y') }}
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-muted small mb-1">Expiry Date</div>
|
||||
<div class="fw-semibold">
|
||||
@if($latestCert->expires_at)
|
||||
{{ $latestCert->expires_at->format('M d, Y') }}
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-calendar-check me-2"></i>
|
||||
Validity Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small mb-1">Days Remaining</div>
|
||||
<div class="h3 mb-0">
|
||||
@if($daysUntilExpiry !== null)
|
||||
@if($daysUntilExpiry < 0)
|
||||
<span class="text-danger">Expired {{ abs($daysUntilExpiry) }} days ago</span>
|
||||
@else
|
||||
<span class="text-{{ $daysUntilExpiry < 30 ? 'warning' : 'success' }}">{{ $daysUntilExpiry }} days</span>
|
||||
@endif
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small mb-1">Validity Period</div>
|
||||
<div class="fw-semibold">
|
||||
@if($latestCert->issued_at && $latestCert->expires_at)
|
||||
{{ $latestCert->issued_at->diffInDays($latestCert->expires_at) }} days
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small mb-1">Last Checked</div>
|
||||
<div class="fw-semibold">
|
||||
{{ $latestCert->created_at->diffForHumans() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
No SSL certificate information available for this site yet.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Certificate History -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-clock-history me-2"></i>
|
||||
Certificate History
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Checked At</th>
|
||||
<th>Issuer</th>
|
||||
<th>Issued Date</th>
|
||||
<th>Expiry Date</th>
|
||||
<th>Days Until Expiry</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($sslCertificates as $cert)
|
||||
@php
|
||||
$daysUntilExpiry = $cert->expires_at ? now()->diffInDays($cert->expires_at, false) : null;
|
||||
@endphp
|
||||
<tr>
|
||||
<td>{{ $cert->created_at->format('M d, Y H:i:s') }}</td>
|
||||
<td>{{ Str::limit($cert->issuer ?? 'Unknown', 40) }}</td>
|
||||
<td>
|
||||
@if($cert->issued_at)
|
||||
{{ $cert->issued_at->format('M d, Y') }}
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($cert->expires_at)
|
||||
{{ $cert->expires_at->format('M d, Y') }}
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($daysUntilExpiry !== null)
|
||||
@if($daysUntilExpiry < 0)
|
||||
<span class="text-danger">{{ $daysUntilExpiry }} days</span>
|
||||
@elseif($daysUntilExpiry < 30)
|
||||
<span class="text-warning">{{ $daysUntilExpiry }} days</span>
|
||||
@else
|
||||
<span class="text-success">{{ $daysUntilExpiry }} days</span>
|
||||
@endif
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($daysUntilExpiry === null)
|
||||
<span class="badge bg-secondary">Unknown</span>
|
||||
@elseif($daysUntilExpiry < 0)
|
||||
<span class="badge bg-danger">
|
||||
<i class="bi bi-shield-x"></i> Expired
|
||||
</span>
|
||||
@elseif($daysUntilExpiry < 30)
|
||||
<span class="badge bg-warning text-dark">
|
||||
<i class="bi bi-clock-history"></i> Expiring
|
||||
</span>
|
||||
@else
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-shield-check"></i> Valid
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-4 text-muted">
|
||||
No SSL certificate checks recorded yet.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@if($sslCertificates->hasPages())
|
||||
<div class="card-footer bg-white">
|
||||
{{ $sslCertificates->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,187 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'TCP Ports Overview')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h2 mb-2">
|
||||
<i class="bi bi-hdd-network text-dark"></i>
|
||||
TCP Port Monitoring
|
||||
</h1>
|
||||
<p class="text-muted">Monitor TCP port availability and connectivity across all your servers</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Overview -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-primary bg-opacity-10 text-primary rounded p-3">
|
||||
<i class="bi bi-globe fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Total Sites</div>
|
||||
<div class="h4 mb-0">{{ $stats['total_sites'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-dark bg-opacity-10 text-dark rounded p-3">
|
||||
<i class="bi bi-hdd-network fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Monitored Ports</div>
|
||||
<div class="h4 mb-0">{{ $stats['total_ports_monitored'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-success bg-opacity-10 text-success rounded p-3">
|
||||
<i class="bi bi-check-circle fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Sites with Ports</div>
|
||||
<div class="h4 mb-0">{{ $stats['sites_with_ports'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sites List -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-list-check me-2"></i>
|
||||
TCP Ports by Site
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($sites->isNotEmpty())
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Website</th>
|
||||
<th>Monitored Ports</th>
|
||||
<th>Active Ports</th>
|
||||
<th>Common Ports</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($sites as $site)
|
||||
@php
|
||||
$activePorts = $site->tcpPorts->where('is_active', true);
|
||||
@endphp
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold">{{ $site->name }}</div>
|
||||
<div class="small text-muted">{{ $site->url }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-primary fs-6">
|
||||
{{ $site->tcp_ports_count }} ports
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@if($activePorts->count() > 0)
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-check-circle"></i> {{ $activePorts->count() }} active
|
||||
</span>
|
||||
@else
|
||||
<span class="badge bg-secondary">
|
||||
<i class="bi bi-dash-circle"></i> None active
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($site->tcpPorts->isNotEmpty())
|
||||
@foreach($site->tcpPorts->take(5) as $port)
|
||||
<span class="badge bg-dark me-1">{{ $port->port }}</span>
|
||||
@endforeach
|
||||
@if($site->tcpPorts->count() > 5)
|
||||
<span class="badge bg-light text-dark">+{{ $site->tcpPorts->count() - 5 }}</span>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('tcp-ports.index', $site) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-eye"></i> View Ports
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-hdd-network text-muted" style="font-size: 4rem; opacity: 0.3;"></i>
|
||||
<h5 class="mt-3">No TCP Ports Monitored</h5>
|
||||
<p class="text-muted">Add sites and configure TCP ports to start monitoring connectivity.</p>
|
||||
<a href="{{ route('sites.create') }}" class="btn btn-primary mt-3">
|
||||
<i class="bi bi-plus-circle"></i> Add Site
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Common Ports Reference -->
|
||||
<div class="card border-0 shadow-sm mt-4">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
Common TCP Ports Reference
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li class="mb-2"><span class="badge bg-dark">80</span> <span class="ms-2">HTTP</span></li>
|
||||
<li class="mb-2"><span class="badge bg-dark">443</span> <span class="ms-2">HTTPS</span></li>
|
||||
<li class="mb-2"><span class="badge bg-dark">22</span> <span class="ms-2">SSH</span></li>
|
||||
<li class="mb-2"><span class="badge bg-dark">21</span> <span class="ms-2">FTP</span></li>
|
||||
<li class="mb-2"><span class="badge bg-dark">25</span> <span class="ms-2">SMTP</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li class="mb-2"><span class="badge bg-dark">3306</span> <span class="ms-2">MySQL</span></li>
|
||||
<li class="mb-2"><span class="badge bg-dark">5432</span> <span class="ms-2">PostgreSQL</span></li>
|
||||
<li class="mb-2"><span class="badge bg-dark">6379</span> <span class="ms-2">Redis</span></li>
|
||||
<li class="mb-2"><span class="badge bg-dark">27017</span> <span class="ms-2">MongoDB</span></li>
|
||||
<li class="mb-2"><span class="badge bg-dark">3000</span> <span class="ms-2">Node.js (common)</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,232 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Uptime Monitoring')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h2 mb-2">
|
||||
<i class="bi bi-arrow-up-circle text-primary"></i>
|
||||
Uptime Monitoring
|
||||
</h1>
|
||||
<p class="text-muted">Monitor website availability and response times across all your sites</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Overview -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-primary bg-opacity-10 text-primary rounded p-3">
|
||||
<i class="bi bi-globe fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Total Sites</div>
|
||||
<div class="h4 mb-0">{{ $stats['total_sites'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-success bg-opacity-10 text-success rounded p-3">
|
||||
<i class="bi bi-check-circle-fill fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Sites Up</div>
|
||||
<div class="h4 mb-0 text-success">{{ $stats['sites_up'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-danger bg-opacity-10 text-danger rounded p-3">
|
||||
<i class="bi bi-x-circle-fill fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Sites Down</div>
|
||||
<div class="h4 mb-0 text-danger">{{ $stats['sites_down'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-info bg-opacity-10 text-info rounded p-3">
|
||||
<i class="bi bi-speedometer2 fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="text-muted small">Avg Response</div>
|
||||
<div class="h4 mb-0">{{ $stats['avg_response_time'] }}ms</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sites List -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-list-check me-2"></i>
|
||||
All Sites
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($sites->isNotEmpty())
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Website</th>
|
||||
<th>Status</th>
|
||||
<th>Last Check</th>
|
||||
<th>Response Time</th>
|
||||
<th>Uptime (30d)</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($sites as $site)
|
||||
@php
|
||||
$latestCheck = $site->uptimeChecks->first();
|
||||
$isOnline = $latestCheck && $latestCheck->is_online;
|
||||
@endphp
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold">{{ $site->name }}</div>
|
||||
<div class="small text-muted">{{ $site->url }}</div>
|
||||
</td>
|
||||
<td>
|
||||
@if(!$latestCheck)
|
||||
<span class="badge bg-secondary">
|
||||
<i class="bi bi-dash-circle"></i> Not Checked
|
||||
</span>
|
||||
@elseif($isOnline)
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-check-circle-fill"></i> Online
|
||||
</span>
|
||||
@else
|
||||
<span class="badge bg-danger">
|
||||
<i class="bi bi-x-circle-fill"></i> Down
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($latestCheck)
|
||||
<span class="text-muted">{{ $latestCheck->created_at->diffForHumans() }}</span>
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($latestCheck && $latestCheck->response_time)
|
||||
<span class="fw-semibold">{{ round($latestCheck->response_time) }}ms</span>
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="progress" style="width: 100px; height: 8px;">
|
||||
<div class="progress-bar bg-success"
|
||||
style="width: 99.5%"></div>
|
||||
</div>
|
||||
<span class="small fw-semibold">99.5%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('uptime.show', $site) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-eye"></i> View Details
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-arrow-up-circle text-muted" style="font-size: 4rem; opacity: 0.3;"></i>
|
||||
<h5 class="mt-3">No Sites Being Monitored</h5>
|
||||
<p class="text-muted">Add sites to start monitoring their uptime.</p>
|
||||
<a href="{{ route('sites.create') }}" class="btn btn-primary mt-3">
|
||||
<i class="bi bi-plus-circle"></i> Add Site
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Checks -->
|
||||
@if($recentChecks->isNotEmpty())
|
||||
<div class="card border-0 shadow-sm mt-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-clock-history me-2"></i>
|
||||
Recent Checks
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Site</th>
|
||||
<th>Status</th>
|
||||
<th>Response Time</th>
|
||||
<th>Status Code</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($recentChecks as $check)
|
||||
<tr>
|
||||
<td class="text-muted small">{{ $check->created_at->format('M d, H:i:s') }}</td>
|
||||
<td>{{ $check->site->name }}</td>
|
||||
<td>
|
||||
@if($check->is_online)
|
||||
<span class="badge bg-success">Online</span>
|
||||
@else
|
||||
<span class="badge bg-danger">Down</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>{{ round($check->response_time ?? 0) }}ms</td>
|
||||
<td>
|
||||
<span class="badge bg-{{ $check->status_code >= 200 && $check->status_code < 300 ? 'success' : 'warning' }}">
|
||||
{{ $check->status_code }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,181 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Uptime Details - ' . $site->name)
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-4">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('uptime.index') }}">Uptime Monitoring</a></li>
|
||||
<li class="breadcrumb-item active">{{ $site->name }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="h2 mb-2">{{ $site->name }}</h1>
|
||||
<p class="text-muted">{{ $site->url }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge bg-success fs-6 px-3 py-2">
|
||||
<i class="bi bi-graph-up-arrow"></i> {{ $uptimePercentage }}% Uptime
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-percent text-success fs-1 mb-3"></i>
|
||||
<h3 class="mb-1">{{ $uptimePercentage }}%</h3>
|
||||
<p class="text-muted mb-0">Uptime (30 days)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-speedometer2 text-primary fs-1 mb-3"></i>
|
||||
<h3 class="mb-1">{{ round($avgResponseTime ?? 0) }}ms</h3>
|
||||
<p class="text-muted mb-0">Avg Response Time</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-clock-history text-info fs-1 mb-3"></i>
|
||||
<h3 class="mb-1">{{ $uptimeChecks->count() }}</h3>
|
||||
<p class="text-muted mb-0">Total Checks</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Uptime Chart -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-graph-up me-2"></i>
|
||||
Response Time Trend
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="responseTimeChart" height="100"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Check History -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-list-ul me-2"></i>
|
||||
Check History
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Date & Time</th>
|
||||
<th>Status</th>
|
||||
<th>Status Code</th>
|
||||
<th>Response Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($uptimeChecks as $check)
|
||||
<tr>
|
||||
<td>{{ $check->created_at->format('M d, Y H:i:s') }}</td>
|
||||
<td>
|
||||
@if($check->is_online)
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-check-circle"></i> Online
|
||||
</span>
|
||||
@else
|
||||
<span class="badge bg-danger">
|
||||
<i class="bi bi-x-circle"></i> Down
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-{{ $check->status_code >= 200 && $check->status_code < 300 ? 'success' : ($check->status_code >= 300 && $check->status_code < 400 ? 'info' : 'danger') }}">
|
||||
{{ $check->status_code }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ round($check->response_time ?? 0) }}ms</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4 text-muted">
|
||||
No uptime checks recorded yet.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@if($uptimeChecks->hasPages())
|
||||
<div class="card-footer bg-white">
|
||||
{{ $uptimeChecks->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const ctx = document.getElementById('responseTimeChart');
|
||||
if (!ctx) return;
|
||||
|
||||
const isDarkMode = localStorage.getItem('darkMode') === 'true';
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: @json($uptimeChecks->pluck('created_at')->map(fn($date) => $date->format('M d H:i'))->reverse()),
|
||||
datasets: [{
|
||||
label: 'Response Time (ms)',
|
||||
data: @json($uptimeChecks->pluck('response_time')->reverse()),
|
||||
borderColor: '#667eea',
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.06)'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user