diff --git a/src/app.py b/src/app.py index 8af0492..dc466e9 100755 --- a/src/app.py +++ b/src/app.py @@ -3,7 +3,7 @@ from flask_minify import Minify import json import werkzeug.exceptions as HTTPerror from config import * -from monitor import monitor +from monitor import monitor, SERVICES app = flask.Flask(__name__) @@ -43,6 +43,7 @@ pages['projects']['skillList'] = skillList pages['projects']['projects'] = proj pages['home']['books'] = books pages['books']['books'] = books +pages['status']['services'] = SERVICES @app.route('/api/status') def api_status(): diff --git a/src/monitor.py b/src/monitor.py index 18d7c4e..e89cfbd 100644 --- a/src/monitor.py +++ b/src/monitor.py @@ -30,15 +30,9 @@ SERVICES = [ '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', + 'id': 'balls', + 'name': 'balls.asimonson.com', + 'url': 'https://balls.asimonson.com', 'timeout': 10 } ] @@ -182,8 +176,22 @@ class ServiceMonitor: if datetime.fromisoformat(c['timestamp']) > cutoff ] - if not checks: - return None + if not checks: + return None + + # Require minimum data coverage for the time period + # Calculate expected number of checks for this period + expected_checks = (hours * 3600) / CHECK_INTERVAL + + # Require at least 50% of expected checks to show this metric + minimum_checks = max(3, expected_checks * 0.5) + + if len(checks) < minimum_checks: + return None + else: + # For all-time, require at least 3 checks + if len(checks) < 3: + return None online_count = sum(1 for c in checks if c['status'] == 'online') uptime = (online_count / len(checks)) * 100 diff --git a/src/static/css/App.css b/src/static/css/App.css index 05efc1f..c9c33c1 100755 --- a/src/static/css/App.css +++ b/src/static/css/App.css @@ -104,11 +104,30 @@ strong { color: #ecebeb; } -p, li { +ul { + margin: 0; + padding-left: 1.5em; + color: #a8a8a8; +} + +li { + margin-bottom: 0.5em; + color: #a8a8a8; +} + +strong { + color: #ecebeb; +} + +p, li, span { color: rgb(212, 212, 212); font-size: 1rem; } +span { + font-size: .8rem; +} + a, a p { color: #a0a0a0a0; text-decoration: none; @@ -1297,20 +1316,116 @@ tr { font-size: 1rem; } -.status-info { +/* Overall Status Bar */ +/* Overall Status Bar */ +.overall-status-bar { + background: rgba(24, 24, 24, 0.95); + border-radius: 0.5em; + padding: 2em; + margin-bottom: 2em; + border-top: solid 4px rgba(139, 36, 36, 0.5); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + transition: border-color 0.5s ease; +} + +.overall-status-bar.all-operational { + border-top-color: rgba(76, 175, 80, 0.8); +} + +.overall-status-bar.partial-outage { + border-top-color: rgba(255, 193, 7, 0.8); +} + +.overall-status-bar.major-outage { + border-top-color: rgba(244, 67, 54, 0.8); +} + +.overall-status-content { 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); + flex-wrap: wrap; + gap: 2em; } -.status-info span { +.overall-status-indicator { + display: flex; + align-items: center; + gap: 1.5em; +} + +.overall-status-icon { + font-size: 3rem; + line-height: 1; + animation: spin 2s linear infinite; +} + +.overall-status-icon.operational { + color: #4caf50; + animation: none; +} + +.overall-status-icon.partial { + color: #ffc107; + animation: pulse-icon 1.5s ease-in-out infinite; +} + +.overall-status-icon.major { + color: #f44336; + animation: none; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@keyframes pulse-icon { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.overall-status-title { + margin: 0; + font-size: 1.5rem; + color: #ecebeb; + font-weight: 600; +} + +.overall-status-subtitle { + margin: 0.3em 0 0 0; + font-size: 1rem; color: #a8a8a8; - font-size: 0.9rem; +} + +.overall-status-metrics { + display: flex; + gap: 2em; +} + +.metric-box { + display: flex; + flex-direction: column; + align-items: center; + padding: 1em 1.5em; + background: rgba(0, 0, 0, 0.3); + border-radius: 0.5em; + min-width: 80px; +} + +.metric-number { + font-size: 2rem; + font-weight: bold; + color: #ecebeb; + line-height: 1; +} + +.metric-label { + font-size: 0.85rem; + color: #a8a8a8; + margin-top: 0.3em; + text-transform: uppercase; + letter-spacing: 0.5px; } #refreshBtn { @@ -1504,35 +1619,28 @@ tr { } .uptime-values strong { - color: #4caf50; + font-weight: 600; } -.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; +/* Uptime color coding based on percentage */ +.uptime-excellent { + color: #4caf50 !important; /* 99%+ - Green */ } -.status-info-box h4 { - margin-top: 0; - color: #ecebeb; +.uptime-good { + color: #8bc34a !important; /* 95-99% - Light green */ } -.status-info-box ul { - margin: 0; - padding-left: 1.5em; - color: #a8a8a8; +.uptime-fair { + color: #ffc107 !important; /* 90-95% - Yellow */ } -.status-info-box li { - margin-bottom: 0.5em; - color: #a8a8a8; +.uptime-poor { + color: #f44336 !important; /* <90% - Red */ } -.status-info-box strong { - color: #ecebeb; +.uptime-none { + color: #888 !important; /* No data - Gray */ } @media screen and (max-width: 1400px) { @@ -1549,4 +1657,19 @@ tr { flex-direction: column; gap: 0.5em; } + + .overall-status-content { + flex-direction: column; + text-align: center; + } + + .overall-status-indicator { + flex-direction: column; + text-align: center; + } + + .overall-status-metrics { + width: 100%; + justify-content: center; + } } \ No newline at end of file diff --git a/src/static/js/status.js b/src/static/js/status.js index f050e8b..c537be3 100644 --- a/src/static/js/status.js +++ b/src/static/js/status.js @@ -43,6 +43,9 @@ function updateStatusDisplay(data) { updateServiceCard(service); }); + // Update overall status + updateOverallStatus(data.services); + // Re-enable refresh button const refreshBtn = document.getElementById('refreshBtn'); if (refreshBtn) { @@ -109,24 +112,29 @@ function updateServiceCard(service) { 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}%`); - } + // Helper function to get color class based on uptime percentage + const getUptimeClass = (value) => { + if (value === null) return 'uptime-none'; + if (value >= 99) return 'uptime-excellent'; + if (value >= 95) return 'uptime-good'; + if (value >= 90) return 'uptime-fair'; + return 'uptime-poor'; + }; - if (uptimeHTML.length > 0) { - uptimeDisplay.innerHTML = uptimeHTML.join(' | '); - } else { - uptimeDisplay.textContent = 'No data yet'; - } + // Helper function to format uptime value + const formatUptime = (value, label) => { + const display = value !== null ? `${value}%` : '--'; + const colorClass = getUptimeClass(value); + return `${label}: ${display}`; + }; + + // Add all uptime metrics + uptimeHTML.push(formatUptime(service.uptime['24h'], '24h')); + uptimeHTML.push(formatUptime(service.uptime['7d'], '7d')); + uptimeHTML.push(formatUptime(service.uptime['30d'], '30d')); + uptimeHTML.push(formatUptime(service.uptime.all_time, 'All')); + + uptimeDisplay.innerHTML = uptimeHTML.join(' | '); } // Update total checks @@ -135,6 +143,68 @@ function updateServiceCard(service) { } } +/** + * Update overall status bar + */ +function updateOverallStatus(services) { + const overallBar = document.getElementById('overallStatus'); + const icon = overallBar.querySelector('.overall-status-icon'); + const title = overallBar.querySelector('.overall-status-title'); + const subtitle = overallBar.querySelector('.overall-status-subtitle'); + const onlineCount = document.getElementById('onlineCount'); + const totalCount = document.getElementById('totalCount'); + + // Count service statuses + const total = services.length; + const online = services.filter(s => s.status === 'online').length; + const degraded = services.filter(s => s.status === 'degraded' || s.status === 'timeout').length; + const offline = services.filter(s => s.status === 'offline').length; + + // Update counts + onlineCount.textContent = online; + totalCount.textContent = total; + + // Remove all status classes + overallBar.classList.remove('all-operational', 'partial-outage', 'major-outage'); + icon.classList.remove('operational', 'partial', 'major', 'loading'); + + // Determine overall status + if (online === total) { + // All systems operational + overallBar.classList.add('all-operational'); + icon.classList.add('operational'); + icon.textContent = '✓'; + title.textContent = 'All Systems Operational'; + subtitle.textContent = `All ${total} services are running normally`; + } else if (offline >= Math.ceil(total / 2)) { + // Major outage (50% or more offline) + overallBar.classList.add('major-outage'); + icon.classList.add('major'); + icon.textContent = '✕'; + title.textContent = 'Major Outage'; + subtitle.textContent = `${offline} service${offline !== 1 ? 's' : ''} offline, ${degraded} degraded`; + } else if (offline > 0 || degraded > 0) { + // Partial outage + overallBar.classList.add('partial-outage'); + icon.classList.add('partial'); + icon.textContent = '⚠'; + title.textContent = 'Partial Outage'; + if (offline > 0 && degraded > 0) { + subtitle.textContent = `${offline} offline, ${degraded} degraded`; + } else if (offline > 0) { + subtitle.textContent = `${offline} service${offline !== 1 ? 's' : ''} offline`; + } else { + subtitle.textContent = `${degraded} service${degraded !== 1 ? 's' : ''} degraded`; + } + } else { + // Unknown state + icon.classList.add('loading'); + icon.textContent = '◐'; + title.textContent = 'Status Unknown'; + subtitle.textContent = 'Waiting for service data'; + } +} + /** * Show error message */ diff --git a/src/static/json/status_history.json b/src/static/json/status_history.json deleted file mode 100644 index 69e90ae..0000000 --- a/src/static/json/status_history.json +++ /dev/null @@ -1,145 +0,0 @@ -{ - "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 297c0c3..a11813c 100644 --- a/src/templates/status.html +++ b/src/templates/status.html @@ -2,160 +2,18 @@

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...
-
-
-
+
+
+ Last checked: Loading... +
+ Next check: -- +
+ +
+
+

Status Legend

@@ -173,13 +31,67 @@
-
+ +
+
+
+ +
+

Checking Systems...

+

Loading service status

+
+
+
+
+ -- + Online +
+
+ -- + Total +
+
+
+
+ + + +

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
  • +
  • Check Frequency: Services are checked automatically every 30 minutes from the server
  • Page Refresh: This page auto-refreshes every 5 minutes to show latest data