From b1e75bd91f782b3e0861cca8d50b271bb87dc62c Mon Sep 17 00:00:00 2001 From: Andrew Simonson Date: Wed, 11 Feb 2026 14:54:45 -0600 Subject: [PATCH] sample status page --- STATUS_MONITOR_README.md | 149 ++++++++++++++++ src/app.py | 9 + src/monitor.py | 250 ++++++++++++++++++++++++++ src/static/css/App.css | 263 ++++++++++++++++++++++++++++ src/static/js/status.js | 180 +++++++++++++++++++ src/static/json/status_history.json | 145 +++++++++++++++ src/templates/status.html | 215 +++++++++++++++++++---- 7 files changed, 1180 insertions(+), 31 deletions(-) create mode 100644 STATUS_MONITOR_README.md create mode 100644 src/monitor.py create mode 100644 src/static/js/status.js create mode 100644 src/static/json/status_history.json diff --git a/STATUS_MONITOR_README.md b/STATUS_MONITOR_README.md new file mode 100644 index 0000000..4d8047d --- /dev/null +++ b/STATUS_MONITOR_README.md @@ -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) diff --git a/src/app.py b/src/app.py index 1adc39b..8af0492 100755 --- a/src/app.py +++ b/src/app.py @@ -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/') def goto(location='home'): diff --git a/src/monitor.py b/src/monitor.py new file mode 100644 index 0000000..e482abc --- /dev/null +++ b/src/monitor.py @@ -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() diff --git a/src/static/css/App.css b/src/static/css/App.css index 35b46cf..757c3cc 100755 --- a/src/static/css/App.css +++ b/src/static/css/App.css @@ -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; + } } \ No newline at end of file diff --git a/src/static/js/status.js b/src/static/js/status.js new file mode 100644 index 0000000..f050e8b --- /dev/null +++ b/src/static/js/status.js @@ -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: ${service.uptime['24h']}%`); + } + if (service.uptime['7d'] !== null) { + uptimeHTML.push(`7d: ${service.uptime['7d']}%`); + } + if (service.uptime['30d'] !== null) { + uptimeHTML.push(`30d: ${service.uptime['30d']}%`); + } + if (service.uptime.all_time !== null) { + uptimeHTML.push(`All: ${service.uptime.all_time}%`); + } + + 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(); +} diff --git a/src/static/json/status_history.json b/src/static/json/status_history.json new file mode 100644 index 0000000..69e90ae --- /dev/null +++ b/src/static/json/status_history.json @@ -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 + } + ] + } + } +} \ No newline at end of file diff --git a/src/templates/status.html b/src/templates/status.html index 6247e0b..297c0c3 100644 --- a/src/templates/status.html +++ b/src/templates/status.html @@ -1,36 +1,189 @@ {% block content %}
-

Server Status Page

-

Page Disabled

- {#

Page under construction

- - - - - - - - - - - - - - - - - - - - - - - - - - -
HostService(s)Status
LC lempPortfolio Website
LC lemphotspots.asimonson.com
LC AntietamgatorwayUnknown
CSH K8s Clusterslate.csh.rit.edu
- #} +

Service Status Monitor

+

Automated monitoring of asimonson.com services

+ +
+
+ Last checked: Loading... +
+ Next check: -- +
+ +
+ +
+
+
+

asimonson.com

+
+ + Loading... +
+
+
+
+ Response Time: + -- +
+
+ Status Code: + -- +
+
+ Total Checks: + -- +
+
+
+
Uptime:
+
Loading...
+
+
+ +
+
+

files.asimonson.com

+
+ + Loading... +
+
+
+
+ Response Time: + -- +
+
+ Status Code: + -- +
+
+ Total Checks: + -- +
+
+
+
Uptime:
+
Loading...
+
+
+ +
+
+

git.asimonson.com

+
+ + Loading... +
+
+
+
+ Response Time: + -- +
+
+ Status Code: + -- +
+
+ Total Checks: + -- +
+
+
+
Uptime:
+
Loading...
+
+
+ +
+
+

pass.asimonson.com

+
+ + Loading... +
+
+
+
+ Response Time: + -- +
+
+ Status Code: + -- +
+
+ Total Checks: + -- +
+
+
+
Uptime:
+
Loading...
+
+
+ +
+
+

ssh.asimonson.com

+
+ + Loading... +
+
+
+
+ Response Time: + -- +
+
+ Status Code: + -- +
+
+ Total Checks: + -- +
+
+
+
Uptime:
+
Loading...
+
+
+
+ +
+

Status Legend

+
+
+ + Operational (response successful) +
+
+ + Degraded (timeout or errors) +
+
+ + Offline (unreachable) +
+
+
+ +
+

About This Monitor

+
    +
  • Check Frequency: Services are checked automatically every 2 hours from the server
  • +
  • Uptime Calculation: Based on historical check data (24h, 7d, 30d, and all-time)
  • +
  • Response Time: Time taken to receive a response from the service
  • +
  • Status Code: HTTP response code from the service
  • +
  • Page Refresh: This page auto-refreshes every 5 minutes to show latest data
  • +
+
+ + {% endblock %}