mirror of
https://github.com/asimonson1125/asimonson1125.github.io.git
synced 2026-02-25 05:09:49 -06:00
sample status page
This commit is contained in:
@@ -3,9 +3,13 @@ from flask_minify import Minify
|
||||
import json
|
||||
import werkzeug.exceptions as HTTPerror
|
||||
from config import *
|
||||
from monitor import monitor
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
|
||||
# Start service monitoring
|
||||
monitor.start_monitoring()
|
||||
|
||||
# Add security and caching headers
|
||||
@app.after_request
|
||||
def add_security_headers(response):
|
||||
@@ -40,6 +44,11 @@ pages['projects']['projects'] = proj
|
||||
pages['home']['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/<location>')
|
||||
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;
|
||||
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 %}
|
||||
<div class="foreground"></div>
|
||||
<div class="foregroundContent">
|
||||
<h2 class='concentratedHead'>Server Status Page</h2>
|
||||
<h3>Page Disabled</h3>
|
||||
{# <h4>Page under construction</h4>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Host</th>
|
||||
<th>Service(s)</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LC lemp</td>
|
||||
<td>Portfolio Website</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LC lemp</td>
|
||||
<td>hotspots.asimonson.com</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LC Antietam</td>
|
||||
<td>gatorway</td>
|
||||
<td>Unknown</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CSH K8s Cluster</td>
|
||||
<td>slate.csh.rit.edu</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
#}
|
||||
<h2 class='concentratedHead'>Service Status Monitor</h2>
|
||||
<p class="status-subtitle">Automated monitoring of asimonson.com services</p>
|
||||
|
||||
<div class="status-info">
|
||||
<div>
|
||||
<span id="lastUpdate">Last checked: Loading...</span>
|
||||
<br>
|
||||
<span id="nextUpdate" style="font-size: 0.85em; color: #888;">Next check: --</span>
|
||||
</div>
|
||||
<button id="refreshBtn" onclick="refreshStatus()">Refresh Now</button>
|
||||
</div>
|
||||
|
||||
<div class="status-container">
|
||||
<div class="status-card" id="status-main">
|
||||
<div class="status-header">
|
||||
<h3>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-main">--</span>
|
||||
</div>
|
||||
<div class="status-metric">
|
||||
<span class="metric-label">Status Code:</span>
|
||||
<span class="metric-value" id="code-main">--</span>
|
||||
</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>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/status.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user