Add site status page

This commit is contained in:
DariusIII
2026-04-01 12:54:16 +02:00
parent 5f21279db2
commit f851fec201
34 changed files with 2218 additions and 2 deletions
+80
View File
@@ -152,6 +152,86 @@
</div>
</div>
<!-- Site Status & Active Incidents -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center justify-between">
<span>
<i class="fas fa-signal mr-2 text-blue-600 dark:text-blue-400"></i>
Site Status
</span>
<a href="{{ route('admin.status.index') }}" class="text-xs text-blue-600 dark:text-blue-400 hover:underline font-normal">Manage &rarr;</a>
</h3>
@php
$dashBadge = static function (string $status): string {
$base = 'px-2 py-0.5 inline-flex text-xs leading-5 font-semibold rounded-full';
return $base.' '.match ($status) {
'operational' => 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
'degraded' => 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200',
'maintenance' => 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
'partial_outage' => 'bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200',
'major_outage' => 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200',
default => 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200',
};
};
@endphp
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-4">
@foreach($serviceStatuses as $svc)
<div class="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-900 border border-gray-100 dark:border-gray-700">
<div>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ $svc->name }}</span>
<span class="block text-xs text-gray-500 dark:text-gray-400">{{ number_format((float) $svc->uptime_percentage, 2) }}% uptime</span>
</div>
<span class="{{ $dashBadge($svc->status->value) }}">{{ $svc->status->label() }}</span>
</div>
@endforeach
</div>
@if($activeIncidents->isNotEmpty())
<div class="border-t border-gray-200 dark:border-gray-700 pt-3">
<h4 class="text-sm font-semibold text-red-600 dark:text-red-400 mb-2">
<i class="fas fa-exclamation-triangle mr-1"></i>
{{ $activeIncidents->count() }} Active {{ Str::plural('Incident', $activeIncidents->count()) }}
</h4>
<div class="space-y-2">
@foreach($activeIncidents->take(5) as $incident)
<div class="flex items-start justify-between gap-2 text-sm py-1">
<div class="min-w-0">
<span class="text-gray-800 dark:text-gray-200 truncate block">{{ Str::limit($incident->title, 50) }}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">
{{ $incident->services->pluck('name')->join(', ') }}
&middot; {{ $incident->started_at->diffForHumans() }}
@if($incident->is_auto)
<span class="ml-1 px-1 py-0.5 text-[10px] leading-3 font-semibold rounded bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200">Auto</span>
@endif
</span>
</div>
<span class="px-2 py-0.5 inline-flex text-xs leading-5 font-semibold rounded-full shrink-0 {{ match($incident->impact->value) {
'critical' => 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200',
'major' => 'bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200',
'minor' => 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200',
default => 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200',
} }}">{{ $incident->impact->label() }}</span>
</div>
@endforeach
@if($activeIncidents->count() > 5)
<p class="text-xs text-gray-500 dark:text-gray-400 pt-1">
+ {{ $activeIncidents->count() - 5 }} more &mdash;
<a href="{{ route('admin.status.index') }}" class="text-blue-600 dark:text-blue-400 hover:underline">view all</a>
</p>
@endif
</div>
</div>
@else
<div class="border-t border-gray-200 dark:border-gray-700 pt-3">
<p class="text-sm text-green-600 dark:text-green-400">
<i class="fas fa-check-circle mr-1"></i>No active incidents
</p>
</div>
@endif
</div>
<!-- Registration Status Widget -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
@@ -0,0 +1,110 @@
@extends('layouts.admin')
@section('content')
@php
use App\Enums\IncidentImpactEnum;
use App\Enums\IncidentStatusEnum;
$oldServiceIds = array_map(
static fn (mixed $id): int => (int) $id,
(array) old('service_status_ids', [])
);
@endphp
<div class="space-y-6">
<x-admin.card>
<x-admin.page-header :title="$title" icon="fas fa-plus" />
<div class="px-6 py-6">
<form action="{{ route('admin.status.store') }}" method="post" class="space-y-6 max-w-2xl">
@csrf
<div>
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Title</label>
<input type="text" name="title" id="title" value="{{ old('title') }}" required
class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-primary-500 focus:ring-primary-500">
@error('title')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
<textarea name="description" id="description" rows="5" required
class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-primary-500 focus:ring-primary-500">{{ old('description') }}</textarea>
@error('description')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<fieldset>
<legend class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Affected services</legend>
<p class="text-xs text-gray-600 dark:text-gray-400 mb-2">Select one or more areas impacted by this incident.</p>
<div class="space-y-2 rounded-lg border border-gray-300 dark:border-gray-600 p-3 dark:bg-gray-900/40">
@foreach($services as $svc)
<label class="flex items-center gap-3 text-sm text-gray-800 dark:text-gray-200 cursor-pointer">
<input type="checkbox" name="service_status_ids[]" value="{{ $svc->id }}"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800"
@checked(in_array($svc->id, $oldServiceIds, true))>
<span>{{ $svc->name }}</span>
</label>
@endforeach
</div>
@error('service_status_ids')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</fieldset>
<div>
<label for="impact" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Impact (severity)</label>
<select name="impact" id="impact" required class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm">
@foreach(IncidentImpactEnum::cases() as $opt)
<option value="{{ $opt->value }}" @selected(old('impact', IncidentImpactEnum::Minor->value) === $opt->value)>{{ $opt->label() }}</option>
@endforeach
</select>
@error('impact')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<div>
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Incident status</label>
<select name="status" id="status" required class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm">
@foreach(IncidentStatusEnum::cases() as $opt)
<option value="{{ $opt->value }}" @selected(old('status', IncidentStatusEnum::Investigating->value) === $opt->value)>{{ $opt->label() }}</option>
@endforeach
</select>
@error('status')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<div>
<label for="started_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Started at</label>
<input type="datetime-local" name="started_at" id="started_at" required
value="{{ old('started_at', now()->format('Y-m-d\TH:i')) }}"
class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm">
@error('started_at')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<div>
<label for="resolved_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Resolved at (optional)</label>
<input type="datetime-local" name="resolved_at" id="resolved_at"
value="{{ old('resolved_at') }}"
class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm">
@error('resolved_at')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<div class="flex gap-3">
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Create</button>
<a href="{{ route('admin.status.index') }}" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-100 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600">Cancel</a>
</div>
</form>
</div>
</x-admin.card>
</div>
@endsection
+111
View File
@@ -0,0 +1,111 @@
@extends('layouts.admin')
@section('content')
@php
use App\Enums\IncidentImpactEnum;
use App\Enums\IncidentStatusEnum;
$selectedIds = array_map(
static fn (mixed $id): int => (int) $id,
(array) old('service_status_ids', $incident->services->pluck('id')->all())
);
@endphp
<div class="space-y-6">
<x-admin.card>
<x-admin.page-header :title="$title" icon="fas fa-edit" />
<div class="px-6 py-6">
<form action="{{ route('admin.status.update', $incident) }}" method="post" class="space-y-6 max-w-2xl">
@csrf
@method('PUT')
<div>
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Title</label>
<input type="text" name="title" id="title" value="{{ old('title', $incident->title) }}" required
class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-primary-500 focus:ring-primary-500">
@error('title')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
<textarea name="description" id="description" rows="5" required
class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-primary-500 focus:ring-primary-500">{{ old('description', $incident->description) }}</textarea>
@error('description')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<fieldset>
<legend class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Affected services</legend>
<p class="text-xs text-gray-600 dark:text-gray-400 mb-2">Select one or more areas impacted by this incident.</p>
<div class="space-y-2 rounded-lg border border-gray-300 dark:border-gray-600 p-3 dark:bg-gray-900/40">
@foreach($services as $svc)
<label class="flex items-center gap-3 text-sm text-gray-800 dark:text-gray-200 cursor-pointer">
<input type="checkbox" name="service_status_ids[]" value="{{ $svc->id }}"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800"
@checked(in_array($svc->id, $selectedIds, true))>
<span>{{ $svc->name }}</span>
</label>
@endforeach
</div>
@error('service_status_ids')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</fieldset>
<div>
<label for="impact" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Impact (severity)</label>
<select name="impact" id="impact" required class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm">
@foreach(IncidentImpactEnum::cases() as $opt)
<option value="{{ $opt->value }}" @selected(old('impact', $incident->impact->value) === $opt->value)>{{ $opt->label() }}</option>
@endforeach
</select>
@error('impact')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<div>
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Incident status</label>
<select name="status" id="status" required class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm">
@foreach(IncidentStatusEnum::cases() as $opt)
<option value="{{ $opt->value }}" @selected(old('status', $incident->status->value) === $opt->value)>{{ $opt->label() }}</option>
@endforeach
</select>
@error('status')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<div>
<label for="started_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Started at</label>
<input type="datetime-local" name="started_at" id="started_at" required
value="{{ old('started_at', $incident->started_at->format('Y-m-d\TH:i')) }}"
class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm">
@error('started_at')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<div>
<label for="resolved_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Resolved at</label>
<input type="datetime-local" name="resolved_at" id="resolved_at"
value="{{ old('resolved_at', $incident->resolved_at?->format('Y-m-d\TH:i')) }}"
class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm">
@error('resolved_at')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<div class="flex flex-wrap gap-3">
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Save</button>
<a href="{{ route('admin.status.index') }}" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-100 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600">Cancel</a>
</div>
</form>
</div>
</x-admin.card>
</div>
@endsection
@@ -0,0 +1,145 @@
@extends('layouts.admin')
@section('content')
@php
use App\Enums\ServiceStatusEnum;
use App\Enums\IncidentImpactEnum;
use App\Enums\IncidentStatusEnum;
$serviceBadge = static function (ServiceStatusEnum $s): string {
$base = 'px-2 py-0.5 inline-flex text-xs leading-5 font-semibold rounded-full';
return $base.' '.match ($s) {
ServiceStatusEnum::Operational => 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
ServiceStatusEnum::Degraded => 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200',
ServiceStatusEnum::Maintenance => 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
ServiceStatusEnum::PartialOutage => 'bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200',
ServiceStatusEnum::MajorOutage => 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200',
};
};
$impactBadge = static function (IncidentImpactEnum $i): string {
$base = 'px-2 py-0.5 inline-flex text-xs leading-5 font-semibold rounded-full';
return $base.' '.match ($i) {
IncidentImpactEnum::None => 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200',
IncidentImpactEnum::Minor => 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200',
IncidentImpactEnum::Major => 'bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200',
IncidentImpactEnum::Critical => 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200',
};
};
$incidentStatusBadge = static function (IncidentStatusEnum $st): string {
$base = 'px-2 py-0.5 inline-flex text-xs leading-5 font-semibold rounded-full';
return $base.' '.match ($st) {
IncidentStatusEnum::Investigating => 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200',
IncidentStatusEnum::Identified => 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
IncidentStatusEnum::Monitoring => 'bg-cyan-100 dark:bg-cyan-900 text-cyan-800 dark:text-cyan-200',
IncidentStatusEnum::Resolved => 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
};
};
@endphp
<div class="space-y-6">
<x-admin.card>
<x-admin.page-header :title="$title" icon="fas fa-signal">
<x-slot:actions>
<a href="{{ route('admin.status.create') }}" class="px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-lg hover:bg-blue-700 dark:hover:bg-blue-800">
<i class="fas fa-plus mr-2"></i>Create incident
</a>
<a href="{{ route('status') }}" target="_blank" rel="noopener" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-100 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600">
<i class="fas fa-external-link-alt mr-2"></i>Public page
</a>
</x-slot:actions>
</x-admin.page-header>
<div class="px-6 py-6 border-t border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Service health</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
@foreach($services as $svc)
<div class="p-4 rounded-lg border border-gray-200 bg-gray-50 text-gray-900 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100">
<div class="flex items-start justify-between gap-2 mb-3">
<div>
<div class="font-semibold text-gray-900 dark:text-gray-100">{{ $svc->name }}</div>
<div class="text-xs text-gray-600 dark:text-gray-400 mt-1">Slug: {{ $svc->slug }}</div>
</div>
<span class="{{ $serviceBadge($svc->status) }} shrink-0">{{ $svc->status->label() }}</span>
</div>
<form action="{{ route('admin.status.update-service', $svc) }}" method="post" class="space-y-2">
@csrf
<label for="status-{{ $svc->id }}" class="sr-only">Update status</label>
<select id="status-{{ $svc->id }}" name="status" class="w-full rounded-md border border-gray-300 bg-white text-gray-900 text-sm dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500">
@foreach(ServiceStatusEnum::cases() as $opt)
<option value="{{ $opt->value }}" @selected($svc->status === $opt)>{{ $opt->label() }}</option>
@endforeach
</select>
<button type="submit" class="w-full py-2 px-3 text-sm font-medium rounded-lg bg-primary-600 text-white hover:bg-primary-700 dark:hover:bg-primary-600">
Update
</button>
</form>
</div>
@endforeach
</div>
</div>
</x-admin.card>
<x-admin.card>
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Incidents</h2>
</div>
<div class="overflow-x-auto">
<x-admin.data-table>
<x-slot:head>
<x-admin.th>ID</x-admin.th>
<x-admin.th>Title</x-admin.th>
<x-admin.th>Services</x-admin.th>
<x-admin.th>Impact</x-admin.th>
<x-admin.th>Status</x-admin.th>
<x-admin.th>Started</x-admin.th>
<x-admin.th class="text-right">Actions</x-admin.th>
</x-slot:head>
@forelse($incidents as $incident)
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{{ $incident->id }}</td>
<td class="px-6 py-4 text-sm text-gray-900 dark:text-gray-100">
{{ Str::limit($incident->title, 48) }}
@if($incident->is_auto)
<span class="ml-1 px-1.5 py-0.5 inline-flex text-[10px] leading-4 font-semibold rounded bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200">Auto</span>
@endif
</td>
<td class="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">{{ $incident->services->pluck('name')->join(', ') ?: '—' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<span class="{{ $impactBadge($incident->impact) }}">{{ $incident->impact->label() }}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<span class="{{ $incidentStatusBadge($incident->status) }}">{{ $incident->status->label() }}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">{{ $incident->started_at->format('Y-m-d H:i') }}</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm space-x-2">
<a href="{{ route('admin.status.edit', $incident) }}" class="text-blue-600 dark:text-blue-400 hover:underline">Edit</a>
@if($incident->status !== IncidentStatusEnum::Resolved)
<form action="{{ route('admin.status.resolve', $incident) }}" method="post" class="inline">
@csrf
<button type="submit" class="text-emerald-600 dark:text-emerald-400 hover:underline">Resolve</button>
</form>
@endif
<form action="{{ route('admin.status.destroy', $incident) }}" method="post" class="inline" onsubmit="return confirm('Delete this incident?');">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 dark:text-red-400 hover:underline">Delete</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400">No incidents yet.</td>
</tr>
@endforelse
</x-admin.data-table>
</div>
@if($incidents->hasPages())
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
{{ $incidents->links() }}
</div>
@endif
</x-admin.card>
</div>
@endsection
@@ -131,6 +131,58 @@
display: block;
margin: 10px 0;
}
.alert-box {
padding: 16px 20px;
margin: 20px 0;
border-radius: 6px;
font-size: 15px;
}
.alert-danger {
background-color: #fef2f2;
border-left: 4px solid #dc2626;
color: #991b1b;
}
.alert-warning {
background-color: #fff3cd;
border-left: 4px solid #f59e0b;
color: #92400e;
}
.alert-info {
background-color: #eff6ff;
border-left: 4px solid #3b82f6;
color: #1e40af;
}
.alert-success {
background-color: #f0fdf4;
border-left: 4px solid #16a34a;
color: #166534;
}
.status-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.status-table td {
padding: 10px 12px;
border-bottom: 1px solid #eaeaea;
font-size: 14px;
color: #51545e;
vertical-align: top;
}
.status-table .status-label {
font-weight: 600;
color: #333333;
width: 140px;
white-space: nowrap;
}
</style>
</head>
<body>
@@ -0,0 +1,67 @@
@extends('emails.email_layout')
@section('title')
{{ $resolved ? 'Incident Resolved' : 'Incident Detected' }} {{ $services }}
@endsection
@section('site_name', $site)
@section('content')
@if($resolved)
<div class="alert-box alert-success">
<strong> Resolved</strong> The following incident has been automatically resolved.
</div>
@else
<div class="alert-box alert-{{ $incident->impact->value === 'critical' ? 'danger' : ($incident->impact->value === 'major' ? 'warning' : 'info') }}">
<strong>⚠️ {{ ucfirst($incident->impact->value) }} Impact</strong> An automated health check has detected a service issue.
</div>
@endif
<table class="status-table">
<tr>
<td class="status-label">Incident</td>
<td>{{ $incident->title }}</td>
</tr>
<tr>
<td class="status-label">Affected Services</td>
<td>{{ $services }}</td>
</tr>
<tr>
<td class="status-label">Impact</td>
<td>{{ ucfirst($incident->impact->value) }}</td>
</tr>
<tr>
<td class="status-label">Status</td>
<td>{{ ucfirst($incident->status->value) }}</td>
</tr>
<tr>
<td class="status-label">Started</td>
<td>{{ $incident->started_at->format('M j, Y g:i A T') }}</td>
</tr>
@if($resolved && $incident->resolved_at)
<tr>
<td class="status-label">Resolved</td>
<td>{{ $incident->resolved_at->format('M j, Y g:i A T') }}</td>
</tr>
<tr>
<td class="status-label">Duration</td>
<td>{{ $incident->started_at->diffForHumans($incident->resolved_at, true) }}</td>
</tr>
@endif
</table>
@if($incident->description)
<div class="info-box">
<strong>Details:</strong><br>
{!! nl2br(e($incident->description)) !!}
</div>
@endif
<p style="text-align: center; margin-top: 24px;">
<a href="{{ $statusUrl }}" class="button">View Status Dashboard</a>
</p>
<div class="signature">
<p> {{ $site }} Monitoring</p>
</div>
@endsection
+1
View File
@@ -24,6 +24,7 @@
<!-- Styles -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
@stack('meta')
@stack('styles')
</head>
<body class="app-shell bg-gray-100 dark:bg-gray-900 font-sans antialiased">
+1
View File
@@ -29,6 +29,7 @@
<!-- Styles -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
@stack('meta')
@stack('styles')
</head>
<body class="app-shell bg-gray-50 dark:bg-gray-900 font-sans antialiased">
@@ -311,6 +311,9 @@
<a href="{{ url('/admin/site-stats') }}" class="block py-2 px-3 text-gray-400 dark:text-gray-500 hover:text-white dark:hover:text-white hover:bg-white/10 dark:hover:bg-white/5 rounded transition">
<i class="fas fa-chart-bar mr-2 text-purple-400"></i>Statistics
</a>
<a href="{{ route('admin.status.index') }}" class="block py-2 px-3 text-gray-400 dark:text-gray-500 hover:text-white dark:hover:text-white hover:bg-white/10 dark:hover:bg-white/5 rounded transition">
<i class="fas fa-signal mr-2 text-emerald-400"></i>Site Status
</a>
<a href="{{ route('admin.logs.index') }}" class="block py-2 px-3 text-gray-400 dark:text-gray-500 hover:text-white dark:hover:text-white hover:bg-white/10 dark:hover:bg-white/5 rounded transition">
<i class="fas fa-file-lines mr-2 text-amber-400"></i>Logs
</a>
@@ -104,6 +104,10 @@
<i class="fa fa-hands-helping fa-fw mr-2"></i>
<span>API V2</span>
</a>
<a href="{{ route('status') }}" class="flex items-center px-4 py-2 text-gray-300 hover:text-white hover:bg-white/10 rounded-lg transition">
<i class="fa fa-signal fa-fw mr-2"></i>
<span>Site status</span>
</a>
<a href="{{ route('contact-us') }}" class="flex items-center px-4 py-2 text-gray-300 hover:text-white hover:bg-white/10 rounded-lg transition">
<i class="fa fa-envelope fa-fw mr-2"></i>
<span>Contact Us</span>
+160
View File
@@ -0,0 +1,160 @@
@extends('layouts.main')
@section('content')
@php
use App\Enums\ServiceStatusEnum;
use App\Enums\IncidentImpactEnum;
use App\Enums\IncidentStatusEnum;
$overallBanner = match ($overallStatus) {
ServiceStatusEnum::Operational => 'border-green-500/50 bg-green-600 text-white dark:bg-green-900 dark:text-green-200',
ServiceStatusEnum::Degraded => 'border-yellow-500/50 bg-yellow-500 text-yellow-950 dark:bg-yellow-900 dark:text-yellow-200',
ServiceStatusEnum::Maintenance => 'border-blue-500/50 bg-blue-600 text-white dark:bg-blue-900 dark:text-blue-200',
ServiceStatusEnum::PartialOutage => 'border-orange-500/50 bg-orange-600 text-white dark:bg-orange-900 dark:text-orange-200',
ServiceStatusEnum::MajorOutage => 'border-red-500/50 bg-red-600 text-white dark:bg-red-900 dark:text-red-200',
};
$serviceBadge = static function (ServiceStatusEnum $s): string {
$base = 'px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full';
return $base.' '.match ($s) {
ServiceStatusEnum::Operational => 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
ServiceStatusEnum::Degraded => 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200',
ServiceStatusEnum::Maintenance => 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
ServiceStatusEnum::PartialOutage => 'bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200',
ServiceStatusEnum::MajorOutage => 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200',
};
};
$impactBadge = static function (IncidentImpactEnum $i): string {
$base = 'px-2 py-0.5 inline-flex text-xs leading-5 font-semibold rounded-full';
return $base.' '.match ($i) {
IncidentImpactEnum::None => 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200',
IncidentImpactEnum::Minor => 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200',
IncidentImpactEnum::Major => 'bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200',
IncidentImpactEnum::Critical => 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200',
};
};
$incidentStatusBadge = static function (IncidentStatusEnum $st): string {
$base = 'px-2 py-0.5 inline-flex text-xs leading-5 font-semibold rounded-full';
return $base.' '.match ($st) {
IncidentStatusEnum::Investigating => 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200',
IncidentStatusEnum::Identified => 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
IncidentStatusEnum::Monitoring => 'bg-cyan-100 dark:bg-cyan-900 text-cyan-800 dark:text-cyan-200',
IncidentStatusEnum::Resolved => 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
};
};
@endphp
<div class="w-full max-w-5xl mx-auto">
<div class="surface-panel rounded-xl shadow-sm mb-6 overflow-hidden border {{ $overallBanner }}">
<div class="px-6 py-5">
<h1 class="text-2xl font-bold tracking-tight">Service status</h1>
<p class="mt-2 text-sm leading-relaxed">
Overall: <strong class="font-semibold">{{ $overallStatus->label() }}</strong>
@if($activeIncidents->isNotEmpty())
&mdash; {{ $activeIncidents->count() }} active {{ Str::plural('incident', $activeIncidents->count()) }}
@else
&mdash; No active incidents
@endif
</p>
</div>
</div>
<div class="surface-panel rounded-xl shadow-sm mb-6">
<div class="surface-panel-alt px-6 py-4 border-b rounded-t-lg">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Services</h2>
<p class="text-sm text-gray-700 dark:text-gray-300 mt-1">Uptime and current health for core endpoints.</p>
</div>
<div class="px-6 py-6 space-y-4">
@forelse($services as $service)
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 p-4 rounded-lg border border-gray-200 dark:border-gray-600 bg-white/50 dark:bg-gray-800/90">
<div>
<div class="font-semibold text-gray-900 dark:text-gray-100">{{ $service->name }}</div>
<div class="text-sm text-gray-700 dark:text-gray-300 mt-1">
Uptime (reported): {{ number_format((float) $service->uptime_percentage, 2) }}%
@if($service->response_time_ms !== null)
&middot; Response ~{{ $service->response_time_ms }} ms
@endif
@if($service->last_checked_at)
&middot; Checked {{ $service->last_checked_at->diffForHumans() }}
@endif
</div>
</div>
<span class="{{ $serviceBadge($service->status) }}">
{{ $service->status->label() }}
</span>
</div>
@empty
<p class="text-gray-700 dark:text-gray-300">No services are configured for display.</p>
@endforelse
</div>
</div>
<div class="surface-panel rounded-xl shadow-sm mb-6">
<div class="surface-panel-alt px-6 py-4 border-b rounded-t-lg">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Active incidents</h2>
</div>
<div class="px-6 py-6 space-y-4">
@forelse($activeIncidents as $incident)
<article class="p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-white/30 dark:bg-gray-900/20">
<div class="flex flex-wrap items-start justify-between gap-2">
<h3 class="font-semibold text-gray-900 dark:text-gray-100">{{ $incident->title }}</h3>
<span class="{{ $impactBadge($incident->impact) }}">
{{ $incident->impact->label() }} impact
</span>
</div>
<p class="text-sm text-gray-700 dark:text-gray-300 mt-2 whitespace-pre-wrap">{{ $incident->description }}</p>
<div class="mt-3 text-xs text-gray-600 dark:text-gray-400 flex flex-wrap items-center gap-x-3 gap-y-2">
<span class="inline-flex items-center gap-1">
<i class="fas fa-layer-group"></i>
<span>{{ $incident->services->pluck('name')->join(', ') ?: '—' }}</span>
</span>
<span class="{{ $incidentStatusBadge($incident->status) }}">
<i class="fas fa-flag text-[10px] opacity-90"></i>{{ $incident->status->label() }}
</span>
<span><i class="fas fa-clock mr-1"></i>Started {{ $incident->started_at->timezone(config('app.timezone'))->format('M j, Y g:i A T') }}</span>
</div>
</article>
@empty
<p class="text-gray-700 dark:text-gray-300">There are no active incidents.</p>
@endforelse
</div>
</div>
<div class="surface-panel rounded-xl shadow-sm mb-6">
<div class="surface-panel-alt px-6 py-4 border-b rounded-t-lg">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Recent resolved incidents</h2>
<p class="text-sm text-gray-700 dark:text-gray-300 mt-1">Last 30 days</p>
</div>
<div class="px-6 py-6 space-y-6">
@forelse($recentResolvedGrouped as $date => $group)
<div>
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">{{ $date === 'unknown' ? 'Unknown date' : \Carbon\Carbon::parse($date)->format('F j, Y') }}</h3>
<ul class="space-y-3">
@foreach($group as $incident)
<li class="p-3 rounded-lg bg-gray-50 dark:bg-gray-800/80 border border-gray-100 dark:border-gray-700">
<div class="font-medium text-gray-900 dark:text-gray-100">{{ $incident->title }}</div>
<div class="text-xs text-gray-600 dark:text-gray-400 mt-1 flex flex-wrap gap-x-2 gap-y-1 items-center">
<span class="{{ $impactBadge($incident->impact) }}">{{ $incident->impact->label() }}</span>
<span>{{ $incident->services->pluck('name')->join(', ') }} &middot; Resolved {{ $incident->resolved_at?->timezone(config('app.timezone'))->format('M j, g:i A') }}</span>
</div>
</li>
@endforeach
</ul>
</div>
@empty
<p class="text-gray-700 dark:text-gray-300">No resolved incidents in the last 30 days.</p>
@endforelse
</div>
</div>
<nav class="text-sm text-gray-700 dark:text-gray-300" aria-label="breadcrumb">
<ol class="flex flex-wrap gap-2">
<li><a href="{{ url($site['home_link'] ?? '/') }}" class="text-primary-600 dark:text-primary-400 hover:underline">Home</a></li>
<li aria-hidden="true">/</li>
<li class="text-gray-800 dark:text-gray-200">Status</li>
</ol>
</nav>
</div>
@endsection