some progress

This commit is contained in:
Doar Mihai
2026-01-24 12:16:10 +02:00
parent 3665a1d5a3
commit 612cc097f8
51 changed files with 8089 additions and 470 deletions
@@ -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
+159
View File
@@ -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
View File
@@ -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
+72
View File
@@ -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
+69
View File
@@ -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
+129
View File
@@ -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
+178
View File
@@ -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
+66
View File
@@ -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>
+72
View File
@@ -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
+69
View File
@@ -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
+124
View File
@@ -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
+169
View File
@@ -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
+257
View File
@@ -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
+233
View File
@@ -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
+232
View File
@@ -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
+181
View File
@@ -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