sample status page

This commit is contained in:
2026-02-11 14:54:45 -06:00
parent 46fc66971d
commit b1e75bd91f
7 changed files with 1180 additions and 31 deletions

View File

@@ -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
View 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()

View File

@@ -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
View 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();
}

View 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
}
]
}
}
}

View File

@@ -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 %}