mirror of
https://github.com/asimonson1125/asimonson1125.github.io.git
synced 2026-02-25 05:09:49 -06:00
clean up clean up everybody do your share
This commit is contained in:
143
src/app.py
143
src/app.py
@@ -1,15 +1,18 @@
|
||||
import flask
|
||||
from flask_minify import Minify
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import hashlib
|
||||
|
||||
import flask
|
||||
from flask_minify import Minify
|
||||
import werkzeug.exceptions as HTTPerror
|
||||
from config import *
|
||||
|
||||
import config # noqa: F401 — side-effect: loads dev env vars
|
||||
from monitor import monitor, SERVICES
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
|
||||
# Compute content hashes for static file fingerprinting
|
||||
# ── Static file fingerprinting ────────────────────────────────────────
|
||||
|
||||
static_file_hashes = {}
|
||||
for dirpath, _, filenames in os.walk(app.static_folder):
|
||||
for filename in filenames:
|
||||
@@ -18,6 +21,7 @@ for dirpath, _, filenames in os.walk(app.static_folder):
|
||||
with open(filepath, 'rb') as f:
|
||||
static_file_hashes[relative] = hashlib.md5(f.read()).hexdigest()[:8]
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def override_url_for():
|
||||
def versioned_url_for(endpoint, **values):
|
||||
@@ -28,17 +32,16 @@ def override_url_for():
|
||||
return flask.url_for(endpoint, **values)
|
||||
return dict(url_for=versioned_url_for)
|
||||
|
||||
# Add security and caching headers
|
||||
|
||||
# ── Security and caching headers ──────────────────────────────────────
|
||||
|
||||
@app.after_request
|
||||
def add_security_headers(response):
|
||||
"""Add security and performance headers to all responses"""
|
||||
# Security headers
|
||||
def add_headers(response):
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
|
||||
response.headers['X-XSS-Protection'] = '1; mode=block'
|
||||
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
|
||||
|
||||
# Cache control for static assets
|
||||
if flask.request.path.startswith('/static/'):
|
||||
response.headers['Cache-Control'] = 'public, max-age=31536000, immutable'
|
||||
elif flask.request.path in ['/sitemap.xml', '/robots.txt']:
|
||||
@@ -49,56 +52,93 @@ def add_security_headers(response):
|
||||
return response
|
||||
|
||||
|
||||
# ── Load page data ────────────────────────────────────────────────────
|
||||
|
||||
def load_json(path):
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
proj = load_json("./static/json/projects.json")
|
||||
|
||||
projects = load_json("./static/json/projects.json")
|
||||
books = load_json("./static/json/books.json")
|
||||
skillList = load_json("./static/json/skills.json")
|
||||
skills = load_json("./static/json/skills.json")
|
||||
timeline = load_json("./static/json/timeline.json")
|
||||
pages = load_json("./static/json/pages.json")
|
||||
|
||||
pages['projects']['skillList'] = skillList
|
||||
# pages['about']['timeline'] = timeline
|
||||
pages['projects']['projects'] = proj
|
||||
pages['projects']['skillList'] = skills
|
||||
pages['projects']['projects'] = projects
|
||||
pages['home']['books'] = books
|
||||
pages['books']['books'] = books
|
||||
pages['status']['services'] = SERVICES
|
||||
|
||||
|
||||
# ── Error rendering ──────────────────────────────────────────────────
|
||||
|
||||
def render_error(code, message):
|
||||
pagevars = {
|
||||
"template": "error.html",
|
||||
"title": f"{code} - Simonson",
|
||||
"description": "Error on Andrew Simonson's Digital Portfolio",
|
||||
"canonical": f"/{code}",
|
||||
}
|
||||
return (
|
||||
flask.render_template(
|
||||
"header.html",
|
||||
var=pagevars,
|
||||
error=code,
|
||||
message=message,
|
||||
title=f"{code} - Simonson Portfolio",
|
||||
),
|
||||
code,
|
||||
)
|
||||
|
||||
|
||||
@app.errorhandler(HTTPerror.HTTPException)
|
||||
def handle_http_error(e):
|
||||
return render_error(e.code, e.description)
|
||||
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
def handle_generic_error(e):
|
||||
return render_error(500, "Internal Server Error")
|
||||
|
||||
|
||||
# ── API routes ────────────────────────────────────────────────────────
|
||||
|
||||
@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'):
|
||||
def api_goto(location='home'):
|
||||
if location not in pages:
|
||||
flask.abort(404)
|
||||
pagevars = pages[location]
|
||||
page = None
|
||||
try:
|
||||
page = flask.render_template(pagevars["template"], var=pagevars)
|
||||
except Exception:
|
||||
e = HTTPerror.InternalServerError()
|
||||
page = handle_http_error(e)
|
||||
page = render_error(500, "Internal Server Error")
|
||||
return [pagevars, page]
|
||||
|
||||
def funcGen(pagename, pages):
|
||||
def dynamicRule():
|
||||
|
||||
# ── Dynamic page routes ──────────────────────────────────────────────
|
||||
|
||||
def make_page_handler(pagename):
|
||||
def handler():
|
||||
try:
|
||||
return flask.render_template('header.html', var=pages[pagename])
|
||||
except Exception:
|
||||
e = HTTPerror.InternalServerError()
|
||||
print(e)
|
||||
return handle_http_error(e)
|
||||
return dynamicRule
|
||||
return render_error(500, "Internal Server Error")
|
||||
return handler
|
||||
|
||||
for i in pages:
|
||||
func = funcGen(i, pages)
|
||||
app.add_url_rule(pages[i]['canonical'], i, func)
|
||||
|
||||
for name in pages:
|
||||
app.add_url_rule(pages[name]['canonical'], name, make_page_handler(name))
|
||||
|
||||
|
||||
# ── Static file routes ───────────────────────────────────────────────
|
||||
|
||||
@app.route("/resume")
|
||||
@app.route("/Resume.pdf")
|
||||
@@ -106,46 +146,6 @@ for i in pages:
|
||||
def resume():
|
||||
return flask.send_file("./static/Resume_Simonson_Andrew.pdf")
|
||||
|
||||
@app.errorhandler(HTTPerror.HTTPException)
|
||||
def handle_http_error(e):
|
||||
eCode = e.code
|
||||
message = e.description
|
||||
pagevars = {
|
||||
"template": "error.html",
|
||||
"title": f"{eCode} - Simonson",
|
||||
"description": "Error on Andrew Simonson's Digital Portfolio",
|
||||
"canonical": f"/{eCode}",
|
||||
}
|
||||
return (
|
||||
flask.render_template(
|
||||
"header.html",
|
||||
var=pagevars,
|
||||
error=eCode,
|
||||
message=message,
|
||||
title=f"{eCode} - Simonson Portfolio",
|
||||
),
|
||||
eCode,
|
||||
)
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
def handle_generic_error(e):
|
||||
pagevars = {
|
||||
"template": "error.html",
|
||||
"title": "500 - Simonson",
|
||||
"description": "Error on Andrew Simonson's Digital Portfolio",
|
||||
"canonical": "/500",
|
||||
}
|
||||
return (
|
||||
flask.render_template(
|
||||
"header.html",
|
||||
var=pagevars,
|
||||
error=500,
|
||||
message="Internal Server Error",
|
||||
title="500 - Simonson Portfolio",
|
||||
),
|
||||
500,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/sitemap.xml")
|
||||
@app.route("/robots.txt")
|
||||
@@ -153,10 +153,9 @@ def static_from_root():
|
||||
return flask.send_from_directory(app.static_folder, flask.request.path[1:])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# import sass
|
||||
# ── Startup ───────────────────────────────────────────────────────────
|
||||
|
||||
# sass.compile(dirname=("static/scss", "static/css"), output_style="compressed")
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=False)
|
||||
else:
|
||||
Minify(app=app, html=True, js=True, cssless=True)
|
||||
|
||||
209
src/monitor.py
209
src/monitor.py
@@ -1,76 +1,57 @@
|
||||
"""
|
||||
Service monitoring module
|
||||
Checks service availability and tracks uptime statistics
|
||||
Service monitoring module.
|
||||
Checks service availability and tracks uptime statistics in PostgreSQL.
|
||||
"""
|
||||
import os
|
||||
import requests
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime, timedelta
|
||||
from threading import Thread, Lock
|
||||
|
||||
import psycopg2
|
||||
import requests
|
||||
|
||||
# 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': '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},
|
||||
]
|
||||
|
||||
# Check interval: 1 min
|
||||
CHECK_INTERVAL = 60
|
||||
|
||||
# Retention: 90 days (quarter year)
|
||||
RETENTION_DAYS = 90
|
||||
CLEANUP_INTERVAL = 86400 # 24 hours
|
||||
CHECK_INTERVAL = 60 # seconds between checks
|
||||
RETENTION_DAYS = 90 # how long to keep records
|
||||
CLEANUP_INTERVAL = 86400 # seconds between purge runs
|
||||
|
||||
DATABASE_URL = os.environ.get('DATABASE_URL')
|
||||
|
||||
# Expected columns (besides id) — name: SQL type
|
||||
# Expected columns (besides id) -- name: SQL type
|
||||
_EXPECTED_COLUMNS = {
|
||||
'service_id': 'VARCHAR(50) NOT NULL',
|
||||
'timestamp': 'TIMESTAMPTZ NOT NULL DEFAULT NOW()',
|
||||
'status': 'VARCHAR(20) NOT NULL',
|
||||
'service_id': 'VARCHAR(50) NOT NULL',
|
||||
'timestamp': 'TIMESTAMPTZ NOT NULL DEFAULT NOW()',
|
||||
'status': 'VARCHAR(20) NOT NULL',
|
||||
'response_time': 'INTEGER',
|
||||
'status_code': 'INTEGER',
|
||||
'error': 'TEXT',
|
||||
'status_code': 'INTEGER',
|
||||
'error': 'TEXT',
|
||||
}
|
||||
|
||||
|
||||
class ServiceMonitor:
|
||||
def __init__(self):
|
||||
self.lock = Lock()
|
||||
# Lightweight in-memory cache of latest status per service
|
||||
self._current = {}
|
||||
for service in SERVICES:
|
||||
self._current[service['id']] = {
|
||||
'name': service['name'],
|
||||
'url': service['url'],
|
||||
self._current = {
|
||||
svc['id']: {
|
||||
'name': svc['name'],
|
||||
'url': svc['url'],
|
||||
'status': 'unknown',
|
||||
'response_time': None,
|
||||
'status_code': None,
|
||||
'last_online': None,
|
||||
}
|
||||
for svc in SERVICES
|
||||
}
|
||||
self._last_check = None
|
||||
self._ensure_schema()
|
||||
|
||||
# ── database helpers ──────────────────────────────────────────
|
||||
# ── Database helpers ──────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def _get_conn():
|
||||
@@ -80,13 +61,11 @@ class ServiceMonitor:
|
||||
return psycopg2.connect(DATABASE_URL)
|
||||
|
||||
def _ensure_schema(self):
|
||||
"""Create the service_checks table (and index) if needed, then
|
||||
reconcile columns with _EXPECTED_COLUMNS."""
|
||||
"""Create or migrate the service_checks table to match _EXPECTED_COLUMNS."""
|
||||
if not DATABASE_URL:
|
||||
print("DATABASE_URL not set — running without persistence")
|
||||
print("DATABASE_URL not set -- running without persistence")
|
||||
return
|
||||
|
||||
# Retry connection in case DB is still starting (e.g. Docker)
|
||||
conn = None
|
||||
for attempt in range(5):
|
||||
try:
|
||||
@@ -97,8 +76,9 @@ class ServiceMonitor:
|
||||
print(f"Database not ready, retrying in 2s (attempt {attempt + 1}/5)...")
|
||||
time.sleep(2)
|
||||
else:
|
||||
print("Could not connect to database — running without persistence")
|
||||
print("Could not connect to database -- running without persistence")
|
||||
return
|
||||
|
||||
try:
|
||||
with conn, conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
@@ -125,23 +105,15 @@ class ServiceMonitor:
|
||||
""")
|
||||
existing = {row[0] for row in cur.fetchall()}
|
||||
|
||||
# Add missing columns
|
||||
for col, col_type in _EXPECTED_COLUMNS.items():
|
||||
if col not in existing:
|
||||
# Strip NOT NULL / DEFAULT for ALTER ADD (can't enforce
|
||||
# NOT NULL on existing rows without a default)
|
||||
bare_type = col_type.split('NOT NULL')[0].split('DEFAULT')[0].strip()
|
||||
cur.execute(
|
||||
f'ALTER TABLE service_checks ADD COLUMN {col} {bare_type}'
|
||||
)
|
||||
cur.execute(f'ALTER TABLE service_checks ADD COLUMN {col} {bare_type}')
|
||||
print(f"Added column {col} to service_checks")
|
||||
|
||||
# Drop unexpected columns (besides 'id')
|
||||
expected_names = set(_EXPECTED_COLUMNS) | {'id'}
|
||||
for col in existing - expected_names:
|
||||
cur.execute(
|
||||
f'ALTER TABLE service_checks DROP COLUMN {col}'
|
||||
)
|
||||
cur.execute(f'ALTER TABLE service_checks DROP COLUMN {col}')
|
||||
print(f"Dropped column {col} from service_checks")
|
||||
|
||||
print("Database schema OK")
|
||||
@@ -149,7 +121,7 @@ class ServiceMonitor:
|
||||
conn.close()
|
||||
|
||||
def _insert_check(self, service_id, result):
|
||||
"""Insert a single check result into the database."""
|
||||
"""Persist a single check result to the database."""
|
||||
conn = self._get_conn()
|
||||
if conn is None:
|
||||
return
|
||||
@@ -171,35 +143,28 @@ class ServiceMonitor:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# ── service checks ────────────────────────────────────────────
|
||||
# ── Service checks ────────────────────────────────────────────
|
||||
|
||||
def check_service(self, service):
|
||||
"""Check a single service and return status"""
|
||||
"""Perform an HTTP HEAD against a service and return a status dict."""
|
||||
start_time = time.time()
|
||||
result = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'status': 'offline',
|
||||
'response_time': None,
|
||||
'status_code': None
|
||||
'status_code': None,
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.head(
|
||||
service['url'],
|
||||
timeout=service['timeout'],
|
||||
allow_redirects=True
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
elapsed = int((time.time() - start_time) * 1000) # ms
|
||||
|
||||
result['response_time'] = elapsed
|
||||
result['response_time'] = int((time.time() - start_time) * 1000)
|
||||
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
|
||||
if response.status_code < 500:
|
||||
result['status'] = 'online'
|
||||
else:
|
||||
result['status'] = 'degraded'
|
||||
@@ -214,10 +179,9 @@ class ServiceMonitor:
|
||||
return result
|
||||
|
||||
def check_all_services(self):
|
||||
"""Check all services and update status data"""
|
||||
"""Check every service concurrently, persist results, and update cache."""
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Checking all services...")
|
||||
|
||||
# Perform all network checks concurrently and OUTSIDE the lock
|
||||
results = {}
|
||||
with ThreadPoolExecutor(max_workers=len(SERVICES)) as executor:
|
||||
futures = {executor.submit(self.check_service, s): s for s in SERVICES}
|
||||
@@ -227,11 +191,9 @@ class ServiceMonitor:
|
||||
results[service['id']] = result
|
||||
print(f" {service['name']}: {result['status']} ({result['response_time']}ms)")
|
||||
|
||||
# Persist to database (outside lock — DB has its own concurrency)
|
||||
for service_id, result in results.items():
|
||||
self._insert_check(service_id, result)
|
||||
|
||||
# Update lightweight in-memory cache under lock
|
||||
with self.lock:
|
||||
for service in SERVICES:
|
||||
result = results[service['id']]
|
||||
@@ -243,13 +205,14 @@ class ServiceMonitor:
|
||||
cached['last_online'] = result['timestamp']
|
||||
self._last_check = datetime.now().isoformat()
|
||||
|
||||
# ── uptime calculations ───────────────────────────────────────
|
||||
# ── Uptime calculations ───────────────────────────────────────
|
||||
|
||||
def _calculate_uptime_unlocked(self, service_id, hours=None):
|
||||
"""Calculate uptime percentage for a service by querying the DB."""
|
||||
def _calculate_uptime(self, service_id, hours=None):
|
||||
"""Return uptime percentage for a service, or None if insufficient data."""
|
||||
conn = self._get_conn()
|
||||
if conn is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
if hours:
|
||||
@@ -273,11 +236,10 @@ class ServiceMonitor:
|
||||
)
|
||||
|
||||
online_count, total_count = cur.fetchone()
|
||||
|
||||
if total_count == 0:
|
||||
return None
|
||||
|
||||
# Only show uptime for a window if we have data older than it
|
||||
# Only report a time-windowed uptime if data exists beyond the window
|
||||
if hours:
|
||||
cur.execute(
|
||||
'SELECT EXISTS(SELECT 1 FROM service_checks WHERE service_id = %s AND timestamp <= %s)',
|
||||
@@ -290,47 +252,8 @@ class ServiceMonitor:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def calculate_uptime(self, service_id, hours=None):
|
||||
"""Calculate uptime percentage for a service"""
|
||||
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._last_check,
|
||||
'next_check': None,
|
||||
'services': []
|
||||
}
|
||||
|
||||
if self._last_check:
|
||||
last_check = datetime.fromisoformat(self._last_check)
|
||||
next_check = last_check + timedelta(seconds=CHECK_INTERVAL)
|
||||
summary['next_check'] = next_check.isoformat()
|
||||
|
||||
for service_id, cached in self._current.items():
|
||||
service_summary = {
|
||||
'id': service_id,
|
||||
'name': cached['name'],
|
||||
'url': cached['url'],
|
||||
'status': cached['status'],
|
||||
'response_time': cached['response_time'],
|
||||
'status_code': cached['status_code'],
|
||||
'last_online': cached['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': self._get_total_checks(service_id),
|
||||
}
|
||||
summary['services'].append(service_summary)
|
||||
|
||||
return summary
|
||||
|
||||
def _get_total_checks(self, service_id):
|
||||
"""Return the total number of checks for a service."""
|
||||
"""Return the total number of recorded checks for a service."""
|
||||
conn = self._get_conn()
|
||||
if conn is None:
|
||||
return 0
|
||||
@@ -344,6 +267,43 @@ class ServiceMonitor:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# ── Status summary ────────────────────────────────────────────
|
||||
|
||||
def get_status_summary(self):
|
||||
"""Build a JSON-serializable status summary with uptime statistics."""
|
||||
with self.lock:
|
||||
summary = {
|
||||
'last_check': self._last_check,
|
||||
'next_check': None,
|
||||
'services': [],
|
||||
}
|
||||
|
||||
if self._last_check:
|
||||
last_check = datetime.fromisoformat(self._last_check)
|
||||
summary['next_check'] = (last_check + timedelta(seconds=CHECK_INTERVAL)).isoformat()
|
||||
|
||||
for service_id, cached in self._current.items():
|
||||
summary['services'].append({
|
||||
'id': service_id,
|
||||
'name': cached['name'],
|
||||
'url': cached['url'],
|
||||
'status': cached['status'],
|
||||
'response_time': cached['response_time'],
|
||||
'status_code': cached['status_code'],
|
||||
'last_online': cached['last_online'],
|
||||
'uptime': {
|
||||
'24h': self._calculate_uptime(service_id, 24),
|
||||
'7d': self._calculate_uptime(service_id, 24 * 7),
|
||||
'30d': self._calculate_uptime(service_id, 24 * 30),
|
||||
'all_time': self._calculate_uptime(service_id),
|
||||
},
|
||||
'total_checks': self._get_total_checks(service_id),
|
||||
})
|
||||
|
||||
return summary
|
||||
|
||||
# ── Background loop ───────────────────────────────────────────
|
||||
|
||||
def _purge_old_records(self):
|
||||
"""Delete check records older than RETENTION_DAYS."""
|
||||
conn = self._get_conn()
|
||||
@@ -352,10 +312,7 @@ class ServiceMonitor:
|
||||
try:
|
||||
cutoff = datetime.now() - timedelta(days=RETENTION_DAYS)
|
||||
with conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
'DELETE FROM service_checks WHERE timestamp < %s',
|
||||
(cutoff,),
|
||||
)
|
||||
cur.execute('DELETE FROM service_checks WHERE timestamp < %s', (cutoff,))
|
||||
deleted = cur.rowcount
|
||||
if deleted:
|
||||
print(f"Purged {deleted} records older than {RETENTION_DAYS} days")
|
||||
@@ -363,7 +320,7 @@ class ServiceMonitor:
|
||||
conn.close()
|
||||
|
||||
def start_monitoring(self):
|
||||
"""Start background monitoring thread"""
|
||||
"""Start the background daemon thread for periodic checks and cleanup."""
|
||||
def monitor_loop():
|
||||
self.check_all_services()
|
||||
self._purge_old_records()
|
||||
@@ -381,7 +338,7 @@ class ServiceMonitor:
|
||||
|
||||
thread = Thread(target=monitor_loop, daemon=True)
|
||||
thread.start()
|
||||
print(f"Service monitoring started (checks every {CHECK_INTERVAL} seconds)")
|
||||
print(f"Service monitoring started (checks every {CHECK_INTERVAL}s)")
|
||||
|
||||
|
||||
# Global monitor instance
|
||||
monitor = ServiceMonitor()
|
||||
|
||||
@@ -271,6 +271,10 @@ tr {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.navElement + .navElement {
|
||||
border-left: 1px solid rgba(var(--accent-rgb), 0.4);
|
||||
}
|
||||
|
||||
.navElement {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
@@ -786,6 +790,7 @@ tr {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
color: inherit;
|
||||
background: currentColor;
|
||||
box-shadow: 0 0 6px currentColor;
|
||||
flex-shrink: 0;
|
||||
@@ -1402,7 +1407,8 @@ tr {
|
||||
}
|
||||
|
||||
.navElement + .navElement {
|
||||
border-top: 1px solid rgba(var(--accent-rgb), 0.1);
|
||||
border-top: 1px solid rgba(var(--accent-rgb), 0.25);
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.navElement * {
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hiddenup {
|
||||
max-height: 0px !important;
|
||||
}
|
||||
|
||||
.checkbox-wrapper > div {
|
||||
display: inline-block;
|
||||
margin-right: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.checkbox-wrapper > div:last-child {
|
||||
margin-bottom: 0;;
|
||||
}
|
||||
|
||||
.checkbox-wrapper .switch {
|
||||
display: flex;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-wrapper .switch > * {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.checkbox-wrapper .switch input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.checkbox-wrapper .slider {
|
||||
background-color: #ccc;
|
||||
transition: 0.4s;
|
||||
height: 34px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.checkbox-wrapper .slider:before {
|
||||
background-color: #fff;
|
||||
bottom: 4px;
|
||||
content: "";
|
||||
height: 26px;
|
||||
left: 4px;
|
||||
position: absolute;
|
||||
transition: 0.4s;
|
||||
width: 26px;
|
||||
}
|
||||
|
||||
.checkbox-wrapper input:checked+.slider {
|
||||
background-color: #66bb6a;
|
||||
}
|
||||
|
||||
.checkbox-wrapper input:checked+.slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
.checkbox-wrapper .slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.checkbox-wrapper .slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.checkbox-wrapper strong {
|
||||
margin-left: .5em;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
function toggleCheckbox(dir) {
|
||||
let toggles = document.querySelectorAll(
|
||||
".checkbox-wrapper input[type=checkbox]"
|
||||
);
|
||||
let allow = [];
|
||||
toggles.forEach(function (x) {
|
||||
if (x.checked) {
|
||||
allow.push(x.id);
|
||||
}
|
||||
});
|
||||
let list = document.querySelectorAll(".checkbox-client > div");
|
||||
if (allow.length === 0) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
list[i].classList.remove("hidden" + dir);
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
list[i].classList.remove("hidden" + dir);
|
||||
for (let x = 0; x < allow.length; x++) {
|
||||
if (!list[i].classList.contains(allow[x])) {
|
||||
list[i].classList.add("hidden" + dir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function activeSkill(obj) {
|
||||
let skill = obj.closest(".skill");
|
||||
if (skill.classList.contains("activeSkill")) {
|
||||
skill.classList.remove("activeSkill");
|
||||
return;
|
||||
}
|
||||
while (skill) {
|
||||
skill.classList.add("activeSkill");
|
||||
skill = skill.parentElement.closest(".skill");
|
||||
}
|
||||
}
|
||||
@@ -7,17 +7,21 @@ async function addChessEmbed(username) {
|
||||
setChess({ cName: "Chess.com request failed" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.status === 200) {
|
||||
user = await user.json();
|
||||
stats = await stats.json();
|
||||
const ratings = {
|
||||
rapid: stats.chess_rapid.last.rating,
|
||||
blitz: stats.chess_blitz.last.rating,
|
||||
bullet: stats.chess_bullet.last.rating,
|
||||
tactics: stats.tactics.highest.rating,
|
||||
};
|
||||
setChess({ cName: user["username"], pic: user.avatar, ratings: ratings });
|
||||
} else if (user === null || user.status === 403 || user.status === null) {
|
||||
setChess({
|
||||
cName: user["username"],
|
||||
pic: user.avatar,
|
||||
ratings: {
|
||||
rapid: stats.chess_rapid.last.rating,
|
||||
blitz: stats.chess_blitz.last.rating,
|
||||
bullet: stats.chess_bullet.last.rating,
|
||||
tactics: stats.tactics.highest.rating,
|
||||
},
|
||||
});
|
||||
} else if (user.status === 403) {
|
||||
setChess({ cName: "Chess.com request failed" });
|
||||
} else {
|
||||
setChess({ cName: "User Not Found" });
|
||||
@@ -33,16 +37,12 @@ function setChess({ cName = null, pic = null, ratings = null }) {
|
||||
document.querySelector(".chessImage").src = pic;
|
||||
}
|
||||
if (ratings) {
|
||||
document.querySelector(".chessRapid .chessStat").textContent =
|
||||
ratings.rapid;
|
||||
document.querySelector(".chessBlitz .chessStat").textContent =
|
||||
ratings.blitz;
|
||||
document.querySelector(".chessBullet .chessStat").textContent =
|
||||
ratings.bullet;
|
||||
document.querySelector(".chessPuzzles .chessStat").textContent =
|
||||
ratings.tactics;
|
||||
document.querySelector(".chessRapid .chessStat").textContent = ratings.rapid;
|
||||
document.querySelector(".chessBlitz .chessStat").textContent = ratings.blitz;
|
||||
document.querySelector(".chessBullet .chessStat").textContent = ratings.bullet;
|
||||
document.querySelector(".chessPuzzles .chessStat").textContent = ratings.tactics;
|
||||
}
|
||||
} catch {
|
||||
console.log("fucker clicking so fast the internet can't even keep up");
|
||||
console.warn("Chess DOM elements not available (navigated away during fetch)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ const density = 0.00005;
|
||||
let screenWidth = window.innerWidth + 10;
|
||||
let screenHeight = window.innerHeight + 10;
|
||||
|
||||
const MAX_DIST = 150;
|
||||
const MAX_DIST_SQUARED = MAX_DIST * MAX_DIST;
|
||||
|
||||
class Ball {
|
||||
constructor(x, y, size, speed, angle) {
|
||||
this.x = x;
|
||||
@@ -14,8 +17,9 @@ class Ball {
|
||||
}
|
||||
|
||||
calcChange() {
|
||||
this.xSpeed = this.speed * Math.sin((this.angle * Math.PI) / 180);
|
||||
this.ySpeed = this.speed * Math.cos((this.angle * Math.PI) / 180);
|
||||
const radians = (this.angle * Math.PI) / 180
|
||||
this.xSpeed = this.speed * Math.sin(radians);
|
||||
this.ySpeed = this.speed * Math.cos(radians);
|
||||
}
|
||||
|
||||
update() {
|
||||
@@ -44,19 +48,17 @@ class Ball {
|
||||
|
||||
function setup() {
|
||||
frameRate(15);
|
||||
const pix = screenHeight * screenWidth;
|
||||
const pixels = screenHeight * screenWidth;
|
||||
createCanvas(screenWidth, screenHeight);
|
||||
for (let i = 0; i < pix * density; i++) {
|
||||
let thisBall = new Ball(
|
||||
for (let i = 0; i < pixels * density; i++) {
|
||||
balls.push(new Ball(
|
||||
random(screenWidth),
|
||||
random(screenHeight),
|
||||
random(6) + 3,
|
||||
Math.exp(random(4) + 3) / 1000 + 1,
|
||||
random(360)
|
||||
);
|
||||
balls.push(thisBall);
|
||||
));
|
||||
}
|
||||
|
||||
stroke(255);
|
||||
}
|
||||
|
||||
@@ -69,42 +71,31 @@ function windowResized() {
|
||||
function draw() {
|
||||
background(24);
|
||||
|
||||
// Update all balls
|
||||
for (let i = 0; i < balls.length; i++) {
|
||||
balls[i].update();
|
||||
}
|
||||
|
||||
// Draw lines with additive blending so overlaps increase brightness
|
||||
// Draw connection lines with additive blending so overlaps brighten
|
||||
blendMode(ADD);
|
||||
strokeWeight(2);
|
||||
|
||||
const maxDist = 150;
|
||||
const maxDistSquared = maxDist * maxDist;
|
||||
|
||||
for (let i = 0; i < balls.length - 1; i++) {
|
||||
const ball1 = balls[i];
|
||||
const a = balls[i];
|
||||
for (let j = i + 1; j < balls.length; j++) {
|
||||
const ball2 = balls[j];
|
||||
|
||||
const dx = ball2.x - ball1.x;
|
||||
const dy = ball2.y - ball1.y;
|
||||
const b = balls[j];
|
||||
const dx = b.x - a.x;
|
||||
const dy = b.y - a.y;
|
||||
const distSquared = dx * dx + dy * dy;
|
||||
|
||||
if (distSquared < maxDistSquared) {
|
||||
if (distSquared < MAX_DIST_SQUARED) {
|
||||
const distance = Math.sqrt(distSquared);
|
||||
|
||||
if (distance < 75) {
|
||||
stroke(255, 85);
|
||||
line(ball1.x, ball1.y, ball2.x, ball2.y);
|
||||
} else {
|
||||
const chance = 0.3 ** (((random(0.2) + 0.8) * distance) / 150);
|
||||
if (chance < 0.5) {
|
||||
stroke(255, 40);
|
||||
} else {
|
||||
stroke(255, 75);
|
||||
}
|
||||
line(ball1.x, ball1.y, ball2.x, ball2.y);
|
||||
const chance = 0.3 ** (((random(0.2) + 0.8) * distance) / MAX_DIST);
|
||||
stroke(255, chance < 0.5 ? 40 : 75);
|
||||
}
|
||||
line(a.x, a.y, b.x, b.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
function toggleMenu(collapse=false) {
|
||||
function toggleMenu(collapse) {
|
||||
if (window.innerWidth < 1400) {
|
||||
const e = document.querySelector(".navControl");
|
||||
const menu = document.querySelector(".navControl");
|
||||
const bar = document.querySelector(".header");
|
||||
const isCollapsed = !e.style.maxHeight || e.style.maxHeight === "0px";
|
||||
const isCollapsed = !menu.style.maxHeight || menu.style.maxHeight === "0px";
|
||||
if (isCollapsed && !collapse) {
|
||||
e.style.maxHeight = `${e.scrollHeight + 10}px`;
|
||||
menu.style.maxHeight = `${menu.scrollHeight + 10}px`;
|
||||
bar.style.borderBottomWidth = "0px";
|
||||
} else {
|
||||
e.style.maxHeight = "0px";
|
||||
menu.style.maxHeight = "0px";
|
||||
bar.style.borderBottomWidth = "3px";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function goto(location, { push = true } = {}) {
|
||||
let a;
|
||||
let response;
|
||||
try {
|
||||
a = await fetch("/api/goto/" + location, {
|
||||
response = await fetch("/api/goto/" + location, {
|
||||
credentials: "include",
|
||||
method: "GET",
|
||||
mode: "cors",
|
||||
});
|
||||
if (!a.ok) {
|
||||
console.error(`Navigation failed: HTTP ${a.status}`);
|
||||
if (!response.ok) {
|
||||
console.error(`Navigation failed: HTTP ${response.status}`);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -32,29 +32,29 @@ async function goto(location, { push = true } = {}) {
|
||||
|
||||
document.dispatchEvent(new Event('beforenavigate'));
|
||||
|
||||
const response = await a.json();
|
||||
const metadata = response[0];
|
||||
const content = response[1];
|
||||
const [metadata, content] = await response.json();
|
||||
const root = document.getElementById("root");
|
||||
root.innerHTML = content;
|
||||
root.querySelectorAll("script").forEach((oldScript) => {
|
||||
|
||||
// Re-execute scripts injected via innerHTML (browser ignores them otherwise)
|
||||
root.querySelectorAll("script").forEach(function(oldScript) {
|
||||
const newScript = document.createElement("script");
|
||||
Array.from(oldScript.attributes).forEach(attr => {
|
||||
Array.from(oldScript.attributes).forEach(function(attr) {
|
||||
newScript.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
newScript.textContent = oldScript.textContent;
|
||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
});
|
||||
|
||||
if (!window.location.href.includes("#")) {
|
||||
window.scrollTo({top: 0, left: 0, behavior:"instant"});
|
||||
} else {
|
||||
const eid = decodeURIComponent(window.location.hash.substring(1));
|
||||
const el = document.getElementById(eid);
|
||||
if (window.location.href.includes("#")) {
|
||||
const id = decodeURIComponent(window.location.hash.substring(1));
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.scrollIntoView();
|
||||
} else {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
||||
}
|
||||
|
||||
toggleMenu(collapse=true);
|
||||
toggleMenu(true);
|
||||
document.querySelector("title").textContent = metadata["title"];
|
||||
if (push) {
|
||||
history.pushState(null, null, metadata["canonical"]);
|
||||
@@ -62,6 +62,18 @@ async function goto(location, { push = true } = {}) {
|
||||
}
|
||||
|
||||
function backButton() {
|
||||
const location = window.location.pathname;
|
||||
goto(location.substring(1), { push: false }); // remove slash, goto already does that
|
||||
const path = window.location.pathname;
|
||||
goto(path.substring(1), { push: false });
|
||||
}
|
||||
|
||||
function activeSkill(obj) {
|
||||
let skill = obj.closest(".skill");
|
||||
if (skill.classList.contains("activeSkill")) {
|
||||
skill.classList.remove("activeSkill");
|
||||
return;
|
||||
}
|
||||
while (skill) {
|
||||
skill.classList.add("activeSkill");
|
||||
skill = skill.parentElement.closest(".skill");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// Fetch and display service status from API
|
||||
let statusIntervalId = null;
|
||||
|
||||
/**
|
||||
* Fetch status data from server
|
||||
*/
|
||||
async function fetchStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/status');
|
||||
@@ -17,36 +14,26 @@ async function fetchStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}`;
|
||||
document.getElementById('lastUpdate').textContent = `Last checked: ${lastCheck.toLocaleString()}`;
|
||||
}
|
||||
|
||||
// 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}`;
|
||||
const nextCheck = new Date(data.next_check);
|
||||
nextCheckEl.textContent = `Next check: ${nextCheck.toLocaleString()}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Update each service
|
||||
data.services.forEach(service => {
|
||||
data.services.forEach(function(service) {
|
||||
updateServiceCard(service);
|
||||
});
|
||||
|
||||
// Update overall status
|
||||
updateOverallStatus(data.services);
|
||||
|
||||
// Re-enable refresh button
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.disabled = false;
|
||||
@@ -54,9 +41,19 @@ function updateStatusDisplay(data) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a single service card
|
||||
*/
|
||||
function getUptimeClass(value) {
|
||||
if (value === null) return 'text-muted';
|
||||
if (value >= 99) return 'text-excellent';
|
||||
if (value >= 95) return 'text-good';
|
||||
if (value >= 90) return 'text-fair';
|
||||
return 'text-poor';
|
||||
}
|
||||
|
||||
function formatUptime(value, label) {
|
||||
const display = value !== null ? `${value}%` : '--';
|
||||
return `${label}: <strong class="${getUptimeClass(value)}">${display}</strong>`;
|
||||
}
|
||||
|
||||
function updateServiceCard(service) {
|
||||
const card = document.getElementById(`status-${service.id}`);
|
||||
if (!card) return;
|
||||
@@ -68,21 +65,14 @@ function updateServiceCard(service) {
|
||||
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 = '--';
|
||||
}
|
||||
timeDisplay.textContent = service.response_time !== null ? `${service.response_time}ms` : '--';
|
||||
|
||||
// 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) {
|
||||
@@ -108,44 +98,20 @@ function updateServiceCard(service) {
|
||||
card.classList.add('unknown');
|
||||
}
|
||||
|
||||
// Update uptime statistics
|
||||
if (uptimeDisplay && service.uptime) {
|
||||
const uptimeHTML = [];
|
||||
|
||||
// Helper function to get color class based on uptime percentage
|
||||
const getUptimeClass = (value) => {
|
||||
if (value === null) return 'text-muted';
|
||||
if (value >= 99) return 'text-excellent';
|
||||
if (value >= 95) return 'text-good';
|
||||
if (value >= 90) return 'text-fair';
|
||||
return 'text-poor';
|
||||
};
|
||||
|
||||
// Helper function to format uptime value
|
||||
const formatUptime = (value, label) => {
|
||||
const display = value !== null ? `${value}%` : '--';
|
||||
const colorClass = getUptimeClass(value);
|
||||
return `${label}: <strong class="${colorClass}">${display}</strong>`;
|
||||
};
|
||||
|
||||
// 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(' | ');
|
||||
uptimeDisplay.innerHTML = [
|
||||
formatUptime(service.uptime['24h'], '24h'),
|
||||
formatUptime(service.uptime['7d'], '7d'),
|
||||
formatUptime(service.uptime['30d'], '30d'),
|
||||
formatUptime(service.uptime.all_time, 'All'),
|
||||
].join(' | ');
|
||||
}
|
||||
|
||||
// Update total checks
|
||||
if (checksDisplay && service.total_checks !== undefined) {
|
||||
checksDisplay.textContent = service.total_checks;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update overall status bar
|
||||
*/
|
||||
function updateOverallStatus(services) {
|
||||
const overallBar = document.getElementById('overallStatus');
|
||||
const icon = overallBar.querySelector('.summary-icon');
|
||||
@@ -154,17 +120,14 @@ function updateOverallStatus(services) {
|
||||
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;
|
||||
const online = services.filter(function(s) { return s.status === 'online'; }).length;
|
||||
const degraded = services.filter(function(s) { return s.status === 'degraded' || s.status === 'timeout'; }).length;
|
||||
const offline = services.filter(function(s) { return s.status === 'offline'; }).length;
|
||||
|
||||
// Update counts
|
||||
onlineCount.textContent = online;
|
||||
totalCount.textContent = total;
|
||||
|
||||
// Remove all status classes
|
||||
overallBar.classList.remove('online', 'degraded', 'offline');
|
||||
icon.classList.remove('operational', 'partial', 'major', 'loading');
|
||||
|
||||
@@ -205,9 +168,6 @@ function updateOverallStatus(services) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message
|
||||
*/
|
||||
function showError(message) {
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'status-error';
|
||||
@@ -217,13 +177,10 @@ function showError(message) {
|
||||
const container = document.querySelector('.foregroundContent');
|
||||
if (container) {
|
||||
container.insertBefore(errorDiv, container.firstChild);
|
||||
setTimeout(() => errorDiv.remove(), 5000);
|
||||
setTimeout(function() { errorDiv.remove(); }, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual refresh
|
||||
*/
|
||||
function refreshStatus() {
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
if (refreshBtn) {
|
||||
@@ -233,30 +190,22 @@ function refreshStatus() {
|
||||
fetchStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize on page load
|
||||
*/
|
||||
var statusIntervalId = null;
|
||||
|
||||
function initStatusPage() {
|
||||
// Clear any existing interval from a previous SPA navigation
|
||||
if (statusIntervalId !== null) {
|
||||
clearInterval(statusIntervalId);
|
||||
}
|
||||
fetchStatus();
|
||||
// Auto-refresh every 1 minute to get latest data
|
||||
statusIntervalId = setInterval(fetchStatus, 60000);
|
||||
}
|
||||
|
||||
// Clean up interval when navigating away via SPA
|
||||
document.addEventListener('beforenavigate', () => {
|
||||
// Clean up polling interval when navigating away via SPA
|
||||
document.addEventListener('beforenavigate', function() {
|
||||
if (statusIntervalId !== null) {
|
||||
clearInterval(statusIntervalId);
|
||||
statusIntervalId = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Start when page loads
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initStatusPage);
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"status": "complete",
|
||||
"classes": "geospacial",
|
||||
"bgi": "watershedTemps.png",
|
||||
"content": "Geospacial analysis of Maryland's Antietam and Conococheague sub-watersheds, monitoring water quality and temperatures through the summer months for reporting to governmental review boards for environmental protection"
|
||||
"content": "Live geospacial analysis of Maryland's Antietam and Conococheague sub-watersheds, monitoring water quality and temperatures through the summer months for governmental environment health review boards."
|
||||
},
|
||||
"Automotive Brand Valuation Analysis": {
|
||||
"status": "complete",
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
href="{{ url_for('static', filename='css/App.css') }}"
|
||||
/>
|
||||
<link rel="canonical" href="{{ request.url_root | trim('/') }}{{ var['canonical'] }}" />
|
||||
<script defer src="{{ url_for('static', filename='js/checkbox.js') }}"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/responsive.js') }}"></script>
|
||||
{# <script src="{{ url_for('static', filename='js/chessbed.js') }}"></script> #}
|
||||
<script defer src="{{ url_for('static', filename='js/idler.js') }}"></script>
|
||||
|
||||
@@ -46,31 +46,6 @@
|
||||
|
||||
<br />
|
||||
<h2 class="concentratedHead">Projects</h2>
|
||||
<!-- >
|
||||
<div class="checkbox-wrapper">
|
||||
<div class="flex start">
|
||||
<label class="switch" htmlFor="pinned">
|
||||
<input type="checkbox" id="pinned" onClick="toggleCheckbox('')" checked/>
|
||||
<div class="slider round"></div>
|
||||
<strong>Pinned</strong>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex start">
|
||||
<label class="switch" htmlFor="programming">
|
||||
<input type="checkbox" id="programming" onClick="toggleCheckbox('')" />
|
||||
<div class="slider round"></div>
|
||||
<strong>Programming</strong>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex start">
|
||||
<label class="switch" htmlFor="geospacial" onClick="toggleCheckbox('')">
|
||||
<input type="checkbox" id="geospacial" />
|
||||
<div class="slider round"></div>
|
||||
<strong>Geospacial</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</!-->
|
||||
<div class="projectList">
|
||||
{% from 'partials/project.html' import project %} {% for i in
|
||||
var["projects"] %} {{ project(i, var["projects"][i]["classes"],
|
||||
@@ -80,5 +55,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--><script>toggleCheckbox('')</script></!-->
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user