some progress
This commit is contained in:
@@ -0,0 +1,406 @@
|
|||||||
|
# API Documentation
|
||||||
|
|
||||||
|
This document describes the API endpoints for the site monitoring application.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
All API endpoints require authentication using Laravel Sanctum tokens. You need to be logged into the web application to generate API tokens.
|
||||||
|
|
||||||
|
### Generate an API Token
|
||||||
|
|
||||||
|
After logging into the web application, you can generate an API token by making a POST request:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST /api/tokens
|
||||||
|
Authorization: Bearer {your-session-token}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"token_name": "My API Token"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "1|abcdef123456...",
|
||||||
|
"type": "Bearer"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Save this token securely. It will only be shown once.
|
||||||
|
|
||||||
|
### Using the API Token
|
||||||
|
|
||||||
|
Include the token in the `Authorization` header for all API requests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Authorization: Bearer 1|abcdef123456...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Token Management
|
||||||
|
|
||||||
|
#### List All Tokens
|
||||||
|
```bash
|
||||||
|
GET /api/tokens
|
||||||
|
Authorization: Bearer {your-token}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Revoke a Specific Token
|
||||||
|
```bash
|
||||||
|
DELETE /api/tokens/{tokenId}
|
||||||
|
Authorization: Bearer {your-token}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Revoke All Tokens
|
||||||
|
```bash
|
||||||
|
DELETE /api/tokens
|
||||||
|
Authorization: Bearer {your-token}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Sites
|
||||||
|
|
||||||
|
#### List All Sites
|
||||||
|
Get all sites for the authenticated user.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/sites
|
||||||
|
Authorization: Bearer {your-token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"user_id": 1,
|
||||||
|
"url": "https://example.com",
|
||||||
|
"name": "My Website",
|
||||||
|
"monitoring_enabled": true,
|
||||||
|
"uptime_checks_count": 150,
|
||||||
|
"ssl_certificates_count": 5,
|
||||||
|
"broken_links_count": 3,
|
||||||
|
"created_at": "2024-01-01T00:00:00.000000Z",
|
||||||
|
"updated_at": "2024-01-15T12:00:00.000000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get a Specific Site
|
||||||
|
```bash
|
||||||
|
GET /api/sites/{site}
|
||||||
|
Authorization: Bearer {your-token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"user_id": 1,
|
||||||
|
"url": "https://example.com",
|
||||||
|
"name": "My Website",
|
||||||
|
"monitoring_enabled": true,
|
||||||
|
"uptime_checks_count": 150,
|
||||||
|
"ssl_certificates_count": 5,
|
||||||
|
"broken_links_count": 3,
|
||||||
|
"created_at": "2024-01-01T00:00:00.000000Z",
|
||||||
|
"updated_at": "2024-01-15T12:00:00.000000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Uptime Monitoring
|
||||||
|
|
||||||
|
#### Get Uptime History
|
||||||
|
Get paginated uptime check history for a site.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/sites/{site}/uptime?per_page=50
|
||||||
|
Authorization: Bearer {your-token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `per_page` (optional): Number of results per page (1-100, default: 50)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"current_page": 1,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"site_id": 1,
|
||||||
|
"status_code": 200,
|
||||||
|
"response_time_ms": 245,
|
||||||
|
"checked_at": "2024-01-15T12:00:00.000000Z",
|
||||||
|
"created_at": "2024-01-15T12:00:00.000000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"first_page_url": "http://localhost/api/sites/1/uptime?page=1",
|
||||||
|
"from": 1,
|
||||||
|
"last_page": 3,
|
||||||
|
"last_page_url": "http://localhost/api/sites/1/uptime?page=3",
|
||||||
|
"links": [...],
|
||||||
|
"next_page_url": "http://localhost/api/sites/1/uptime?page=2",
|
||||||
|
"path": "http://localhost/api/sites/1/uptime",
|
||||||
|
"per_page": 50,
|
||||||
|
"prev_page_url": null,
|
||||||
|
"to": 50,
|
||||||
|
"total": 150
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Uptime Statistics
|
||||||
|
Get uptime statistics for a site.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/sites/{site}/uptime/stats
|
||||||
|
Authorization: Bearer {your-token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"total_checks": 150,
|
||||||
|
"successful_checks": 147,
|
||||||
|
"uptime_percentage": 98.0,
|
||||||
|
"average_response_time_ms": 234.56,
|
||||||
|
"latest_check": {
|
||||||
|
"id": 150,
|
||||||
|
"site_id": 1,
|
||||||
|
"status_code": 200,
|
||||||
|
"response_time_ms": 245,
|
||||||
|
"checked_at": "2024-01-15T12:00:00.000000Z",
|
||||||
|
"created_at": "2024-01-15T12:00:00.000000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL Certificate Monitoring
|
||||||
|
|
||||||
|
#### Get SSL Certificate Data
|
||||||
|
Get the latest SSL certificate information for a site.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/sites/{site}/ssl
|
||||||
|
Authorization: Bearer {your-token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"issuer": "Let's Encrypt Authority X3",
|
||||||
|
"expires_at": "2024-04-15T00:00:00.000000Z",
|
||||||
|
"checked_at": "2024-01-15T12:00:00.000000Z",
|
||||||
|
"days_until_expiry": 90,
|
||||||
|
"is_expiring_soon": false,
|
||||||
|
"is_expired": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get SSL Certificate History
|
||||||
|
Get paginated SSL certificate check history.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/sites/{site}/ssl/history?per_page=20
|
||||||
|
Authorization: Bearer {your-token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `per_page` (optional): Number of results per page (1-100, default: 20)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"current_page": 1,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"site_id": 1,
|
||||||
|
"issuer": "Let's Encrypt Authority X3",
|
||||||
|
"expires_at": "2024-04-15T00:00:00.000000Z",
|
||||||
|
"checked_at": "2024-01-15T12:00:00.000000Z",
|
||||||
|
"created_at": "2024-01-15T12:00:00.000000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"first_page_url": "...",
|
||||||
|
"last_page": 1,
|
||||||
|
"per_page": 20,
|
||||||
|
"total": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Broken Links
|
||||||
|
|
||||||
|
#### Get Broken Links List
|
||||||
|
Get paginated list of broken links for a site.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/sites/{site}/links?per_page=50
|
||||||
|
Authorization: Bearer {your-token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `per_page` (optional): Number of results per page (1-100, default: 50)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"current_page": 1,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"site_id": 1,
|
||||||
|
"url": "https://example.com/broken-page",
|
||||||
|
"status_code": 404,
|
||||||
|
"last_checked_at": "2024-01-15T12:00:00.000000Z",
|
||||||
|
"created_at": "2024-01-15T12:00:00.000000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"first_page_url": "...",
|
||||||
|
"last_page": 1,
|
||||||
|
"per_page": 50,
|
||||||
|
"total": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Broken Links Statistics
|
||||||
|
Get statistics about broken links for a site.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/sites/{site}/links/stats
|
||||||
|
Authorization: Bearer {your-token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"total_broken_links": 3,
|
||||||
|
"status_code_breakdown": [
|
||||||
|
{
|
||||||
|
"status_code": 404,
|
||||||
|
"count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status_code": 500,
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
### Unauthorized (401)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Unauthenticated."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Forbidden (403)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Unauthorized"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Not Found (404)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "No SSL certificate data found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation Error (422)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "The per_page field must be an integer.",
|
||||||
|
"errors": {
|
||||||
|
"per_page": [
|
||||||
|
"The per_page field must be an integer."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
### Using cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all sites
|
||||||
|
curl -X GET http://localhost/api/sites \
|
||||||
|
-H "Authorization: Bearer 1|abcdef123456..." \
|
||||||
|
-H "Accept: application/json"
|
||||||
|
|
||||||
|
# Get uptime history
|
||||||
|
curl -X GET "http://localhost/api/sites/1/uptime?per_page=25" \
|
||||||
|
-H "Authorization: Bearer 1|abcdef123456..." \
|
||||||
|
-H "Accept: application/json"
|
||||||
|
|
||||||
|
# Get SSL certificate data
|
||||||
|
curl -X GET http://localhost/api/sites/1/ssl \
|
||||||
|
-H "Authorization: Bearer 1|abcdef123456..." \
|
||||||
|
-H "Accept: application/json"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using JavaScript (Fetch API)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const apiToken = '1|abcdef123456...';
|
||||||
|
const baseUrl = 'http://localhost/api';
|
||||||
|
|
||||||
|
// List all sites
|
||||||
|
const response = await fetch(`${baseUrl}/sites`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiToken}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Python (requests)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
api_token = '1|abcdef123456...'
|
||||||
|
base_url = 'http://localhost/api'
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Authorization': f'Bearer {api_token}',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
# List all sites
|
||||||
|
response = requests.get(f'{base_url}/sites', headers=headers)
|
||||||
|
data = response.json()
|
||||||
|
print(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
API requests are subject to Laravel's default rate limiting (60 requests per minute for authenticated users). If you exceed the rate limit, you'll receive a `429 Too Many Requests` response.
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Never share your API tokens** - Keep them secure and treat them like passwords
|
||||||
|
2. **Use HTTPS in production** - Always use encrypted connections when making API requests
|
||||||
|
3. **Rotate tokens regularly** - Periodically revoke and recreate your API tokens
|
||||||
|
4. **Use separate tokens for different applications** - Create named tokens for each integration
|
||||||
|
5. **Revoke unused tokens** - Remove tokens that are no longer needed
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Jobs\CheckDnsRecord;
|
||||||
|
use App\Models\DnsRecord;
|
||||||
|
use App\Models\Site;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class MonitorDns extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'monitor:dns
|
||||||
|
{--dns-record= : Specific DNS record ID to monitor}
|
||||||
|
{--site= : Monitor all DNS records for a specific site ID}
|
||||||
|
{--sync : Run checks synchronously instead of queuing}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Monitor DNS records for all sites';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$this->info('Starting DNS monitoring...');
|
||||||
|
|
||||||
|
$query = DnsRecord::with('site')->where('is_active', true);
|
||||||
|
|
||||||
|
// Filter by specific DNS record
|
||||||
|
if ($dnsRecordId = $this->option('dns-record')) {
|
||||||
|
$query->where('id', $dnsRecordId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by site
|
||||||
|
if ($siteId = $this->option('site')) {
|
||||||
|
$query->where('site_id', $siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dnsRecords = $query->get();
|
||||||
|
|
||||||
|
if ($dnsRecords->isEmpty()) {
|
||||||
|
$this->warn('No active DNS records found.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Found {$dnsRecords->count()} active DNS record(s).");
|
||||||
|
|
||||||
|
$bar = $this->output->createProgressBar($dnsRecords->count());
|
||||||
|
$bar->start();
|
||||||
|
|
||||||
|
$dispatched = 0;
|
||||||
|
$errors = 0;
|
||||||
|
|
||||||
|
foreach ($dnsRecords as $dnsRecord) {
|
||||||
|
try {
|
||||||
|
if ($this->option('sync')) {
|
||||||
|
// Run synchronously
|
||||||
|
CheckDnsRecord::dispatchSync($dnsRecord);
|
||||||
|
$this->newLine();
|
||||||
|
$this->line(" ✓ Checked: {$dnsRecord->site->name} - {$dnsRecord->record_type} {$dnsRecord->domain}");
|
||||||
|
} else {
|
||||||
|
// Queue the job
|
||||||
|
CheckDnsRecord::dispatch($dnsRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatched++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors++;
|
||||||
|
$this->newLine();
|
||||||
|
$this->error(" ✗ Error checking {$dnsRecord->domain}: {$e->getMessage()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->newLine(2);
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
$this->info("DNS monitoring completed!");
|
||||||
|
$this->table(
|
||||||
|
['Metric', 'Count'],
|
||||||
|
[
|
||||||
|
['Total DNS Records', $dnsRecords->count()],
|
||||||
|
['Successfully Dispatched', $dispatched],
|
||||||
|
['Errors', $errors],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$this->option('sync')) {
|
||||||
|
$this->comment('Jobs have been queued. Run the queue worker to process them.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Jobs\CheckHealthEndpoint;
|
||||||
|
use App\Models\HealthCheck;
|
||||||
|
use App\Models\Site;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class MonitorHealth extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'monitor:health
|
||||||
|
{--health-check= : Specific health check ID to monitor}
|
||||||
|
{--site= : Monitor all health checks for a specific site ID}
|
||||||
|
{--sync : Run checks synchronously instead of queuing}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Monitor application health endpoints';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$this->info('Starting health endpoint monitoring...');
|
||||||
|
|
||||||
|
$query = HealthCheck::with('site')->where('is_active', true);
|
||||||
|
|
||||||
|
// Filter by specific health check
|
||||||
|
if ($healthCheckId = $this->option('health-check')) {
|
||||||
|
$query->where('id', $healthCheckId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by site
|
||||||
|
if ($siteId = $this->option('site')) {
|
||||||
|
$query->where('site_id', $siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$healthChecks = $query->get();
|
||||||
|
|
||||||
|
if ($healthChecks->isEmpty()) {
|
||||||
|
$this->warn('No active health checks found.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Found {$healthChecks->count()} active health check(s).");
|
||||||
|
|
||||||
|
$bar = $this->output->createProgressBar($healthChecks->count());
|
||||||
|
$bar->start();
|
||||||
|
|
||||||
|
$dispatched = 0;
|
||||||
|
$errors = 0;
|
||||||
|
|
||||||
|
foreach ($healthChecks as $healthCheck) {
|
||||||
|
try {
|
||||||
|
if ($this->option('sync')) {
|
||||||
|
// Run synchronously
|
||||||
|
CheckHealthEndpoint::dispatchSync($healthCheck);
|
||||||
|
$this->newLine();
|
||||||
|
$this->line(" ✓ Checked: {$healthCheck->site->name} - {$healthCheck->endpoint}");
|
||||||
|
} else {
|
||||||
|
// Queue the job
|
||||||
|
CheckHealthEndpoint::dispatch($healthCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatched++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors++;
|
||||||
|
$this->newLine();
|
||||||
|
$this->error(" ✗ Error checking {$healthCheck->site->name} - {$healthCheck->endpoint}: {$e->getMessage()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->newLine(2);
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
$this->info("Health monitoring completed!");
|
||||||
|
$this->table(
|
||||||
|
['Metric', 'Count'],
|
||||||
|
[
|
||||||
|
['Total Health Checks', $healthChecks->count()],
|
||||||
|
['Successfully Dispatched', $dispatched],
|
||||||
|
['Errors', $errors],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$this->option('sync')) {
|
||||||
|
$this->comment('Jobs have been queued. Run the queue worker to process them.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Jobs\CheckSitePing;
|
||||||
|
use App\Models\Site;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class MonitorPing extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'monitor:ping
|
||||||
|
{--site= : Specific site ID to ping}
|
||||||
|
{--sync : Run synchronously instead of dispatching jobs}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Ping all enabled sites to check their uptime status';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$this->info('Starting site ping monitoring...');
|
||||||
|
|
||||||
|
$query = Site::where('monitoring_enabled', true);
|
||||||
|
|
||||||
|
if ($siteId = $this->option('site')) {
|
||||||
|
$query->where('id', $siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sites = $query->get();
|
||||||
|
|
||||||
|
if ($sites->isEmpty()) {
|
||||||
|
$this->warn('No enabled sites found to monitor.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Found {$sites->count()} site(s) to ping.");
|
||||||
|
|
||||||
|
$progressBar = $this->output->createProgressBar($sites->count());
|
||||||
|
$progressBar->start();
|
||||||
|
|
||||||
|
$dispatched = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
foreach ($sites as $site) {
|
||||||
|
try {
|
||||||
|
if ($this->option('sync')) {
|
||||||
|
// Run synchronously for testing/debugging
|
||||||
|
CheckSitePing::dispatchSync($site);
|
||||||
|
} else {
|
||||||
|
// Dispatch to queue
|
||||||
|
CheckSitePing::dispatch($site);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatched++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$failed++;
|
||||||
|
Log::error('MonitorPing: Failed to dispatch job', [
|
||||||
|
'site_id' => $site->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$progressBar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$progressBar->finish();
|
||||||
|
$this->newLine(2);
|
||||||
|
|
||||||
|
$this->info("Successfully dispatched {$dispatched} ping job(s).");
|
||||||
|
|
||||||
|
if ($failed > 0) {
|
||||||
|
$this->error("Failed to dispatch {$failed} job(s).");
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Jobs\CheckTcpPort;
|
||||||
|
use App\Models\TcpPort;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class MonitorTcpPorts extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'monitor:tcp-ports
|
||||||
|
{--tcp-port= : Specific TCP port ID to check}
|
||||||
|
{--site= : Specific site ID to check all ports for}
|
||||||
|
{--sync : Run synchronously instead of dispatching jobs}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Monitor all active TCP ports';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$this->info('Starting TCP port monitoring...');
|
||||||
|
|
||||||
|
$query = TcpPort::with('site')->where('is_active', true);
|
||||||
|
|
||||||
|
if ($tcpPortId = $this->option('tcp-port')) {
|
||||||
|
$query->where('id', $tcpPortId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($siteId = $this->option('site')) {
|
||||||
|
$query->where('site_id', $siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tcpPorts = $query->get();
|
||||||
|
|
||||||
|
if ($tcpPorts->isEmpty()) {
|
||||||
|
$this->warn('No active TCP ports found to monitor.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Found {$tcpPorts->count()} TCP port(s) to check.");
|
||||||
|
|
||||||
|
$progressBar = $this->output->createProgressBar($tcpPorts->count());
|
||||||
|
$progressBar->start();
|
||||||
|
|
||||||
|
$dispatched = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
foreach ($tcpPorts as $tcpPort) {
|
||||||
|
try {
|
||||||
|
if ($this->option('sync')) {
|
||||||
|
// Run synchronously for testing/debugging
|
||||||
|
CheckTcpPort::dispatchSync($tcpPort);
|
||||||
|
} else {
|
||||||
|
// Dispatch to queue
|
||||||
|
CheckTcpPort::dispatch($tcpPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatched++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$failed++;
|
||||||
|
Log::error('MonitorTcpPorts: Failed to dispatch job', [
|
||||||
|
'tcp_port_id' => $tcpPort->id,
|
||||||
|
'site_id' => $tcpPort->site_id,
|
||||||
|
'port' => $tcpPort->port,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$progressBar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$progressBar->finish();
|
||||||
|
$this->newLine(2);
|
||||||
|
|
||||||
|
$this->info("Successfully dispatched {$dispatched} TCP port check job(s).");
|
||||||
|
|
||||||
|
if ($failed > 0) {
|
||||||
|
$this->error("Failed to dispatch {$failed} job(s).");
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class AuthController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new API token for the authenticated user.
|
||||||
|
*/
|
||||||
|
public function createToken(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'token_name' => ['required', 'string', 'max:255'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$token = $request->user()->createToken($validated['token_name']);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'token' => $token->plainTextToken,
|
||||||
|
'type' => 'Bearer',
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all tokens for the authenticated user.
|
||||||
|
*/
|
||||||
|
public function tokens(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$tokens = $request->user()->tokens()->get(['id', 'name', 'last_used_at', 'created_at']);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => $tokens,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke a specific token.
|
||||||
|
*/
|
||||||
|
public function revokeToken(Request $request, string $tokenId): JsonResponse
|
||||||
|
{
|
||||||
|
$deleted = $request->user()->tokens()->where('id', $tokenId)->delete();
|
||||||
|
|
||||||
|
if (!$deleted) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Token not found',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Token revoked successfully',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke all tokens for the authenticated user.
|
||||||
|
*/
|
||||||
|
public function revokeAllTokens(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$request->user()->tokens()->delete();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'All tokens revoked successfully',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Api\BrokenLinksIndexRequest;
|
||||||
|
use App\Models\Site;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class LinkController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get broken links list for a site.
|
||||||
|
*/
|
||||||
|
public function index(BrokenLinksIndexRequest $request, Site $site): JsonResponse
|
||||||
|
{
|
||||||
|
// Ensure the site belongs to the authenticated user
|
||||||
|
if ($site->user_id !== $request->user()->id) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Unauthorized',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$perPage = $request->input('per_page', 50);
|
||||||
|
$perPage = min($perPage, 100); // Max 100 per page
|
||||||
|
|
||||||
|
$brokenLinks = $site->brokenLinks()
|
||||||
|
->orderBy('last_checked_at', 'desc')
|
||||||
|
->paginate($perPage);
|
||||||
|
|
||||||
|
return response()->json($brokenLinks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get broken links statistics for a site.
|
||||||
|
*/
|
||||||
|
public function stats(Request $request, Site $site): JsonResponse
|
||||||
|
{
|
||||||
|
// Ensure the site belongs to the authenticated user
|
||||||
|
if ($site->user_id !== $request->user()->id) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Unauthorized',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalBrokenLinks = $site->brokenLinks()->count();
|
||||||
|
|
||||||
|
$statusCodeBreakdown = $site->brokenLinks()
|
||||||
|
->selectRaw('status_code, COUNT(*) as count')
|
||||||
|
->groupBy('status_code')
|
||||||
|
->orderBy('count', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => [
|
||||||
|
'total_broken_links' => $totalBrokenLinks,
|
||||||
|
'status_code_breakdown' => $statusCodeBreakdown,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Site;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SiteController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* List all sites for the authenticated user.
|
||||||
|
*/
|
||||||
|
public function index(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$sites = $request->user()
|
||||||
|
->sites()
|
||||||
|
->withCount(['uptimeChecks', 'sslCertificates', 'brokenLinks'])
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => $sites,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific site for the authenticated user.
|
||||||
|
*/
|
||||||
|
public function show(Request $request, Site $site): JsonResponse
|
||||||
|
{
|
||||||
|
// Ensure the site belongs to the authenticated user
|
||||||
|
if ($site->user_id !== $request->user()->id) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Unauthorized',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$site->loadCount(['uptimeChecks', 'sslCertificates', 'brokenLinks']);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => $site,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Api\SslHistoryRequest;
|
||||||
|
use App\Models\Site;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SslController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get SSL certificate data for a site.
|
||||||
|
*/
|
||||||
|
public function show(Request $request, Site $site): JsonResponse
|
||||||
|
{
|
||||||
|
// Ensure the site belongs to the authenticated user
|
||||||
|
if ($site->user_id !== $request->user()->id) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Unauthorized',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$latestCertificate = $site->sslCertificates()
|
||||||
|
->latest('checked_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$latestCertificate) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'No SSL certificate data found',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$daysUntilExpiry = null;
|
||||||
|
$isExpiring = false;
|
||||||
|
|
||||||
|
if ($latestCertificate->expires_at) {
|
||||||
|
$daysUntilExpiry = now()->diffInDays($latestCertificate->expires_at, false);
|
||||||
|
$isExpiring = $daysUntilExpiry <= 30 && $daysUntilExpiry > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => [
|
||||||
|
'issuer' => $latestCertificate->issuer,
|
||||||
|
'expires_at' => $latestCertificate->expires_at,
|
||||||
|
'checked_at' => $latestCertificate->checked_at,
|
||||||
|
'days_until_expiry' => $daysUntilExpiry ? (int) $daysUntilExpiry : null,
|
||||||
|
'is_expiring_soon' => $isExpiring,
|
||||||
|
'is_expired' => $daysUntilExpiry !== null && $daysUntilExpiry < 0,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get SSL certificate history for a site.
|
||||||
|
*/
|
||||||
|
public function history(SslHistoryRequest $request, Site $site): JsonResponse
|
||||||
|
{
|
||||||
|
// Ensure the site belongs to the authenticated user
|
||||||
|
if ($site->user_id !== $request->user()->id) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Unauthorized',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$perPage = $request->input('per_page', 20);
|
||||||
|
$perPage = min($perPage, 100); // Max 100 per page
|
||||||
|
|
||||||
|
$certificates = $site->sslCertificates()
|
||||||
|
->orderBy('checked_at', 'desc')
|
||||||
|
->paginate($perPage);
|
||||||
|
|
||||||
|
return response()->json($certificates);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Api\UptimeIndexRequest;
|
||||||
|
use App\Models\Site;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class UptimeController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get uptime history for a site (paginated).
|
||||||
|
*/
|
||||||
|
public function index(UptimeIndexRequest $request, Site $site): JsonResponse
|
||||||
|
{
|
||||||
|
// Ensure the site belongs to the authenticated user
|
||||||
|
if ($site->user_id !== $request->user()->id) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Unauthorized',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$perPage = $request->input('per_page', 50);
|
||||||
|
$perPage = min($perPage, 100); // Max 100 per page
|
||||||
|
|
||||||
|
$uptimeChecks = $site->uptimeChecks()
|
||||||
|
->orderBy('checked_at', 'desc')
|
||||||
|
->paginate($perPage);
|
||||||
|
|
||||||
|
return response()->json($uptimeChecks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get uptime statistics for a site.
|
||||||
|
*/
|
||||||
|
public function stats(Request $request, Site $site): JsonResponse
|
||||||
|
{
|
||||||
|
// Ensure the site belongs to the authenticated user
|
||||||
|
if ($site->user_id !== $request->user()->id) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Unauthorized',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalChecks = $site->uptimeChecks()->count();
|
||||||
|
$successfulChecks = $site->uptimeChecks()
|
||||||
|
->whereBetween('status_code', [200, 299])
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$uptimePercentage = $totalChecks > 0
|
||||||
|
? round(($successfulChecks / $totalChecks) * 100, 2)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
$averageResponseTime = $site->uptimeChecks()
|
||||||
|
->avg('response_time_ms');
|
||||||
|
|
||||||
|
$latestCheck = $site->uptimeChecks()
|
||||||
|
->latest('checked_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => [
|
||||||
|
'total_checks' => $totalChecks,
|
||||||
|
'successful_checks' => $successfulChecks,
|
||||||
|
'uptime_percentage' => $uptimePercentage,
|
||||||
|
'average_response_time_ms' => $averageResponseTime ? round($averageResponseTime, 2) : null,
|
||||||
|
'latest_check' => $latestCheck,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\DnsRecord;
|
||||||
|
use App\Models\Site;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class DnsRecordController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the DNS records for a site.
|
||||||
|
*/
|
||||||
|
public function index(Site $site): View
|
||||||
|
{
|
||||||
|
// Ensure the user owns the site
|
||||||
|
if ($site->user_id !== Auth::id()) {
|
||||||
|
abort(403, 'Unauthorized access to this site.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$dnsRecords = $site->dnsRecords()
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('dns-records.index', compact('site', 'dnsRecords'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new DNS record.
|
||||||
|
*/
|
||||||
|
public function create(Site $site): View
|
||||||
|
{
|
||||||
|
// Ensure the user owns the site
|
||||||
|
if ($site->user_id !== Auth::id()) {
|
||||||
|
abort(403, 'Unauthorized access to this site.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$recordTypes = DnsRecord::getRecordTypes();
|
||||||
|
|
||||||
|
return view('dns-records.create', compact('site', 'recordTypes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created DNS record in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request, Site $site): RedirectResponse
|
||||||
|
{
|
||||||
|
// Ensure the user owns the site
|
||||||
|
if ($site->user_id !== Auth::id()) {
|
||||||
|
abort(403, 'Unauthorized access to this site.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'domain' => ['required', 'string', 'max:255'],
|
||||||
|
'record_type' => ['required', 'string', 'in:A,AAAA,CNAME,MX,TXT,NS,SOA,PTR,SRV'],
|
||||||
|
'expected_value' => ['nullable', 'string', 'max:1000'],
|
||||||
|
'is_active' => ['boolean'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check for duplicate record
|
||||||
|
$exists = $site->dnsRecords()
|
||||||
|
->where('domain', $validated['domain'])
|
||||||
|
->where('record_type', $validated['record_type'])
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
return back()
|
||||||
|
->withInput()
|
||||||
|
->withErrors(['domain' => "A {$validated['record_type']} record for {$validated['domain']} already exists for this site."]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dnsRecord = $site->dnsRecords()->create([
|
||||||
|
'domain' => $validated['domain'],
|
||||||
|
'record_type' => $validated['record_type'],
|
||||||
|
'expected_value' => $validated['expected_value'] ?? null,
|
||||||
|
'is_active' => $validated['is_active'] ?? true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('dns-records.show', [$site, $dnsRecord])
|
||||||
|
->with('success', 'DNS record created successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified DNS record.
|
||||||
|
*/
|
||||||
|
public function show(Site $site, DnsRecord $dnsRecord): View
|
||||||
|
{
|
||||||
|
// Ensure the user owns the site and the DNS record belongs to the site
|
||||||
|
if ($site->user_id !== Auth::id() || $dnsRecord->site_id !== $site->id) {
|
||||||
|
abort(403, 'Unauthorized access to this DNS record.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('dns-records.show', compact('site', 'dnsRecord'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified DNS record.
|
||||||
|
*/
|
||||||
|
public function edit(Site $site, DnsRecord $dnsRecord): View
|
||||||
|
{
|
||||||
|
// Ensure the user owns the site and the DNS record belongs to the site
|
||||||
|
if ($site->user_id !== Auth::id() || $dnsRecord->site_id !== $site->id) {
|
||||||
|
abort(403, 'Unauthorized access to this DNS record.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$recordTypes = DnsRecord::getRecordTypes();
|
||||||
|
|
||||||
|
return view('dns-records.edit', compact('site', 'dnsRecord', 'recordTypes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified DNS record in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, Site $site, DnsRecord $dnsRecord): RedirectResponse
|
||||||
|
{
|
||||||
|
// Ensure the user owns the site and the DNS record belongs to the site
|
||||||
|
if ($site->user_id !== Auth::id() || $dnsRecord->site_id !== $site->id) {
|
||||||
|
abort(403, 'Unauthorized access to this DNS record.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'domain' => ['required', 'string', 'max:255'],
|
||||||
|
'record_type' => ['required', 'string', 'in:A,AAAA,CNAME,MX,TXT,NS,SOA,PTR,SRV'],
|
||||||
|
'expected_value' => ['nullable', 'string', 'max:1000'],
|
||||||
|
'is_active' => ['boolean'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check for duplicate record (excluding current record)
|
||||||
|
$exists = $site->dnsRecords()
|
||||||
|
->where('domain', $validated['domain'])
|
||||||
|
->where('record_type', $validated['record_type'])
|
||||||
|
->where('id', '!=', $dnsRecord->id)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
return back()
|
||||||
|
->withInput()
|
||||||
|
->withErrors(['domain' => "A {$validated['record_type']} record for {$validated['domain']} already exists for this site."]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dnsRecord->update([
|
||||||
|
'domain' => $validated['domain'],
|
||||||
|
'record_type' => $validated['record_type'],
|
||||||
|
'expected_value' => $validated['expected_value'] ?? null,
|
||||||
|
'is_active' => $validated['is_active'] ?? $dnsRecord->is_active,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('dns-records.show', [$site, $dnsRecord])
|
||||||
|
->with('success', 'DNS record updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified DNS record from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(Site $site, DnsRecord $dnsRecord): RedirectResponse
|
||||||
|
{
|
||||||
|
// Ensure the user owns the site and the DNS record belongs to the site
|
||||||
|
if ($site->user_id !== Auth::id() || $dnsRecord->site_id !== $site->id) {
|
||||||
|
abort(403, 'Unauthorized access to this DNS record.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$dnsRecord->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('dns-records.index', $site)
|
||||||
|
->with('success', 'DNS record deleted successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\HealthCheck;
|
||||||
|
use App\Models\Site;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class HealthCheckController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the health checks for a site.
|
||||||
|
*/
|
||||||
|
public function index(Site $site): View
|
||||||
|
{
|
||||||
|
// Ensure the user owns the site
|
||||||
|
if ($site->user_id !== Auth::id()) {
|
||||||
|
abort(403, 'Unauthorized access to this site.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$healthChecks = $site->healthChecks()
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('health-checks.index', compact('site', 'healthChecks'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new health check.
|
||||||
|
*/
|
||||||
|
public function create(Site $site): View
|
||||||
|
{
|
||||||
|
// Ensure the user owns the site
|
||||||
|
if ($site->user_id !== Auth::id()) {
|
||||||
|
abort(403, 'Unauthorized access to this site.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('health-checks.create', compact('site'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created health check in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request, Site $site): RedirectResponse
|
||||||
|
{
|
||||||
|
// Ensure the user owns the site
|
||||||
|
if ($site->user_id !== Auth::id()) {
|
||||||
|
abort(403, 'Unauthorized access to this site.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'endpoint' => ['required', 'string', 'max:255'],
|
||||||
|
'expected_json_keys' => ['nullable', 'string'],
|
||||||
|
'performance_threshold' => ['nullable', 'integer', 'min:1', 'max:60000'],
|
||||||
|
'is_active' => ['boolean'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Convert comma-separated keys to array
|
||||||
|
$expectedKeys = null;
|
||||||
|
if (!empty($validated['expected_json_keys'])) {
|
||||||
|
$expectedKeys = array_map(
|
||||||
|
'trim',
|
||||||
|
explode(',', $validated['expected_json_keys'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate endpoint
|
||||||
|
$exists = $site->healthChecks()
|
||||||
|
->where('endpoint', $validated['endpoint'])
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
return back()
|
||||||
|
->withInput()
|
||||||
|
->withErrors(['endpoint' => 'This endpoint is already being monitored for this site.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$healthCheck = $site->healthChecks()->create([
|
||||||
|
'endpoint' => $validated['endpoint'],
|
||||||
|
'expected_json_keys' => $expectedKeys,
|
||||||
|
'performance_threshold' => $validated['performance_threshold'] ?? null,
|
||||||
|
'is_active' => $validated['is_active'] ?? true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('health-checks.show', [$site, $healthCheck])
|
||||||
|
->with('success', 'Health check created successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified health check.
|
||||||
|
*/
|
||||||
|
public function show(Site $site, HealthCheck $healthCheck): View
|
||||||
|
{
|
||||||
|
// Ensure the user owns the site and the health check belongs to the site
|
||||||
|
if ($site->user_id !== Auth::id() || $healthCheck->site_id !== $site->id) {
|
||||||
|
abort(403, 'Unauthorized access to this health check.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('health-checks.show', compact('site', 'healthCheck'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified health check.
|
||||||
|
*/
|
||||||
|
public function edit(Site $site, HealthCheck $healthCheck): View
|
||||||
|
{
|
||||||
|
// Ensure the user owns the site and the health check belongs to the site
|
||||||
|
if ($site->user_id !== Auth::id() || $healthCheck->site_id !== $site->id) {
|
||||||
|
abort(403, 'Unauthorized access to this health check.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('health-checks.edit', compact('site', 'healthCheck'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified health check in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, Site $site, HealthCheck $healthCheck): RedirectResponse
|
||||||
|
{
|
||||||
|
// Ensure the user owns the site and the health check belongs to the site
|
||||||
|
if ($site->user_id !== Auth::id() || $healthCheck->site_id !== $site->id) {
|
||||||
|
abort(403, 'Unauthorized access to this health check.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'endpoint' => ['required', 'string', 'max:255'],
|
||||||
|
'expected_json_keys' => ['nullable', 'string'],
|
||||||
|
'performance_threshold' => ['nullable', 'integer', 'min:1', 'max:60000'],
|
||||||
|
'is_active' => ['boolean'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Convert comma-separated keys to array
|
||||||
|
$expectedKeys = null;
|
||||||
|
if (!empty($validated['expected_json_keys'])) {
|
||||||
|
$expectedKeys = array_map(
|
||||||
|
'trim',
|
||||||
|
explode(',', $validated['expected_json_keys'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate endpoint (excluding current health check)
|
||||||
|
$exists = $site->healthChecks()
|
||||||
|
->where('endpoint', $validated['endpoint'])
|
||||||
|
->where('id', '!=', $healthCheck->id)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
return back()
|
||||||
|
->withInput()
|
||||||
|
->withErrors(['endpoint' => 'This endpoint is already being monitored for this site.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$healthCheck->update([
|
||||||
|
'endpoint' => $validated['endpoint'],
|
||||||
|
'expected_json_keys' => $expectedKeys,
|
||||||
|
'performance_threshold' => $validated['performance_threshold'] ?? null,
|
||||||
|
'is_active' => $validated['is_active'] ?? $healthCheck->is_active,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('health-checks.show', [$site, $healthCheck])
|
||||||
|
->with('success', 'Health check updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified health check from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(Site $site, HealthCheck $healthCheck): RedirectResponse
|
||||||
|
{
|
||||||
|
// Ensure the user owns the site and the health check belongs to the site
|
||||||
|
if ($site->user_id !== Auth::id() || $healthCheck->site_id !== $site->id) {
|
||||||
|
abort(403, 'Unauthorized access to this health check.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$healthCheck->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('health-checks.index', $site)
|
||||||
|
->with('success', 'Health check deleted successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Site;
|
||||||
|
use App\Models\TcpPort;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class TcpPortController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index(Request $request, Site $site)
|
||||||
|
{
|
||||||
|
// Ensure the site belongs to the authenticated user
|
||||||
|
if ($site->user_id !== Auth::id()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tcpPorts = $site->tcpPorts()
|
||||||
|
->orderBy('port')
|
||||||
|
->paginate(20);
|
||||||
|
|
||||||
|
return view('tcp-ports.index', compact('site', 'tcpPorts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*/
|
||||||
|
public function create(Site $site)
|
||||||
|
{
|
||||||
|
// Ensure the site belongs to the authenticated user
|
||||||
|
if ($site->user_id !== Auth::id()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$commonPorts = TcpPort::getCommonPorts();
|
||||||
|
|
||||||
|
return view('tcp-ports.create', compact('site', 'commonPorts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request, Site $site)
|
||||||
|
{
|
||||||
|
// Ensure the site belongs to the authenticated user
|
||||||
|
if ($site->user_id !== Auth::id()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'port' => ['required', 'integer', 'min:1', 'max:65535'],
|
||||||
|
'name' => ['nullable', 'string', 'max:255'],
|
||||||
|
'is_active' => ['boolean'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check if port already exists for this site
|
||||||
|
$exists = $site->tcpPorts()
|
||||||
|
->where('port', $validated['port'])
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
return back()
|
||||||
|
->withInput()
|
||||||
|
->withErrors(['port' => 'This port is already being monitored for this site.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$site->tcpPorts()->create([
|
||||||
|
'port' => $validated['port'],
|
||||||
|
'name' => $validated['name'] ?? null,
|
||||||
|
'is_active' => $validated['is_active'] ?? true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('tcp-ports.index', $site)
|
||||||
|
->with('success', 'TCP port added successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(Site $site, TcpPort $tcpPort)
|
||||||
|
{
|
||||||
|
// Ensure the port belongs to the site and user
|
||||||
|
if ($tcpPort->site_id !== $site->id || $site->user_id !== Auth::id()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('tcp-ports.show', compact('site', 'tcpPort'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit(Site $site, TcpPort $tcpPort)
|
||||||
|
{
|
||||||
|
// Ensure the port belongs to the site and user
|
||||||
|
if ($tcpPort->site_id !== $site->id || $site->user_id !== Auth::id()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$commonPorts = TcpPort::getCommonPorts();
|
||||||
|
|
||||||
|
return view('tcp-ports.edit', compact('site', 'tcpPort', 'commonPorts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, Site $site, TcpPort $tcpPort)
|
||||||
|
{
|
||||||
|
// Ensure the port belongs to the site and user
|
||||||
|
if ($tcpPort->site_id !== $site->id || $site->user_id !== Auth::id()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'port' => ['required', 'integer', 'min:1', 'max:65535'],
|
||||||
|
'name' => ['nullable', 'string', 'max:255'],
|
||||||
|
'is_active' => ['boolean'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check if port already exists for this site (excluding current)
|
||||||
|
$exists = $site->tcpPorts()
|
||||||
|
->where('port', $validated['port'])
|
||||||
|
->where('id', '!=', $tcpPort->id)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
return back()
|
||||||
|
->withInput()
|
||||||
|
->withErrors(['port' => 'This port is already being monitored for this site.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tcpPort->update([
|
||||||
|
'port' => $validated['port'],
|
||||||
|
'name' => $validated['name'] ?? null,
|
||||||
|
'is_active' => $validated['is_active'] ?? $tcpPort->is_active,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('tcp-ports.index', $site)
|
||||||
|
->with('success', 'TCP port updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(Site $site, TcpPort $tcpPort)
|
||||||
|
{
|
||||||
|
// Ensure the port belongs to the site and user
|
||||||
|
if ($tcpPort->site_id !== $site->id || $site->user_id !== Auth::id()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tcpPort->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('tcp-ports.index', $site)
|
||||||
|
->with('success', 'TCP port deleted successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Api;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class BrokenLinksIndexRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Api;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class SslHistoryRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Api;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UptimeIndexRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\DnsRecord;
|
||||||
|
use App\Notifications\DnsRecordAlert;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class CheckDnsRecord implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of times the job may be attempted.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $tries = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of seconds the job can run before timing out.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $timeout = 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public DnsRecord $dnsRecord
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Log::info('CheckDnsRecord: Starting check', [
|
||||||
|
'dns_record_id' => $this->dnsRecord->id,
|
||||||
|
'domain' => $this->dnsRecord->domain,
|
||||||
|
'record_type' => $this->dnsRecord->record_type,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Perform DNS lookup
|
||||||
|
$result = $this->performDnsLookup();
|
||||||
|
|
||||||
|
$wasValid = $this->dnsRecord->is_valid;
|
||||||
|
$isValid = $result['is_valid'];
|
||||||
|
$actualValue = $result['actual_value'];
|
||||||
|
$errorMessage = $result['error_message'];
|
||||||
|
|
||||||
|
// Update DNS record
|
||||||
|
$this->dnsRecord->update([
|
||||||
|
'last_checked_at' => now(),
|
||||||
|
'is_valid' => $isValid,
|
||||||
|
'actual_value' => $actualValue,
|
||||||
|
'error_message' => $errorMessage,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::info('CheckDnsRecord: Check completed', [
|
||||||
|
'dns_record_id' => $this->dnsRecord->id,
|
||||||
|
'is_valid' => $isValid,
|
||||||
|
'actual_value' => $actualValue,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Send notification if status changed from valid to invalid
|
||||||
|
if ($wasValid === true && $isValid === false) {
|
||||||
|
$this->dnsRecord->site->user->notify(new DnsRecordAlert(
|
||||||
|
$this->dnsRecord,
|
||||||
|
'invalid',
|
||||||
|
$actualValue,
|
||||||
|
$errorMessage
|
||||||
|
));
|
||||||
|
|
||||||
|
Log::warning('CheckDnsRecord: Invalid alert sent', [
|
||||||
|
'dns_record_id' => $this->dnsRecord->id,
|
||||||
|
'error_message' => $errorMessage,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notification when DNS is restored
|
||||||
|
if ($wasValid === false && $isValid === true) {
|
||||||
|
$this->dnsRecord->site->user->notify(new DnsRecordAlert(
|
||||||
|
$this->dnsRecord,
|
||||||
|
'valid',
|
||||||
|
$actualValue
|
||||||
|
));
|
||||||
|
|
||||||
|
Log::info('CheckDnsRecord: Valid alert sent', [
|
||||||
|
'dns_record_id' => $this->dnsRecord->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('CheckDnsRecord: Unexpected error', [
|
||||||
|
'dns_record_id' => $this->dnsRecord->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->dnsRecord->update([
|
||||||
|
'last_checked_at' => now(),
|
||||||
|
'is_valid' => false,
|
||||||
|
'error_message' => 'Unexpected error: ' . $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform DNS lookup and validation.
|
||||||
|
*/
|
||||||
|
protected function performDnsLookup(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Map record type to DNS_* constant
|
||||||
|
$dnsType = $this->getDnsTypeConstant($this->dnsRecord->record_type);
|
||||||
|
|
||||||
|
if ($dnsType === null) {
|
||||||
|
return [
|
||||||
|
'is_valid' => false,
|
||||||
|
'actual_value' => null,
|
||||||
|
'error_message' => "Unsupported DNS record type: {$this->dnsRecord->record_type}",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform DNS lookup
|
||||||
|
$records = @dns_get_record($this->dnsRecord->domain, $dnsType);
|
||||||
|
|
||||||
|
if ($records === false) {
|
||||||
|
$error = error_get_last();
|
||||||
|
return [
|
||||||
|
'is_valid' => false,
|
||||||
|
'actual_value' => null,
|
||||||
|
'error_message' => 'DNS lookup failed: ' . ($error['message'] ?? 'Unknown error'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($records)) {
|
||||||
|
return [
|
||||||
|
'is_valid' => false,
|
||||||
|
'actual_value' => null,
|
||||||
|
'error_message' => "No {$this->dnsRecord->record_type} records found for {$this->dnsRecord->domain}",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract values based on record type
|
||||||
|
$actualValues = $this->extractDnsValues($records, $this->dnsRecord->record_type);
|
||||||
|
|
||||||
|
// If no expected value, just return that records exist
|
||||||
|
if (empty($this->dnsRecord->expected_value)) {
|
||||||
|
return [
|
||||||
|
'is_valid' => true,
|
||||||
|
'actual_value' => $actualValues,
|
||||||
|
'error_message' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate against expected value
|
||||||
|
$isValid = $this->validateDnsValue($actualValues, $this->dnsRecord->expected_value);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'is_valid' => $isValid,
|
||||||
|
'actual_value' => $actualValues,
|
||||||
|
'error_message' => $isValid ? null : 'DNS record does not match expected value',
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return [
|
||||||
|
'is_valid' => false,
|
||||||
|
'actual_value' => null,
|
||||||
|
'error_message' => 'DNS lookup error: ' . $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get DNS type constant for dns_get_record().
|
||||||
|
*/
|
||||||
|
protected function getDnsTypeConstant(string $recordType): ?int
|
||||||
|
{
|
||||||
|
return match (strtoupper($recordType)) {
|
||||||
|
'A' => DNS_A,
|
||||||
|
'AAAA' => DNS_AAAA,
|
||||||
|
'CNAME' => DNS_CNAME,
|
||||||
|
'MX' => DNS_MX,
|
||||||
|
'TXT' => DNS_TXT,
|
||||||
|
'NS' => DNS_NS,
|
||||||
|
'SOA' => DNS_SOA,
|
||||||
|
'PTR' => DNS_PTR,
|
||||||
|
'SRV' => DNS_SRV,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract DNS values from records.
|
||||||
|
*/
|
||||||
|
protected function extractDnsValues(array $records, string $recordType): array
|
||||||
|
{
|
||||||
|
$values = [];
|
||||||
|
|
||||||
|
foreach ($records as $record) {
|
||||||
|
$value = match (strtoupper($recordType)) {
|
||||||
|
'A' => $record['ip'] ?? null,
|
||||||
|
'AAAA' => $record['ipv6'] ?? null,
|
||||||
|
'CNAME' => $record['target'] ?? null,
|
||||||
|
'MX' => isset($record['target']) ? "{$record['pri']} {$record['target']}" : null,
|
||||||
|
'TXT' => $record['txt'] ?? null,
|
||||||
|
'NS' => $record['target'] ?? null,
|
||||||
|
'SOA' => isset($record['mname']) ? $record['mname'] : null,
|
||||||
|
'PTR' => $record['target'] ?? null,
|
||||||
|
'SRV' => isset($record['target']) ? "{$record['pri']} {$record['weight']} {$record['port']} {$record['target']}" : null,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($value !== null) {
|
||||||
|
$values[] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate DNS value against expected value.
|
||||||
|
*/
|
||||||
|
protected function validateDnsValue(array $actualValues, string $expectedValue): bool
|
||||||
|
{
|
||||||
|
// Normalize expected value
|
||||||
|
$expectedValue = trim($expectedValue);
|
||||||
|
|
||||||
|
// Check if any actual value matches expected
|
||||||
|
foreach ($actualValues as $actualValue) {
|
||||||
|
// Exact match
|
||||||
|
if (strcasecmp($actualValue, $expectedValue) === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Partial match for MX and SRV records (check target only)
|
||||||
|
if (stripos($actualValue, $expectedValue) !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a job failure.
|
||||||
|
*/
|
||||||
|
public function failed(\Throwable $exception): void
|
||||||
|
{
|
||||||
|
Log::error('CheckDnsRecord: Job failed after all retries', [
|
||||||
|
'dns_record_id' => $this->dnsRecord->id,
|
||||||
|
'exception' => $exception->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\HealthCheck;
|
||||||
|
use App\Notifications\HealthCheckAlert;
|
||||||
|
use App\Notifications\PerformanceAlert;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Exception\ConnectException;
|
||||||
|
use GuzzleHttp\Exception\RequestException;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class CheckHealthEndpoint implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of times the job may be attempted.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $tries = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of seconds the job can run before timing out.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $timeout = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public HealthCheck $healthCheck
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$client = new Client([
|
||||||
|
'timeout' => 10,
|
||||||
|
'connect_timeout' => 5,
|
||||||
|
'allow_redirects' => [
|
||||||
|
'max' => 3,
|
||||||
|
'track_redirects' => true,
|
||||||
|
],
|
||||||
|
'http_errors' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$url = $this->healthCheck->full_url;
|
||||||
|
$startTime = microtime(true);
|
||||||
|
|
||||||
|
Log::info('CheckHealthEndpoint: Starting check', [
|
||||||
|
'health_check_id' => $this->healthCheck->id,
|
||||||
|
'url' => $url,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $client->get($url, [
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'User-Agent' => 'IsMySiteUp Health Monitor/1.0',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$endTime = microtime(true);
|
||||||
|
$responseTime = (int) round(($endTime - $startTime) * 1000);
|
||||||
|
$statusCode = $response->getStatusCode();
|
||||||
|
$body = (string) $response->getBody();
|
||||||
|
|
||||||
|
// Try to decode JSON
|
||||||
|
$responseData = null;
|
||||||
|
$isValidJson = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$responseData = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
$isValidJson = true;
|
||||||
|
} catch (\JsonException $e) {
|
||||||
|
Log::warning('CheckHealthEndpoint: Invalid JSON response', [
|
||||||
|
'health_check_id' => $this->healthCheck->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'body_preview' => substr($body, 0, 500),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if endpoint is healthy
|
||||||
|
$isHealthy = $this->determineHealth($statusCode, $isValidJson, $responseData);
|
||||||
|
|
||||||
|
$wasHealthy = $this->healthCheck->is_healthy;
|
||||||
|
|
||||||
|
// Update health check record
|
||||||
|
$this->healthCheck->update([
|
||||||
|
'last_checked_at' => now(),
|
||||||
|
'is_healthy' => $isHealthy,
|
||||||
|
'response_data' => $responseData,
|
||||||
|
'response_time_ms' => $responseTime,
|
||||||
|
'status_code' => $statusCode,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::info('CheckHealthEndpoint: Check completed', [
|
||||||
|
'health_check_id' => $this->healthCheck->id,
|
||||||
|
'is_healthy' => $isHealthy,
|
||||||
|
'status_code' => $statusCode,
|
||||||
|
'response_time_ms' => $responseTime,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Send notification if health status changed from healthy to unhealthy
|
||||||
|
if ($wasHealthy === true && $isHealthy === false) {
|
||||||
|
$this->healthCheck->site->user->notify(new HealthCheckAlert(
|
||||||
|
$this->healthCheck,
|
||||||
|
'unhealthy',
|
||||||
|
$statusCode,
|
||||||
|
$responseTime
|
||||||
|
));
|
||||||
|
|
||||||
|
Log::warning('CheckHealthEndpoint: Unhealthy alert sent', [
|
||||||
|
'health_check_id' => $this->healthCheck->id,
|
||||||
|
'status_code' => $statusCode,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally send notification when health is restored
|
||||||
|
if ($wasHealthy === false && $isHealthy === true) {
|
||||||
|
$this->healthCheck->site->user->notify(new HealthCheckAlert(
|
||||||
|
$this->healthCheck,
|
||||||
|
'healthy',
|
||||||
|
$statusCode,
|
||||||
|
$responseTime
|
||||||
|
));
|
||||||
|
|
||||||
|
Log::info('CheckHealthEndpoint: Healthy alert sent', [
|
||||||
|
'health_check_id' => $this->healthCheck->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check performance threshold and send alert if exceeded
|
||||||
|
if ($this->healthCheck->performance_threshold && $responseTime > $this->healthCheck->performance_threshold) {
|
||||||
|
$this->healthCheck->site->user->notify(new PerformanceAlert(
|
||||||
|
resourceType: 'Health Check',
|
||||||
|
resourceName: $this->healthCheck->endpoint,
|
||||||
|
responseTime: $responseTime,
|
||||||
|
threshold: $this->healthCheck->performance_threshold,
|
||||||
|
url: $this->healthCheck->full_url,
|
||||||
|
resourceId: $this->healthCheck->site_id
|
||||||
|
));
|
||||||
|
|
||||||
|
Log::warning('CheckHealthEndpoint: Performance threshold exceeded', [
|
||||||
|
'health_check_id' => $this->healthCheck->id,
|
||||||
|
'response_time_ms' => $responseTime,
|
||||||
|
'threshold_ms' => $this->healthCheck->performance_threshold,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (ConnectException $e) {
|
||||||
|
// Connection timeout or DNS failure
|
||||||
|
Log::error('CheckHealthEndpoint: Connection failed', [
|
||||||
|
'health_check_id' => $this->healthCheck->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->healthCheck->update([
|
||||||
|
'last_checked_at' => now(),
|
||||||
|
'is_healthy' => false,
|
||||||
|
'response_data' => ['error' => 'Connection failed: ' . $e->getMessage()],
|
||||||
|
'response_time_ms' => null,
|
||||||
|
'status_code' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (RequestException $e) {
|
||||||
|
// HTTP error occurred
|
||||||
|
$statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : null;
|
||||||
|
|
||||||
|
Log::error('CheckHealthEndpoint: Request failed', [
|
||||||
|
'health_check_id' => $this->healthCheck->id,
|
||||||
|
'status_code' => $statusCode,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->healthCheck->update([
|
||||||
|
'last_checked_at' => now(),
|
||||||
|
'is_healthy' => false,
|
||||||
|
'response_data' => ['error' => 'Request failed: ' . $e->getMessage()],
|
||||||
|
'response_time_ms' => null,
|
||||||
|
'status_code' => $statusCode,
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Any other exception
|
||||||
|
Log::error('CheckHealthEndpoint: Unexpected error', [
|
||||||
|
'health_check_id' => $this->healthCheck->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->healthCheck->update([
|
||||||
|
'last_checked_at' => now(),
|
||||||
|
'is_healthy' => false,
|
||||||
|
'response_data' => ['error' => 'Unexpected error: ' . $e->getMessage()],
|
||||||
|
'response_time_ms' => null,
|
||||||
|
'status_code' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the endpoint is healthy based on response.
|
||||||
|
*/
|
||||||
|
protected function determineHealth(int $statusCode, bool $isValidJson, ?array $responseData): bool
|
||||||
|
{
|
||||||
|
// Check if status code is successful (2xx)
|
||||||
|
if ($statusCode < 200 || $statusCode >= 300) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if response is valid JSON
|
||||||
|
if (!$isValidJson) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate expected JSON keys if configured
|
||||||
|
if (!empty($this->healthCheck->expected_json_keys)) {
|
||||||
|
if (!$this->healthCheck->validateResponseKeys($responseData ?? [])) {
|
||||||
|
Log::warning('CheckHealthEndpoint: Missing expected JSON keys', [
|
||||||
|
'health_check_id' => $this->healthCheck->id,
|
||||||
|
'expected_keys' => $this->healthCheck->expected_json_keys,
|
||||||
|
'response_keys' => array_keys($responseData ?? []),
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a job failure.
|
||||||
|
*/
|
||||||
|
public function failed(\Throwable $exception): void
|
||||||
|
{
|
||||||
|
Log::error('CheckHealthEndpoint: Job failed after all retries', [
|
||||||
|
'health_check_id' => $this->healthCheck->id,
|
||||||
|
'exception' => $exception->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\Site;
|
||||||
|
use App\Models\UptimeCheck;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class CheckSitePing implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of times the job may be attempted.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $tries = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of seconds the job can run before timing out.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $timeout = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public Site $site
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$host = $this->extractHost($this->site->url);
|
||||||
|
|
||||||
|
if (!$host) {
|
||||||
|
Log::error('CheckSitePing: Invalid URL', [
|
||||||
|
'site_id' => $this->site->id,
|
||||||
|
'url' => $this->site->url,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Record failed check due to invalid URL
|
||||||
|
UptimeCheck::create([
|
||||||
|
'site_id' => $this->site->id,
|
||||||
|
'check_type' => 'ping',
|
||||||
|
'status_code' => null,
|
||||||
|
'response_time_ms' => null,
|
||||||
|
'is_online' => false,
|
||||||
|
'checked_at' => now(),
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->ping($host);
|
||||||
|
|
||||||
|
Log::info('CheckSitePing: Raw result', [
|
||||||
|
'site_id' => $this->site->id,
|
||||||
|
'host' => $host,
|
||||||
|
'result' => $result,
|
||||||
|
]);
|
||||||
|
|
||||||
|
UptimeCheck::create([
|
||||||
|
'site_id' => $this->site->id,
|
||||||
|
'check_type' => 'ping',
|
||||||
|
'status_code' => null, // ICMP doesn't have HTTP status codes
|
||||||
|
'response_time_ms' => $result['avg_time'],
|
||||||
|
'is_online' => $result['is_online'],
|
||||||
|
'checked_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::info('CheckSitePing: Ping completed', [
|
||||||
|
'site_id' => $this->site->id,
|
||||||
|
'host' => $host,
|
||||||
|
'is_online' => $result['is_online'],
|
||||||
|
'avg_time' => $result['avg_time'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('CheckSitePing: Failed', [
|
||||||
|
'site_id' => $this->site->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Record failed check
|
||||||
|
UptimeCheck::create([
|
||||||
|
'site_id' => $this->site->id,
|
||||||
|
'check_type' => 'ping',
|
||||||
|
'status_code' => null,
|
||||||
|
'response_time_ms' => null,
|
||||||
|
'is_online' => false,
|
||||||
|
'checked_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract host from URL.
|
||||||
|
*/
|
||||||
|
protected function extractHost(string $url): ?string
|
||||||
|
{
|
||||||
|
$parsed = parse_url($url);
|
||||||
|
return $parsed['host'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute ping command and parse results.
|
||||||
|
*/
|
||||||
|
protected function ping(string $host): array
|
||||||
|
{
|
||||||
|
$isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
|
||||||
|
|
||||||
|
// Check if ping command exists
|
||||||
|
$pingPath = $this->findPingCommand($isWindows);
|
||||||
|
|
||||||
|
if (!$pingPath) {
|
||||||
|
// Ping command not found, use TCP connection as fallback
|
||||||
|
Log::warning('CheckSitePing: ping command not found, using TCP fallback', [
|
||||||
|
'site_id' => $this->site->id,
|
||||||
|
'host' => $host,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->tcpCheck($host);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build ping command based on OS
|
||||||
|
if ($isWindows) {
|
||||||
|
$command = sprintf('%s -n 4 %s 2>&1', $pingPath, escapeshellarg($host));
|
||||||
|
} else {
|
||||||
|
$command = sprintf('%s -c 4 -W 5 %s 2>&1', $pingPath, escapeshellarg($host));
|
||||||
|
}
|
||||||
|
|
||||||
|
$output = [];
|
||||||
|
$returnVar = 0;
|
||||||
|
|
||||||
|
exec($command, $output, $returnVar);
|
||||||
|
|
||||||
|
$isOnline = $returnVar === 0;
|
||||||
|
$avgTime = null;
|
||||||
|
|
||||||
|
// Parse average response time
|
||||||
|
if ($isOnline) {
|
||||||
|
$avgTime = $this->parseAverageTime($output, $isWindows);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'is_online' => $isOnline,
|
||||||
|
'avg_time' => $avgTime,
|
||||||
|
'output' => implode("\n", $output),
|
||||||
|
'return_code' => $returnVar,
|
||||||
|
'command' => $command,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find ping command path.
|
||||||
|
*/
|
||||||
|
protected function findPingCommand(bool $isWindows): ?string
|
||||||
|
{
|
||||||
|
if ($isWindows) {
|
||||||
|
return 'ping';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check common Linux locations
|
||||||
|
$locations = ['/bin/ping', '/usr/bin/ping', '/sbin/ping', '/usr/sbin/ping'];
|
||||||
|
|
||||||
|
foreach ($locations as $location) {
|
||||||
|
if (file_exists($location) && is_executable($location)) {
|
||||||
|
return $location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find using which
|
||||||
|
exec('which ping 2>&1', $whichOutput, $whichReturn);
|
||||||
|
if ($whichReturn === 0 && !empty($whichOutput[0])) {
|
||||||
|
return trim($whichOutput[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TCP connection check as fallback when ping is not available.
|
||||||
|
*/
|
||||||
|
protected function tcpCheck(string $host): array
|
||||||
|
{
|
||||||
|
$port = 80;
|
||||||
|
$timeout = 5;
|
||||||
|
$startTime = microtime(true);
|
||||||
|
|
||||||
|
$fp = @fsockopen($host, $port, $errno, $errstr, $timeout);
|
||||||
|
|
||||||
|
$endTime = microtime(true);
|
||||||
|
$responseTime = (int) round(($endTime - $startTime) * 1000);
|
||||||
|
|
||||||
|
$isOnline = $fp !== false;
|
||||||
|
|
||||||
|
if ($fp) {
|
||||||
|
fclose($fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'is_online' => $isOnline,
|
||||||
|
'avg_time' => $isOnline ? $responseTime : null,
|
||||||
|
'output' => $isOnline ? "TCP connection successful on port {$port}" : "TCP connection failed: {$errstr}",
|
||||||
|
'return_code' => $isOnline ? 0 : $errno,
|
||||||
|
'command' => "fsockopen({$host}, {$port}, {$timeout}s)",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse average response time from ping output.
|
||||||
|
*/
|
||||||
|
protected function parseAverageTime(array $output, bool $isWindows): ?int
|
||||||
|
{
|
||||||
|
$outputText = implode("\n", $output);
|
||||||
|
|
||||||
|
if ($isWindows) {
|
||||||
|
// Windows format: "Average = 23ms"
|
||||||
|
if (preg_match('/Average\s*=\s*(\d+)ms/i', $outputText, $matches)) {
|
||||||
|
return (int) $matches[1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Linux format: "rtt min/avg/max/mdev = 12.345/23.456/34.567/8.901 ms"
|
||||||
|
if (preg_match('/rtt min\/avg\/max\/mdev = [\d.]+\/([\d.]+)\/[\d.]+\/[\d.]+ ms/i', $outputText, $matches)) {
|
||||||
|
return (int) round((float) $matches[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alternative Linux format: "round-trip min/avg/max = 12.345/23.456/34.567 ms"
|
||||||
|
if (preg_match('/round-trip min\/avg\/max = [\d.]+\/([\d.]+)\/[\d.]+ ms/i', $outputText, $matches)) {
|
||||||
|
return (int) round((float) $matches[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a job failure.
|
||||||
|
*/
|
||||||
|
public function failed(\Throwable $exception): void
|
||||||
|
{
|
||||||
|
Log::error('CheckSitePing: Job failed after all retries', [
|
||||||
|
'site_id' => $this->site->id,
|
||||||
|
'exception' => $exception->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\TcpPort;
|
||||||
|
use App\Notifications\TcpPortAlert;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class CheckTcpPort implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of times the job may be attempted.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $tries = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of seconds the job can run before timing out.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $timeout = 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public TcpPort $tcpPort
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$host = $this->extractHost($this->tcpPort->site->url);
|
||||||
|
|
||||||
|
if (!$host) {
|
||||||
|
Log::error('CheckTcpPort: Invalid URL', [
|
||||||
|
'tcp_port_id' => $this->tcpPort->id,
|
||||||
|
'site_id' => $this->tcpPort->site_id,
|
||||||
|
'url' => $this->tcpPort->site->url,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->tcpPort->update([
|
||||||
|
'is_open' => false,
|
||||||
|
'response_time_ms' => null,
|
||||||
|
'last_checked_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->checkPort($host, $this->tcpPort->port);
|
||||||
|
|
||||||
|
$wasOpen = $this->tcpPort->is_open;
|
||||||
|
|
||||||
|
$this->tcpPort->update([
|
||||||
|
'is_open' => $result['is_open'],
|
||||||
|
'response_time_ms' => $result['response_time'],
|
||||||
|
'last_checked_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::info('CheckTcpPort: Check completed', [
|
||||||
|
'tcp_port_id' => $this->tcpPort->id,
|
||||||
|
'site_id' => $this->tcpPort->site_id,
|
||||||
|
'host' => $host,
|
||||||
|
'port' => $this->tcpPort->port,
|
||||||
|
'is_open' => $result['is_open'],
|
||||||
|
'response_time_ms' => $result['response_time'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Send notification if port status changed from open to closed
|
||||||
|
if ($wasOpen === true && $result['is_open'] === false) {
|
||||||
|
$this->tcpPort->site->user->notify(new TcpPortAlert(
|
||||||
|
$this->tcpPort,
|
||||||
|
'closed'
|
||||||
|
));
|
||||||
|
|
||||||
|
Log::warning('CheckTcpPort: Port closed alert sent', [
|
||||||
|
'tcp_port_id' => $this->tcpPort->id,
|
||||||
|
'port' => $this->tcpPort->port,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally send notification if port opened
|
||||||
|
if ($wasOpen === false && $result['is_open'] === true) {
|
||||||
|
$this->tcpPort->site->user->notify(new TcpPortAlert(
|
||||||
|
$this->tcpPort,
|
||||||
|
'opened'
|
||||||
|
));
|
||||||
|
|
||||||
|
Log::info('CheckTcpPort: Port opened alert sent', [
|
||||||
|
'tcp_port_id' => $this->tcpPort->id,
|
||||||
|
'port' => $this->tcpPort->port,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('CheckTcpPort: Failed', [
|
||||||
|
'tcp_port_id' => $this->tcpPort->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->tcpPort->update([
|
||||||
|
'is_open' => false,
|
||||||
|
'response_time_ms' => null,
|
||||||
|
'last_checked_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract host from URL.
|
||||||
|
*/
|
||||||
|
protected function extractHost(string $url): ?string
|
||||||
|
{
|
||||||
|
$parsed = parse_url($url);
|
||||||
|
return $parsed['host'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a TCP port is open.
|
||||||
|
*/
|
||||||
|
protected function checkPort(string $host, int $port): array
|
||||||
|
{
|
||||||
|
$timeout = 5;
|
||||||
|
$startTime = microtime(true);
|
||||||
|
|
||||||
|
$errno = 0;
|
||||||
|
$errstr = '';
|
||||||
|
|
||||||
|
// Suppress warnings and capture errors
|
||||||
|
$fp = @fsockopen($host, $port, $errno, $errstr, $timeout);
|
||||||
|
|
||||||
|
$endTime = microtime(true);
|
||||||
|
$responseTime = (int) round(($endTime - $startTime) * 1000);
|
||||||
|
|
||||||
|
$isOpen = $fp !== false;
|
||||||
|
|
||||||
|
if ($fp) {
|
||||||
|
fclose($fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$isOpen) {
|
||||||
|
Log::debug('CheckTcpPort: Port check failed', [
|
||||||
|
'host' => $host,
|
||||||
|
'port' => $port,
|
||||||
|
'errno' => $errno,
|
||||||
|
'errstr' => $errstr,
|
||||||
|
'response_time_ms' => $responseTime,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'is_open' => $isOpen,
|
||||||
|
'response_time' => $isOpen ? $responseTime : null,
|
||||||
|
'error_code' => $errno,
|
||||||
|
'error_message' => $errstr,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a job failure.
|
||||||
|
*/
|
||||||
|
public function failed(\Throwable $exception): void
|
||||||
|
{
|
||||||
|
Log::error('CheckTcpPort: Job failed after all retries', [
|
||||||
|
'tcp_port_id' => $this->tcpPort->id,
|
||||||
|
'exception' => $exception->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -87,8 +87,10 @@ class CheckUptime implements ShouldQueue
|
|||||||
// Store the uptime check result
|
// Store the uptime check result
|
||||||
UptimeCheck::create([
|
UptimeCheck::create([
|
||||||
'site_id' => $this->site->id,
|
'site_id' => $this->site->id,
|
||||||
|
'check_type' => 'http',
|
||||||
'status_code' => $statusCode,
|
'status_code' => $statusCode,
|
||||||
'response_time_ms' => $responseTime,
|
'response_time_ms' => $responseTime,
|
||||||
|
'is_online' => $statusCode !== null && $statusCode < 400,
|
||||||
'checked_at' => now(),
|
'checked_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class DnsRecord extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'site_id',
|
||||||
|
'domain',
|
||||||
|
'record_type',
|
||||||
|
'expected_value',
|
||||||
|
'last_checked_at',
|
||||||
|
'is_valid',
|
||||||
|
'actual_value',
|
||||||
|
'error_message',
|
||||||
|
'is_active',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'last_checked_at' => 'datetime',
|
||||||
|
'is_valid' => 'boolean',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'actual_value' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available DNS record types.
|
||||||
|
*/
|
||||||
|
public static function getRecordTypes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'A' => 'A (IPv4 Address)',
|
||||||
|
'AAAA' => 'AAAA (IPv6 Address)',
|
||||||
|
'CNAME' => 'CNAME (Canonical Name)',
|
||||||
|
'MX' => 'MX (Mail Exchange)',
|
||||||
|
'TXT' => 'TXT (Text Record)',
|
||||||
|
'NS' => 'NS (Name Server)',
|
||||||
|
'SOA' => 'SOA (Start of Authority)',
|
||||||
|
'PTR' => 'PTR (Pointer)',
|
||||||
|
'SRV' => 'SRV (Service)',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the site that owns the DNS record.
|
||||||
|
*/
|
||||||
|
public function site(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Site::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter active DNS records.
|
||||||
|
*/
|
||||||
|
public function scopeActive($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_active', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter valid records.
|
||||||
|
*/
|
||||||
|
public function scopeValid($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_valid', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter invalid records.
|
||||||
|
*/
|
||||||
|
public function scopeInvalid($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_valid', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status badge color.
|
||||||
|
*/
|
||||||
|
public function getStatusColorAttribute(): string
|
||||||
|
{
|
||||||
|
if ($this->is_valid === null) {
|
||||||
|
return 'secondary';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->is_valid ? 'success' : 'danger';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status text.
|
||||||
|
*/
|
||||||
|
public function getStatusTextAttribute(): string
|
||||||
|
{
|
||||||
|
if ($this->is_valid === null) {
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->is_valid ? 'Valid' : 'Invalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get display name for the record.
|
||||||
|
*/
|
||||||
|
public function getDisplayNameAttribute(): string
|
||||||
|
{
|
||||||
|
return "{$this->record_type} - {$this->domain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get formatted actual value for display.
|
||||||
|
*/
|
||||||
|
public function getFormattedActualValueAttribute(): ?string
|
||||||
|
{
|
||||||
|
if (!$this->actual_value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($this->actual_value)) {
|
||||||
|
if (count($this->actual_value) === 1) {
|
||||||
|
return $this->actual_value[0];
|
||||||
|
}
|
||||||
|
return implode(', ', $this->actual_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->actual_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class HealthCheck extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'site_id',
|
||||||
|
'endpoint',
|
||||||
|
'expected_json_keys',
|
||||||
|
'last_checked_at',
|
||||||
|
'is_healthy',
|
||||||
|
'response_data',
|
||||||
|
'response_time_ms',
|
||||||
|
'performance_threshold',
|
||||||
|
'status_code',
|
||||||
|
'is_active',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'expected_json_keys' => 'array',
|
||||||
|
'response_data' => 'array',
|
||||||
|
'last_checked_at' => 'datetime',
|
||||||
|
'is_healthy' => 'boolean',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the site that owns the health check.
|
||||||
|
*/
|
||||||
|
public function site(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Site::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter active health checks.
|
||||||
|
*/
|
||||||
|
public function scopeActive($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_active', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter healthy checks.
|
||||||
|
*/
|
||||||
|
public function scopeHealthy($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_healthy', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter unhealthy checks.
|
||||||
|
*/
|
||||||
|
public function scopeUnhealthy($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_healthy', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the full URL for the health check endpoint.
|
||||||
|
*/
|
||||||
|
public function getFullUrlAttribute(): string
|
||||||
|
{
|
||||||
|
$siteUrl = rtrim($this->site->url, '/');
|
||||||
|
$endpoint = ltrim($this->endpoint, '/');
|
||||||
|
|
||||||
|
return $siteUrl . '/' . $endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status badge color.
|
||||||
|
*/
|
||||||
|
public function getStatusColorAttribute(): string
|
||||||
|
{
|
||||||
|
if ($this->is_healthy === null) {
|
||||||
|
return 'secondary';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->is_healthy ? 'success' : 'danger';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status text.
|
||||||
|
*/
|
||||||
|
public function getStatusTextAttribute(): string
|
||||||
|
{
|
||||||
|
if ($this->is_healthy === null) {
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->is_healthy ? 'Healthy' : 'Unhealthy';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get display name for the endpoint.
|
||||||
|
*/
|
||||||
|
public function getDisplayNameAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->endpoint ?: 'Health Check';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if response contains all expected keys.
|
||||||
|
*/
|
||||||
|
public function validateResponseKeys(array $responseData): bool
|
||||||
|
{
|
||||||
|
if (empty($this->expected_json_keys)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->expected_json_keys as $key) {
|
||||||
|
if (!$this->hasNestedKey($responseData, $key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if nested key exists in array using dot notation.
|
||||||
|
*/
|
||||||
|
protected function hasNestedKey(array $array, string $key): bool
|
||||||
|
{
|
||||||
|
if (array_key_exists($key, $array)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (explode('.', $key) as $segment) {
|
||||||
|
if (!is_array($array) || !array_key_exists($segment, $array)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$array = $array[$segment];
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,4 +38,19 @@ class Site extends Model
|
|||||||
{
|
{
|
||||||
return $this->hasMany(BrokenLink::class);
|
return $this->hasMany(BrokenLink::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tcpPorts(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(TcpPort::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function healthChecks(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(HealthCheck::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dnsRecords(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(DnsRecord::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class TcpPort extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'site_id',
|
||||||
|
'port',
|
||||||
|
'name',
|
||||||
|
'is_active',
|
||||||
|
'last_checked_at',
|
||||||
|
'is_open',
|
||||||
|
'response_time_ms',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'is_open' => 'boolean',
|
||||||
|
'last_checked_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the site that owns the TCP port.
|
||||||
|
*/
|
||||||
|
public function site(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Site::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter active ports.
|
||||||
|
*/
|
||||||
|
public function scopeActive($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_active', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter open ports.
|
||||||
|
*/
|
||||||
|
public function scopeOpen($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_open', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter closed ports.
|
||||||
|
*/
|
||||||
|
public function scopeClosed($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_open', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get common port names.
|
||||||
|
*/
|
||||||
|
public static function getCommonPorts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
21 => 'FTP',
|
||||||
|
22 => 'SSH',
|
||||||
|
23 => 'Telnet',
|
||||||
|
25 => 'SMTP',
|
||||||
|
53 => 'DNS',
|
||||||
|
80 => 'HTTP',
|
||||||
|
110 => 'POP3',
|
||||||
|
143 => 'IMAP',
|
||||||
|
443 => 'HTTPS',
|
||||||
|
465 => 'SMTPS',
|
||||||
|
587 => 'SMTP (Submission)',
|
||||||
|
993 => 'IMAPS',
|
||||||
|
995 => 'POP3S',
|
||||||
|
3306 => 'MySQL',
|
||||||
|
3389 => 'RDP',
|
||||||
|
5432 => 'PostgreSQL',
|
||||||
|
6379 => 'Redis',
|
||||||
|
8080 => 'HTTP Alternate',
|
||||||
|
8443 => 'HTTPS Alternate',
|
||||||
|
27017 => 'MongoDB',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get display name for the port.
|
||||||
|
*/
|
||||||
|
public function getDisplayNameAttribute(): string
|
||||||
|
{
|
||||||
|
if ($this->name) {
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
$commonPorts = self::getCommonPorts();
|
||||||
|
return $commonPorts[$this->port] ?? "Port {$this->port}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status badge color.
|
||||||
|
*/
|
||||||
|
public function getStatusColorAttribute(): string
|
||||||
|
{
|
||||||
|
if ($this->is_open === null) {
|
||||||
|
return 'secondary';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->is_open ? 'success' : 'danger';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status text.
|
||||||
|
*/
|
||||||
|
public function getStatusTextAttribute(): string
|
||||||
|
{
|
||||||
|
if ($this->is_open === null) {
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->is_open ? 'Open' : 'Closed';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,15 +11,42 @@ class UptimeCheck extends Model
|
|||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'site_id',
|
'site_id',
|
||||||
|
'check_type',
|
||||||
'status_code',
|
'status_code',
|
||||||
'response_time_ms',
|
'response_time_ms',
|
||||||
|
'is_online',
|
||||||
'checked_at',
|
'checked_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'checked_at' => 'datetime',
|
'checked_at' => 'datetime',
|
||||||
|
'is_online' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter by check type.
|
||||||
|
*/
|
||||||
|
public function scopeOfType($query, string $type)
|
||||||
|
{
|
||||||
|
return $query->where('check_type', $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter HTTP checks.
|
||||||
|
*/
|
||||||
|
public function scopeHttpChecks($query)
|
||||||
|
{
|
||||||
|
return $query->where('check_type', 'http');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope to filter ping checks.
|
||||||
|
*/
|
||||||
|
public function scopePingChecks($query)
|
||||||
|
{
|
||||||
|
return $query->where('check_type', 'ping');
|
||||||
|
}
|
||||||
|
|
||||||
public function site(): BelongsTo
|
public function site(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Site::class);
|
return $this->belongsTo(Site::class);
|
||||||
|
|||||||
+2
-1
@@ -7,11 +7,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable;
|
use HasFactory, Notifiable, HasApiTokens;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use App\Models\DnsRecord;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
|
||||||
|
class DnsRecordAlert extends Notification implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new notification instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public DnsRecord $dnsRecord,
|
||||||
|
public string $status,
|
||||||
|
public ?array $actualValue = null,
|
||||||
|
public ?string $errorMessage = null
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification's delivery channels.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return ['mail', 'database'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the mail representation of the notification.
|
||||||
|
*/
|
||||||
|
public function toMail(object $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
$subject = $this->status === 'invalid'
|
||||||
|
? "DNS Record Invalid: {$this->dnsRecord->domain}"
|
||||||
|
: "DNS Record Restored: {$this->dnsRecord->domain}";
|
||||||
|
|
||||||
|
$message = (new MailMessage)
|
||||||
|
->subject($subject);
|
||||||
|
|
||||||
|
if ($this->status === 'invalid') {
|
||||||
|
$message->error()
|
||||||
|
->line("DNS record validation failed for {$this->dnsRecord->record_type} record of {$this->dnsRecord->domain}")
|
||||||
|
->line("This could indicate a DNS configuration issue or propagation delay.");
|
||||||
|
|
||||||
|
if ($this->errorMessage) {
|
||||||
|
$message->line("Error: {$this->errorMessage}");
|
||||||
|
} elseif ($this->actualValue && $this->dnsRecord->expected_value) {
|
||||||
|
$actualFormatted = is_array($this->actualValue) ? implode(', ', $this->actualValue) : $this->actualValue;
|
||||||
|
$message->line("Expected: {$this->dnsRecord->expected_value}")
|
||||||
|
->line("Actual: {$actualFormatted}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$message->success()
|
||||||
|
->line("DNS record for {$this->dnsRecord->record_type} of {$this->dnsRecord->domain} is now valid.")
|
||||||
|
->line("The DNS configuration has been restored.");
|
||||||
|
|
||||||
|
if ($this->actualValue) {
|
||||||
|
$actualFormatted = is_array($this->actualValue) ? implode(', ', $this->actualValue) : $this->actualValue;
|
||||||
|
$message->line("Current value: {$actualFormatted}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $message
|
||||||
|
->line("Domain: {$this->dnsRecord->domain}")
|
||||||
|
->line("Record Type: {$this->dnsRecord->record_type}")
|
||||||
|
->line("Checked at: {$this->dnsRecord->last_checked_at->format('Y-m-d H:i:s')}")
|
||||||
|
->action('View Details', url("/sites/{$this->dnsRecord->site_id}/dns-records/{$this->dnsRecord->id}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the array representation of the notification.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(object $notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'dns_record',
|
||||||
|
'dns_record_id' => $this->dnsRecord->id,
|
||||||
|
'site_id' => $this->dnsRecord->site_id,
|
||||||
|
'site_name' => $this->dnsRecord->site->name,
|
||||||
|
'domain' => $this->dnsRecord->domain,
|
||||||
|
'record_type' => $this->dnsRecord->record_type,
|
||||||
|
'status' => $this->status,
|
||||||
|
'is_valid' => $this->dnsRecord->is_valid,
|
||||||
|
'expected_value' => $this->dnsRecord->expected_value,
|
||||||
|
'actual_value' => $this->actualValue,
|
||||||
|
'error_message' => $this->errorMessage,
|
||||||
|
'checked_at' => $this->dnsRecord->last_checked_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use App\Models\HealthCheck;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
|
||||||
|
class HealthCheckAlert extends Notification implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new notification instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public HealthCheck $healthCheck,
|
||||||
|
public string $status,
|
||||||
|
public ?int $statusCode = null,
|
||||||
|
public ?int $responseTime = null
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification's delivery channels.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return ['mail', 'database'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the mail representation of the notification.
|
||||||
|
*/
|
||||||
|
public function toMail(object $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
$subject = $this->status === 'unhealthy'
|
||||||
|
? "Health Check Failed: {$this->healthCheck->endpoint}"
|
||||||
|
: "Health Check Restored: {$this->healthCheck->endpoint}";
|
||||||
|
|
||||||
|
$message = (new MailMessage)
|
||||||
|
->subject($subject)
|
||||||
|
->line("Health check alert for {$this->healthCheck->site->name}");
|
||||||
|
|
||||||
|
if ($this->status === 'unhealthy') {
|
||||||
|
$message->error()
|
||||||
|
->line("The health endpoint {$this->healthCheck->endpoint} is reporting as unhealthy.")
|
||||||
|
->line("This could indicate an application issue or service degradation.");
|
||||||
|
|
||||||
|
if ($this->statusCode) {
|
||||||
|
$message->line("HTTP Status Code: {$this->statusCode}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$message->success()
|
||||||
|
->line("The health endpoint {$this->healthCheck->endpoint} is now healthy.")
|
||||||
|
->line("The service has been restored.");
|
||||||
|
|
||||||
|
if ($this->statusCode) {
|
||||||
|
$message->line("HTTP Status Code: {$this->statusCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->responseTime) {
|
||||||
|
$message->line("Response Time: {$this->responseTime}ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $message
|
||||||
|
->line("Endpoint: {$this->healthCheck->full_url}")
|
||||||
|
->line("Checked at: {$this->healthCheck->last_checked_at->format('Y-m-d H:i:s')}")
|
||||||
|
->action('View Details', url("/sites/{$this->healthCheck->site_id}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the array representation of the notification.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(object $notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'health_check',
|
||||||
|
'health_check_id' => $this->healthCheck->id,
|
||||||
|
'site_id' => $this->healthCheck->site_id,
|
||||||
|
'site_name' => $this->healthCheck->site->name,
|
||||||
|
'endpoint' => $this->healthCheck->endpoint,
|
||||||
|
'full_url' => $this->healthCheck->full_url,
|
||||||
|
'status' => $this->status,
|
||||||
|
'is_healthy' => $this->healthCheck->is_healthy,
|
||||||
|
'status_code' => $this->statusCode,
|
||||||
|
'response_time_ms' => $this->responseTime,
|
||||||
|
'checked_at' => $this->healthCheck->last_checked_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use App\Models\HealthCheck;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
|
||||||
|
class PerformanceAlert extends Notification implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new notification instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public string $resourceType,
|
||||||
|
public string $resourceName,
|
||||||
|
public int $responseTime,
|
||||||
|
public int $threshold,
|
||||||
|
public ?string $url = null,
|
||||||
|
public ?int $resourceId = null
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification's delivery channels.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return ['mail', 'database'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the mail representation of the notification.
|
||||||
|
*/
|
||||||
|
public function toMail(object $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
$subject = "Performance Alert: {$this->resourceName}";
|
||||||
|
$percentageOver = round((($this->responseTime - $this->threshold) / $this->threshold) * 100);
|
||||||
|
|
||||||
|
$message = (new MailMessage)
|
||||||
|
->subject($subject)
|
||||||
|
->warning()
|
||||||
|
->line("Performance threshold exceeded for {$this->resourceType}: {$this->resourceName}")
|
||||||
|
->line("Response time ({$this->responseTime}ms) exceeded the threshold of {$this->threshold}ms by {$percentageOver}%.")
|
||||||
|
->line("This could indicate a performance degradation or resource constraint.");
|
||||||
|
|
||||||
|
$message->line("Response Time: {$this->responseTime}ms")
|
||||||
|
->line("Threshold: {$this->threshold}ms")
|
||||||
|
->line("Difference: " . ($this->responseTime - $this->threshold) . "ms");
|
||||||
|
|
||||||
|
if ($this->url) {
|
||||||
|
$message->line("URL: {$this->url}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->resourceId) {
|
||||||
|
$actionUrl = match($this->resourceType) {
|
||||||
|
'Health Check' => url("/sites/{$this->resourceId}"),
|
||||||
|
default => url('/dashboard'),
|
||||||
|
};
|
||||||
|
$message->action('View Details', $actionUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the array representation of the notification.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(object $notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'performance_alert',
|
||||||
|
'resource_type' => $this->resourceType,
|
||||||
|
'resource_name' => $this->resourceName,
|
||||||
|
'resource_id' => $this->resourceId,
|
||||||
|
'response_time_ms' => $this->responseTime,
|
||||||
|
'threshold_ms' => $this->threshold,
|
||||||
|
'difference_ms' => $this->responseTime - $this->threshold,
|
||||||
|
'percentage_over' => round((($this->responseTime - $this->threshold) / $this->threshold) * 100, 2),
|
||||||
|
'url' => $this->url,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use App\Models\TcpPort;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
|
||||||
|
class TcpPortAlert extends Notification implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new notification instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public TcpPort $tcpPort,
|
||||||
|
public string $status
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification's delivery channels.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return ['mail', 'database'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the mail representation of the notification.
|
||||||
|
*/
|
||||||
|
public function toMail(object $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
$subject = $this->status === 'closed'
|
||||||
|
? "TCP Port {$this->tcpPort->port} is Closed"
|
||||||
|
: "TCP Port {$this->tcpPort->port} is Now Open";
|
||||||
|
|
||||||
|
$message = (new MailMessage)
|
||||||
|
->subject($subject)
|
||||||
|
->line("Port monitoring alert for {$this->tcpPort->site->name}");
|
||||||
|
|
||||||
|
if ($this->status === 'closed') {
|
||||||
|
$message->error()
|
||||||
|
->line("Port {$this->tcpPort->display_name} ({$this->tcpPort->port}) is currently closed.")
|
||||||
|
->line("This could indicate a service outage or firewall issue.");
|
||||||
|
} else {
|
||||||
|
$message->success()
|
||||||
|
->line("Port {$this->tcpPort->display_name} ({$this->tcpPort->port}) is now accessible.")
|
||||||
|
->line("The service has been restored.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $message
|
||||||
|
->line("Site: {$this->tcpPort->site->url}")
|
||||||
|
->line("Checked at: {$this->tcpPort->last_checked_at->format('Y-m-d H:i:s')}")
|
||||||
|
->action('View Details', url("/sites/{$this->tcpPort->site_id}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the array representation of the notification.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(object $notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'tcp_port',
|
||||||
|
'tcp_port_id' => $this->tcpPort->id,
|
||||||
|
'site_id' => $this->tcpPort->site_id,
|
||||||
|
'site_name' => $this->tcpPort->site->name,
|
||||||
|
'port' => $this->tcpPort->port,
|
||||||
|
'port_name' => $this->tcpPort->display_name,
|
||||||
|
'status' => $this->status,
|
||||||
|
'is_open' => $this->tcpPort->is_open,
|
||||||
|
'checked_at' => $this->tcpPort->last_checked_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ use Illuminate\Foundation\Configuration\Middleware;
|
|||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
web: __DIR__.'/../routes/web.php',
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
api: __DIR__.'/../routes/api.php',
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"guzzlehttp/guzzle": "^7.0",
|
"guzzlehttp/guzzle": "^7.0",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/sanctum": "^4.2",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
"laravel/ui": "^4.6",
|
"laravel/ui": "^4.6",
|
||||||
"spatie/crawler": "^8.4"
|
"spatie/crawler": "^8.4"
|
||||||
|
|||||||
Generated
+65
-2
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "d9587b74374c571fcfc09447a91fcb15",
|
"content-hash": "88cbc7f24cd10b72bef514dc03a4dfd8",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@@ -1333,6 +1333,69 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-11-21T20:52:52+00:00"
|
"time": "2025-11-21T20:52:52+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "laravel/sanctum",
|
||||||
|
"version": "v4.2.3",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/laravel/sanctum.git",
|
||||||
|
"reference": "47d26f1d310879ff757b971f5a6fc631d18663fd"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/laravel/sanctum/zipball/47d26f1d310879ff757b971f5a6fc631d18663fd",
|
||||||
|
"reference": "47d26f1d310879ff757b971f5a6fc631d18663fd",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"illuminate/console": "^11.0|^12.0",
|
||||||
|
"illuminate/contracts": "^11.0|^12.0",
|
||||||
|
"illuminate/database": "^11.0|^12.0",
|
||||||
|
"illuminate/support": "^11.0|^12.0",
|
||||||
|
"php": "^8.2",
|
||||||
|
"symfony/console": "^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"orchestra/testbench": "^9.15|^10.8",
|
||||||
|
"phpstan/phpstan": "^1.10"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Laravel\\Sanctum\\SanctumServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Laravel\\Sanctum\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Taylor Otwell",
|
||||||
|
"email": "taylor@laravel.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
|
||||||
|
"keywords": [
|
||||||
|
"auth",
|
||||||
|
"laravel",
|
||||||
|
"sanctum"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/laravel/sanctum/issues",
|
||||||
|
"source": "https://github.com/laravel/sanctum"
|
||||||
|
},
|
||||||
|
"time": "2026-01-11T18:20:25+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/serializable-closure",
|
"name": "laravel/serializable-closure",
|
||||||
"version": "v2.0.7",
|
"version": "v2.0.7",
|
||||||
@@ -9786,5 +9849,5 @@
|
|||||||
"php": "^8.2"
|
"php": "^8.2"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.9.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Stateful Domains
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Requests from the following domains / hosts will receive stateful API
|
||||||
|
| authentication cookies. Typically, these should include your local
|
||||||
|
| and production domains which access your API via a frontend SPA.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||||
|
'%s%s',
|
||||||
|
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||||
|
Sanctum::currentApplicationUrlWithPort(),
|
||||||
|
// Sanctum::currentRequestHost(),
|
||||||
|
))),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Sanctum Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This array contains the authentication guards that will be checked when
|
||||||
|
| Sanctum is trying to authenticate a request. If none of these guards
|
||||||
|
| are able to authenticate the request, Sanctum will use the bearer
|
||||||
|
| token that's present on an incoming request for authentication.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guard' => ['web'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Expiration Minutes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value controls the number of minutes until an issued token will be
|
||||||
|
| considered expired. This will override any values set in the token's
|
||||||
|
| "expires_at" attribute, but first-party sessions are not affected.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'expiration' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Token Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Sanctum can prefix new tokens in order to take advantage of numerous
|
||||||
|
| security scanning initiatives maintained by open source platforms
|
||||||
|
| that notify developers if they commit tokens into repositories.
|
||||||
|
|
|
||||||
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Sanctum Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When authenticating your first-party SPA with Sanctum you may need to
|
||||||
|
| customize some of the middleware Sanctum uses while processing the
|
||||||
|
| request. You may change the middleware listed below as required.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'middleware' => [
|
||||||
|
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
||||||
|
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||||
|
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->morphs('tokenable');
|
||||||
|
$table->text('name');
|
||||||
|
$table->string('token', 64)->unique();
|
||||||
|
$table->text('abilities')->nullable();
|
||||||
|
$table->timestamp('last_used_at')->nullable();
|
||||||
|
$table->timestamp('expires_at')->nullable()->index();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('personal_access_tokens');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('uptime_checks', function (Blueprint $table) {
|
||||||
|
$table->boolean('is_online')->nullable()->after('response_time_ms');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('uptime_checks', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('is_online');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('uptime_checks', function (Blueprint $table) {
|
||||||
|
$table->string('check_type', 10)->default('http')->after('site_id')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('uptime_checks', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('check_type');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('tcp_ports', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('site_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->integer('port');
|
||||||
|
$table->string('name')->nullable();
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->timestamp('last_checked_at')->nullable();
|
||||||
|
$table->boolean('is_open')->nullable();
|
||||||
|
$table->integer('response_time_ms')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Indexes
|
||||||
|
$table->index('site_id');
|
||||||
|
$table->index('is_active');
|
||||||
|
$table->index(['site_id', 'port']);
|
||||||
|
$table->index('last_checked_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('tcp_ports');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('health_checks', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('site_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->string('endpoint');
|
||||||
|
$table->json('expected_json_keys')->nullable();
|
||||||
|
$table->timestamp('last_checked_at')->nullable();
|
||||||
|
$table->boolean('is_healthy')->nullable();
|
||||||
|
$table->json('response_data')->nullable();
|
||||||
|
$table->integer('response_time_ms')->nullable();
|
||||||
|
$table->integer('status_code')->nullable();
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Indexes
|
||||||
|
$table->index('site_id');
|
||||||
|
$table->index('is_active');
|
||||||
|
$table->index('is_healthy');
|
||||||
|
$table->index('last_checked_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('health_checks');
|
||||||
|
}
|
||||||
|
};
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('health_checks', function (Blueprint $table) {
|
||||||
|
$table->integer('performance_threshold')->nullable()->after('response_time_ms');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('health_checks', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('performance_threshold');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('dns_records', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('site_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->string('domain');
|
||||||
|
$table->string('record_type'); // A, AAAA, MX, CNAME, TXT, NS, SOA
|
||||||
|
$table->text('expected_value')->nullable();
|
||||||
|
$table->timestamp('last_checked_at')->nullable();
|
||||||
|
$table->boolean('is_valid')->nullable();
|
||||||
|
$table->json('actual_value')->nullable();
|
||||||
|
$table->text('error_message')->nullable();
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Indexes
|
||||||
|
$table->index('site_id');
|
||||||
|
$table->index('is_active');
|
||||||
|
$table->index('is_valid');
|
||||||
|
$table->index('last_checked_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('dns_records');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Add DNS Record - ' . $site->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-3">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('sites.index') }}">Sites</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('dashboard.show', $site) }}">{{ $site->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('dns-records.index', $site) }}">DNS Records</a></li>
|
||||||
|
<li class="breadcrumb-item active">Add DNS Record</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h1 class="mb-4"><i class="bi bi-plus-circle"></i> Add DNS Record</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('dns-records.store', $site) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="domain" class="form-label">Domain Name</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control @error('domain') is-invalid @enderror"
|
||||||
|
id="domain"
|
||||||
|
name="domain"
|
||||||
|
value="{{ old('domain') }}"
|
||||||
|
placeholder="example.com"
|
||||||
|
required>
|
||||||
|
@error('domain')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
<div class="form-text">Enter the domain name to monitor (e.g., example.com, subdomain.example.com)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="record_type" class="form-label">Record Type</label>
|
||||||
|
<select class="form-select @error('record_type') is-invalid @enderror"
|
||||||
|
id="record_type"
|
||||||
|
name="record_type"
|
||||||
|
required>
|
||||||
|
<option value="">Select record type...</option>
|
||||||
|
@foreach($recordTypes as $value => $label)
|
||||||
|
<option value="{{ $value }}" {{ old('record_type') == $value ? 'selected' : '' }}>
|
||||||
|
{{ $label }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('record_type')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
<div class="form-text">Select the DNS record type to monitor</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="expected_value" class="form-label">Expected Value (Optional)</label>
|
||||||
|
<textarea class="form-control @error('expected_value') is-invalid @enderror"
|
||||||
|
id="expected_value"
|
||||||
|
name="expected_value"
|
||||||
|
rows="3"
|
||||||
|
placeholder="e.g., 192.168.1.1 or mail.example.com">{{ old('expected_value') }}</textarea>
|
||||||
|
@error('expected_value')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
<div class="form-text">
|
||||||
|
Specify the expected value to validate against. Leave empty to only check if the record exists.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="is_active"
|
||||||
|
name="is_active"
|
||||||
|
value="1"
|
||||||
|
{{ old('is_active', true) ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="is_active">
|
||||||
|
Enable monitoring for this DNS record
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-circle"></i> Add DNS Record
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('dns-records.index', $site) }}" class="btn btn-secondary">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-info-circle"></i> DNS Record Types</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="small mb-0">
|
||||||
|
<dt>A</dt>
|
||||||
|
<dd>Maps domain to IPv4 address</dd>
|
||||||
|
|
||||||
|
<dt>AAAA</dt>
|
||||||
|
<dd>Maps domain to IPv6 address</dd>
|
||||||
|
|
||||||
|
<dt>CNAME</dt>
|
||||||
|
<dd>Alias to another domain</dd>
|
||||||
|
|
||||||
|
<dt>MX</dt>
|
||||||
|
<dd>Mail server records</dd>
|
||||||
|
|
||||||
|
<dt>TXT</dt>
|
||||||
|
<dd>Text records (SPF, DKIM, etc.)</dd>
|
||||||
|
|
||||||
|
<dt>NS</dt>
|
||||||
|
<dd>Name server records</dd>
|
||||||
|
|
||||||
|
<dt>SOA</dt>
|
||||||
|
<dd>Start of authority</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-lightbulb"></i> Tips</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="small mb-0">
|
||||||
|
<li>DNS checks run hourly</li>
|
||||||
|
<li>Leave expected value empty to only verify record exists</li>
|
||||||
|
<li>For MX records, include priority (e.g., "10 mail.example.com")</li>
|
||||||
|
<li>Alerts are sent when records become invalid</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Edit DNS Record - ' . $site->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-3">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('sites.index') }}">Sites</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('dashboard.show', $site) }}">{{ $site->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('dns-records.index', $site) }}">DNS Records</a></li>
|
||||||
|
<li class="breadcrumb-item active">Edit</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h1 class="mb-4"><i class="bi bi-pencil"></i> Edit DNS Record</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('dns-records.update', [$site, $dnsRecord]) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="domain" class="form-label">Domain Name</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control @error('domain') is-invalid @enderror"
|
||||||
|
id="domain"
|
||||||
|
name="domain"
|
||||||
|
value="{{ old('domain', $dnsRecord->domain) }}"
|
||||||
|
required>
|
||||||
|
@error('domain')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
<div class="form-text">Enter the domain name to monitor</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="record_type" class="form-label">Record Type</label>
|
||||||
|
<select class="form-select @error('record_type') is-invalid @enderror"
|
||||||
|
id="record_type"
|
||||||
|
name="record_type"
|
||||||
|
required>
|
||||||
|
@foreach($recordTypes as $value => $label)
|
||||||
|
<option value="{{ $value }}" {{ old('record_type', $dnsRecord->record_type) == $value ? 'selected' : '' }}>
|
||||||
|
{{ $label }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('record_type')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
<div class="form-text">Select the DNS record type to monitor</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="expected_value" class="form-label">Expected Value (Optional)</label>
|
||||||
|
<textarea class="form-control @error('expected_value') is-invalid @enderror"
|
||||||
|
id="expected_value"
|
||||||
|
name="expected_value"
|
||||||
|
rows="3"
|
||||||
|
placeholder="e.g., 192.168.1.1 or mail.example.com">{{ old('expected_value', $dnsRecord->expected_value) }}</textarea>
|
||||||
|
@error('expected_value')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
<div class="form-text">
|
||||||
|
Specify the expected value to validate against. Leave empty to only check if the record exists.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="is_active"
|
||||||
|
name="is_active"
|
||||||
|
value="1"
|
||||||
|
{{ old('is_active', $dnsRecord->is_active) ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="is_active">
|
||||||
|
Enable monitoring for this DNS record
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-circle"></i> Update DNS Record
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('dns-records.show', [$site, $dnsRecord]) }}" class="btn btn-secondary">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-activity"></i> Current Status</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
<dt class="col-sm-5">Status:</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
<span class="badge bg-{{ $dnsRecord->status_color }}">
|
||||||
|
{{ $dnsRecord->status_text }}
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-5">Last Checked:</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
@if($dnsRecord->last_checked_at)
|
||||||
|
{{ $dnsRecord->last_checked_at->diffForHumans() }}
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Never</span>
|
||||||
|
@endif
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
@if($dnsRecord->formatted_actual_value)
|
||||||
|
<dt class="col-sm-5">Current Value:</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
<small>{{ Str::limit($dnsRecord->formatted_actual_value, 40) }}</small>
|
||||||
|
</dd>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($dnsRecord->error_message)
|
||||||
|
<dt class="col-sm-5">Error:</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
<small class="text-danger">{{ Str::limit($dnsRecord->error_message, 40) }}</small>
|
||||||
|
</dd>
|
||||||
|
@endif
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'DNS Records - ' . $site->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-3">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('sites.index') }}">Sites</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('dashboard.show', $site) }}">{{ $site->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">DNS Records</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1><i class="bi bi-hdd-network"></i> DNS Monitoring</h1>
|
||||||
|
<a href="{{ route('dns-records.create', $site) }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle"></i> Add DNS Record
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="alert alert-success alert-dismissible fade show">
|
||||||
|
{{ session('success') }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($dnsRecords->isEmpty())
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i> No DNS records are being monitored for this site yet. Click "Add DNS Record" to start monitoring.
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Domain</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Expected Value</th>
|
||||||
|
<th>Last Checked</th>
|
||||||
|
<th>Active</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($dnsRecords as $record)
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ $record->domain }}</strong></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-info">{{ $record->record_type }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{{ $record->status_color }}">
|
||||||
|
{{ $record->status_text }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($record->expected_value)
|
||||||
|
<small class="text-muted">{{ Str::limit($record->expected_value, 30) }}</small>
|
||||||
|
@else
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($record->last_checked_at)
|
||||||
|
{{ $record->last_checked_at->diffForHumans() }}
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Never</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($record->is_active)
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-secondary">Inactive</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<a href="{{ route('dns-records.show', [$site, $record]) }}"
|
||||||
|
class="btn btn-outline-primary" title="View">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('dns-records.edit', [$site, $record]) }}"
|
||||||
|
class="btn btn-outline-secondary" title="Edit">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('dns-records.destroy', [$site, $record]) }}"
|
||||||
|
method="POST"
|
||||||
|
class="d-inline"
|
||||||
|
onsubmit="return confirm('Are you sure you want to delete this DNS record?');">
|
||||||
|
@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>
|
||||||
|
@endif
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'DNS Record - ' . $dnsRecord->display_name . ' - ' . $site->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-3">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('sites.index') }}">Sites</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('dashboard.show', $site) }}">{{ $site->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('dns-records.index', $site) }}">DNS Records</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ $dnsRecord->record_type }} - {{ $dnsRecord->domain }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1><i class="bi bi-hdd-network"></i> {{ $dnsRecord->record_type }} - {{ $dnsRecord->domain }}</h1>
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{{ route('dns-records.edit', [$site, $dnsRecord]) }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-pencil"></i> Edit
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('dns-records.destroy', [$site, $dnsRecord]) }}"
|
||||||
|
method="POST"
|
||||||
|
class="d-inline"
|
||||||
|
onsubmit="return confirm('Are you sure you want to delete this DNS record?');">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-outline-danger">
|
||||||
|
<i class="bi bi-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="alert alert-success alert-dismissible fade show">
|
||||||
|
{{ session('success') }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">DNS Record Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
<dt class="col-sm-3">Site</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
<a href="{{ route('dashboard.show', $site) }}">{{ $site->name }}</a>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ $site->url }}</small>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Domain</dt>
|
||||||
|
<dd class="col-sm-9"><strong>{{ $dnsRecord->domain }}</strong></dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Record Type</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
<span class="badge bg-info">{{ $dnsRecord->record_type }}</span>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Status</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
<span class="badge bg-{{ $dnsRecord->status_color }}">
|
||||||
|
{{ $dnsRecord->status_text }}
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Monitoring</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
@if($dnsRecord->is_active)
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-secondary">Inactive</span>
|
||||||
|
@endif
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Last Checked</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
@if($dnsRecord->last_checked_at)
|
||||||
|
{{ $dnsRecord->last_checked_at->format('Y-m-d H:i:s') }}
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ $dnsRecord->last_checked_at->diffForHumans() }}</small>
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Never</span>
|
||||||
|
@endif
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Expected Value</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
@if($dnsRecord->expected_value)
|
||||||
|
<code>{{ $dnsRecord->expected_value }}</code>
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Not specified (only checking existence)</span>
|
||||||
|
@endif
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
@if($dnsRecord->actual_value)
|
||||||
|
<dt class="col-sm-3">Actual Value(s)</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
@if(is_array($dnsRecord->actual_value))
|
||||||
|
<ul class="mb-0">
|
||||||
|
@foreach($dnsRecord->actual_value as $value)
|
||||||
|
<li><code>{{ $value }}</code></li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@else
|
||||||
|
<code>{{ $dnsRecord->actual_value }}</code>
|
||||||
|
@endif
|
||||||
|
</dd>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($dnsRecord->error_message)
|
||||||
|
<dt class="col-sm-3">Error</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
<div class="alert alert-danger mb-0">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i> {{ $dnsRecord->error_message }}
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Created</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
{{ $dnsRecord->created_at->format('Y-m-d H:i:s') }}
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ $dnsRecord->created_at->diffForHumans() }}</small>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">About {{ $dnsRecord->record_type }} Records</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@php
|
||||||
|
$descriptions = [
|
||||||
|
'A' => 'A records map a domain name to an IPv4 address. This is the most common type of DNS record.',
|
||||||
|
'AAAA' => 'AAAA records map a domain name to an IPv6 address, the newer version of IP addressing.',
|
||||||
|
'CNAME' => 'CNAME records create an alias from one domain name to another. They\'re often used for subdomains.',
|
||||||
|
'MX' => 'MX records specify the mail servers responsible for accepting email on behalf of your domain.',
|
||||||
|
'TXT' => 'TXT records store text information. They\'re commonly used for email authentication (SPF, DKIM, DMARC).',
|
||||||
|
'NS' => 'NS records identify the authoritative name servers for your domain.',
|
||||||
|
'SOA' => 'SOA records contain administrative information about the domain, including the primary name server.',
|
||||||
|
'PTR' => 'PTR records provide reverse DNS lookups, mapping IP addresses back to domain names.',
|
||||||
|
'SRV' => 'SRV records specify the location of services, including the hostname and port number.',
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
<p class="mb-0">{{ $descriptions[$dnsRecord->record_type] ?? 'DNS record type for monitoring.' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Current Status</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="mb-3">
|
||||||
|
<span class="badge bg-{{ $dnsRecord->status_color }} fs-5 px-4 py-2">
|
||||||
|
{{ $dnsRecord->status_text }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($dnsRecord->is_valid === true)
|
||||||
|
<p class="text-success mb-0">
|
||||||
|
<i class="bi bi-check-circle-fill"></i> DNS record is valid
|
||||||
|
</p>
|
||||||
|
@elseif($dnsRecord->is_valid === false)
|
||||||
|
<p class="text-danger mb-0">
|
||||||
|
<i class="bi bi-x-circle-fill"></i> DNS record is invalid
|
||||||
|
</p>
|
||||||
|
@else
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
<i class="bi bi-question-circle"></i> Not yet checked
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Monitoring Info</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="mb-2">
|
||||||
|
<i class="bi bi-clock"></i> <strong>Check Frequency:</strong> Hourly
|
||||||
|
</p>
|
||||||
|
<p class="mb-2">
|
||||||
|
<i class="bi bi-bell"></i> <strong>Alerts:</strong> Sent when DNS becomes invalid
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
<i class="bi bi-arrow-repeat"></i> <strong>Validation:</strong>
|
||||||
|
@if($dnsRecord->expected_value)
|
||||||
|
Matches expected value
|
||||||
|
@else
|
||||||
|
Record exists
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Quick Actions</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a href="{{ route('dns-records.index', $site) }}" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-list-ul"></i> View All DNS Records
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('dashboard.show', $site) }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-bar-chart"></i> Site Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Add Health Check - ' . $site->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-3">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('sites.index') }}">Sites</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('dashboard.show', $site) }}">{{ $site->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('health-checks.index', $site) }}">Health Checks</a></li>
|
||||||
|
<li class="breadcrumb-item active">Add Health Check</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h1 class="mb-4"><i class="bi bi-plus-circle"></i> Add Health Check</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('health-checks.store', $site) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="endpoint" class="form-label">Health Endpoint Path</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">{{ rtrim($site->url, '/') }}/</span>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control @error('endpoint') is-invalid @enderror"
|
||||||
|
id="endpoint"
|
||||||
|
name="endpoint"
|
||||||
|
value="{{ old('endpoint') }}"
|
||||||
|
placeholder="health"
|
||||||
|
required>
|
||||||
|
@error('endpoint')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Enter the path to your health check endpoint (e.g., "health", "api/health", "status").</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="expected_json_keys" class="form-label">Expected JSON Keys (Optional)</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control @error('expected_json_keys') is-invalid @enderror"
|
||||||
|
id="expected_json_keys"
|
||||||
|
name="expected_json_keys"
|
||||||
|
value="{{ old('expected_json_keys') }}"
|
||||||
|
placeholder="status, database, cache">
|
||||||
|
@error('expected_json_keys')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
<div class="form-text">
|
||||||
|
Comma-separated list of JSON keys that must be present in the response. Supports dot notation for nested keys (e.g., "data.status, meta.version").
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="performance_threshold" class="form-label">Performance Threshold (Optional)</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="number"
|
||||||
|
class="form-control @error('performance_threshold') is-invalid @enderror"
|
||||||
|
id="performance_threshold"
|
||||||
|
name="performance_threshold"
|
||||||
|
value="{{ old('performance_threshold') }}"
|
||||||
|
min="1"
|
||||||
|
max="60000"
|
||||||
|
placeholder="1000">
|
||||||
|
<span class="input-group-text">ms</span>
|
||||||
|
@error('performance_threshold')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
Alert when response time exceeds this threshold in milliseconds. Leave empty to disable performance alerts.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="is_active"
|
||||||
|
name="is_active"
|
||||||
|
value="1"
|
||||||
|
{{ old('is_active', true) ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="is_active">
|
||||||
|
Enable monitoring for this health check
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-circle"></i> Add Health Check
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('health-checks.index', $site) }}" class="btn btn-secondary">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Health Check Guide</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h6>What is a Health Check?</h6>
|
||||||
|
<p class="small">
|
||||||
|
A health check endpoint is a URL that returns information about your application's status, including database connectivity, cache availability, and other critical services.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h6 class="mt-3">Common Endpoints</h6>
|
||||||
|
<ul class="small mb-0">
|
||||||
|
<li><code>/health</code></li>
|
||||||
|
<li><code>/api/health</code></li>
|
||||||
|
<li><code>/status</code></li>
|
||||||
|
<li><code>/healthz</code></li>
|
||||||
|
<li><code>/.well-known/health</code></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h6 class="mt-3">Response Format</h6>
|
||||||
|
<p class="small mb-0">
|
||||||
|
Your endpoint should return JSON with a 2xx status code when healthy. Example:
|
||||||
|
</p>
|
||||||
|
<pre class="small bg-light p-2 mt-2"><code>{
|
||||||
|
"status": "healthy",
|
||||||
|
"database": "ok",
|
||||||
|
"cache": "ok"
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Edit Health Check - ' . $site->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-3">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('sites.index') }}">Sites</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('dashboard.show', $site) }}">{{ $site->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('health-checks.index', $site) }}">Health Checks</a></li>
|
||||||
|
<li class="breadcrumb-item active">Edit</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h1 class="mb-4"><i class="bi bi-pencil"></i> Edit Health Check</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('health-checks.update', [$site, $healthCheck]) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="endpoint" class="form-label">Health Endpoint Path</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">{{ rtrim($site->url, '/') }}/</span>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control @error('endpoint') is-invalid @enderror"
|
||||||
|
id="endpoint"
|
||||||
|
name="endpoint"
|
||||||
|
value="{{ old('endpoint', $healthCheck->endpoint) }}"
|
||||||
|
required>
|
||||||
|
@error('endpoint')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Enter the path to your health check endpoint.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="expected_json_keys" class="form-label">Expected JSON Keys (Optional)</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control @error('expected_json_keys') is-invalid @enderror"
|
||||||
|
id="expected_json_keys"
|
||||||
|
name="expected_json_keys"
|
||||||
|
value="{{ old('expected_json_keys', $healthCheck->expected_json_keys ? implode(', ', $healthCheck->expected_json_keys) : '') }}"
|
||||||
|
placeholder="status, database, cache">
|
||||||
|
@error('expected_json_keys')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
<div class="form-text">
|
||||||
|
Comma-separated list of JSON keys that must be present in the response. Supports dot notation for nested keys.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="performance_threshold" class="form-label">Performance Threshold (Optional)</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="number"
|
||||||
|
class="form-control @error('performance_threshold') is-invalid @enderror"
|
||||||
|
id="performance_threshold"
|
||||||
|
name="performance_threshold"
|
||||||
|
value="{{ old('performance_threshold', $healthCheck->performance_threshold) }}"
|
||||||
|
min="1"
|
||||||
|
max="60000"
|
||||||
|
placeholder="1000">
|
||||||
|
<span class="input-group-text">ms</span>
|
||||||
|
@error('performance_threshold')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
Alert when response time exceeds this threshold in milliseconds. Leave empty to disable performance alerts.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="is_active"
|
||||||
|
name="is_active"
|
||||||
|
value="1"
|
||||||
|
{{ old('is_active', $healthCheck->is_active) ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="is_active">
|
||||||
|
Enable monitoring for this health check
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-circle"></i> Update Health Check
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('health-checks.show', [$site, $healthCheck]) }}" class="btn btn-secondary">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-activity"></i> Current Status</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
<dt class="col-sm-5">Status:</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
<span class="badge bg-{{ $healthCheck->status_color }}">
|
||||||
|
{{ $healthCheck->status_text }}
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-5">Last Checked:</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
@if($healthCheck->last_checked_at)
|
||||||
|
{{ $healthCheck->last_checked_at->diffForHumans() }}
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Never</span>
|
||||||
|
@endif
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
@if($healthCheck->status_code)
|
||||||
|
<dt class="col-sm-5">Status Code:</dt>
|
||||||
|
<dd class="col-sm-7">
|
||||||
|
<span class="badge bg-{{ $healthCheck->status_code < 300 ? 'success' : 'danger' }}">
|
||||||
|
{{ $healthCheck->status_code }}
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($healthCheck->response_time_ms)
|
||||||
|
<dt class="col-sm-5">Response Time:</dt>
|
||||||
|
<dd class="col-sm-7">{{ $healthCheck->response_time_ms }}ms</dd>
|
||||||
|
@endif
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Health Checks - ' . $site->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-3">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('sites.index') }}">Sites</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('dashboard.show', $site) }}">{{ $site->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">Health Checks</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1><i class="bi bi-heart-pulse"></i> Health Check Monitoring</h1>
|
||||||
|
<a href="{{ route('health-checks.create', $site) }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle"></i> Add Health Check
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="alert alert-success alert-dismissible fade show">
|
||||||
|
{{ session('success') }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($healthChecks->isEmpty())
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i> No health check endpoints are being monitored for this site yet. Click "Add Health Check" to start monitoring.
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Endpoint</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Status Code</th>
|
||||||
|
<th>Response Time</th>
|
||||||
|
<th>Last Checked</th>
|
||||||
|
<th>Active</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($healthChecks as $healthCheck)
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{ $healthCheck->endpoint }}</strong>
|
||||||
|
@if($healthCheck->expected_json_keys)
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-key"></i> Validates: {{ implode(', ', $healthCheck->expected_json_keys) }}
|
||||||
|
</small>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{{ $healthCheck->status_color }}">
|
||||||
|
{{ $healthCheck->status_text }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($healthCheck->status_code)
|
||||||
|
<span class="badge bg-{{ $healthCheck->status_code < 300 ? 'success' : 'danger' }}">
|
||||||
|
{{ $healthCheck->status_code }}
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($healthCheck->response_time_ms)
|
||||||
|
{{ $healthCheck->response_time_ms }}ms
|
||||||
|
@else
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($healthCheck->last_checked_at)
|
||||||
|
{{ $healthCheck->last_checked_at->diffForHumans() }}
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Never</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($healthCheck->is_active)
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-secondary">Inactive</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<a href="{{ route('health-checks.show', [$site, $healthCheck]) }}"
|
||||||
|
class="btn btn-outline-primary" title="View">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('health-checks.edit', [$site, $healthCheck]) }}"
|
||||||
|
class="btn btn-outline-secondary" title="Edit">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('health-checks.destroy', [$site, $healthCheck]) }}"
|
||||||
|
method="POST"
|
||||||
|
class="d-inline"
|
||||||
|
onsubmit="return confirm('Are you sure you want to delete this health check?');">
|
||||||
|
@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>
|
||||||
|
@endif
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Health Check - ' . $healthCheck->endpoint . ' - ' . $site->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-3">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('sites.index') }}">Sites</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('dashboard.show', $site) }}">{{ $site->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('health-checks.index', $site) }}">Health Checks</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ $healthCheck->endpoint }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1><i class="bi bi-heart-pulse"></i> {{ $healthCheck->endpoint }}</h1>
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{{ route('health-checks.edit', [$site, $healthCheck]) }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-pencil"></i> Edit
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('health-checks.destroy', [$site, $healthCheck]) }}"
|
||||||
|
method="POST"
|
||||||
|
class="d-inline"
|
||||||
|
onsubmit="return confirm('Are you sure you want to delete this health check?');">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-outline-danger">
|
||||||
|
<i class="bi bi-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="alert alert-success alert-dismissible fade show">
|
||||||
|
{{ session('success') }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Health Check Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
<dt class="col-sm-3">Site</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
<a href="{{ route('dashboard.show', $site) }}">{{ $site->name }}</a>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ $site->url }}</small>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Endpoint URL</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
<a href="{{ $healthCheck->full_url }}" target="_blank" rel="noopener">
|
||||||
|
{{ $healthCheck->full_url }}
|
||||||
|
<i class="bi bi-box-arrow-up-right"></i>
|
||||||
|
</a>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Status</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
<span class="badge bg-{{ $healthCheck->status_color }}">
|
||||||
|
{{ $healthCheck->status_text }}
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Monitoring</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
@if($healthCheck->is_active)
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-secondary">Inactive</span>
|
||||||
|
@endif
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Last Checked</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
@if($healthCheck->last_checked_at)
|
||||||
|
{{ $healthCheck->last_checked_at->format('Y-m-d H:i:s') }}
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ $healthCheck->last_checked_at->diffForHumans() }}</small>
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Never</span>
|
||||||
|
@endif
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
@if($healthCheck->status_code)
|
||||||
|
<dt class="col-sm-3">Status Code</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
<span class="badge bg-{{ $healthCheck->status_code < 300 ? 'success' : 'danger' }}">
|
||||||
|
{{ $healthCheck->status_code }}
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($healthCheck->response_time_ms)
|
||||||
|
<dt class="col-sm-3">Response Time</dt>
|
||||||
|
<dd class="col-sm-9">{{ $healthCheck->response_time_ms }}ms</dd>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Expected Keys</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
@if($healthCheck->expected_json_keys && count($healthCheck->expected_json_keys) > 0)
|
||||||
|
@foreach($healthCheck->expected_json_keys as $key)
|
||||||
|
<span class="badge bg-info text-dark me-1">{{ $key }}</span>
|
||||||
|
@endforeach
|
||||||
|
@else
|
||||||
|
<span class="text-muted">None specified</span>
|
||||||
|
@endif
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Performance Threshold</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
@if($healthCheck->performance_threshold)
|
||||||
|
<span class="badge bg-warning text-dark">{{ $healthCheck->performance_threshold }}ms</span>
|
||||||
|
@if($healthCheck->response_time_ms && $healthCheck->response_time_ms > $healthCheck->performance_threshold)
|
||||||
|
<br><small class="text-danger"><i class="bi bi-exclamation-triangle"></i> Currently exceeding threshold</small>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Not set</span>
|
||||||
|
@endif
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Created</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
{{ $healthCheck->created_at->format('Y-m-d H:i:s') }}
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ $healthCheck->created_at->diffForHumans() }}</small>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($healthCheck->response_data)
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Latest Response Data</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@if(isset($healthCheck->response_data['error']))
|
||||||
|
<div class="alert alert-danger mb-0">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
<strong>Error:</strong> {{ $healthCheck->response_data['error'] }}
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<pre class="bg-light p-3 rounded mb-0"><code>{{ json_encode($healthCheck->response_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}</code></pre>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Current Status</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="mb-3">
|
||||||
|
<span class="badge bg-{{ $healthCheck->status_color }} fs-5 px-4 py-2">
|
||||||
|
{{ $healthCheck->status_text }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($healthCheck->is_healthy === true)
|
||||||
|
<p class="text-success mb-0">
|
||||||
|
<i class="bi bi-check-circle-fill"></i> All checks passed
|
||||||
|
</p>
|
||||||
|
@elseif($healthCheck->is_healthy === false)
|
||||||
|
<p class="text-danger mb-0">
|
||||||
|
<i class="bi bi-x-circle-fill"></i> Health check failed
|
||||||
|
</p>
|
||||||
|
@else
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
<i class="bi bi-question-circle"></i> Not yet checked
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Monitoring Info</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="mb-2">
|
||||||
|
<i class="bi bi-clock"></i> <strong>Check Frequency:</strong> Every 5 minutes
|
||||||
|
</p>
|
||||||
|
<p class="mb-2">
|
||||||
|
<i class="bi bi-check2-square"></i> <strong>Requirements:</strong>
|
||||||
|
</p>
|
||||||
|
<ul class="small mb-0">
|
||||||
|
<li>2xx HTTP status code</li>
|
||||||
|
<li>Valid JSON response</li>
|
||||||
|
@if($healthCheck->expected_json_keys && count($healthCheck->expected_json_keys) > 0)
|
||||||
|
<li>Contains required keys: {{ implode(', ', $healthCheck->expected_json_keys) }}</li>
|
||||||
|
@endif
|
||||||
|
@if($healthCheck->performance_threshold)
|
||||||
|
<li class="text-warning">Performance alert if exceeds {{ $healthCheck->performance_threshold }}ms</li>
|
||||||
|
@endif
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Quick Actions</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a href="{{ route('health-checks.index', $site) }}" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-list-ul"></i> View All Health Checks
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('dashboard.show', $site) }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-bar-chart"></i> Site Dashboard
|
||||||
|
</a>
|
||||||
|
<a href="{{ $healthCheck->full_url }}" target="_blank" rel="noopener" class="btn btn-outline-info">
|
||||||
|
<i class="bi bi-box-arrow-up-right"></i> Open Endpoint
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Add TCP Port - ' . $site->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-3">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('sites.index') }}">Sites</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('dashboard.show', $site) }}">{{ $site->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('tcp-ports.index', $site) }}">TCP Ports</a></li>
|
||||||
|
<li class="breadcrumb-item active">Add Port</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h1 class="mb-4"><i class="bi bi-plus-circle"></i> Add TCP Port</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('tcp-ports.store', $site) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="port" class="form-label">Port Number</label>
|
||||||
|
<input type="number"
|
||||||
|
class="form-control @error('port') is-invalid @enderror"
|
||||||
|
id="port"
|
||||||
|
name="port"
|
||||||
|
min="1"
|
||||||
|
max="65535"
|
||||||
|
value="{{ old('port') }}"
|
||||||
|
required>
|
||||||
|
@error('port')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
<div class="form-text">Enter a port number between 1 and 65535.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Port Name (Optional)</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control @error('name') is-invalid @enderror"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value="{{ old('name') }}"
|
||||||
|
maxlength="255"
|
||||||
|
placeholder="e.g., Web Server, Database">
|
||||||
|
@error('name')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
<div class="form-text">Optional custom name for this port.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="is_active"
|
||||||
|
name="is_active"
|
||||||
|
value="1"
|
||||||
|
{{ old('is_active', true) ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="is_active">
|
||||||
|
Enable monitoring for this port
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-circle"></i> Add Port
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('tcp-ports.index', $site) }}" class="btn btn-secondary">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Common Ports</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<small class="text-muted">Click to use:</small>
|
||||||
|
<div class="list-group list-group-flush mt-2">
|
||||||
|
@foreach($commonPorts as $portNumber => $portName)
|
||||||
|
<a href="#" class="list-group-item list-group-item-action py-2 px-3 common-port"
|
||||||
|
data-port="{{ $portNumber }}"
|
||||||
|
data-name="{{ $portName }}">
|
||||||
|
<strong>{{ $portNumber }}</strong> - {{ $portName }}
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
document.querySelectorAll('.common-port').forEach(function(element) {
|
||||||
|
element.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById('port').value = this.dataset.port;
|
||||||
|
document.getElementById('name').value = this.dataset.name;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Edit TCP Port - ' . $site->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-3">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('sites.index') }}">Sites</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('dashboard.show', $site) }}">{{ $site->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('tcp-ports.index', $site) }}">TCP Ports</a></li>
|
||||||
|
<li class="breadcrumb-item active">Edit Port {{ $tcpPort->port }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h1 class="mb-4"><i class="bi bi-pencil"></i> Edit TCP Port</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('tcp-ports.update', [$site, $tcpPort]) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="port" class="form-label">Port Number</label>
|
||||||
|
<input type="number"
|
||||||
|
class="form-control @error('port') is-invalid @enderror"
|
||||||
|
id="port"
|
||||||
|
name="port"
|
||||||
|
min="1"
|
||||||
|
max="65535"
|
||||||
|
value="{{ old('port', $tcpPort->port) }}"
|
||||||
|
required>
|
||||||
|
@error('port')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
<div class="form-text">Enter a port number between 1 and 65535.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Port Name (Optional)</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control @error('name') is-invalid @enderror"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value="{{ old('name', $tcpPort->name) }}"
|
||||||
|
maxlength="255"
|
||||||
|
placeholder="e.g., Web Server, Database">
|
||||||
|
@error('name')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
<div class="form-text">Optional custom name for this port.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="is_active"
|
||||||
|
name="is_active"
|
||||||
|
value="1"
|
||||||
|
{{ old('is_active', $tcpPort->is_active) ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="is_active">
|
||||||
|
Enable monitoring for this port
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-circle"></i> Update Port
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('tcp-ports.index', $site) }}" class="btn btn-secondary">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Port Status</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="mb-0">
|
||||||
|
<dt>Current Status</dt>
|
||||||
|
<dd>
|
||||||
|
<span class="badge bg-{{ $tcpPort->status_color }}">
|
||||||
|
{{ $tcpPort->status_text }}
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>Last Checked</dt>
|
||||||
|
<dd>
|
||||||
|
@if($tcpPort->last_checked_at)
|
||||||
|
{{ $tcpPort->last_checked_at->format('Y-m-d H:i:s') }}
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ $tcpPort->last_checked_at->diffForHumans() }}</small>
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Never</span>
|
||||||
|
@endif
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
@if($tcpPort->response_time_ms)
|
||||||
|
<dt>Response Time</dt>
|
||||||
|
<dd>{{ $tcpPort->response_time_ms }}ms</dd>
|
||||||
|
@endif
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Common Ports</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<small class="text-muted">Click to use:</small>
|
||||||
|
<div class="list-group list-group-flush mt-2" style="max-height: 300px; overflow-y: auto;">
|
||||||
|
@foreach($commonPorts as $portNumber => $portName)
|
||||||
|
<a href="#" class="list-group-item list-group-item-action py-2 px-3 common-port"
|
||||||
|
data-port="{{ $portNumber }}"
|
||||||
|
data-name="{{ $portName }}">
|
||||||
|
<strong>{{ $portNumber }}</strong> - {{ $portName }}
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
document.querySelectorAll('.common-port').forEach(function(element) {
|
||||||
|
element.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById('port').value = this.dataset.port;
|
||||||
|
document.getElementById('name').value = this.dataset.name;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'TCP Ports - ' . $site->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-3">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('sites.index') }}">Sites</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('dashboard.show', $site) }}">{{ $site->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">TCP Ports</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1><i class="bi bi-ethernet"></i> TCP Port Monitoring</h1>
|
||||||
|
<a href="{{ route('tcp-ports.create', $site) }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle"></i> Add Port
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="alert alert-success alert-dismissible fade show">
|
||||||
|
{{ session('success') }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($tcpPorts->isEmpty())
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i> No TCP ports are being monitored for this site yet. Click "Add Port" to start monitoring.
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Port</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Response Time</th>
|
||||||
|
<th>Last Checked</th>
|
||||||
|
<th>Active</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($tcpPorts as $port)
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ $port->port }}</strong></td>
|
||||||
|
<td>{{ $port->display_name }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{{ $port->status_color }}">
|
||||||
|
{{ $port->status_text }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($port->response_time_ms)
|
||||||
|
{{ $port->response_time_ms }}ms
|
||||||
|
@else
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($port->last_checked_at)
|
||||||
|
{{ $port->last_checked_at->diffForHumans() }}
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Never</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($port->is_active)
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-secondary">Inactive</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<a href="{{ route('tcp-ports.show', [$site, $port]) }}"
|
||||||
|
class="btn btn-outline-primary" title="View">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('tcp-ports.edit', [$site, $port]) }}"
|
||||||
|
class="btn btn-outline-secondary" title="Edit">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('tcp-ports.destroy', [$site, $port]) }}"
|
||||||
|
method="POST"
|
||||||
|
class="d-inline"
|
||||||
|
onsubmit="return confirm('Are you sure you want to delete this port monitor?');">
|
||||||
|
@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>
|
||||||
|
|
||||||
|
{{ $tcpPorts->links() }}
|
||||||
|
@endif
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'TCP Port ' . $tcpPort->port . ' - ' . $site->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-3">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('sites.index') }}">Sites</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('dashboard.show', $site) }}">{{ $site->name }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ route('tcp-ports.index', $site) }}">TCP Ports</a></li>
|
||||||
|
<li class="breadcrumb-item active">Port {{ $tcpPort->port }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1><i class="bi bi-ethernet"></i> Port {{ $tcpPort->port }} - {{ $tcpPort->display_name }}</h1>
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{{ route('tcp-ports.edit', [$site, $tcpPort]) }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-pencil"></i> Edit
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('tcp-ports.destroy', [$site, $tcpPort]) }}"
|
||||||
|
method="POST"
|
||||||
|
class="d-inline"
|
||||||
|
onsubmit="return confirm('Are you sure you want to delete this port monitor?');">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-outline-danger">
|
||||||
|
<i class="bi bi-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Port Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
<dt class="col-sm-3">Site</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
<a href="{{ route('dashboard.show', $site) }}">{{ $site->name }}</a>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ $site->url }}</small>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Port Number</dt>
|
||||||
|
<dd class="col-sm-9"><strong>{{ $tcpPort->port }}</strong></dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Port Name</dt>
|
||||||
|
<dd class="col-sm-9">{{ $tcpPort->display_name }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Status</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
<span class="badge bg-{{ $tcpPort->status_color }}">
|
||||||
|
{{ $tcpPort->status_text }}
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Monitoring</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
@if($tcpPort->is_active)
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-secondary">Inactive</span>
|
||||||
|
@endif
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Last Checked</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
@if($tcpPort->last_checked_at)
|
||||||
|
{{ $tcpPort->last_checked_at->format('Y-m-d H:i:s') }}
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ $tcpPort->last_checked_at->diffForHumans() }}</small>
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Never</span>
|
||||||
|
@endif
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
@if($tcpPort->response_time_ms)
|
||||||
|
<dt class="col-sm-3">Response Time</dt>
|
||||||
|
<dd class="col-sm-9">{{ $tcpPort->response_time_ms }}ms</dd>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Created</dt>
|
||||||
|
<dd class="col-sm-9">
|
||||||
|
{{ $tcpPort->created_at->format('Y-m-d H:i:s') }}
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ $tcpPort->created_at->diffForHumans() }}</small>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">About Port {{ $tcpPort->port }}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@php
|
||||||
|
$commonPorts = \App\Models\TcpPort::getCommonPorts();
|
||||||
|
$portInfo = $commonPorts[$tcpPort->port] ?? null;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if($portInfo)
|
||||||
|
<p class="mb-2">
|
||||||
|
<strong>Common Service:</strong> {{ $portInfo }}
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
This monitor checks if port {{ $tcpPort->port }} is accessible on {{ $site->url }}.
|
||||||
|
Checks run every 10 minutes when monitoring is active.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Current Status</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="mb-3">
|
||||||
|
<span class="badge bg-{{ $tcpPort->status_color }} fs-5 px-4 py-2">
|
||||||
|
{{ $tcpPort->status_text }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($tcpPort->is_open)
|
||||||
|
<p class="text-success mb-0">
|
||||||
|
<i class="bi bi-check-circle-fill"></i> Port is accessible
|
||||||
|
</p>
|
||||||
|
@elseif($tcpPort->is_open === false)
|
||||||
|
<p class="text-danger mb-0">
|
||||||
|
<i class="bi bi-x-circle-fill"></i> Port is not accessible
|
||||||
|
</p>
|
||||||
|
@else
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
<i class="bi bi-question-circle"></i> Not yet checked
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Quick Actions</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a href="{{ route('tcp-ports.index', $site) }}" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-list-ul"></i> View All Ports
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('dashboard.show', $site) }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-bar-chart"></i> Site Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\AuthController;
|
||||||
|
use App\Http\Controllers\Api\LinkController;
|
||||||
|
use App\Http\Controllers\Api\SiteController;
|
||||||
|
use App\Http\Controllers\Api\SslController;
|
||||||
|
use App\Http\Controllers\Api\UptimeController;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| API Routes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here is where you can register API routes for your application. These
|
||||||
|
| routes are loaded by the RouteServiceProvider and all of them will
|
||||||
|
| be assigned to the "api" middleware group. Make something great!
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
Route::get('/test', function (Request $request) {
|
||||||
|
return response()->json(['message' => 'API is working']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
|
// Authentication & Token Management
|
||||||
|
Route::post('/tokens', [AuthController::class, 'createToken']);
|
||||||
|
Route::get('/tokens', [AuthController::class, 'tokens']);
|
||||||
|
Route::delete('/tokens/{tokenId}', [AuthController::class, 'revokeToken']);
|
||||||
|
Route::delete('/tokens', [AuthController::class, 'revokeAllTokens']);
|
||||||
|
|
||||||
|
// Sites
|
||||||
|
Route::get('/sites', [SiteController::class, 'index']);
|
||||||
|
Route::get('/sites/{site}', [SiteController::class, 'show']);
|
||||||
|
|
||||||
|
// Uptime
|
||||||
|
Route::get('/sites/{site}/uptime', [UptimeController::class, 'index']);
|
||||||
|
Route::get('/sites/{site}/uptime/stats', [UptimeController::class, 'stats']);
|
||||||
|
|
||||||
|
// SSL
|
||||||
|
Route::get('/sites/{site}/ssl', [SslController::class, 'show']);
|
||||||
|
Route::get('/sites/{site}/ssl/history', [SslController::class, 'history']);
|
||||||
|
|
||||||
|
// Broken Links
|
||||||
|
Route::get('/sites/{site}/links', [LinkController::class, 'index']);
|
||||||
|
Route::get('/sites/{site}/links/stats', [LinkController::class, 'stats']);
|
||||||
|
});
|
||||||
@@ -15,6 +15,13 @@ Schedule::command('monitor:uptime')
|
|||||||
->runInBackground()
|
->runInBackground()
|
||||||
->appendOutputTo(storage_path('logs/uptime-monitoring.log'));
|
->appendOutputTo(storage_path('logs/uptime-monitoring.log'));
|
||||||
|
|
||||||
|
// Schedule ping monitoring to run every 5 minutes
|
||||||
|
Schedule::command('monitor:ping')
|
||||||
|
->everyFiveMinutes()
|
||||||
|
->withoutOverlapping()
|
||||||
|
->runInBackground()
|
||||||
|
->appendOutputTo(storage_path('logs/ping-monitoring.log'));
|
||||||
|
|
||||||
// Schedule SSL certificate monitoring to run daily
|
// Schedule SSL certificate monitoring to run daily
|
||||||
Schedule::command('monitor:ssl')
|
Schedule::command('monitor:ssl')
|
||||||
->daily()
|
->daily()
|
||||||
@@ -28,3 +35,24 @@ Schedule::command('monitor:links')
|
|||||||
->withoutOverlapping()
|
->withoutOverlapping()
|
||||||
->runInBackground()
|
->runInBackground()
|
||||||
->appendOutputTo(storage_path('logs/link-monitoring.log'));
|
->appendOutputTo(storage_path('logs/link-monitoring.log'));
|
||||||
|
|
||||||
|
// Schedule TCP port monitoring to run every 10 minutes
|
||||||
|
Schedule::command('monitor:tcp-ports')
|
||||||
|
->everyTenMinutes()
|
||||||
|
->withoutOverlapping()
|
||||||
|
->runInBackground()
|
||||||
|
->appendOutputTo(storage_path('logs/tcp-port-monitoring.log'));
|
||||||
|
|
||||||
|
// Schedule health endpoint monitoring to run every 5 minutes
|
||||||
|
Schedule::command('monitor:health')
|
||||||
|
->everyFiveMinutes()
|
||||||
|
->withoutOverlapping()
|
||||||
|
->runInBackground()
|
||||||
|
->appendOutputTo(storage_path('logs/health-monitoring.log'));
|
||||||
|
|
||||||
|
// Schedule DNS monitoring to run hourly
|
||||||
|
Schedule::command('monitor:dns')
|
||||||
|
->hourly()
|
||||||
|
->withoutOverlapping()
|
||||||
|
->runInBackground()
|
||||||
|
->appendOutputTo(storage_path('logs/dns-monitoring.log'));
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::resource('sites', SiteController::class);
|
Route::resource('sites', SiteController::class);
|
||||||
Route::get('dashboard/{site}', [DashboardController::class, 'show'])->name('dashboard.show');
|
Route::get('dashboard/{site}', [DashboardController::class, 'show'])->name('dashboard.show');
|
||||||
|
|
||||||
|
// Resources nested under sites
|
||||||
|
Route::prefix('sites/{site}')->group(function () {
|
||||||
|
Route::resource('tcp-ports', \App\Http\Controllers\TcpPortController::class);
|
||||||
|
Route::resource('health-checks', \App\Http\Controllers\HealthCheckController::class);
|
||||||
|
Route::resource('dns-records', \App\Http\Controllers\DnsRecordController::class);
|
||||||
|
});
|
||||||
|
|
||||||
// Alerts
|
// Alerts
|
||||||
Route::get('/alerts', [AlertController::class, 'index'])->name('alerts.index');
|
Route::get('/alerts', [AlertController::class, 'index'])->name('alerts.index');
|
||||||
Route::patch('/alerts/{notification}/read', [AlertController::class, 'markAsRead'])->name('alerts.markAsRead');
|
Route::patch('/alerts/{notification}/read', [AlertController::class, 'markAsRead'])->name('alerts.markAsRead');
|
||||||
|
|||||||
Reference in New Issue
Block a user