Compare commits

2 Commits

Author SHA1 Message Date
b1bd11a5c5 oops 2024-05-05 21:17:00 -05:00
252aee0ff0 doot 2024-05-05 21:03:57 -05:00
148 changed files with 703 additions and 2019 deletions

15
.dockerignore Executable file → Normal file
View File

@@ -1,15 +1,6 @@
.git react_OLD
.gitignore
.env
.venv .venv
.vscode .vscode
.claude .git
CLAUDE.md .git*
README.md
STATUS_MONITOR_README.md
Dockerfile
docker-compose.yml
notes.txt
react_OLD
__pycache__ __pycache__
*.pyc

0
.gitattributes vendored Executable file → Normal file
View File

View File

@@ -1,2 +0,0 @@
[core]
filemode = false

6
.gitignore vendored Executable file → Normal file
View File

@@ -3,9 +3,3 @@ __pycache__
notes.txt notes.txt
react_OLD react_OLD
envs.py envs.py
.env
status_history.json
.claude
CLAUDE.md
.aider*

0
.vscode/launch.json vendored Executable file → Normal file
View File

0
.vscode/settings.json vendored Executable file → Normal file
View File

34
Dockerfile Executable file → Normal file
View File

@@ -1,10 +1,34 @@
FROM python:3.10-bullseye FROM ubuntu:lunar
LABEL maintainer="Andrew Simonson <asimonson1125@gmail.com>" LABEL maintainer="Andrew Simonson <asimonson1125@gmail.com>"
WORKDIR /app ENV DEBIAN_FRONTEND noninteractive
COPY src/ . RUN apt-get update
RUN apt-get install -y python3-pip nginx gunicorn supervisor
RUN pip install --no-cache-dir -r requirements.txt # Setup flask application
RUN mkdir -p /deploy/app
COPY src /deploy/app
RUN pip install -r /deploy/app/requirements.txt --break-system-packages
CMD [ "gunicorn", "--bind", "0.0.0.0:8080", "app:app"] # Setup nginx
RUN rm /etc/nginx/sites-enabled/default
COPY flask.conf /etc/nginx/sites-available/
RUN ln -s /etc/nginx/sites-available/flask.conf /etc/nginx/sites-enabled/flask.conf && \
echo "daemon off;" >> /etc/nginx/nginx.conf
# Setup supervisord
RUN mkdir -p /var/log/supervisor
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY gunicorn.conf /etc/supervisor/conf.d/gunicorn.conf
# Permissions
# RUN adduser --disabled-password --gecos '' supervisor && \
RUN chmod -R 777 /var/* && \
chown -R root /var/*
# Entrypoint
USER root
# Start processes
CMD ["/usr/bin/supervisord"]

2
README.md Executable file → Normal file
View File

@@ -7,5 +7,3 @@ So people can see how excellent my coding standards are.
* Viruses: not included * Viruses: not included
You gotta uhh `pip3 install -r requirements.txt` and `python3 app.py` that thing You gotta uhh `pip3 install -r requirements.txt` and `python3 app.py` that thing
Docker compose configured to expose at `localhost:8080`

View File

@@ -1,149 +0,0 @@
# Service Status Monitor
## Overview
Server-side monitoring system that checks the availability of asimonson.com services every 2 hours and provides uptime statistics.
## Architecture
### Backend Components
#### 1. `monitor.py` - Service Monitoring Module
- **Purpose**: Performs automated health checks on all services
- **Check Interval**: Every 2 hours (7200 seconds)
- **Services Monitored**:
- asimonson.com
- files.asimonson.com
- git.asimonson.com
- pass.asimonson.com
- ssh.asimonson.com
**Features**:
- Tracks response times and HTTP status codes
- Stores check history (up to 720 checks = 60 days of data)
- Calculates uptime percentages for multiple time periods (24h, 7d, 30d, all-time)
- Persists data to `static/json/status_history.json`
- Runs in a background thread
#### 2. `app.py` - Flask Integration
- **New API Endpoint**: `/api/status`
- Returns current status for all services
- Includes uptime statistics
- Provides last check and next check times
- **Auto-start**: Monitoring begins when the Flask app starts
### Frontend Components
#### 1. `templates/status.html` - Status Page Template
- Displays real-time service status
- Shows uptime percentages (24h, 7d, 30d, all-time)
- Displays response times and status codes
- Shows total number of checks performed
- Manual refresh button
- Auto-refreshes every 5 minutes
#### 2. `static/js/status.js` - Frontend Logic
- Fetches status data from `/api/status` API
- Updates UI with service status and uptime
- Handles error states gracefully
- Auto-refresh every 5 minutes
#### 3. `static/css/App.css` - Styling
- Color-coded status indicators:
- Green: Operational
- Yellow: Degraded/Timeout
- Red: Offline
- Responsive grid layout
- Dark theme matching existing site design
## Data Storage
Status history is stored in `src/static/json/status_history.json`:
```json
{
"last_check": "2026-02-11T14:30:00",
"services": {
"main": {
"name": "asimonson.com",
"url": "https://asimonson.com",
"status": "online",
"response_time": 156,
"status_code": 200,
"last_online": "2026-02-11T14:30:00",
"checks": [
{
"timestamp": "2026-02-11T14:30:00",
"status": "online",
"response_time": 156,
"status_code": 200
}
]
}
}
}
```
## Status Types
- **online**: HTTP status 2xx-4xx, service responding
- **degraded**: HTTP status 5xx or slow response
- **timeout**: Request exceeded timeout limit (10 seconds)
- **offline**: Unable to reach service
- **unknown**: No checks performed yet
## Uptime Calculation
Uptime percentage = (number of online checks / total checks) × 100
Calculated for:
- Last 24 hours
- Last 7 days
- Last 30 days
- All-time (since monitoring began)
## Usage
### Starting the Server
```bash
cd src
python3 app.py
```
The monitoring will start automatically and perform an initial check immediately, then every 2 hours thereafter.
### Accessing the Status Page
Navigate to: `https://asimonson.com/status`
### API Access
Direct API access: `https://asimonson.com/api/status`
Returns JSON with current status and uptime statistics for all services.
## Configuration
To modify monitoring behavior, edit `src/monitor.py`:
```python
# Change check interval (in seconds)
CHECK_INTERVAL = 7200 # 2 hours
# Modify service list
SERVICES = [
{
'id': 'main',
'name': 'asimonson.com',
'url': 'https://asimonson.com',
'timeout': 10 # seconds
},
# Add more services here
]
```
## Notes
- First deployment will show limited uptime data until enough checks accumulate
- Historical data is preserved across server restarts
- Maximum 720 checks stored per service (60 days at 2-hour intervals)
- Page auto-refreshes every 5 minutes to show latest server data
- Manual refresh button available for immediate updates
- All checks performed server-side (no client-side CORS issues)

View File

@@ -1,9 +0,0 @@
services:
portfolio:
image: 'asimonson1125/portfolio'
build:
context: ./
dockerfile: Dockerfile
restart: 'no'
ports:
- 8080:8080

24
flask.conf Normal file
View File

@@ -0,0 +1,24 @@
server {
listen 8080;
server_name www.asimonson.com;
return 301 https://asimonson.com$request_uri;
}
server {
listen 8080;
server_name asimonson.com;
gzip on;
gzip_types text/plain text/javascript text/css;
gunzip on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options 'nosniff';
add_header X-Frame-Options 'SAMEORIGIN';
location / {
proxy_pass http://localhost:5000/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

3
gunicorn.conf Normal file
View File

@@ -0,0 +1,3 @@
[program:gunicorn]
command=/usr/bin/gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker app:app -b localhost:5000
directory=/deploy/app

143
src/app.py Executable file → Normal file
View File

@@ -1,89 +1,34 @@
import flask import flask
from flask_minify import Minify from flask_minify import Minify
import json import json
import os
import hashlib
import werkzeug.exceptions as HTTPerror import werkzeug.exceptions as HTTPerror
import requests
from config import * from config import *
from monitor import monitor, SERVICES
app = flask.Flask(__name__) proj = json.load(open("./static/json/projects.json", "r"))
books = json.load(open("./static/json/books.json", "r"))
# Compute content hashes for static file fingerprinting skillList = json.load(open("./static/json/skills.json", "r"))
static_file_hashes = {} timeline = json.load(open("./static/json/timeline.json", "r"))
for dirpath, _, filenames in os.walk(app.static_folder): pages = json.load(open("./static/json/pages.json", "r"))
for filename in filenames: pages['about']['skillList'] = skillList
filepath = os.path.join(dirpath, filename) pages['about']['timeline'] = timeline
relative = os.path.relpath(filepath, 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):
if endpoint == 'static':
filename = values.get('filename')
if filename and filename in static_file_hashes:
values['v'] = static_file_hashes[filename]
return flask.url_for(endpoint, **values)
return dict(url_for=versioned_url_for)
# Add security and caching headers
@app.after_request
def add_security_headers(response):
"""Add security and performance headers to all responses"""
# Security headers
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']:
response.headers['Cache-Control'] = 'public, max-age=86400'
else:
response.headers['Cache-Control'] = 'no-cache, must-revalidate'
return response
def load_json(path):
with open(path, "r") as f:
return json.load(f)
proj = load_json("./static/json/projects.json")
books = load_json("./static/json/books.json")
skillList = 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']['projects'] = proj
pages['home']['books'] = books pages['home']['books'] = books
pages['books']['books'] = books pages['books']['books'] = books
pages['status']['services'] = SERVICES
@app.route('/api/status') app = flask.Flask(__name__)
def api_status():
"""API endpoint for service status"""
return flask.jsonify(monitor.get_status_summary())
@app.route('/api/goto/') @app.route('/api/goto/')
@app.route('/api/goto/<location>') @app.route('/api/goto/<location>')
def goto(location='home'): def goto(location='home'):
if location not in pages:
flask.abort(404)
pagevars = pages[location] pagevars = pages[location]
page = None page = None
try: try:
page = flask.render_template(pagevars["template"], var=pagevars) page = flask.render_template(pagevars["template"], var=pagevars)
except Exception: except Exception as e:
e = HTTPerror.InternalServerError() # raise e
page = handle_http_error(e) e = HTTPerror.InternalServerError(None, e)
page = page404(e)
return [pagevars, page] return [pagevars, page]
def funcGen(pagename, pages): def funcGen(pagename, pages):
@@ -92,29 +37,55 @@ def funcGen(pagename, pages):
return flask.render_template('header.html', var=pages[pagename]) return flask.render_template('header.html', var=pages[pagename])
except Exception: except Exception:
e = HTTPerror.InternalServerError() e = HTTPerror.InternalServerError()
print(e) return page404(e)
return handle_http_error(e)
return dynamicRule return dynamicRule
for i in pages: for i in pages:
func = funcGen(i, pages) func = funcGen(i, pages)
app.add_url_rule(pages[i]['canonical'], i, func) app.add_url_rule(pages[i]['canonical'], i, func)
# for i in pages:
# exec(f"@app.route(pages['{i}']['canonical'])\ndef {i}(): return flask.render_template('header.html', var=pages['{i}'])")
@app.route("/huge")
def huge():
return flask.render_template('huge.html')
@app.route("/resume") @app.route("/resume")
@app.route("/Resume.pdf") @app.route("/Resume.pdf")
@app.route("/Resume_Simonson_Andrew.pdf")
def resume(): def resume():
return flask.send_file("./static/Resume_Simonson_Andrew.pdf") return flask.send_file("./static/Resume.pdf")
@app.errorhandler(HTTPerror.HTTPException) @app.route("/hotspots")
def handle_http_error(e): def hotspotsRIT():
url = HotspotsURL
if flask.request.args.get("legend") == "false":
url += "?legend=false"
pagevars = {
"template": "iframe.html",
"title": f"Hotspots @ RIT",
"description": "Hotspots @ RIT by Andrew Simonson",
"canonical": "/hotspots",
}
return flask.render_template("iframe.html", url=url, var=pagevars)
@app.route("/hotspots/<path>")
def hotspotsProxy(path):
return requests.get(f"{HotspotsURL}/{path}").content
@app.errorhandler(Exception)
def page404(e):
eCode = e.code eCode = e.code
message = e.description message = e.description
try:
message = e.length
finally:
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": f"/{eCode}", "canonical": "404",
} }
return ( return (
flask.render_template( flask.render_template(
@@ -127,25 +98,6 @@ def handle_http_error(e):
eCode, 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("/sitemap.xml")
@app.route("/robots.txt") @app.route("/robots.txt")
@@ -157,7 +109,6 @@ if __name__ == "__main__":
# import sass # import sass
# sass.compile(dirname=("static/scss", "static/css"), output_style="compressed") # sass.compile(dirname=("static/scss", "static/css"), output_style="compressed")
app.run(debug=False) app.run()
else: else:
Minify(app=app, html=True, js=True, cssless=True) Minify(app=app, html=True, js=True, cssless=True)
monitor.start_monitoring()

4
src/config.py Executable file → Normal file
View File

@@ -1,6 +1,8 @@
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') __import__('envs.py')
except ImportError: except ImportError:
pass pass
HotspotsURL = env.get('HotspotsURL', 'https://asimonson.com/hotspots')

View File

@@ -1,256 +0,0 @@
"""
Service monitoring module
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
# 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
}
]
# Check interval: 30 mins
CHECK_INTERVAL = 1800
# File to store status history
STATUS_FILE = Path(__file__).parent / 'static' / 'json' / 'status_history.json'
class ServiceMonitor:
def __init__(self):
self.status_data = {}
self.lock = Lock()
self.load_history()
def load_history(self):
"""Load status history from file"""
if STATUS_FILE.exists():
try:
with open(STATUS_FILE, 'r') as f:
self.status_data = json.load(f)
except Exception as e:
print(f"Error loading status history: {e}")
self.initialize_status_data()
else:
self.initialize_status_data()
def initialize_status_data(self):
"""Initialize empty status data structure"""
self.status_data = {
'last_check': None,
'services': {}
}
for service in SERVICES:
self.status_data['services'][service['id']] = {
'name': service['name'],
'url': service['url'],
'status': 'unknown',
'response_time': None,
'status_code': None,
'last_online': None,
'checks': [] # List of check results
}
def save_history(self):
"""Save status history to file"""
try:
STATUS_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(STATUS_FILE, 'w') as f:
json.dump(self.status_data, f, indent=2)
except Exception as e:
print(f"Error saving status history: {e}")
def check_service(self, service):
"""Check a single service and return status"""
start_time = time.time()
result = {
'timestamp': datetime.now().isoformat(),
'status': 'offline',
'response_time': None,
'status_code': None
}
try:
response = requests.head(
service['url'],
timeout=service['timeout'],
allow_redirects=True
)
elapsed = int((time.time() - start_time) * 1000) # ms
result['response_time'] = elapsed
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
result['status'] = 'online'
else:
result['status'] = 'degraded'
except requests.exceptions.Timeout:
result['status'] = 'timeout'
result['response_time'] = service['timeout'] * 1000
except Exception as e:
result['status'] = 'offline'
result['error'] = str(e)
return result
def check_all_services(self):
"""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 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}
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:
for service in SERVICES:
result = results[service['id']]
service_data = self.status_data['services'][service['id']]
# Update current status
service_data['status'] = result['status']
service_data['response_time'] = result['response_time']
service_data['status_code'] = result['status_code']
if result['status'] == 'online':
service_data['last_online'] = result['timestamp']
# Add to check history (keep last 2880 checks = 60 days at 2hr intervals)
service_data['checks'].append(result)
if len(service_data['checks']) > 2880:
service_data['checks'] = service_data['checks'][-2880:]
self.status_data['last_check'] = datetime.now().isoformat()
self.save_history()
def _calculate_uptime_unlocked(self, service_id, hours=None):
"""Calculate uptime percentage for a service (assumes lock is held)"""
service_data = self.status_data['services'].get(service_id)
if not service_data or not service_data['checks']:
return None
checks = service_data['checks']
# Filter by time period if specified
if hours:
cutoff = datetime.now() - timedelta(hours=hours)
checks = [
c for c in checks
if datetime.fromisoformat(c['timestamp']) > cutoff
]
if not checks:
return None
# Require minimum data coverage for the time period
# Calculate expected number of checks for this period
expected_checks = (hours * 3600) / CHECK_INTERVAL
# Require at least 50% of expected checks to show this metric
minimum_checks = max(3, expected_checks * 0.5)
if len(checks) < minimum_checks:
return None
else:
# For all-time, require at least 3 checks
if len(checks) < 3:
return None
online_count = sum(1 for c in checks if c['status'] == 'online')
uptime = (online_count / len(checks)) * 100
return round(uptime, 2)
def calculate_uptime(self, service_id, hours=None):
"""Calculate uptime percentage for a service"""
with self.lock:
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.status_data['last_check'],
'next_check': None,
'services': []
}
# Calculate next check time
if self.status_data['last_check']:
last_check = datetime.fromisoformat(self.status_data['last_check'])
next_check = last_check + timedelta(seconds=CHECK_INTERVAL)
summary['next_check'] = next_check.isoformat()
for service_id, service_data in self.status_data['services'].items():
service_summary = {
'id': service_id,
'name': service_data['name'],
'url': service_data['url'],
'status': service_data['status'],
'response_time': service_data['response_time'],
'status_code': service_data['status_code'],
'last_online': service_data['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': len(service_data['checks'])
}
summary['services'].append(service_summary)
return summary
def start_monitoring(self):
"""Start background monitoring thread"""
def monitor_loop():
# Initial check
self.check_all_services()
# Periodic checks
while True:
time.sleep(CHECK_INTERVAL)
self.check_all_services()
thread = Thread(target=monitor_loop, daemon=True)
thread.start()
print(f"Service monitoring started (checks every {CHECK_INTERVAL/3600} hours)")
# Global monitor instance
monitor = ServiceMonitor()

57
src/requirements.txt Executable file → Normal file
View File

@@ -1,22 +1,43 @@
blinker==1.8.2 bidict==0.22.1
certifi==2024.7.4 black==22.12.0
charset-normalizer==3.3.2 blinker==1.5
click==8.1.7 certifi==2023.7.22
Flask==3.0.3 cffi==1.15.1
Flask-Minify==0.48 charset-normalizer==3.3.1
gunicorn==22.0.0 click==8.1.3
htmlminf==0.1.13 colorama==0.4.6
idna==3.7 Flask==2.2.2
itsdangerous==2.2.0 Flask-Minify==0.41
Jinja2==3.1.4 Flask-SocketIO==5.3.2
gevent==22.10.2
gevent-websocket==0.10.1
greenlet==2.0.2
gunicorn==20.1.0
htmlmin==0.1.12
idna==3.4
importlib-metadata==6.0.0
itsdangerous==2.1.2
Jinja2==3.1.2
jsmin==3.0.1 jsmin==3.0.1
lesscpy==0.15.1 lesscpy==0.15.1
MarkupSafe==2.1.5 libsass==0.22.0
packaging==24.1 MarkupSafe==2.1.2
mypy-extensions==0.4.3
pathspec==0.11.0
platformdirs==2.6.2
ply==3.11 ply==3.11
rcssmin==1.1.2 pycparser==2.21
requests==2.32.3 pyScss==1.4.0
python-engineio==4.3.4
python-socketio==5.7.2
rcssmin==1.1.1
requests==2.31.0
six==1.16.0 six==1.16.0
urllib3==2.2.2 tomli==2.0.1
Werkzeug==3.0.3 typing_extensions==4.4.0
xxhash==3.4.1 urllib3==2.0.7
Werkzeug==2.2.2
xxhash==3.2.0
zipp==3.11.0
zope.event==4.6
zope.interface==5.5.2

Binary file not shown.

0
src/static/chesscom-embed/default.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

0
src/static/chesscom-embed/diamonds.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

706
src/static/css/App.css Executable file → Normal file

File diff suppressed because it is too large Load Diff

0
src/static/css/checkbox.css Executable file → Normal file
View File

0
src/static/css/head.css Executable file → Normal file
View File

0
src/static/favicon.ico Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

0
src/static/fonts/NeonFuture.ttf Executable file → Normal file
View File

0
src/static/fonts/RobotoCondensed-Regular.ttf Executable file → Normal file
View File

0
src/static/fonts/SHUTTLE-X.ttf Executable file → Normal file
View File

0
src/static/fonts/SunsetClub.otf Executable file → Normal file
View File

0
src/static/icons/email.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

0
src/static/icons/github.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 759 B

After

Width:  |  Height:  |  Size: 759 B

0
src/static/icons/globe.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

0
src/static/icons/instagram.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 683 B

After

Width:  |  Height:  |  Size: 683 B

0
src/static/icons/linkedin.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

0
src/static/icons/log.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 177 KiB

0
src/static/icons/menu.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 764 B

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

0
src/static/icons/neonfinal3.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

0
src/static/icons/rasterLogo.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 382 KiB

After

Width:  |  Height:  |  Size: 382 KiB

0
src/static/icons/rasterLogoCircle.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 213 KiB

0
src/static/icons/withBackground.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

0
src/static/js/checkbox.js Executable file → Normal file
View File

2
src/static/js/chessbed.js Executable file → Normal file
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();
const ratings = { 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,

30
src/static/js/idler.js Executable file → Normal file
View File

@@ -69,39 +69,23 @@ function windowResized() {
function draw() { function draw() {
background(24); background(24);
// Update all balls
for (let i = 0; i < balls.length; i++) { for (let i = 0; i < balls.length; i++) {
balls[i].update(); balls[i].update();
} }
// Optimize line drawing with early distance checks
const maxDist = 150;
const maxDistSquared = maxDist * maxDist; // Avoid sqrt in distance calculation
for (let i = 0; i < balls.length - 1; i++) { for (let i = 0; i < balls.length - 1; i++) {
const ball1 = balls[i];
for (let j = i + 1; j < balls.length; j++) { for (let j = i + 1; j < balls.length; j++) {
const ball2 = balls[j]; let distance = dist(balls[i].x, balls[i].y, balls[j].x, balls[j].y);
if (distance < 100){
// Quick rejection test using squared distance (faster than sqrt)
const dx = ball2.x - ball1.x;
const dy = ball2.y - ball1.y;
const distSquared = dx * dx + dy * dy;
if (distSquared < maxDistSquared) {
const distance = Math.sqrt(distSquared); // Only calculate sqrt if needed
if (distance < 100) {
stroke(150); stroke(150);
line(ball1.x, ball1.y, ball2.x, ball2.y); line(balls[i].x, balls[i].y, balls[j].x, balls[j].y);
} else { }
else if (distance < 150) {
stroke(100); stroke(100);
const chance = 0.3 ** (((random(0.2) + 0.8) * distance) / 150); let chance = 0.3 ** (((random(0.2) + 0.8) * distance) / 150);
if (chance < 0.5) { if (chance < 0.5) {
stroke(50); stroke(50);
} }
line(ball1.x, ball1.y, ball2.x, ball2.y); line(balls[i].x, balls[i].y, balls[j].x, balls[j].y);
}
} }
} }
} }

74
src/static/js/responsive.js Executable file → Normal file
View File

@@ -1,9 +1,35 @@
function toggleMenu(collapse=false) { window.onload = function () {
onLoaded();
};
function onLoaded() {
window.onresize = function () {
resizer();
};
resizer();
}
function resizer() {
const e = document.querySelector(".navControl");
if (window.innerWidth > 1400) {
// desktop view
e.style.maxHeight = `${e.scrollHeight + 10}px`;
} else {
// mobile view
document.querySelector(".header").style.borderBottomWidth = "3px";
e.style.maxHeight = "0px";
document.querySelectorAll(".navElement *").forEach((x) => {
x.style.paddingTop = ".3rem";
x.style.paddingBottom = ".3rem";
x.style.fontSize = "1rem";
});
}
}
function toggleMenu() {
if (window.innerWidth < 1400) { if (window.innerWidth < 1400) {
const e = document.querySelector(".navControl"); const e = document.querySelector(".navControl");
const bar = document.querySelector(".header"); const bar = document.querySelector(".header");
const isCollapsed = !e.style.maxHeight || e.style.maxHeight === "0px"; if (e.style.maxHeight === "0px") {
if (isCollapsed && !collapse) {
e.style.maxHeight = `${e.scrollHeight + 10}px`; e.style.maxHeight = `${e.scrollHeight + 10}px`;
bar.style.borderBottomWidth = "0px"; bar.style.borderBottomWidth = "0px";
} else { } else {
@@ -13,48 +39,26 @@ function toggleMenu(collapse=false) {
} }
} }
async function goto(location, { push = true } = {}) { async function goto(location, { push = true, toggle = true } = {}) {
let a; let a = await fetch("/api/goto/" + location, {
try {
a = await fetch("/api/goto/" + location, {
credentials: "include", credentials: "include",
method: "GET", method: "GET",
mode: "cors", 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();
if (!location.includes("#")) {
window.scrollTo({top: 0, left: 0, behavior:"instant"});
}
const metadata = response[0]; const metadata = response[0];
const content = response[1]; const content = response[1];
const root = document.getElementById("root"); let root = document.getElementById("root");
root.innerHTML = content; root.innerHTML = content;
root.querySelectorAll("script").forEach((oldScript) => { root.querySelectorAll("script").forEach((x) => {
const newScript = document.createElement("script"); eval(x.innerHTML);
Array.from(oldScript.attributes).forEach(attr => {
newScript.setAttribute(attr.name, attr.value);
}); });
newScript.textContent = oldScript.textContent; if (toggle) {
oldScript.parentNode.replaceChild(newScript, oldScript); toggleMenu();
});
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"]; document.querySelector("title").textContent = metadata["title"];
if (push) { if (push) {
history.pushState(null, null, metadata["canonical"]); history.pushState(null, null, metadata["canonical"]);

View File

@@ -1,264 +0,0 @@
// Fetch and display service status from API
/**
* Fetch status data from server
*/
async function fetchStatus() {
try {
const response = await fetch('/api/status');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
updateStatusDisplay(data);
} catch (error) {
console.error('Error fetching status:', error);
showError('Failed to fetch service status. Please try again later.');
}
}
/**
* 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}`;
}
// 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}`;
}
}
// Update each service
data.services.forEach(service => {
updateServiceCard(service);
});
// Update overall status
updateOverallStatus(data.services);
// Re-enable refresh button
const refreshBtn = document.getElementById('refreshBtn');
if (refreshBtn) {
refreshBtn.disabled = false;
refreshBtn.textContent = 'Refresh Now';
}
}
/**
* Update a single service card
*/
function updateServiceCard(service) {
const card = document.getElementById(`status-${service.id}`);
if (!card) return;
const stateDot = card.querySelector('.state-dot');
const stateText = card.querySelector('.state-text');
const timeDisplay = document.getElementById(`time-${service.id}`);
const codeDisplay = document.getElementById(`code-${service.id}`);
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 = '--';
}
// 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) {
case 'online':
stateDot.className = 'state-dot online';
stateText.textContent = 'Operational';
card.classList.add('online');
break;
case 'degraded':
case 'timeout':
stateDot.className = 'state-dot degraded';
stateText.textContent = service.status === 'timeout' ? 'Timeout' : 'Degraded';
card.classList.add('degraded');
break;
case 'offline':
stateDot.className = 'state-dot offline';
stateText.textContent = 'Offline';
card.classList.add('offline');
break;
default:
stateDot.className = 'state-dot loading';
stateText.textContent = 'Unknown';
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(' | ');
}
// 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');
const title = overallBar.querySelector('.summary-title');
const subtitle = document.getElementById('summary-subtitle');
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;
// 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');
// Determine overall status
if (online === total) {
// All systems operational
overallBar.classList.add('online');
icon.classList.add('operational');
icon.textContent = '\u2713';
title.textContent = 'All Systems Operational';
subtitle.textContent = `All ${total} services are running normally`;
} else if (offline >= Math.ceil(total / 2)) {
// Major outage (50% or more offline)
overallBar.classList.add('offline');
icon.classList.add('major');
icon.textContent = '\u2715';
title.textContent = 'Major Outage';
subtitle.textContent = `${offline} service${offline !== 1 ? 's' : ''} offline, ${degraded} degraded`;
} else if (offline > 0 || degraded > 0) {
// Partial outage
overallBar.classList.add('degraded');
icon.classList.add('partial');
icon.textContent = '\u26A0';
title.textContent = 'Partial Outage';
if (offline > 0 && degraded > 0) {
subtitle.textContent = `${offline} offline, ${degraded} degraded`;
} else if (offline > 0) {
subtitle.textContent = `${offline} service${offline !== 1 ? 's' : ''} offline`;
} else {
subtitle.textContent = `${degraded} service${degraded !== 1 ? 's' : ''} degraded`;
}
} else {
// Unknown state
icon.classList.add('loading');
icon.textContent = '\u25D0';
title.textContent = 'Status Unknown';
subtitle.textContent = 'Waiting for service data';
}
}
/**
* Show error message
*/
function showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'status-error';
errorDiv.textContent = message;
errorDiv.style.cssText = 'background: rgba(244, 67, 54, 0.2); color: #f44336; padding: 1em; margin: 1em 0; border-radius: 0.5em; text-align: center;';
const container = document.querySelector('.foregroundContent');
if (container) {
container.insertBefore(errorDiv, container.firstChild);
setTimeout(() => errorDiv.remove(), 5000);
}
}
/**
* Manual refresh
*/
function refreshStatus() {
const refreshBtn = document.getElementById('refreshBtn');
if (refreshBtn) {
refreshBtn.disabled = true;
refreshBtn.textContent = 'Checking...';
}
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 5 minutes to get latest data
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);
} else {
initStatusPage();
}

