mirror of
https://github.com/asimonson1125/asimonson1125.github.io.git
synced 2026-02-24 21:09:49 -06:00
sample status page
This commit is contained in:
149
STATUS_MONITOR_README.md
Normal file
149
STATUS_MONITOR_README.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# Service Status Monitor
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Server-side monitoring system that checks the availability of asimonson.com services every 2 hours and provides uptime statistics.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Backend Components
|
||||||
|
|
||||||
|
#### 1. `monitor.py` - Service Monitoring Module
|
||||||
|
- **Purpose**: Performs automated health checks on all services
|
||||||
|
- **Check Interval**: Every 2 hours (7200 seconds)
|
||||||
|
- **Services Monitored**:
|
||||||
|
- asimonson.com
|
||||||
|
- files.asimonson.com
|
||||||
|
- git.asimonson.com
|
||||||
|
- pass.asimonson.com
|
||||||
|
- ssh.asimonson.com
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Tracks response times and HTTP status codes
|
||||||
|
- Stores check history (up to 720 checks = 60 days of data)
|
||||||
|
- Calculates uptime percentages for multiple time periods (24h, 7d, 30d, all-time)
|
||||||
|
- Persists data to `static/json/status_history.json`
|
||||||
|
- Runs in a background thread
|
||||||
|
|
||||||
|
#### 2. `app.py` - Flask Integration
|
||||||
|
- **New API Endpoint**: `/api/status`
|
||||||
|
- Returns current status for all services
|
||||||
|
- Includes uptime statistics
|
||||||
|
- Provides last check and next check times
|
||||||
|
- **Auto-start**: Monitoring begins when the Flask app starts
|
||||||
|
|
||||||
|
### Frontend Components
|
||||||
|
|
||||||
|
#### 1. `templates/status.html` - Status Page Template
|
||||||
|
- Displays real-time service status
|
||||||
|
- Shows uptime percentages (24h, 7d, 30d, all-time)
|
||||||
|
- Displays response times and status codes
|
||||||
|
- Shows total number of checks performed
|
||||||
|
- Manual refresh button
|
||||||
|
- Auto-refreshes every 5 minutes
|
||||||
|
|
||||||
|
#### 2. `static/js/status.js` - Frontend Logic
|
||||||
|
- Fetches status data from `/api/status` API
|
||||||
|
- Updates UI with service status and uptime
|
||||||
|
- Handles error states gracefully
|
||||||
|
- Auto-refresh every 5 minutes
|
||||||
|
|
||||||
|
#### 3. `static/css/App.css` - Styling
|
||||||
|
- Color-coded status indicators:
|
||||||
|
- Green: Operational
|
||||||
|
- Yellow: Degraded/Timeout
|
||||||
|
- Red: Offline
|
||||||
|
- Responsive grid layout
|
||||||
|
- Dark theme matching existing site design
|
||||||
|
|
||||||
|
## Data Storage
|
||||||
|
|
||||||
|
Status history is stored in `src/static/json/status_history.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"last_check": "2026-02-11T14:30:00",
|
||||||
|
"services": {
|
||||||
|
"main": {
|
||||||
|
"name": "asimonson.com",
|
||||||
|
"url": "https://asimonson.com",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 156,
|
||||||
|
"status_code": 200,
|
||||||
|
"last_online": "2026-02-11T14:30:00",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:30:00",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 156,
|
||||||
|
"status_code": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Status Types
|
||||||
|
|
||||||
|
- **online**: HTTP status 2xx-4xx, service responding
|
||||||
|
- **degraded**: HTTP status 5xx or slow response
|
||||||
|
- **timeout**: Request exceeded timeout limit (10 seconds)
|
||||||
|
- **offline**: Unable to reach service
|
||||||
|
- **unknown**: No checks performed yet
|
||||||
|
|
||||||
|
## Uptime Calculation
|
||||||
|
|
||||||
|
Uptime percentage = (number of online checks / total checks) × 100
|
||||||
|
|
||||||
|
Calculated for:
|
||||||
|
- Last 24 hours
|
||||||
|
- Last 7 days
|
||||||
|
- Last 30 days
|
||||||
|
- All-time (since monitoring began)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Starting the Server
|
||||||
|
```bash
|
||||||
|
cd src
|
||||||
|
python3 app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The monitoring will start automatically and perform an initial check immediately, then every 2 hours thereafter.
|
||||||
|
|
||||||
|
### Accessing the Status Page
|
||||||
|
Navigate to: `https://asimonson.com/status`
|
||||||
|
|
||||||
|
### API Access
|
||||||
|
Direct API access: `https://asimonson.com/api/status`
|
||||||
|
|
||||||
|
Returns JSON with current status and uptime statistics for all services.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
To modify monitoring behavior, edit `src/monitor.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Change check interval (in seconds)
|
||||||
|
CHECK_INTERVAL = 7200 # 2 hours
|
||||||
|
|
||||||
|
# Modify service list
|
||||||
|
SERVICES = [
|
||||||
|
{
|
||||||
|
'id': 'main',
|
||||||
|
'name': 'asimonson.com',
|
||||||
|
'url': 'https://asimonson.com',
|
||||||
|
'timeout': 10 # seconds
|
||||||
|
},
|
||||||
|
# Add more services here
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- First deployment will show limited uptime data until enough checks accumulate
|
||||||
|
- Historical data is preserved across server restarts
|
||||||
|
- Maximum 720 checks stored per service (60 days at 2-hour intervals)
|
||||||
|
- Page auto-refreshes every 5 minutes to show latest server data
|
||||||
|
- Manual refresh button available for immediate updates
|
||||||
|
- All checks performed server-side (no client-side CORS issues)
|
||||||
@@ -3,9 +3,13 @@ from flask_minify import Minify
|
|||||||
import json
|
import json
|
||||||
import werkzeug.exceptions as HTTPerror
|
import werkzeug.exceptions as HTTPerror
|
||||||
from config import *
|
from config import *
|
||||||
|
from monitor import monitor
|
||||||
|
|
||||||
app = flask.Flask(__name__)
|
app = flask.Flask(__name__)
|
||||||
|
|
||||||
|
# Start service monitoring
|
||||||
|
monitor.start_monitoring()
|
||||||
|
|
||||||
# Add security and caching headers
|
# Add security and caching headers
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def add_security_headers(response):
|
def add_security_headers(response):
|
||||||
@@ -40,6 +44,11 @@ pages['projects']['projects'] = proj
|
|||||||
pages['home']['books'] = books
|
pages['home']['books'] = books
|
||||||
pages['books']['books'] = books
|
pages['books']['books'] = books
|
||||||
|
|
||||||
|
@app.route('/api/status')
|
||||||
|
def api_status():
|
||||||
|
"""API endpoint for service status"""
|
||||||
|
return flask.jsonify(monitor.get_status_summary())
|
||||||
|
|
||||||
@app.route('/api/goto/')
|
@app.route('/api/goto/')
|
||||||
@app.route('/api/goto/<location>')
|
@app.route('/api/goto/<location>')
|
||||||
def goto(location='home'):
|
def goto(location='home'):
|
||||||
|
|||||||
250
src/monitor.py
Normal file
250
src/monitor.py
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
"""
|
||||||
|
Service monitoring module
|
||||||
|
Checks service availability and tracks uptime statistics
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from threading import Thread, Lock
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Service configuration
|
||||||
|
SERVICES = [
|
||||||
|
{
|
||||||
|
'id': 'main',
|
||||||
|
'name': 'asimonson.com',
|
||||||
|
'url': 'https://asimonson.com',
|
||||||
|
'timeout': 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'files',
|
||||||
|
'name': 'files.asimonson.com',
|
||||||
|
'url': 'https://files.asimonson.com',
|
||||||
|
'timeout': 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'git',
|
||||||
|
'name': 'git.asimonson.com',
|
||||||
|
'url': 'https://git.asimonson.com',
|
||||||
|
'timeout': 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'pass',
|
||||||
|
'name': 'pass.asimonson.com',
|
||||||
|
'url': 'https://pass.asimonson.com',
|
||||||
|
'timeout': 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'ssh',
|
||||||
|
'name': 'ssh.asimonson.com',
|
||||||
|
'url': 'https://ssh.asimonson.com',
|
||||||
|
'timeout': 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check interval: 2 hours = 7200 seconds
|
||||||
|
CHECK_INTERVAL = 1800
|
||||||
|
|
||||||
|
# File to store status history
|
||||||
|
STATUS_FILE = Path(__file__).parent / 'static' / 'json' / 'status_history.json'
|
||||||
|
|
||||||
|
class ServiceMonitor:
|
||||||
|
def __init__(self):
|
||||||
|
self.status_data = {}
|
||||||
|
self.lock = Lock()
|
||||||
|
self.load_history()
|
||||||
|
|
||||||
|
def load_history(self):
|
||||||
|
"""Load status history from file"""
|
||||||
|
if STATUS_FILE.exists():
|
||||||
|
try:
|
||||||
|
with open(STATUS_FILE, 'r') as f:
|
||||||
|
self.status_data = json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading status history: {e}")
|
||||||
|
self.initialize_status_data()
|
||||||
|
else:
|
||||||
|
self.initialize_status_data()
|
||||||
|
|
||||||
|
def initialize_status_data(self):
|
||||||
|
"""Initialize empty status data structure"""
|
||||||
|
self.status_data = {
|
||||||
|
'last_check': None,
|
||||||
|
'services': {}
|
||||||
|
}
|
||||||
|
for service in SERVICES:
|
||||||
|
self.status_data['services'][service['id']] = {
|
||||||
|
'name': service['name'],
|
||||||
|
'url': service['url'],
|
||||||
|
'status': 'unknown',
|
||||||
|
'response_time': None,
|
||||||
|
'status_code': None,
|
||||||
|
'last_online': None,
|
||||||
|
'checks': [] # List of check results
|
||||||
|
}
|
||||||
|
|
||||||
|
def save_history(self):
|
||||||
|
"""Save status history to file"""
|
||||||
|
try:
|
||||||
|
STATUS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(STATUS_FILE, 'w') as f:
|
||||||
|
json.dump(self.status_data, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving status history: {e}")
|
||||||
|
|
||||||
|
def check_service(self, service):
|
||||||
|
"""Check a single service and return status"""
|
||||||
|
start_time = time.time()
|
||||||
|
result = {
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'status': 'offline',
|
||||||
|
'response_time': None,
|
||||||
|
'status_code': None
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.head(
|
||||||
|
service['url'],
|
||||||
|
timeout=service['timeout'],
|
||||||
|
allow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
elapsed = int((time.time() - start_time) * 1000) # ms
|
||||||
|
|
||||||
|
result['response_time'] = elapsed
|
||||||
|
result['status_code'] = response.status_code
|
||||||
|
|
||||||
|
# Consider 2xx and 3xx as online
|
||||||
|
if 200 <= response.status_code < 400:
|
||||||
|
result['status'] = 'online'
|
||||||
|
elif 400 <= response.status_code < 500:
|
||||||
|
# Client errors might still mean service is up
|
||||||
|
result['status'] = 'online'
|
||||||
|
else:
|
||||||
|
result['status'] = 'degraded'
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
result['status'] = 'timeout'
|
||||||
|
result['response_time'] = service['timeout'] * 1000
|
||||||
|
except Exception as e:
|
||||||
|
result['status'] = 'offline'
|
||||||
|
result['error'] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def check_all_services(self):
|
||||||
|
"""Check all services and update status data"""
|
||||||
|
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Checking all services...")
|
||||||
|
|
||||||
|
# Perform all network checks OUTSIDE the lock to avoid blocking API calls
|
||||||
|
results = {}
|
||||||
|
for service in SERVICES:
|
||||||
|
result = self.check_service(service)
|
||||||
|
results[service['id']] = result
|
||||||
|
print(f" {service['name']}: {result['status']} ({result['response_time']}ms)")
|
||||||
|
|
||||||
|
# Only acquire lock when updating the shared data structure
|
||||||
|
with self.lock:
|
||||||
|
for service in SERVICES:
|
||||||
|
result = results[service['id']]
|
||||||
|
service_data = self.status_data['services'][service['id']]
|
||||||
|
|
||||||
|
# Update current status
|
||||||
|
service_data['status'] = result['status']
|
||||||
|
service_data['response_time'] = result['response_time']
|
||||||
|
service_data['status_code'] = result['status_code']
|
||||||
|
|
||||||
|
if result['status'] == 'online':
|
||||||
|
service_data['last_online'] = result['timestamp']
|
||||||
|
|
||||||
|
# Add to check history (keep last 720 checks = 60 days at 2hr intervals)
|
||||||
|
service_data['checks'].append(result)
|
||||||
|
if len(service_data['checks']) > 720:
|
||||||
|
service_data['checks'] = service_data['checks'][-720:]
|
||||||
|
|
||||||
|
self.status_data['last_check'] = datetime.now().isoformat()
|
||||||
|
self.save_history()
|
||||||
|
|
||||||
|
def _calculate_uptime_unlocked(self, service_id, hours=None):
|
||||||
|
"""Calculate uptime percentage for a service (assumes lock is held)"""
|
||||||
|
service_data = self.status_data['services'].get(service_id)
|
||||||
|
if not service_data or not service_data['checks']:
|
||||||
|
return None
|
||||||
|
|
||||||
|
checks = service_data['checks']
|
||||||
|
|
||||||
|
# Filter by time period if specified
|
||||||
|
if hours:
|
||||||
|
cutoff = datetime.now() - timedelta(hours=hours)
|
||||||
|
checks = [
|
||||||
|
c for c in checks
|
||||||
|
if datetime.fromisoformat(c['timestamp']) > cutoff
|
||||||
|
]
|
||||||
|
|
||||||
|
if not checks:
|
||||||
|
return None
|
||||||
|
|
||||||
|
online_count = sum(1 for c in checks if c['status'] == 'online')
|
||||||
|
uptime = (online_count / len(checks)) * 100
|
||||||
|
|
||||||
|
return round(uptime, 2)
|
||||||
|
|
||||||
|
def calculate_uptime(self, service_id, hours=None):
|
||||||
|
"""Calculate uptime percentage for a service"""
|
||||||
|
with self.lock:
|
||||||
|
return self._calculate_uptime_unlocked(service_id, hours)
|
||||||
|
|
||||||
|
def get_status_summary(self):
|
||||||
|
"""Get current status summary with uptime statistics"""
|
||||||
|
with self.lock:
|
||||||
|
summary = {
|
||||||
|
'last_check': self.status_data['last_check'],
|
||||||
|
'next_check': None,
|
||||||
|
'services': []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate next check time
|
||||||
|
if self.status_data['last_check']:
|
||||||
|
last_check = datetime.fromisoformat(self.status_data['last_check'])
|
||||||
|
next_check = last_check + timedelta(seconds=CHECK_INTERVAL)
|
||||||
|
summary['next_check'] = next_check.isoformat()
|
||||||
|
|
||||||
|
for service_id, service_data in self.status_data['services'].items():
|
||||||
|
service_summary = {
|
||||||
|
'id': service_id,
|
||||||
|
'name': service_data['name'],
|
||||||
|
'url': service_data['url'],
|
||||||
|
'status': service_data['status'],
|
||||||
|
'response_time': service_data['response_time'],
|
||||||
|
'status_code': service_data['status_code'],
|
||||||
|
'last_online': service_data['last_online'],
|
||||||
|
'uptime': {
|
||||||
|
'24h': self._calculate_uptime_unlocked(service_id, 24),
|
||||||
|
'7d': self._calculate_uptime_unlocked(service_id, 24 * 7),
|
||||||
|
'30d': self._calculate_uptime_unlocked(service_id, 24 * 30),
|
||||||
|
'all_time': self._calculate_uptime_unlocked(service_id)
|
||||||
|
},
|
||||||
|
'total_checks': len(service_data['checks'])
|
||||||
|
}
|
||||||
|
summary['services'].append(service_summary)
|
||||||
|
|
||||||
|
return summary
|
||||||
|
|
||||||
|
def start_monitoring(self):
|
||||||
|
"""Start background monitoring thread"""
|
||||||
|
def monitor_loop():
|
||||||
|
# Initial check
|
||||||
|
self.check_all_services()
|
||||||
|
|
||||||
|
# Periodic checks
|
||||||
|
while True:
|
||||||
|
time.sleep(CHECK_INTERVAL)
|
||||||
|
self.check_all_services()
|
||||||
|
|
||||||
|
thread = Thread(target=monitor_loop, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
print(f"Service monitoring started (checks every {CHECK_INTERVAL/3600} hours)")
|
||||||
|
|
||||||
|
# Global monitor instance
|
||||||
|
monitor = ServiceMonitor()
|
||||||
@@ -1190,4 +1190,267 @@ tr {
|
|||||||
display: none;
|
display: none;
|
||||||
height: 0px;
|
height: 0px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Page Styles */
|
||||||
|
.status-subtitle {
|
||||||
|
text-align: center;
|
||||||
|
color: #a8a8a8;
|
||||||
|
margin-top: -10px;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
padding: 1em;
|
||||||
|
background: rgba(24, 24, 24, 0.85);
|
||||||
|
border-radius: 0.5em;
|
||||||
|
border: solid 2px rgba(139, 36, 36, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info span {
|
||||||
|
color: #a8a8a8;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#refreshBtn {
|
||||||
|
background: rgba(156, 49, 45, 0.8);
|
||||||
|
color: #ecebeb;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5em 1.5em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#refreshBtn:hover:not(:disabled) {
|
||||||
|
background: rgba(156, 49, 45, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#refreshBtn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1.5em;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
background: rgba(24, 24, 24, 0.85);
|
||||||
|
border-radius: 0.5em;
|
||||||
|
padding: 1.5em;
|
||||||
|
border-top: solid 4px rgba(139, 36, 36, 0.5);
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card.online {
|
||||||
|
border-top-color: rgba(76, 175, 80, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card.degraded {
|
||||||
|
border-top-color: rgba(255, 193, 7, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card.offline {
|
||||||
|
border-top-color: rgba(244, 67, 54, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
border-bottom: 1px solid rgba(168, 168, 168, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #ecebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.online {
|
||||||
|
background: #4caf50;
|
||||||
|
box-shadow: 0 0 10px #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.degraded {
|
||||||
|
background: #ffc107;
|
||||||
|
box-shadow: 0 0 10px #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.offline {
|
||||||
|
background: #f44336;
|
||||||
|
box-shadow: 0 0 10px #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.loading {
|
||||||
|
background: #a8a8a8;
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-metric {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.3em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
color: #a8a8a8;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
color: #ecebeb;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-legend {
|
||||||
|
background: rgba(24, 24, 24, 0.85);
|
||||||
|
border-radius: 0.5em;
|
||||||
|
padding: 1.5em;
|
||||||
|
border: solid 2px rgba(139, 36, 36, 0.5);
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-legend h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #ecebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-items {
|
||||||
|
display: flex;
|
||||||
|
gap: 2em;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
color: #a8a8a8;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-note {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #a8a8a8;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-uptime {
|
||||||
|
margin-top: 1em;
|
||||||
|
padding-top: 0.8em;
|
||||||
|
border-top: 1px solid rgba(168, 168, 168, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uptime-label {
|
||||||
|
color: #a8a8a8;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uptime-values {
|
||||||
|
color: #ecebeb;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uptime-values strong {
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info-box {
|
||||||
|
background: rgba(24, 24, 24, 0.85);
|
||||||
|
border-radius: 0.5em;
|
||||||
|
padding: 1.5em;
|
||||||
|
border: solid 2px rgba(139, 36, 36, 0.5);
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info-box h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #ecebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info-box ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
color: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info-box li {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
color: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info-box strong {
|
||||||
|
color: #ecebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1400px) {
|
||||||
|
.status-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-items {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
180
src/static/js/status.js
Normal file
180
src/static/js/status.js
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
// Fetch and display service status from API
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch status data from server
|
||||||
|
*/
|
||||||
|
async function fetchStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/status');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
updateStatusDisplay(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching status:', error);
|
||||||
|
showError('Failed to fetch service status. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the status display with fetched data
|
||||||
|
*/
|
||||||
|
function updateStatusDisplay(data) {
|
||||||
|
// Update last check time
|
||||||
|
if (data.last_check) {
|
||||||
|
const lastCheck = new Date(data.last_check);
|
||||||
|
const timeString = lastCheck.toLocaleString();
|
||||||
|
document.getElementById('lastUpdate').textContent = `Last checked: ${timeString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update next check time
|
||||||
|
if (data.next_check) {
|
||||||
|
const nextCheck = new Date(data.next_check);
|
||||||
|
const timeString = nextCheck.toLocaleString();
|
||||||
|
const nextCheckEl = document.getElementById('nextUpdate');
|
||||||
|
if (nextCheckEl) {
|
||||||
|
nextCheckEl.textContent = `Next check: ${timeString}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update each service
|
||||||
|
data.services.forEach(service => {
|
||||||
|
updateServiceCard(service);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-enable refresh button
|
||||||
|
const refreshBtn = document.getElementById('refreshBtn');
|
||||||
|
if (refreshBtn) {
|
||||||
|
refreshBtn.disabled = false;
|
||||||
|
refreshBtn.textContent = 'Refresh Now';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a single service card
|
||||||
|
*/
|
||||||
|
function updateServiceCard(service) {
|
||||||
|
const card = document.getElementById(`status-${service.id}`);
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
const statusDot = card.querySelector('.status-dot');
|
||||||
|
const statusText = card.querySelector('.status-text');
|
||||||
|
const timeDisplay = document.getElementById(`time-${service.id}`);
|
||||||
|
const codeDisplay = document.getElementById(`code-${service.id}`);
|
||||||
|
const uptimeDisplay = document.getElementById(`uptime-${service.id}`);
|
||||||
|
const checksDisplay = document.getElementById(`checks-${service.id}`);
|
||||||
|
|
||||||
|
// Update response time
|
||||||
|
if (service.response_time !== null) {
|
||||||
|
timeDisplay.textContent = `${service.response_time}ms`;
|
||||||
|
} else {
|
||||||
|
timeDisplay.textContent = '--';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status code
|
||||||
|
if (service.status_code !== null) {
|
||||||
|
codeDisplay.textContent = service.status_code;
|
||||||
|
} else {
|
||||||
|
codeDisplay.textContent = service.status === 'unknown' ? 'Unknown' : 'Error';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status indicator
|
||||||
|
card.classList.remove('online', 'degraded', 'offline', 'unknown');
|
||||||
|
|
||||||
|
switch (service.status) {
|
||||||
|
case 'online':
|
||||||
|
statusDot.className = 'status-dot online';
|
||||||
|
statusText.textContent = 'Operational';
|
||||||
|
card.classList.add('online');
|
||||||
|
break;
|
||||||
|
case 'degraded':
|
||||||
|
case 'timeout':
|
||||||
|
statusDot.className = 'status-dot degraded';
|
||||||
|
statusText.textContent = service.status === 'timeout' ? 'Timeout' : 'Degraded';
|
||||||
|
card.classList.add('degraded');
|
||||||
|
break;
|
||||||
|
case 'offline':
|
||||||
|
statusDot.className = 'status-dot offline';
|
||||||
|
statusText.textContent = 'Offline';
|
||||||
|
card.classList.add('offline');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
statusDot.className = 'status-dot loading';
|
||||||
|
statusText.textContent = 'Unknown';
|
||||||
|
card.classList.add('unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update uptime statistics
|
||||||
|
if (uptimeDisplay && service.uptime) {
|
||||||
|
const uptimeHTML = [];
|
||||||
|
|
||||||
|
if (service.uptime['24h'] !== null) {
|
||||||
|
uptimeHTML.push(`24h: <strong>${service.uptime['24h']}%</strong>`);
|
||||||
|
}
|
||||||
|
if (service.uptime['7d'] !== null) {
|
||||||
|
uptimeHTML.push(`7d: <strong>${service.uptime['7d']}%</strong>`);
|
||||||
|
}
|
||||||
|
if (service.uptime['30d'] !== null) {
|
||||||
|
uptimeHTML.push(`30d: <strong>${service.uptime['30d']}%</strong>`);
|
||||||
|
}
|
||||||
|
if (service.uptime.all_time !== null) {
|
||||||
|
uptimeHTML.push(`All: <strong>${service.uptime.all_time}%</strong>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uptimeHTML.length > 0) {
|
||||||
|
uptimeDisplay.innerHTML = uptimeHTML.join(' | ');
|
||||||
|
} else {
|
||||||
|
uptimeDisplay.textContent = 'No data yet';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update total checks
|
||||||
|
if (checksDisplay && service.total_checks !== undefined) {
|
||||||
|
checksDisplay.textContent = service.total_checks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show error message
|
||||||
|
*/
|
||||||
|
function showError(message) {
|
||||||
|
const errorDiv = document.createElement('div');
|
||||||
|
errorDiv.className = 'status-error';
|
||||||
|
errorDiv.textContent = message;
|
||||||
|
errorDiv.style.cssText = 'background: rgba(244, 67, 54, 0.2); color: #f44336; padding: 1em; margin: 1em 0; border-radius: 0.5em; text-align: center;';
|
||||||
|
|
||||||
|
const container = document.querySelector('.foregroundContent');
|
||||||
|
if (container) {
|
||||||
|
container.insertBefore(errorDiv, container.firstChild);
|
||||||
|
setTimeout(() => errorDiv.remove(), 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manual refresh
|
||||||
|
*/
|
||||||
|
function refreshStatus() {
|
||||||
|
const refreshBtn = document.getElementById('refreshBtn');
|
||||||
|
if (refreshBtn) {
|
||||||
|
refreshBtn.disabled = true;
|
||||||
|
refreshBtn.textContent = 'Checking...';
|
||||||
|
}
|
||||||
|
fetchStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize on page load
|
||||||
|
*/
|
||||||
|
function initStatusPage() {
|
||||||
|
fetchStatus();
|
||||||
|
// Auto-refresh every 5 minutes to get latest data
|
||||||
|
setInterval(fetchStatus, 300000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start when page loads
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initStatusPage);
|
||||||
|
} else {
|
||||||
|
initStatusPage();
|
||||||
|
}
|
||||||
145
src/static/json/status_history.json
Normal file
145
src/static/json/status_history.json
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
{
|
||||||
|
"last_check": "2026-02-11T14:53:09.045018",
|
||||||
|
"services": {
|
||||||
|
"main": {
|
||||||
|
"name": "asimonson.com",
|
||||||
|
"url": "https://asimonson.com",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 170,
|
||||||
|
"status_code": 200,
|
||||||
|
"last_online": "2026-02-11T14:53:08.089141",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:45:35.008804",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 171,
|
||||||
|
"status_code": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:51:02.673733",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 138,
|
||||||
|
"status_code": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:53:08.089141",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 170,
|
||||||
|
"status_code": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"name": "files.asimonson.com",
|
||||||
|
"url": "https://files.asimonson.com",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 215,
|
||||||
|
"status_code": 200,
|
||||||
|
"last_online": "2026-02-11T14:53:08.259522",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:45:35.180195",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 285,
|
||||||
|
"status_code": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:51:02.812769",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 259,
|
||||||
|
"status_code": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:53:08.259522",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 215,
|
||||||
|
"status_code": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"git": {
|
||||||
|
"name": "git.asimonson.com",
|
||||||
|
"url": "https://git.asimonson.com",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 145,
|
||||||
|
"status_code": 200,
|
||||||
|
"last_online": "2026-02-11T14:53:08.475376",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:45:35.465748",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 297,
|
||||||
|
"status_code": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:51:03.072293",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 293,
|
||||||
|
"status_code": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:53:08.475376",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 145,
|
||||||
|
"status_code": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pass": {
|
||||||
|
"name": "pass.asimonson.com",
|
||||||
|
"url": "https://pass.asimonson.com",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 160,
|
||||||
|
"status_code": 200,
|
||||||
|
"last_online": "2026-02-11T14:53:08.621016",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:45:35.763775",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 253,
|
||||||
|
"status_code": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:51:03.365544",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 228,
|
||||||
|
"status_code": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:53:08.621016",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 160,
|
||||||
|
"status_code": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ssh": {
|
||||||
|
"name": "ssh.asimonson.com",
|
||||||
|
"url": "https://ssh.asimonson.com",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 263,
|
||||||
|
"status_code": 200,
|
||||||
|
"last_online": "2026-02-11T14:53:08.781704",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:45:36.017180",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 378,
|
||||||
|
"status_code": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:51:03.594307",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 411,
|
||||||
|
"status_code": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-11T14:53:08.781704",
|
||||||
|
"status": "online",
|
||||||
|
"response_time": 263,
|
||||||
|
"status_code": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,36 +1,189 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="foreground"></div>
|
<div class="foreground"></div>
|
||||||
<div class="foregroundContent">
|
<div class="foregroundContent">
|
||||||
<h2 class='concentratedHead'>Server Status Page</h2>
|
<h2 class='concentratedHead'>Service Status Monitor</h2>
|
||||||
<h3>Page Disabled</h3>
|
<p class="status-subtitle">Automated monitoring of asimonson.com services</p>
|
||||||
{# <h4>Page under construction</h4>
|
|
||||||
<table>
|
<div class="status-info">
|
||||||
<tr>
|
<div>
|
||||||
<th>Host</th>
|
<span id="lastUpdate">Last checked: Loading...</span>
|
||||||
<th>Service(s)</th>
|
<br>
|
||||||
<th>Status</th>
|
<span id="nextUpdate" style="font-size: 0.85em; color: #888;">Next check: --</span>
|
||||||
</tr>
|
</div>
|
||||||
<tr>
|
<button id="refreshBtn" onclick="refreshStatus()">Refresh Now</button>
|
||||||
<td>LC lemp</td>
|
</div>
|
||||||
<td>Portfolio Website</td>
|
|
||||||
<td></td>
|
<div class="status-container">
|
||||||
</tr>
|
<div class="status-card" id="status-main">
|
||||||
<tr>
|
<div class="status-header">
|
||||||
<td>LC lemp</td>
|
<h3>asimonson.com</h3>
|
||||||
<td>hotspots.asimonson.com</td>
|
<div class="status-indicator">
|
||||||
<td></td>
|
<span class="status-dot loading"></span>
|
||||||
</tr>
|
<span class="status-text">Loading...</span>
|
||||||
<tr>
|
</div>
|
||||||
<td>LC Antietam</td>
|
</div>
|
||||||
<td>gatorway</td>
|
<div class="status-details">
|
||||||
<td>Unknown</td>
|
<div class="status-metric">
|
||||||
</tr>
|
<span class="metric-label">Response Time:</span>
|
||||||
<tr>
|
<span class="metric-value" id="time-main">--</span>
|
||||||
<td>CSH K8s Cluster</td>
|
</div>
|
||||||
<td>slate.csh.rit.edu</td>
|
<div class="status-metric">
|
||||||
<td></td>
|
<span class="metric-label">Status Code:</span>
|
||||||
</tr>
|
<span class="metric-value" id="code-main">--</span>
|
||||||
</table>
|
</div>
|
||||||
#}
|
<div class="status-metric">
|
||||||
|
<span class="metric-label">Total Checks:</span>
|
||||||
|
<span class="metric-value" id="checks-main">--</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-uptime">
|
||||||
|
<div class="uptime-label">Uptime:</div>
|
||||||
|
<div class="uptime-values" id="uptime-main">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-card" id="status-files">
|
||||||
|
<div class="status-header">
|
||||||
|
<h3>files.asimonson.com</h3>
|
||||||
|
<div class="status-indicator">
|
||||||
|
<span class="status-dot loading"></span>
|
||||||
|
<span class="status-text">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-details">
|
||||||
|
<div class="status-metric">
|
||||||
|
<span class="metric-label">Response Time:</span>
|
||||||
|
<span class="metric-value" id="time-files">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-metric">
|
||||||
|
<span class="metric-label">Status Code:</span>
|
||||||
|
<span class="metric-value" id="code-files">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-metric">
|
||||||
|
<span class="metric-label">Total Checks:</span>
|
||||||
|
<span class="metric-value" id="checks-files">--</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-uptime">
|
||||||
|
<div class="uptime-label">Uptime:</div>
|
||||||
|
<div class="uptime-values" id="uptime-files">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-card" id="status-git">
|
||||||
|
<div class="status-header">
|
||||||
|
<h3>git.asimonson.com</h3>
|
||||||
|
<div class="status-indicator">
|
||||||
|
<span class="status-dot loading"></span>
|
||||||
|
<span class="status-text">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-details">
|
||||||
|
<div class="status-metric">
|
||||||
|
<span class="metric-label">Response Time:</span>
|
||||||
|
<span class="metric-value" id="time-git">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-metric">
|
||||||
|
<span class="metric-label">Status Code:</span>
|
||||||
|
<span class="metric-value" id="code-git">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-metric">
|
||||||
|
<span class="metric-label">Total Checks:</span>
|
||||||
|
<span class="metric-value" id="checks-git">--</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-uptime">
|
||||||
|
<div class="uptime-label">Uptime:</div>
|
||||||
|
<div class="uptime-values" id="uptime-git">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-card" id="status-pass">
|
||||||
|
<div class="status-header">
|
||||||
|
<h3>pass.asimonson.com</h3>
|
||||||
|
<div class="status-indicator">
|
||||||
|
<span class="status-dot loading"></span>
|
||||||
|
<span class="status-text">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-details">
|
||||||
|
<div class="status-metric">
|
||||||
|
<span class="metric-label">Response Time:</span>
|
||||||
|
<span class="metric-value" id="time-pass">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-metric">
|
||||||
|
<span class="metric-label">Status Code:</span>
|
||||||
|
<span class="metric-value" id="code-pass">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-metric">
|
||||||
|
<span class="metric-label">Total Checks:</span>
|
||||||
|
<span class="metric-value" id="checks-pass">--</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-uptime">
|
||||||
|
<div class="uptime-label">Uptime:</div>
|
||||||
|
<div class="uptime-values" id="uptime-pass">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-card" id="status-ssh">
|
||||||
|
<div class="status-header">
|
||||||
|
<h3>ssh.asimonson.com</h3>
|
||||||
|
<div class="status-indicator">
|
||||||
|
<span class="status-dot loading"></span>
|
||||||
|
<span class="status-text">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-details">
|
||||||
|
<div class="status-metric">
|
||||||
|
<span class="metric-label">Response Time:</span>
|
||||||
|
<span class="metric-value" id="time-ssh">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-metric">
|
||||||
|
<span class="metric-label">Status Code:</span>
|
||||||
|
<span class="metric-value" id="code-ssh">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-metric">
|
||||||
|
<span class="metric-label">Total Checks:</span>
|
||||||
|
<span class="metric-value" id="checks-ssh">--</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-uptime">
|
||||||
|
<div class="uptime-label">Uptime:</div>
|
||||||
|
<div class="uptime-values" id="uptime-ssh">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-legend">
|
||||||
|
<h4>Status Legend</h4>
|
||||||
|
<div class="legend-items">
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="status-dot online"></span>
|
||||||
|
<span>Operational (response successful)</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="status-dot degraded"></span>
|
||||||
|
<span>Degraded (timeout or errors)</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="status-dot offline"></span>
|
||||||
|
<span>Offline (unreachable)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-info-box">
|
||||||
|
<h4>About This Monitor</h4>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Check Frequency:</strong> Services are checked automatically every 2 hours from the server</li>
|
||||||
|
<li><strong>Uptime Calculation:</strong> Based on historical check data (24h, 7d, 30d, and all-time)</li>
|
||||||
|
<li><strong>Response Time:</strong> Time taken to receive a response from the service</li>
|
||||||
|
<li><strong>Status Code:</strong> HTTP response code from the service</li>
|
||||||
|
<li><strong>Page Refresh:</strong> This page auto-refreshes every 5 minutes to show latest data</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/status.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user