From 44948a6e9f8a60e27715b5ccec8d3de0c35ee1bd Mon Sep 17 00:00:00 2001 From: Andrew Simonson Date: Tue, 17 Feb 2026 20:42:52 -0600 Subject: [PATCH] clean up clean up everybody do your share --- src/app.py | 143 ++++++++++++----------- src/monitor.py | 209 ++++++++++++++-------------------- src/static/css/App.css | 8 +- src/static/css/checkbox.css | 70 ------------ src/static/js/checkbox.js | 39 ------- src/static/js/chessbed.js | 34 +++--- src/static/js/idler.js | 47 ++++---- src/static/js/responsive.js | 56 +++++---- src/static/js/status.js | 113 +++++------------- src/static/json/projects.json | 2 +- src/templates/header.html | 1 - src/templates/projects.html | 26 ----- 12 files changed, 263 insertions(+), 485 deletions(-) delete mode 100755 src/static/css/checkbox.css delete mode 100755 src/static/js/checkbox.js diff --git a/src/app.py b/src/app.py index 988bb73..25d28cd 100755 --- a/src/app.py +++ b/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/') -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) diff --git a/src/monitor.py b/src/monitor.py index 9f25c88..736a129 100644 --- a/src/monitor.py +++ b/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() diff --git a/src/static/css/App.css b/src/static/css/App.css index d96ed43..0654037 100755 --- a/src/static/css/App.css +++ b/src/static/css/App.css @@ -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 * { diff --git a/src/static/css/checkbox.css b/src/static/css/checkbox.css deleted file mode 100755 index 326f863..0000000 --- a/src/static/css/checkbox.css +++ /dev/null @@ -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; -} diff --git a/src/static/js/checkbox.js b/src/static/js/checkbox.js deleted file mode 100755 index a9939ab..0000000 --- a/src/static/js/checkbox.js +++ /dev/null @@ -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"); - } -} diff --git a/src/static/js/chessbed.js b/src/static/js/chessbed.js index 3407672..193b7b3 100755 --- a/src/static/js/chessbed.js +++ b/src/static/js/chessbed.js @@ -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)"); } } diff --git a/src/static/js/idler.js b/src/static/js/idler.js index 5f9171b..6219843 100755 --- a/src/static/js/idler.js +++ b/src/static/js/idler.js @@ -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); } } } diff --git a/src/static/js/responsive.js b/src/static/js/responsive.js index 98cae17..aebcbd5 100755 --- a/src/static/js/responsive.js +++ b/src/static/js/responsive.js @@ -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"); + } } diff --git a/src/static/js/status.js b/src/static/js/status.js index 13b0cbb..45744ab 100644 --- a/src/static/js/status.js +++ b/src/static/js/status.js @@ -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}: ${display}`; +} + 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}: ${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(' | '); + 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 { diff --git a/src/static/json/projects.json b/src/static/json/projects.json index 3b3cdcc..bb63d14 100755 --- a/src/static/json/projects.json +++ b/src/static/json/projects.json @@ -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", diff --git a/src/templates/header.html b/src/templates/header.html index 9af7f1a..2a9b733 100755 --- a/src/templates/header.html +++ b/src/templates/header.html @@ -51,7 +51,6 @@ href="{{ url_for('static', filename='css/App.css') }}" /> - {# #} diff --git a/src/templates/projects.html b/src/templates/projects.html index 6a4473d..d99ef16 100755 --- a/src/templates/projects.html +++ b/src/templates/projects.html @@ -46,31 +46,6 @@

Projects

-
{% from 'partials/project.html' import project %} {% for i in var["projects"] %} {{ project(i, var["projects"][i]["classes"], @@ -80,5 +55,4 @@
- {% endblock %}