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

149
STATUS_MONITOR_README.md Normal file
View 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)

View File

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