From c8b1f124f2d197ed6d17f2df2dfa575ceb518be1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Feb 2026 14:39:43 +0000 Subject: [PATCH] fix architectural issues: SPA nav, error handling, CSS bugs, perf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - responsive.js: fix scroll race condition (scroll after innerHTML), add error handling for fetch failures, fix implicit global `eid`, dispatch `beforenavigate` event for cleanup hooks - chessbed.js: fix implicit global `ratings` variable - status.js: clear polling interval on SPA navigation via `beforenavigate` event to prevent leak - App.css: add font-display:swap to all @font-face, fix broken media query (missing px unit), consolidate duplicate selectors (.concentratedHead, .relative, strong), fix hardcoded bookshelf background-image path to use relative URL - header.html: defer chessbed.js, use p5.min.js instead of p5.js - monitor.py: use ThreadPoolExecutor for concurrent service checks - config.py: fix __import__('envs.py') → __import__('envs') - app.py: rename misleading error handlers (page404→handle_http_error, page500→handle_generic_error), fix error info leakage by not passing raw exception to InternalServerError, fix hardcoded canonical "404" https://claude.ai/code/session_01FUhPqQLahEoL6FMxhXkDKa --- src/app.py | 16 +++++------ src/config.py | 2 +- src/monitor.py | 14 ++++++---- src/static/css/App.css | 32 +++++++++------------- src/static/js/chessbed.js | 2 +- src/static/js/responsive.js | 54 ++++++++++++++++++++++++------------- src/static/js/status.js | 8 ++++++ src/templates/header.html | 4 +-- 8 files changed, 77 insertions(+), 55 deletions(-) diff --git a/src/app.py b/src/app.py index 3228f56..5f29cdf 100755 --- a/src/app.py +++ b/src/app.py @@ -60,9 +60,9 @@ def goto(location='home'): page = None try: page = flask.render_template(pagevars["template"], var=pagevars) - except Exception as e: - e = HTTPerror.InternalServerError(None, e) - page = page404(e) + except Exception: + e = HTTPerror.InternalServerError() + page = handle_http_error(e) return [pagevars, page] def funcGen(pagename, pages): @@ -72,7 +72,7 @@ def funcGen(pagename, pages): except Exception: e = HTTPerror.InternalServerError() print(e) - return page404(e) + return handle_http_error(e) return dynamicRule for i in pages: @@ -86,14 +86,14 @@ def resume(): return flask.send_file("./static/Resume_Simonson_Andrew.pdf") @app.errorhandler(HTTPerror.HTTPException) -def page404(e): +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": "404", + "canonical": f"/{eCode}", } return ( flask.render_template( @@ -107,12 +107,12 @@ def page404(e): ) @app.errorhandler(Exception) -def page500(e): +def handle_generic_error(e): pagevars = { "template": "error.html", "title": "500 - Simonson", "description": "Error on Andrew Simonson's Digital Portfolio", - "canonical": "404", + "canonical": "/500", } return ( flask.render_template( diff --git a/src/config.py b/src/config.py index c5f3d7e..4391619 100755 --- a/src/config.py +++ b/src/config.py @@ -1,6 +1,6 @@ from os import environ as env # automatically updates some dev envs. need to remove for production. try: - __import__('envs.py') + __import__('envs') except ImportError: pass diff --git a/src/monitor.py b/src/monitor.py index 6597930..40678fb 100644 --- a/src/monitor.py +++ b/src/monitor.py @@ -5,6 +5,7 @@ Checks service availability and tracks uptime statistics import requests import time import json +from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta from threading import Thread, Lock from pathlib import Path @@ -125,12 +126,15 @@ class ServiceMonitor: """Check all services and update status data""" print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Checking all services...") - # Perform all network checks OUTSIDE the lock to avoid blocking API calls + # Perform all network checks concurrently and OUTSIDE the lock results = {} - for service in SERVICES: - result = self.check_service(service) - results[service['id']] = result - print(f" {service['name']}: {result['status']} ({result['response_time']}ms)") + with ThreadPoolExecutor(max_workers=len(SERVICES)) as executor: + futures = {executor.submit(self.check_service, s): s for s in SERVICES} + for future in futures: + service = futures[future] + result = future.result() + results[service['id']] = result + print(f" {service['name']}: {result['status']} ({result['response_time']}ms)") # Only acquire lock when updating the shared data structure with self.lock: diff --git a/src/static/css/App.css b/src/static/css/App.css index 6dfe4fc..f1b91b7 100755 --- a/src/static/css/App.css +++ b/src/static/css/App.css @@ -1,23 +1,27 @@ @font-face { font-family: "neon-future"; - src: url("../fonts/NeonFuture.ttf") + src: url("../fonts/NeonFuture.ttf"); + font-display: swap; } @font-face { font-family: "shuttlex"; src: url("../fonts/SHUTTLE-X.ttf"); + font-display: swap; } @font-face { font-family: "sunset-club"; - src: url("../fonts/SunsetClub.otf") + src: url("../fonts/SunsetClub.otf"); + font-display: swap; } @font-face { font-family: "robotoreg"; - src: url("../fonts/RobotoCondensed-Regular.ttf") + src: url("../fonts/RobotoCondensed-Regular.ttf"); + font-display: swap; } html, body { @@ -117,10 +121,6 @@ li { color: #a8a8a8; } -strong { - color: #ecebeb; -} - p, li, span { color: rgb(212, 212, 212); font-size: 1rem; @@ -289,6 +289,9 @@ tr { padding-right: 4rem; border-bottom: #0f0f0f solid 5px; color: white; + position: relative; + padding-bottom: 0.5em; + margin-bottom: 1em; } .foreground { @@ -313,12 +316,6 @@ tr { line-height: 1.5em; } -.concentratedHead { - position: relative; - padding-bottom: 0.5em; - margin-bottom: 1em; -} - .concentratedHead::after { content: ''; position: absolute; @@ -442,10 +439,6 @@ tr { height: 0; } -.relative { - position: relative; -} - .onRight { overflow: scroll; margin: auto; @@ -955,7 +948,7 @@ tr { padding: 10px; border: solid 2px #553; border-radius: 0.5em; - background-image: url("/static/photos/wood.jpg"); + background-image: url("../photos/wood.jpg"); cursor: pointer; transition: transform 0.3s ease, box-shadow 0.3s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); @@ -1125,9 +1118,10 @@ tr { @font-face { font-family: Chess New; src: url('../fonts/chessglyph-new.0cc8115c.woff2'); + font-display: swap; } -@media screen and (max-width: 600) { +@media screen and (max-width: 600px) { #nametagContainer { width: unset; } diff --git a/src/static/js/chessbed.js b/src/static/js/chessbed.js index c97da4b..3407672 100755 --- a/src/static/js/chessbed.js +++ b/src/static/js/chessbed.js @@ -10,7 +10,7 @@ async function addChessEmbed(username) { if (user.status === 200) { user = await user.json(); stats = await stats.json(); - ratings = { + const ratings = { rapid: stats.chess_rapid.last.rating, blitz: stats.chess_blitz.last.rating, bullet: stats.chess_bullet.last.rating, diff --git a/src/static/js/responsive.js b/src/static/js/responsive.js index 71d5d66..fbea260 100755 --- a/src/static/js/responsive.js +++ b/src/static/js/responsive.js @@ -13,30 +13,46 @@ function toggleMenu(collapse=false) { } async function goto(location, { push = true } = {}) { - let a = await fetch("/api/goto/" + location, { - credentials: "include", - method: "GET", - mode: "cors", - }); + let a; + try { + a = await fetch("/api/goto/" + location, { + credentials: "include", + method: "GET", + mode: "cors", + }); + if (!a.ok) { + console.error(`Navigation failed: HTTP ${a.status}`); + return; + } + } catch (err) { + console.error("Navigation fetch failed:", err); + return; + } + + document.dispatchEvent(new Event('beforenavigate')); + const response = await a.json(); + const metadata = response[0]; + const content = response[1]; + const root = document.getElementById("root"); + root.innerHTML = content; + root.querySelectorAll("script").forEach((oldScript) => { + const newScript = document.createElement("script"); + Array.from(oldScript.attributes).forEach(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 { - eid = decodeURIComponent(window.location.hash.substring(1)) - document.getElementById(eid).scrollIntoView() + const eid = decodeURIComponent(window.location.hash.substring(1)); + const el = document.getElementById(eid); + if (el) el.scrollIntoView(); } - const metadata = response[0]; - const content = response[1]; - let root = document.getElementById("root"); - root.innerHTML = content; - root.querySelectorAll("script").forEach((oldScript) => { - const newScript = document.createElement("script"); - Array.from(oldScript.attributes).forEach(attr => { - newScript.setAttribute(attr.name, attr.value); - }); - newScript.textContent = oldScript.textContent; - oldScript.parentNode.replaceChild(newScript, oldScript); - }); + toggleMenu(collapse=true); document.querySelector("title").textContent = metadata["title"]; if (push) { diff --git a/src/static/js/status.js b/src/static/js/status.js index eb19784..19585e5 100644 --- a/src/static/js/status.js +++ b/src/static/js/status.js @@ -248,6 +248,14 @@ function initStatusPage() { statusIntervalId = setInterval(fetchStatus, 300000); } +// Clean up interval when navigating away via SPA +document.addEventListener('beforenavigate', () => { + if (statusIntervalId !== null) { + clearInterval(statusIntervalId); + statusIntervalId = null; + } +}); + // Start when page loads if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initStatusPage); diff --git a/src/templates/header.html b/src/templates/header.html index 2eaf366..475ab9f 100755 --- a/src/templates/header.html +++ b/src/templates/header.html @@ -53,9 +53,9 @@ - + - + {{ var['title'] }} {% block header %}