fix architectural issues: SPA nav, error handling, CSS bugs, perf

- 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
This commit is contained in:
Claude
2026-02-12 14:39:43 +00:00
parent 9b6e29a15c
commit c8b1f124f2
8 changed files with 77 additions and 55 deletions

View File

@@ -60,9 +60,9 @@ def goto(location='home'):
page = None page = None
try: try:
page = flask.render_template(pagevars["template"], var=pagevars) page = flask.render_template(pagevars["template"], var=pagevars)
except Exception as e: except Exception:
e = HTTPerror.InternalServerError(None, e) e = HTTPerror.InternalServerError()
page = page404(e) page = handle_http_error(e)
return [pagevars, page] return [pagevars, page]
def funcGen(pagename, pages): def funcGen(pagename, pages):
@@ -72,7 +72,7 @@ def funcGen(pagename, pages):
except Exception: except Exception:
e = HTTPerror.InternalServerError() e = HTTPerror.InternalServerError()
print(e) print(e)
return page404(e) return handle_http_error(e)
return dynamicRule return dynamicRule
for i in pages: for i in pages:
@@ -86,14 +86,14 @@ def resume():
return flask.send_file("./static/Resume_Simonson_Andrew.pdf") return flask.send_file("./static/Resume_Simonson_Andrew.pdf")
@app.errorhandler(HTTPerror.HTTPException) @app.errorhandler(HTTPerror.HTTPException)
def page404(e): def handle_http_error(e):
eCode = e.code eCode = e.code
message = e.description message = e.description
pagevars = { pagevars = {
"template": "error.html", "template": "error.html",
"title": f"{eCode} - Simonson", "title": f"{eCode} - Simonson",
"description": "Error on Andrew Simonson's Digital Portfolio", "description": "Error on Andrew Simonson's Digital Portfolio",
"canonical": "404", "canonical": f"/{eCode}",
} }
return ( return (
flask.render_template( flask.render_template(
@@ -107,12 +107,12 @@ def page404(e):
) )
@app.errorhandler(Exception) @app.errorhandler(Exception)
def page500(e): def handle_generic_error(e):
pagevars = { pagevars = {
"template": "error.html", "template": "error.html",
"title": "500 - Simonson", "title": "500 - Simonson",
"description": "Error on Andrew Simonson's Digital Portfolio", "description": "Error on Andrew Simonson's Digital Portfolio",
"canonical": "404", "canonical": "/500",
} }
return ( return (
flask.render_template( flask.render_template(

View File

@@ -1,6 +1,6 @@
from os import environ as env from os import environ as env
# automatically updates some dev envs. need to remove for production. # automatically updates some dev envs. need to remove for production.
try: try:
__import__('envs.py') __import__('envs')
except ImportError: except ImportError:
pass pass

View File

@@ -5,6 +5,7 @@ Checks service availability and tracks uptime statistics
import requests import requests
import time import time
import json import json
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta from datetime import datetime, timedelta
from threading import Thread, Lock from threading import Thread, Lock
from pathlib import Path from pathlib import Path
@@ -125,12 +126,15 @@ class ServiceMonitor:
"""Check all services and update status data""" """Check all services and update status data"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Checking all services...") 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 = {} results = {}
for service in SERVICES: with ThreadPoolExecutor(max_workers=len(SERVICES)) as executor:
result = self.check_service(service) futures = {executor.submit(self.check_service, s): s for s in SERVICES}
results[service['id']] = result for future in futures:
print(f" {service['name']}: {result['status']} ({result['response_time']}ms)") 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 # Only acquire lock when updating the shared data structure
with self.lock: with self.lock:

View File

@@ -1,23 +1,27 @@
@font-face { @font-face {
font-family: "neon-future"; font-family: "neon-future";
src: url("../fonts/NeonFuture.ttf") src: url("../fonts/NeonFuture.ttf");
font-display: swap;
} }
@font-face { @font-face {
font-family: "shuttlex"; font-family: "shuttlex";
src: url("../fonts/SHUTTLE-X.ttf"); src: url("../fonts/SHUTTLE-X.ttf");
font-display: swap;
} }
@font-face { @font-face {
font-family: "sunset-club"; font-family: "sunset-club";
src: url("../fonts/SunsetClub.otf") src: url("../fonts/SunsetClub.otf");
font-display: swap;
} }
@font-face { @font-face {
font-family: "robotoreg"; font-family: "robotoreg";
src: url("../fonts/RobotoCondensed-Regular.ttf") src: url("../fonts/RobotoCondensed-Regular.ttf");
font-display: swap;
} }
html, body { html, body {
@@ -117,10 +121,6 @@ li {
color: #a8a8a8; color: #a8a8a8;
} }
strong {
color: #ecebeb;
}
p, li, span { p, li, span {
color: rgb(212, 212, 212); color: rgb(212, 212, 212);
font-size: 1rem; font-size: 1rem;
@@ -289,6 +289,9 @@ tr {
padding-right: 4rem; padding-right: 4rem;
border-bottom: #0f0f0f solid 5px; border-bottom: #0f0f0f solid 5px;
color: white; color: white;
position: relative;
padding-bottom: 0.5em;
margin-bottom: 1em;
} }
.foreground { .foreground {
@@ -313,12 +316,6 @@ tr {
line-height: 1.5em; line-height: 1.5em;
} }
.concentratedHead {
position: relative;
padding-bottom: 0.5em;
margin-bottom: 1em;
}
.concentratedHead::after { .concentratedHead::after {
content: ''; content: '';
position: absolute; position: absolute;
@@ -442,10 +439,6 @@ tr {
height: 0; height: 0;
} }
.relative {
position: relative;
}
.onRight { .onRight {
overflow: scroll; overflow: scroll;
margin: auto; margin: auto;
@@ -955,7 +948,7 @@ tr {
padding: 10px; padding: 10px;
border: solid 2px #553; border: solid 2px #553;
border-radius: 0.5em; border-radius: 0.5em;
background-image: url("/static/photos/wood.jpg"); background-image: url("../photos/wood.jpg");
cursor: pointer; cursor: pointer;
transition: transform 0.3s ease, box-shadow 0.3s ease; transition: transform 0.3s ease, box-shadow 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
@@ -1125,9 +1118,10 @@ tr {
@font-face { @font-face {
font-family: Chess New; font-family: Chess New;
src: url('../fonts/chessglyph-new.0cc8115c.woff2'); src: url('../fonts/chessglyph-new.0cc8115c.woff2');
font-display: swap;
} }
@media screen and (max-width: 600) { @media screen and (max-width: 600px) {
#nametagContainer { #nametagContainer {
width: unset; width: unset;
} }

View File

@@ -10,7 +10,7 @@ async function addChessEmbed(username) {
if (user.status === 200) { if (user.status === 200) {
user = await user.json(); user = await user.json();
stats = await stats.json(); stats = await stats.json();
ratings = { const ratings = {
rapid: stats.chess_rapid.last.rating, rapid: stats.chess_rapid.last.rating,
blitz: stats.chess_blitz.last.rating, blitz: stats.chess_blitz.last.rating,
bullet: stats.chess_bullet.last.rating, bullet: stats.chess_bullet.last.rating,

View File

@@ -13,30 +13,46 @@ function toggleMenu(collapse=false) {
} }
async function goto(location, { push = true } = {}) { async function goto(location, { push = true } = {}) {
let a = await fetch("/api/goto/" + location, { let a;
credentials: "include", try {
method: "GET", a = await fetch("/api/goto/" + location, {
mode: "cors", 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 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("#")) { if (!window.location.href.includes("#")) {
window.scrollTo({top: 0, left: 0, behavior:"instant"}); window.scrollTo({top: 0, left: 0, behavior:"instant"});
} else { } else {
eid = decodeURIComponent(window.location.hash.substring(1)) const eid = decodeURIComponent(window.location.hash.substring(1));
document.getElementById(eid).scrollIntoView() 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); toggleMenu(collapse=true);
document.querySelector("title").textContent = metadata["title"]; document.querySelector("title").textContent = metadata["title"];
if (push) { if (push) {

View File

@@ -248,6 +248,14 @@ function initStatusPage() {
statusIntervalId = setInterval(fetchStatus, 300000); 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 // Start when page loads
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initStatusPage); document.addEventListener('DOMContentLoaded', initStatusPage);

View File

@@ -53,9 +53,9 @@
<link rel="canonical" href="{{ var['canonical'] }}" /> <link rel="canonical" href="{{ var['canonical'] }}" />
<script defer src="{{ url_for('static', filename='js/checkbox.js') }}"></script> <script defer src="{{ url_for('static', filename='js/checkbox.js') }}"></script>
<script defer src="{{ url_for('static', filename='js/responsive.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/chessbed.js') }}"></script>
<script defer src="{{ url_for('static', filename='js/idler.js') }}"></script> <script defer src="{{ url_for('static', filename='js/idler.js') }}"></script>
<script defer src="https://cdn.jsdelivr.net/npm/p5@1.4.1/lib/p5.js"></script> <script defer src="https://cdn.jsdelivr.net/npm/p5@1.4.1/lib/p5.min.js"></script>
<title>{{ var['title'] }}</title> <title>{{ var['title'] }}</title>
</head> </head>
{% block header %} {% block header %}