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
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(

View File

@@ -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

View File

@@ -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,10 +126,13 @@ 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)
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)")

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -13,21 +13,28 @@ function toggleMenu(collapse=false) {
}
async function goto(location, { push = true } = {}) {
let a = await fetch("/api/goto/" + location, {
let a;
try {
a = await fetch("/api/goto/" + location, {
credentials: "include",
method: "GET",
mode: "cors",
});
const response = await a.json();
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()
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];
let root = document.getElementById("root");
const root = document.getElementById("root");
root.innerHTML = content;
root.querySelectorAll("script").forEach((oldScript) => {
const newScript = document.createElement("script");
@@ -37,6 +44,15 @@ async function goto(location, { push = true } = {}) {
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 (el) el.scrollIntoView();
}
toggleMenu(collapse=true);
document.querySelector("title").textContent = metadata["title"];
if (push) {

View File

@@ -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);

View File

@@ -53,9 +53,9 @@
<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/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="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>
</head>
{% block header %}