clean up clean up everybody do your share

This commit is contained in:
2026-02-17 20:42:52 -06:00
parent 3f0f9907ed
commit 44948a6e9f
12 changed files with 263 additions and 485 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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