40
src/static/json/books.json Executable file → Normal file
View File

@@ -1,19 +1,11 @@
{ {
"selection": [ "selection": [
"The Rational Optimist", "The Rational Optimist",
"The End of the World is Just the Beginning",
"When to Rob a Bank",
"Freakonomics",
"The Accidental Superpower", "The Accidental Superpower",
"Verbal Judo", "Freakonomics",
"Zero To One" "Zero To One"
], ],
"books": { "books": {
"Fooled By Randomness": {
"filename": "fooledbyrandomness.jpg",
"link": "https://www.amazon.com/Fooled-Randomness-Hidden-Chance-Markets-dp-B006Q7VYC4/dp/B006Q7VYC4/ref=dp_ob_title_bk",
"review": "A lengthy compendium on probabilistic reasoning that helped kick off a curiosity of indefinite computation. There's more ancient philosophy than a book like this really needs but the occasional brazen punchline from the contemporary anecdotes make it bearable."
},
"The Rational Optimist": { "The Rational Optimist": {
"filename": "ratOpt.jpg", "filename": "ratOpt.jpg",
"link": "https://www.amazon.com/Rational-Optimist-Prosperity-Evolves-P-s/dp/0061452068", "link": "https://www.amazon.com/Rational-Optimist-Prosperity-Evolves-P-s/dp/0061452068",
@@ -39,11 +31,6 @@
"link": "https://freakonomics.com/books/", "link": "https://freakonomics.com/books/",
"review": "More like the other Freakonomics books than I expected (cracked storytelling), which is still excellent, but I wished there was greater insights into seeing past conventional wisdom, which is what thinking like a freak means. Still a great book." "review": "More like the other Freakonomics books than I expected (cracked storytelling), which is still excellent, but I wished there was greater insights into seeing past conventional wisdom, which is what thinking like a freak means. Still a great book."
}, },
"The Tyranny of Metrics": {
"filename": "TyrannyOfMetrics.jpg",
"link": "https://www.amazon.com/Tyranny-Metrics-Jerry-Z-Muller/dp/0691174954",
"review": "Library find. Very appreciated read given my field of study. Adds a new lens on the cost of information and how it impacts us from the cube office to the oval office."
},
"The Accidental Superpower": { "The Accidental Superpower": {
"filename": "theAccidentalSuperpower.jpeg", "filename": "theAccidentalSuperpower.jpeg",
"link": "https://zeihan.com/", "link": "https://zeihan.com/",
@@ -79,11 +66,6 @@
"link": "https://www.amazon.com/Give-Me-Break-Exposed-Hucksters-ebook/dp/B000FC2NF8/", "link": "https://www.amazon.com/Give-Me-Break-Exposed-Hucksters-ebook/dp/B000FC2NF8/",
"review": "I expected a boring autobiography-type book, but instead is a glimpse inside Stossel's work that transformed itself as it transformed his view. Was very happy to see a figure of similar personal ideology. Probably made it a little too easy to swallow that pill." "review": "I expected a boring autobiography-type book, but instead is a glimpse inside Stossel's work that transformed itself as it transformed his view. Was very happy to see a figure of similar personal ideology. Probably made it a little too easy to swallow that pill."
}, },
"Reign of Terror": {
"filename": "reignofterror.jpg",
"link": "https://www.amazon.com/Reign-Terror-Destabilized-America-Produced/dp/1984879774",
"review": "Packed with real news events and first person accounts, Reign of Terror chronicles the story of politics and intelligence agencies during the War on Terror. In typical journalist fashion, the populist (read: racist) cause for the events is mostly conjecture to fit a progressive narrative. Nonetheless, a comprehensive history of malpractice in public office."
},
"Zero To One": { "Zero To One": {
"filename": "zeroToOne.jpeg", "filename": "zeroToOne.jpeg",
"link": "https://www.amazon.com/Zero-One-Notes-Startups-Future/dp/0804139296", "link": "https://www.amazon.com/Zero-One-Notes-Startups-Future/dp/0804139296",
@@ -99,26 +81,21 @@
"link": "https://www.amazon.com/Discipline-Destiny-Power-Self-Control-Virtues/dp/0593191692", "link": "https://www.amazon.com/Discipline-Destiny-Power-Self-Control-Virtues/dp/0593191692",
"review": "Much like the first in its series - small chapters (very helpful) each with inspiring insider stories of figures of history. Anyone capable of learning from these figures would benefit greatly from implementing the virtues in this series." "review": "Much like the first in its series - small chapters (very helpful) each with inspiring insider stories of figures of history. Anyone capable of learning from these figures would benefit greatly from implementing the virtues in this series."
}, },
"Right Thing, Right Now": {
"filename": "rightthingrightnow.png",
"link": "https://www.amazon.com/Right-Thing-Now-Values-Character/dp/0593191714",
"review": "As the third in its series, the virtue of justice derives a large portion of its meaning from the previous two. While still an good read with a valuable influence for personal growth, it lacks a distinction between justice as a virtue and fighting for the right cause. Some sections preach for ideological purity while others insist on pragmatism which is a pretty important detail regarding justice."
},
"On Grand Strategy": { "On Grand Strategy": {
"filename": "onGrandStrategy.jpeg", "filename": "onGrandStrategy.jpeg",
"link": "https://www.amazon.com/Grand-Strategy-John-Lewis-Gaddis/dp/1594203512", "link": "https://www.amazon.com/Grand-Strategy-John-Lewis-Gaddis/dp/1594203512",
"review": "Book for the academically-inclined. Not fun to read. Big words scary. It's insightful to be sure but I wouldn't read it again. The message on conceptual contradictions has stuck with me. Quite the brain food." "review": "Book for the academically-inclined. Not fun to read. Big words scary. It's insightful to be sure but I wouldn't read it again. The message on conceptual contradictions has stuck with me. Quite the brain food."
}, },
"The Parasitic Mind": {
"filename": "theParasiticMind.jpeg",
"link": "https://www.amazon.com/Parasitic-Mind-Infectious-Killing-Common/dp/1684512298/",
"review": "The humor is the most memorable part but the concepts are no slouches. The contemporary culture war basis makes it tricky to talk about, but it absolutely should be discussed."
},
"David and Goliath": { "David and Goliath": {
"filename": "davidAndGoliath.png", "filename": "davidAndGoliath.png",
"link": "https://www.amazon.com/David-Goliath-Underdogs-Misfits-Battling/dp/0316239852/", "link": "https://www.amazon.com/David-Goliath-Underdogs-Misfits-Battling/dp/0316239852/",
"review": "Book contains takes that may not be hot, but *are* incredibly based. In a sentence: Goliath is only the giant from the wrong perspectives. The only reason it's not one of my favorites is that it's tamer than the aggressively standoffish and hilarious." "review": "Book contains takes that may not be hot, but *are* incredibly based. In a sentence: Goliath is only the giant from the wrong perspectives. The only reason it's not one of my favorites is that it's tamer than the aggressively standoffish and hilarious."
}, },
"The Scout Mindset": {
"filename": "scoutMindset.png",
"link": "https://www.amazon.com/Scout-Mindset-People-Things-Clearly-ebook/dp/B07L2HQ26K/",
"review": "Felt like a list of things that I already do that I should be more mindful of. Maybe that's just me. There was some interesting mental probablism sprinkled in the first half but the second half did not have much new to say. Good but not eye-opening."
},
"Verbal Judo": { "Verbal Judo": {
"filename": "verbalJudo.png", "filename": "verbalJudo.png",
"link": "https://www.amazon.com/Verbal-Judo-Second-Gentle-Persuasion-ebook/dp/B00FJ3CMI6/", "link": "https://www.amazon.com/Verbal-Judo-Second-Gentle-Persuasion-ebook/dp/B00FJ3CMI6/",
@@ -129,11 +106,6 @@
"link": "https://www.amazon.com/YOU-READ-ANYONE-David-Lieberman-ebook/dp/B001J6OV0Y", "link": "https://www.amazon.com/YOU-READ-ANYONE-David-Lieberman-ebook/dp/B001J6OV0Y",
"review": "Not as page-turning as many of the others and clearly not as memorable. The techniques pique curiosity but are difficult to use without practice." "review": "Not as page-turning as many of the others and clearly not as memorable. The techniques pique curiosity but are difficult to use without practice."
}, },
"The Parasitic Mind": {
"filename": "theParasiticMind.jpeg",
"link": "https://www.amazon.com/Parasitic-Mind-Infectious-Killing-Common/dp/1684512298/",
"review": "The humor is the most memorable part but the concepts are no slouches. The contemporary culture war basis makes it tricky to talk about, but it absolutely should be discussed."
},
"Profiles in Courage": { "Profiles in Courage": {
"filename": "profilesInCourage.jpeg", "filename": "profilesInCourage.jpeg",
"link": "https://www.amazon.com/Profiles-Courage-John-F-Kennedy/dp/0060854936", "link": "https://www.amazon.com/Profiles-Courage-John-F-Kennedy/dp/0060854936",

18
src/static/json/pages.json Executable file → Normal file
View File

@@ -5,18 +5,18 @@
"description": "Andrew Simonson's Digital Portfolio home", "description": "Andrew Simonson's Digital Portfolio home",
"canonical": "/" "canonical": "/"
}, },
"status": {
"template": "status.html",
"title":"Andrew Simonson - Status Page",
"description": "Status page for my services",
"canonical": "/status"
},
"projects": { "projects": {
"template": "projects.html", "template": "projects.html",
"title": "Andrew Simonson - Projects", "title": "Andrew Simonson - Projects",
"description": "Recent projects by Andrew Simonson on his lovely portfolio website :)", "description": "Recent projects by Andrew Simonson on his lovely portfolio website :)",
"canonical": "/projects" "canonical": "/projects"
}, },
"about": {
"template": "about.html",
"title": "Andrew Simonson - About Me",
"description": "About Andrew Simonson",
"canonical": "/about"
},
"books": { "books": {
"template": "books.html", "template": "books.html",
"title": "Andrew Simonson - Bookshelf", "title": "Andrew Simonson - Bookshelf",
@@ -28,11 +28,5 @@
"title":"You've been ducked!", "title":"You've been ducked!",
"description": "Face it, you've been ducked", "description": "Face it, you've been ducked",
"canonical": "/duck" "canonical": "/duck"
},
"certificates": {
"template": "certs.html",
"title": "Certificates and Awards",
"description": "Certificates and Awards Listing",
"canonical": "/certs"
} }
} }

139
src/static/json/projects.json Executable file → Normal file
View File

@@ -1,65 +1,81 @@
{ {
"Antietam-Conococheague Watershed Monitoring": {
"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"
},
"Automotive Brand Valuation Analysis": {
"status": "complete",
"classes": "programming",
"bgi": "automotiveBrandAnalysis.png",
"content": "Brand valuation analysis of the used car market, measuring value decay by mileage to extrapolate qualities such as percieved reliability and persistent value of luxury features."
},
"RIT Hotspots": { "RIT Hotspots": {
"status": "incomplete", "status": "WIP",
"classes": "pinned geospacial programming", "classes": "pinned geospacial programming",
"bgi": "hotspotsrit.png", "bgi": "hotspotsrit.png",
"content": "Live crowd migration map using RIT occupancy data. It seems RIT didn't like me exposing their surveilance state but since they didn't want to talk to me about it they instead changed the service response schema a few times. When that didn't stop me they just shut down the whole service. Nerds.", "content": "Live crowd migration map using RIT occupancy data",
"links": [ "links": [
["github", "https://github.com/asimonson1125/hotspotsrit", "git repo"] [
"github", "https://github.com/asimonson1125/hotspotsrit", "git repo"
],
[
"globe", "https://asimonson.com/hotspots", "demo"
]
] ]
}, },
"Calorimetry Analysis Engineering": { "LogicFlow": {
"status": "complete", "status": "incomplete",
"classes": "pinned programming", "classes": "programming",
"bgi": "calorimeterAnalysis.png", "bgi": "logicflow.jpg",
"content": "An analytical toolkit designed for reactive chemistry analysis, especially calorimetry. Works include automatic analysis, alerting unusual and dangerous results derived from a wide range of testing envrionments and equipment", "content": "Translate paragraphs to logical flowcharts, powered by ChatGPT Winner of CSHacks' Best Use of AI by Paychex",
"links": [] "links": [
[
"github", "https://github.com/asimonson1125/LogicFlow", "git repo"
],
[
"globe", "https://devpost.com/software/logicflow", "Hackathon listing"
]
]
}, },
"Geography of Alternative Energy": { "Alternative Energy Map": {
"status": "complete", "status": "complete",
"classes": "pinned geospacial", "classes": "pinned geospacial",
"bgi": "energyGeography.png", "bgi": "geovisF.png",
"content": "An ArcGIS geospacial analysis comparing the difference in effectiveness of wind, solar, and geothermal energy across the continental 48 United States.", "content": "ArcGIS Map of the most effective alternative energy sources in the continental United States",
"links": [ "links": [
[ [
"globe", "globe",
"https://ritarcgis.maps.arcgis.com/apps/dashboards/17d5bda01edc4a2eb6205a4922d889c9", "https://ritarcgis.maps.arcgis.com/apps/dashboards/17d5bda01edc4a2eb6205a4922d889c9",
"Dashboard" "ArcGIS"
] ]
] ]
}, },
"OccupyRIT": { "OccupyRIT": {
"status": "complete", "status": "WIP",
"classes": "pinned programming", "classes": "programming",
"bgi": "occupyRIT.png", "bgi": "occupyRIT.png",
"content": "Collects RIT Gym Occupancy data, determining busiest workout times", "content": "Collects RIT Gym Occupancy data, determining busiest workout times",
"links": [ "links": [
["github", "https://github.com/asimonson1125/Occupy-RIT", "git repo"] ["github", "https://github.com/asimonson1125/Occupy-RIT", "git repo"]
] ]
}, },
"Portfolio Website": { "Chesscom Embeds": {
"status": "complete",
"classes": "programming",
"bgi": "chessbed.png",
"content": "A template for creating Chess.com user profile embeds",
"links": [
["github", "https://github.com/asimonson1125/chesscom-embed", "git repo"]
]
},
"Resume": {
"status": "WIP", "status": "WIP",
"classes": "programming", "classes": "programming",
"content": "This website is my personal sandbox where I've integrated some of my data projects via docker cluster. It is self hosted and zero-trust secure while remaining dynamic and free of the tech debt that comes with pre-designed sites and excessive framework application. Yeah, I can do E2E.", "bgi": "resume.png",
"content": "My Resume, made in LaTeX with a custom design derived by the AltaCV template on OverLeaf",
"links": [ "links": [
["globe", "https://asimonson.com", "Homepage"], ["github", "https://github.com/asimonson1125/Resume", "git repo"],
[ ["globe", "https://asimonson.com/Resume.pdf", "Resume"]
"github",
"https://github.com/asimonson1125/asimonson1125.github.io",
"git repo"
] ]
},
"Digital Portfolio": {
"status": "WIP",
"classes": "programming",
"bgi": "website.png",
"content": "My personal portfolio website (you're on it now!)",
"links": [
["github", "https://github.com/asimonson1125/asimonson1125.github.io", "git repo"],
["globe", "https://asimonson.com", "site link"]
] ]
}, },
"Slate": { "Slate": {
@@ -68,21 +84,62 @@
"bgi": "slate.png", "bgi": "slate.png",
"content": "Slate is a web app designed to help event coordinators schedule events by congregating participant calendar data. Includes Computer Science House account integration", "content": "Slate is a web app designed to help event coordinators schedule events by congregating participant calendar data. Includes Computer Science House account integration",
"links": [ "links": [
["globe", "https://slate.csh.rit.edu", "site link"], ["github", "https://github.com/asimonson1125/Slate", "git repo"],
["github", "https://github.com/asimonson1125/Slate", "git repo"] ["globe", "https://slate.csh.rit.edu", "site link"]
] ]
}, },
"Monte Carlo Engine for NationsGame": { "HvZ Bot": {
"status": "complete",
"classes": "programming",
"bgi": "",
"content": "A Discord bot to handle role management and statistics for RIT's Humans vs. Zombies games",
"links": [
["github", "https://github.com/asimonson1125/HvZ-bot", "git repo"]
]
},
"FinTech": {
"status": "WIP",
"classes": "pinned programming",
"bgi": "",
"content": "A team derived from the RIT Financial Management Association dedicated to learning about financial management of equities using automated solutions developed by students",
"links": [
["github", "https://github.com/LukeHorigan/Financial-Management-Assocation-", "git repo"]
]
},
"Browser Trivia Bot": {
"status": "complete",
"classes": "programming",
"bgi": "",
"content": "A tampermonkey tool used to automatically answer and submit online trivia forms, which can be tailored to different site layouts. Source currently private.",
"links": [
]
},
"NationsGame Rolls Sim": {
"status": "complete", "status": "complete",
"classes": "programming", "classes": "programming",
"bgi": "ceoOfYugo.png", "bgi": "ceoOfYugo.png",
"content": "A simulator for the browser game, NationsGame, to analyze unit composition and predict in-game victors and unit statistics. Unfortunately, NationsGame is now defunct. Limited screenshots of functionality.", "content": "A simulator for the browser game, NationsGame, to analyze unit composition and predict in-game victors and unit statistics. Unfortunately, NationsGame is now defunct. Limited screenshots of functionality.",
"links": [ "links": [
[ ["github", "https://github.com/asimonson1125/NG-Rolls-Simulator", "git repo"]
"github",
"https://github.com/asimonson1125/NG-Rolls-Simulator",
"git repo"
] ]
},
"VEXcode Button Engine": {
"status": "complete",
"classes": "programming",
"bgi": "vexcodeButtons.jpeg",
"content": "VEXcode button library + examples and template for the VEX V5 brain",
"links": [
["github", "https://github.com/asimonson1125/VEXcode-Button-Generator", "git repo"],
["globe", "https://www.vexforum.com/t/vexcode-button-generator/72450", "Forum post"]
]
},
"WinKeylogger": {
"status": "complete",
"classes": "programming",
"bgi": "",
"content": "A C++ keylogger for windows based off a Udemy course with my custom modifications and powershell script",
"links": [
["github", "https://github.com/asimonson1125/WinKeylogger", "git repo"]
] ]
} }
} }

25
src/static/json/skills.json Executable file → Normal file
View File

@@ -1,21 +1,9 @@
{ {
"Tools": {
"Microsoft Azure": {
"Databricks": {},
"Data Factory": {},
"Stream Analytics": {}
},
"Databricks": {},
"Apache Spark": {},
"Visual Basic for Applications (Excel)": {}
},
"Data and AI": { "Data and AI": {
"Python": { "Python": {
"PyTorch/TensorFlow": {}, "PyTorch/TensorFlow": {},
"Numpy/Pandas": {}, "Numpy/Pandas": {},
"Scikit/Sklearn": {}, "Selenium/BS4": {}
"Selenium/BS4": {},
"Pyspark": {}
}, },
"R": {}, "R": {},
"SQL": {} "SQL": {}
@@ -23,19 +11,16 @@
"Frontend": { "Frontend": {
"Flask (Python)": {}, "Flask (Python)": {},
"React (Javascript)": {}, "React (Javascript)": {},
"SASS/SCSS": {} "Angular (Typescript)": {}
}, },
"Backend & DevOps": { "Backend & DevOps": {
"Backend": {
"Rust": {},
"C#": {}
},
"DevOps": { "DevOps": {
"Docker": {}, "Docker": {},
"Microsoft Azure": {}, "Microsoft Azure": {},
"Kubernetes/Openshift": {}, "Kubernetes/Openshift": {},
"Cloudflare": {},
"Bash": {} "Bash": {}
} },
"C#": {},
"C++": {}
} }
} }

0
src/static/json/timeline.json Executable file → Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 B

0
src/static/photos/books/12RulesForLife.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

0
src/static/photos/books/BeyondOrder.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

0
src/static/photos/books/autobioOfJefferson.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

0
src/static/photos/books/courageIsCalling.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

0
src/static/photos/books/davidAndGoliath.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

0
src/static/photos/books/disciplineIsDestiny.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

0
src/static/photos/books/disunitedNations.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

0
src/static/photos/books/freakonomics.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

0
src/static/photos/books/giveMeABreak.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

0
src/static/photos/books/makeYourBed.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

0
src/static/photos/books/no-they-cant.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 308 KiB

After

Width:  |  Height:  |  Size: 308 KiB

0
src/static/photos/books/onGrandStrategy.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

0
src/static/photos/books/profilesInCourage.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 144 KiB

0
src/static/photos/books/ratOpt.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

0
src/static/photos/books/superfreakonomics.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 235 KiB

After

Width:  |  Height:  |  Size: 235 KiB

0
src/static/photos/books/theAbsentSuperpower.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

0
src/static/photos/books/theAccidentalSuperpower.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

0
src/static/photos/books/theParasiticMind.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

0
src/static/photos/books/theStormBeforeTheCalm.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

0
src/static/photos/books/thinkLikeAFreak.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

0
src/static/photos/books/verbalJudo.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 201 KiB

0
src/static/photos/books/whenToRobABank.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

0
src/static/photos/books/where-good-ideas-come-from.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 184 KiB

0
src/static/photos/books/youCanReadAnyone.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

0
src/static/photos/books/zeroToOne.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

0
src/static/photos/gifs/duck-spinning.gif Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 936 KiB

After

Width:  |  Height:  |  Size: 936 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Some files were not shown because too many files have changed in this diff Show More