Compare commits

138 Commits

Author SHA1 Message Date
44948a6e9f clean up clean up everybody do your share 2026-02-17 20:42:52 -06:00
3f0f9907ed projects restyle and timeline deprecation 2026-02-17 14:55:20 -06:00
7b969ea6c2 css parameters 2026-02-17 00:44:15 -06:00
085ade75bf backmerge 2026-02-13 12:25:21 -06:00
dae0882e0f min checks bug 2026-02-13 11:29:15 -06:00
d0f50141c7 update status intervals 2026-02-13 11:25:02 -06:00
b59842899b replace status history with db 2026-02-12 21:00:47 -06:00
9553e77b2f summary 2026-02-12 18:46:00 -06:00
2ae714db48 misc 2026-02-12 17:57:30 -06:00
efaf2fb169 static file fingerprinting
used to clear caches when static files are changed
2026-02-12 14:06:43 -06:00
dd6eeb6e40 Merge pull request #36 from asimonson1125/claude/review-architecture-hxQz0
improve css practices
2026-02-12 14:36:39 -05:00
00f8d707d8 improve css practices 2026-02-12 13:35:24 -06:00
cf0f66452d Merge pull request #35 from asimonson1125/claude/review-architecture-hxQz0
Claude/review architecture hx qz0
2026-02-12 10:15:35 -05:00
5e2acb3ae9 restore unused generic css 2026-02-12 09:15:11 -06:00
Claude
e66b675979 fix statusIntervalId redeclaration error on SPA re-navigation
Use var instead of let for statusIntervalId so the script can be
re-executed by the SPA script-cloning logic without throwing a
redeclaration error when navigating back to the status page.

https://claude.ai/code/session_01FUhPqQLahEoL6FMxhXkDKa
2026-02-12 14:49:01 +00:00
Claude
9847d6422e revert chessbed.js to non-deferred to fix addChessEmbed race
chessbed.js cannot be deferred because chess.html's inline script
calls addChessEmbed() immediately, which requires the function to
already be defined. Reverting to the original non-deferred load.

https://claude.ai/code/session_01FUhPqQLahEoL6FMxhXkDKa
2026-02-12 14:45:03 +00:00
Claude
c8b1f124f2 fix architectural issues: SPA nav, error handling, CSS bugs, perf
- responsive.js: fix scroll race condition (scroll after innerHTML),
  add error handling for fetch failures, fix implicit global `eid`,
  dispatch `beforenavigate` event for cleanup hooks
- chessbed.js: fix implicit global `ratings` variable
- status.js: clear polling interval on SPA navigation via
  `beforenavigate` event to prevent leak
- App.css: add font-display:swap to all @font-face, fix broken
  media query (missing px unit), consolidate duplicate selectors
  (.concentratedHead, .relative, strong), fix hardcoded bookshelf
  background-image path to use relative URL
- header.html: defer chessbed.js, use p5.min.js instead of p5.js
- monitor.py: use ThreadPoolExecutor for concurrent service checks
- config.py: fix __import__('envs.py') → __import__('envs')
- app.py: rename misleading error handlers (page404→handle_http_error,
  page500→handle_generic_error), fix error info leakage by not passing
  raw exception to InternalServerError, fix hardcoded canonical "404"

https://claude.ai/code/session_01FUhPqQLahEoL6FMxhXkDKa
2026-02-12 14:39:43 +00:00
9b6e29a15c silent bugs 2026-02-12 00:01:40 -06:00
b55a96c51c finalize claudilization 2026-02-11 23:19:58 -06:00
800f42c9bb css reduction 2026-02-11 17:42:01 -06:00
a7635c62d3 productionalizing? 2026-02-11 17:12:00 -06:00
99bf0f6c5f test stylistic choices 2026-02-11 15:15:32 -06:00
b1e75bd91f sample status page 2026-02-11 14:54:45 -06:00
46fc66971d fix huge project images on mobile 2026-02-11 13:08:52 -06:00
d54aa6009a code review 2026-02-11 12:58:23 -06:00
68e9facdc7 clear up 2026-02-01 11:27:08 -06:00
0b311678c6 fun. 2026-01-30 22:22:08 -06:00
652ac886a4 edX certs 2026-01-30 21:59:04 -06:00
be055ae98c lmao 2025-12-15 22:05:24 -06:00
b1f94b990c deprecate hotspots RIT 2025-12-09 19:40:40 -06:00
2f90b03e6b load-bearing coconut 2025-12-09 10:51:46 -06:00
e9abe472ff bizzare non-added file 2025-08-24 16:42:44 -05:00
18c291d37a performance enhancers 2025-08-24 16:35:23 -05:00
a6aa73bb21 fix naming convention 2025-08-03 21:23:21 -05:00
fba1eab648 remove references to old filesystem management 2025-08-03 18:25:08 -05:00
c142b4da3d update file storage method 2025-08-03 18:23:58 -05:00
fdc5b12a61 temp finish 2025-07-26 14:25:22 -05:00
34e5512a8d start status page 2025-07-17 19:31:19 -05:00
9bc0eb1ce5 move from deprecated buster 2025-07-14 18:24:29 -05:00
1721a2d885 inserting too many gifs 2025-07-14 18:11:58 -05:00
850e27a8e2 homepage content purge 2025-05-09 15:49:17 -05:00
f437dd4271 localize chess font 2025-05-09 15:43:32 -05:00
ea918ff778 Merge branch 'master' of https://github.com/asimonson1125/asimonson1125.github.io 2025-05-09 15:35:44 -05:00
986a30735a blanks? 2025-05-09 15:35:42 -05:00
1b17f0bf60 shorter and sweeter about me 2025-02-04 19:45:47 -06:00
ed0d715b18 self-host ready 2025-01-25 13:07:05 -06:00
f18b57bc0d resume reroute 2024-12-29 20:51:43 -05:00
a2c05e5c97 oh yeah I have a job 2024-12-29 20:22:19 -05:00
a6fb8ab43a vibe check 2024-12-29 20:15:07 -05:00
2bf7c6837b update hotspots url in header 2024-10-20 22:30:29 -04:00
c19496f3da Merge pull request #32 from asimonson1125/nginxless
self hosting setup
2024-10-20 20:02:56 -05:00
80bfaca041 whoopsies 2024-10-20 21:01:32 -04:00
0da1c405d1 basic filesystem explorer 2024-10-17 16:07:44 -04:00
45e1967b74 freaky hotspots 2024-10-13 20:17:10 -04:00
13c1eac9b7 composing 2024-10-13 17:41:54 -04:00
c023481303 mounting from env 2024-10-13 17:41:21 -04:00
53956a0be1 replace deb dockerfile 2024-10-08 13:00:37 -04:00
6b274dea52 certs cleanup, first step filesystem mount 2024-09-29 18:20:07 -04:00
3e0b269f57 funny bug hehe 2024-09-08 19:06:02 -04:00
1113658d9b idk memeing i guess 2024-09-08 18:58:40 -04:00
11c96416e8 Review: Reign of Terror 2024-08-20 14:50:16 -05:00
1e69486ae5 separate hashchange, goto events 2024-07-28 15:12:55 -05:00
7b0e58d8bd book reviews redo 2024-07-28 14:51:06 -05:00
55d8f22816 sitemap 2024-07-24 15:02:22 -05:00
912ed8fae2 adding curated books 2024-07-24 15:02:09 -05:00
4f833202d1 remove websocket support 2024-07-20 18:47:20 -05:00
8b8163399c update deps 2024-07-20 18:39:49 -05:00
3f47565426 content update 2024-07-20 18:36:46 -05:00
abd414f692 scout book review 2024-06-29 17:24:48 -05:00
11c884be63 Just general pain.
But Project rewrite is technically done now.  Will look wrong on big monitors but idgaf anymore
2024-06-19 20:24:45 -05:00
719d99e2c0 Merge pull request #31 from asimonson1125/certificates-page
strava embed
2024-06-12 19:57:58 -04:00
06e072b050 strava embed 2024-06-12 18:57:33 -05:00
689c523e0f Merge pull request #30 from asimonson1125/certificates-page
quick and easy certs page
2024-06-03 18:48:05 -04:00
7486949ff9 quick and easy certs page 2024-06-03 17:47:24 -05:00
1bfa26a7d6 Merge pull request #29 from asimonson1125/projects-overhaul
Projects overhaul
2024-06-03 18:16:23 -04:00
233208c910 big frickin project commit 2024-06-03 17:15:52 -05:00
cb27948649 Merge pull request #28 from asimonson1125/master
catch up projects branch
2024-05-19 14:31:42 -04:00
2981d4ccc2 Merge pull request #26 from asimonson1125/home-about-redo
Home about redo
2024-05-19 12:46:03 -04:00
d27f874483 update skills / skills init 2024-05-19 11:45:31 -05:00
2cb1368440 replace about page with homepage content 2024-05-19 11:16:05 -05:00
7744786431 text update 2024-05-18 12:58:57 -05:00
f679f970f7 just a whole lot of things 2024-05-05 15:12:52 -05:00
391d5a1768 Learned a fat lesson about sticky and floats 2024-05-04 19:06:11 -05:00
c92cbf8abc tyranny of metrics 2024-05-04 11:58:13 -05:00
44f9b4eb74 Removing stuff I don't like 2024-04-19 15:18:51 -05:00
1ceb172c94 pre-fall resume update 2024-04-19 15:04:02 -05:00
75d2e46578 url UX(?) improvement
the no argument version of /hotspots should include the legend because it's expected to be used as a dashboard :)
2024-04-15 08:31:49 -05:00
d022436a6e handle hotspots legend parameter 2024-04-14 11:35:32 -05:00
7ae923d58f Move hotspots to separate service 2024-04-14 11:16:04 -05:00
6dbb44f5c0 iOS logo compatability 2024-03-05 18:51:04 -06:00
8e3046c4fb h 2024-03-04 11:58:08 -06:00
0dbdea5800 upload 2024-02-28 11:01:17 -06:00
c499b2a7ce fixed invisible overflow glitch 2024-02-26 15:45:03 -06:00
62526a5ff4 just a whole lot of things 2024-02-26 13:53:06 -06:00
dd4eb883d2 Obsessive error handling 2024-02-26 10:25:25 -06:00
b19062eb65 Device experiments results 2024-02-25 16:07:29 -06:00
be738e2e65 Navbar update 2024-02-25 15:30:32 -06:00
7f95fdc4ca Logo update 2024-02-25 13:17:18 -06:00
6deb4ee147 rebuild home padding 2024-02-23 15:37:11 -06:00
01ac070b40 Merge branch 'master' of https://github.com/asimonson1125/asimonson1125.github.io 2024-02-23 14:46:56 -06:00
bb0a8fcc54 experimental logo 2024-02-23 14:46:49 -06:00
Andrew Simonson
455bc71fef mac revealed content overlap on small screen, need to test 2024-02-23 09:47:40 -06:00
ed685f0ff7 Merge branch 'master' of https://github.com/asimonson1125/asimonson1125.github.io 2024-02-12 19:22:35 -06:00
aa620d0667 Book Update 2024-02-12 19:22:09 -06:00
d9edde3556 Create README.md 2024-01-30 08:17:42 -06:00
44a902c8f2 update resume for summer search 2024-01-18 17:30:23 -06:00
3904ee18cb Improved Style Dynamics 2023-12-30 12:36:53 -05:00
4cd39b4ace cv reorg 2023-11-27 11:47:45 -05:00
a8b0b04b85 update resume 2023-11-13 14:59:38 -05:00
b5ff8d7fef fix trail bug 2023-11-02 11:21:34 -04:00
befe832516 fix legendless bug 2023-11-02 10:48:53 -04:00
8676ae3950 much improved hotspots graphics efficiency 2023-11-02 00:17:04 -04:00
7d146a772b Update hotspots for legend 2023-10-28 15:27:48 -04:00
2d411e8634 scroll patch + marginally better home style 2023-10-25 16:41:51 -04:00
5b35a89399 remove csp (safety is for the weak) 2023-10-24 19:23:01 -04:00
3a30986356 cry 2023-10-24 19:15:28 -04:00
095274b76e add to leaflet to csp 2023-10-24 18:54:12 -04:00
4dc1796a69 add reqs to deps 2023-10-24 18:47:44 -04:00
e2cb39d5a8 fuckin gunicorn 2023-10-24 18:21:02 -04:00
2243fc25c4 replace idler and accidentally fix foreground 2023-10-24 18:10:50 -04:00
43d9812406 add hotspotsRIT page 2023-10-24 17:06:48 -04:00
0f116ecaf5 career fair resume update 2023-09-19 11:50:05 -04:00
e82dfbf253 about update 2023-09-03 11:47:08 -04:00
456ecb3e98 Start of semester update 2023-08-28 12:43:20 -04:00
abb505aa57 Add home books 2023-05-11 12:12:00 -04:00
814a70c088 fix book review overlaps 2023-05-11 11:06:13 -04:00
de41b96388 Add more books, update resume 2023-05-02 11:44:47 -05:00
8e8d0a8492 book reviews 2023-04-30 16:43:51 -05:00
06d3642d08 fix for jenk autoscroll on page change 2023-04-24 14:15:20 -05:00
024c962567 misc 2023-04-24 13:50:15 -05:00
938dae2586 bookshelf style 2023-04-24 12:54:51 -05:00
7a38cdaaf5 stylish duck 2023-04-23 21:31:32 -05:00
2925a7c8e9 get ducked! 2023-04-23 21:26:35 -05:00
726ff889c3 ducks spinning 2023-04-23 14:38:51 -05:00
23d6e12768 Become legally insane 2023-04-23 14:17:23 -05:00
02dc26d54b First iteration bookshelf 2023-04-23 14:17:06 -05:00
b82d00fa3a About Me page to grid 2023-04-23 11:12:31 -05:00
eeff278c1b enable gzip 2023-02-19 11:44:58 -06:00
148 changed files with 6700 additions and 958 deletions

15
.dockerignore Normal file → Executable file
View File

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

0
.gitattributes vendored Normal file → Executable file
View File

2
.gitconfig Normal file
View File

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

7
.gitignore vendored Normal file → Executable file
View File

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

26
.vscode/launch.json vendored Executable file
View File

@@ -0,0 +1,26 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Flask",
"type": "debugpy",
"request": "launch",
"cwd": "${workspaceFolder}/src",
"module": "flask",
"env": {
"FLASK_APP": "app.py",
"FLASK_DEBUG": "1",
},
"args": [
"run",
"--no-debugger",
"--no-reload"
],
"jinja": true
}
]
}

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

34
Dockerfile Normal file → Executable file
View File

@@ -1,34 +1,10 @@
FROM ubuntu:lunar
FROM python:3.10-bullseye
LABEL maintainer="Andrew Simonson <asimonson1125@gmail.com>"
ENV DEBIAN_FRONTEND noninteractive
WORKDIR /app
RUN apt-get update
RUN apt-get install -y python3-pip nginx gunicorn supervisor
COPY src/ .
# Setup flask application
RUN mkdir -p /deploy/app
COPY src /deploy/app
RUN pip install -r /deploy/app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# 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"]
CMD [ "gunicorn", "--bind", "0.0.0.0:8080", "app:app"]

11
README.md Executable file
View File

@@ -0,0 +1,11 @@
# I made a uhh website
So people can see how excellent my coding standards are.
* Style: 5/10
* Originality: 3/10
* Security: Yes*
* Viruses: not included
You gotta uhh `pip3 install -r requirements.txt` and `python3 app.py` that thing
Docker compose configured to expose at `localhost:8080`

138
STATUS_MONITOR_README.md Normal file
View File

@@ -0,0 +1,138 @@
# 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
- Calculates uptime percentages for multiple time periods (24h, 7d, 30d, all-time)
- Persists data to PostgreSQL (`service_checks` table) via `DATABASE_URL` env var
- Gracefully degrades when no database is configured (local dev)
- 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
Check history is stored in a PostgreSQL `service_checks` table. The connection is configured via the `DATABASE_URL` environment variable (e.g. `postgresql://user:pass@host:5432/dbname`).
```sql
CREATE TABLE service_checks (
id SERIAL PRIMARY KEY,
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
);
```
The table and index are created automatically on startup. If `DATABASE_URL` is not set, the monitor runs without persistence (useful for local development).
## 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 (stored in PostgreSQL)
- 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)

32
docker-compose.yml Executable file
View File

@@ -0,0 +1,32 @@
services:
portfolio:
image: 'asimonson1125/portfolio'
build:
context: ./
dockerfile: Dockerfile
restart: 'no'
ports:
- 8080:8080
environment:
DATABASE_URL: postgresql://portfolio:portfolio@db:5432/portfolio
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
restart: 'no'
environment:
POSTGRES_USER: portfolio
POSTGRES_PASSWORD: portfolio
POSTGRES_DB: portfolio
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U portfolio"]
interval: 5s
timeout: 3s
retries: 5
volumes:
pgdata:

View File

@@ -1,21 +0,0 @@
server {
listen 8080;
server_name www.asimonson.com;
return 301 https://asimonson.com$request_uri;
}
server {
listen 8080;
server_name asimonson.com;
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' *.cloudflare.com *.chesscomfiles.com *.chess.com *.googletagmanager.com cdn.jsdelivr.net www.google-analytics.com ajax.googleapis.com;";
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;
}
}

View File

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

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

@@ -1,81 +1,162 @@
import hashlib
import json
import os
import flask
from flask_minify import Minify
import json
import werkzeug.exceptions as HTTPerror
proj = json.load(open("./static/json/projects.json", "r"))
timeline = json.load(open("./static/json/timeline.json", "r"))
pages = json.load(open("./static/json/pages.json", "r"))
pages['about']['timeline'] = timeline
pages['projects']['projects'] = proj
import config # noqa: F401 — side-effect: loads dev env vars
from monitor import monitor, SERVICES
app = flask.Flask(__name__)
# ── Static file fingerprinting ────────────────────────────────────────
@app.route('/api/goto/')
@app.route('/api/goto/<location>')
def goto(location='home'):
pagevars = pages[location]
return [pagevars, flask.render_template(pagevars["template"], var=pagevars)]
@app.route("/")
def home():
pagevars = pages["home"]
return flask.render_template("header.html", var=pagevars)
static_file_hashes = {}
for dirpath, _, filenames in os.walk(app.static_folder):
for filename in filenames:
filepath = os.path.join(dirpath, filename)
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.route("/projects")
def projects():
pagevars = pages["projects"]
return flask.render_template("header.html", var=pagevars)
@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)
@app.route("/about")
def about():
pagevars = pages["about"]
return flask.render_template("header.html", var=pagevars)
# ── Security and caching headers ──────────────────────────────────────
@app.after_request
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'
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
@app.route("/resume")
@app.route("/Resume.pdf")
def resume():
return flask.send_file("./static/Resume.pdf")
# ── Load page data ────────────────────────────────────────────────────
def load_json(path):
with open(path, "r") as f:
return json.load(f)
@app.errorhandler(Exception)
def page404(e):
eCode = e.code
message = e.description
try:
message = e.length
finally:
projects = load_json("./static/json/projects.json")
books = load_json("./static/json/books.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'] = 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"{eCode} - Simonson",
"title": f"{code} - Simonson",
"description": "Error on Andrew Simonson's Digital Portfolio",
"canonical": "404",
"canonical": f"/{code}",
}
return (
flask.render_template(
"header.html",
var=pagevars,
error=eCode,
error=code,
message=message,
title=f"{eCode} - Simonson Portfolio",
title=f"{code} - Simonson Portfolio",
),
eCode,
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():
return flask.jsonify(monitor.get_status_summary())
@app.route('/api/goto/')
@app.route('/api/goto/<location>')
def api_goto(location='home'):
if location not in pages:
flask.abort(404)
pagevars = pages[location]
try:
page = flask.render_template(pagevars["template"], var=pagevars)
except Exception:
page = render_error(500, "Internal Server Error")
return [pagevars, page]
# ── Dynamic page routes ──────────────────────────────────────────────
def make_page_handler(pagename):
def handler():
try:
return flask.render_template('header.html', var=pages[pagename])
except Exception:
return render_error(500, "Internal Server Error")
return handler
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")
@app.route("/Resume_Simonson_Andrew.pdf")
def resume():
return flask.send_file("./static/Resume_Simonson_Andrew.pdf")
@app.route("/sitemap.xml")
@app.route("/robots.txt")
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")
app.run()
if __name__ == "__main__":
app.run(debug=False)
else:
Minify(app=app, html=True, js=True, cssless=True)
monitor.start_monitoring()

6
src/config.py Executable file
View File

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

344
src/monitor.py Normal file
View File

@@ -0,0 +1,344 @@
"""
Service monitoring module.
Checks service availability and tracks uptime statistics in PostgreSQL.
"""
import os
import time
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta
from threading import Thread, Lock
import psycopg2
import requests
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 = 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 = {
'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',
}
class ServiceMonitor:
def __init__(self):
self.lock = Lock()
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 ──────────────────────────────────────────
@staticmethod
def _get_conn():
"""Return a new psycopg2 connection, or None if DATABASE_URL is unset."""
if not DATABASE_URL:
return None
return psycopg2.connect(DATABASE_URL)
def _ensure_schema(self):
"""Create or migrate the service_checks table to match _EXPECTED_COLUMNS."""
if not DATABASE_URL:
print("DATABASE_URL not set -- running without persistence")
return
conn = None
for attempt in range(5):
try:
conn = psycopg2.connect(DATABASE_URL)
break
except psycopg2.OperationalError:
if attempt < 4:
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")
return
try:
with conn, conn.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS service_checks (
id SERIAL PRIMARY KEY,
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
);
""")
cur.execute("""
CREATE INDEX IF NOT EXISTS idx_service_checks_service_timestamp
ON service_checks (service_id, timestamp DESC);
""")
# Introspect existing columns
cur.execute("""
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'service_checks'
""")
existing = {row[0] for row in cur.fetchall()}
for col, col_type in _EXPECTED_COLUMNS.items():
if col not in existing:
bare_type = col_type.split('NOT NULL')[0].split('DEFAULT')[0].strip()
cur.execute(f'ALTER TABLE service_checks ADD COLUMN {col} {bare_type}')
print(f"Added column {col} to service_checks")
expected_names = set(_EXPECTED_COLUMNS) | {'id'}
for col in existing - expected_names:
cur.execute(f'ALTER TABLE service_checks DROP COLUMN {col}')
print(f"Dropped column {col} from service_checks")
print("Database schema OK")
finally:
conn.close()
def _insert_check(self, service_id, result):
"""Persist a single check result to the database."""
conn = self._get_conn()
if conn is None:
return
try:
with conn, conn.cursor() as cur:
cur.execute(
"""INSERT INTO service_checks
(service_id, timestamp, status, response_time, status_code, error)
VALUES (%s, %s, %s, %s, %s, %s)""",
(
service_id,
result['timestamp'],
result['status'],
result.get('response_time'),
result.get('status_code'),
result.get('error'),
),
)
finally:
conn.close()
# ── Service checks ────────────────────────────────────────────
def check_service(self, service):
"""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,
}
try:
response = requests.head(
service['url'],
timeout=service['timeout'],
allow_redirects=True,
)
result['response_time'] = int((time.time() - start_time) * 1000)
result['status_code'] = response.status_code
if response.status_code < 500:
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 every service concurrently, persist results, and update cache."""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Checking all services...")
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)")
for service_id, result in results.items():
self._insert_check(service_id, result)
with self.lock:
for service in SERVICES:
result = results[service['id']]
cached = self._current[service['id']]
cached['status'] = result['status']
cached['response_time'] = result['response_time']
cached['status_code'] = result['status_code']
if result['status'] == 'online':
cached['last_online'] = result['timestamp']
self._last_check = datetime.now().isoformat()
# ── Uptime calculations ───────────────────────────────────────
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:
cutoff = datetime.now() - timedelta(hours=hours)
cur.execute(
"""SELECT
COUNT(*) FILTER (WHERE status = 'online'),
COUNT(*)
FROM service_checks
WHERE service_id = %s AND timestamp > %s""",
(service_id, cutoff),
)
else:
cur.execute(
"""SELECT
COUNT(*) FILTER (WHERE status = 'online'),
COUNT(*)
FROM service_checks
WHERE service_id = %s""",
(service_id,),
)
online_count, total_count = cur.fetchone()
if total_count == 0:
return None
# 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)',
(service_id, cutoff),
)
if not cur.fetchone()[0]:
return None
return round((online_count / total_count) * 100, 2)
finally:
conn.close()
def _get_total_checks(self, service_id):
"""Return the total number of recorded checks for a service."""
conn = self._get_conn()
if conn is None:
return 0
try:
with conn.cursor() as cur:
cur.execute(
'SELECT COUNT(*) FROM service_checks WHERE service_id = %s',
(service_id,),
)
return cur.fetchone()[0]
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()
if conn is None:
return
try:
cutoff = datetime.now() - timedelta(days=RETENTION_DAYS)
with conn, conn.cursor() as cur:
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")
finally:
conn.close()
def start_monitoring(self):
"""Start the background daemon thread for periodic checks and cleanup."""
def monitor_loop():
self.check_all_services()
self._purge_old_records()
checks_since_cleanup = 0
checks_per_cleanup = CLEANUP_INTERVAL // CHECK_INTERVAL
while True:
time.sleep(CHECK_INTERVAL)
self.check_all_services()
checks_since_cleanup += 1
if checks_since_cleanup >= checks_per_cleanup:
self._purge_old_records()
checks_since_cleanup = 0
thread = Thread(target=monitor_loop, daemon=True)
thread.start()
print(f"Service monitoring started (checks every {CHECK_INTERVAL}s)")
monitor = ServiceMonitor()

BIN
src/requirements.txt Normal file → Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

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

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

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

File diff suppressed because it is too large Load Diff

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

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

@@ -1 +1 @@
.line:not(:first-child){position:absolute;top:0;left:0}.line:nth-child(1){animation:clip 6000ms -600ms linear infinite,glitch1 2500ms -540ms linear infinite}@keyframes glitch1{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(3px);color:#4E9A26}98%{transform:translateX(-3px);color:#AC1212}99%{transform:translateX(-3px);color:#fff}100%{transform:translateX(0)}}.line:nth-child(2){animation:clip 6000ms -1200ms linear infinite,glitch2 2500ms -210ms linear infinite}@keyframes glitch2{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(1px);color:#4E9A26}98%{transform:translateX(-2px);color:#AC1212}99%{transform:translateX(-1px);color:#fff}100%{transform:translateX(0)}}.line:nth-child(3){animation:clip 6000ms -1800ms linear infinite,glitch3 2500ms -866ms linear infinite}@keyframes glitch3{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(2px);color:#4E9A26}98%{transform:translateX(0px);color:#AC1212}99%{transform:translateX(4px);color:#fff}100%{transform:translateX(0)}}.line:nth-child(4){animation:clip 6000ms -2400ms linear infinite,glitch4 2500ms -60ms linear infinite}@keyframes glitch4{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(-3px);color:#4E9A26}98%{transform:translateX(1px);color:#AC1212}99%{transform:translateX(1px);color:#fff}100%{transform:translateX(0)}}.line:nth-child(5){animation:clip 6000ms -3000ms linear infinite,glitch5 2500ms -221ms linear infinite}@keyframes glitch5{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(0px);color:#4E9A26}98%{transform:translateX(-3px);color:#AC1212}99%{transform:translateX(-4px);color:#fff}100%{transform:translateX(0)}}.line:nth-child(6){animation:clip 6000ms -3600ms linear infinite,glitch6 2500ms -716ms linear infinite}@keyframes glitch6{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(1px);color:#4E9A26}98%{transform:translateX(5px);color:#AC1212}99%{transform:translateX(-1px);color:#fff}100%{transform:translateX(0)}}.line:nth-child(7){animation:clip 6000ms -4200ms linear infinite,glitch7 2500ms -157ms linear infinite}@keyframes glitch7{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(-1px);color:#4E9A26}98%{transform:translateX(-3px);color:#AC1212}99%{transform:translateX(-2px);color:#fff}100%{transform:translateX(0)}}.line:nth-child(8){animation:clip 6000ms -4800ms linear infinite,glitch8 2500ms -955ms linear infinite}@keyframes glitch8{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(1px);color:#4E9A26}98%{transform:translateX(1px);color:#AC1212}99%{transform:translateX(-4px);color:#fff}100%{transform:translateX(0)}}.line:nth-child(9){animation:clip 6000ms -5400ms linear infinite,glitch9 2500ms -54ms linear infinite}@keyframes glitch9{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(0px);color:#4E9A26}98%{transform:translateX(-3px);color:#AC1212}99%{transform:translateX(-1px);color:#fff}100%{transform:translateX(0)}}.line:nth-child(10){animation:clip 6000ms -6000ms linear infinite,glitch10 2500ms -857ms linear infinite}@keyframes glitch10{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(3px);color:#4E9A26}98%{transform:translateX(1px);color:#AC1212}99%{transform:translateX(2px);color:#fff}100%{transform:translateX(0)}}@keyframes clip{0%{clip-path:polygon(0 100%, 100% 100%, 100% 120%, 0 120%)}100%{clip-path:polygon(0 -20%, 100% -20%, 100% 0%, 0 0)}}
.line:not(:first-child){position:absolute;top:0;left:0}@keyframes glitch1{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(2px);color:#4E9A26}98%{transform:translateX(3px);color:#AC1212}99%{transform:translateX(5px);color:#fff}100%{transform:translateX(0)}}@keyframes glitch2{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(3px);color:#4E9A26}98%{transform:translateX(2px);color:#AC1212}99%{transform:translateX(-4px);color:#fff}100%{transform:translateX(0)}}@keyframes glitch3{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(4px);color:#4E9A26}98%{transform:translateX(4px);color:#AC1212}99%{transform:translateX(-3px);color:#fff}100%{transform:translateX(0)}}@keyframes glitch4{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(-1px);color:#4E9A26}98%{transform:translateX(-4px);color:#AC1212}99%{transform:translateX(1px);color:#fff}100%{transform:translateX(0)}}@keyframes glitch5{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(-2px);color:#4E9A26}98%{transform:translateX(2px);color:#AC1212}99%{transform:translateX(-1px);color:#fff}100%{transform:translateX(0)}}@keyframes glitch6{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(0px);color:#4E9A26}98%{transform:translateX(0px);color:#AC1212}99%{transform:translateX(0px);color:#fff}100%{transform:translateX(0)}}@keyframes glitch7{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(0px);color:#4E9A26}98%{transform:translateX(1px);color:#AC1212}99%{transform:translateX(4px);color:#fff}100%{transform:translateX(0)}}@keyframes glitch8{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(-4px);color:#4E9A26}98%{transform:translateX(2px);color:#AC1212}99%{transform:translateX(3px);color:#fff}100%{transform:translateX(0)}}@keyframes glitch9{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(3px);color:#4E9A26}98%{transform:translateX(3px);color:#AC1212}99%{transform:translateX(3px);color:#fff}100%{transform:translateX(0)}}@keyframes glitch10{0%{transform:translateX(0)}96%{transform:translateX(0);color:#fff}97%{transform:translateX(5px);color:#4E9A26}98%{transform:translateX(4px);color:#AC1212}99%{transform:translateX(4px);color:#fff}100%{transform:translateX(0)}}@keyframes clip{0%{clip-path:polygon(0 100%, 100% 100%, 100% 120%, 0 120%)}100%{clip-path:polygon(0 -20%, 100% -20%, 100% 0%, 0 0)}}

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

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

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

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

BIN
src/static/fonts/SHUTTLE-X.ttf Executable file

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

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

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

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

Before

Width:  |  Height:  |  Size: 759 B

After

Width:  |  Height:  |  Size: 759 B

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

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

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

Before

Width:  |  Height:  |  Size: 683 B

After

Width:  |  Height:  |  Size: 683 B

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

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

2796
src/static/icons/log.svg Executable file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 177 KiB

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

Before

Width:  |  Height:  |  Size: 764 B

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

432
src/static/icons/neonfinal3.svg Executable file
View File

@@ -0,0 +1,432 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="216.75919mm"
height="216.7592mm"
viewBox="0 0 216.75919 216.7592"
version="1.1"
id="svg1"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
sodipodi:docname="workingModel.svg"
inkscape:export-filename="neonfinal.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#1a1a1a"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.61646116"
inkscape:cx="438.79488"
inkscape:cy="524.76947"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<linearGradient
id="linearGradient1"
inkscape:collect="always">
<stop
style="stop-color:#ffc919;stop-opacity:1;"
offset="0"
id="stop1" />
<stop
style="stop-color:#d2ae2d;stop-opacity:1;"
offset="1"
id="stop2" />
</linearGradient>
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,7.9986979,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect16"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,8.0780818,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,9.6572028,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect8"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,7.5495738,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect8-2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,7.5495738,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect9-9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,9.6572028,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter9"
x="-0.030747933"
y="-0.028448841"
width="1.0614959"
height="1.0568977">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="1.1764713"
id="feGaussianBlur9" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter29"
x="-0.090988795"
y="-0.084185358"
width="1.1819776"
height="1.1683707">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="4.7058852"
id="feGaussianBlur29" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter30"
x="-0.23378194"
y="-0.21630154"
width="1.4675639"
height="1.4326031">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="13.071903"
id="feGaussianBlur30" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter5"
x="-0.041894038"
y="-0.041893957"
width="1.083788"
height="1.0837879">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="1.0664778"
id="feGaussianBlur5" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter6"
x="-0.23389403"
y="-0.23389395"
width="1.467788"
height="1.4677879">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="9.2941268"
id="feGaussianBlur6" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter8"
x="-0.44132139"
y="-0.4413213"
width="1.8826427"
height="1.8826426">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="10.040917"
id="feGaussianBlur8" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter10"
x="-0.01629857"
y="-0.01629849"
width="1.0325971"
height="1.032597">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.49999998"
id="feGaussianBlur10" />
</filter>
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect16-7"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,8.0780818,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17-5"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,7.9986979,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="radialGradient2"
cx="68.084846"
cy="82.277237"
fx="68.084846"
fy="82.277237"
r="51.629848"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-6.8045576,-27.991731)">
<circle
style="display:none;opacity:1;fill:#1a1a1a;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.09141;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0;paint-order:markers stroke fill"
id="background"
cx="115.18415"
cy="136.37132"
r="108.37959"
inkscape:export-filename="withBackground.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<use
x="0"
y="0"
xlink:href="#use6"
id="use7"
style="display:inline;opacity:1;filter:url(#filter8)"
inkscape:export-filename="orangOutline.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<use
x="0"
y="0"
xlink:href="#star"
id="use6"
style="display:inline;opacity:0.7;mix-blend-mode:normal;filter:url(#filter6)"
inkscape:export-filename="../../Documents/GitHub/asimonson1125.github.io/src/static/icons/neonfinal3.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<g
id="star"
style="display:inline;opacity:1;filter:url(#filter5)"
transform="matrix(1.1617659,0,0,1.1617659,37.635304,39.028635)"
inkscape:export-filename="../../Documents/GitHub/asimonson1125.github.io/src/static/icons/neonfinal2.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<path
style="opacity:1;mix-blend-mode:normal;fill:url(#radialGradient2);fill-opacity:1;fill-rule:evenodd;stroke:#ff9c00;stroke-width:0.860759;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter10)"
d="m 65.084856,67.277241 c -1.287779,9.10845 -5.183319,11.28276 -12.694774,12 -7.511459,0.71725 -34.305226,3 -34.305226,3 0,0 25.891549,1.71223 35,3 9.108454,1.28778 11.282748,4.48855 12,12 0.717252,7.511459 3,34.999999 3,34.999999 0,0 1.712221,-25.89154 3,-34.999999 1.28778,-9.10844 4.488549,-11.28275 11.999999,-12 7.51146,-0.71724 34.999995,-3 34.999995,-3 0,0 -25.891545,-1.71222 -34.999995,-3 -9.10845,-1.28777 -11.282748,-4.48854 -11.999999,-12 -0.717252,-7.51145 -3,-34.999998 -3,-34.999998 0,0 -1.71222,25.891548 -3,34.999998 z"
id="path3-6-8"
sodipodi:nodetypes="sscsscsscsscs" />
</g>
<g
id="neonHighlights"
style="display:inline;opacity:0.75"
transform="translate(18.455527)">
<use
x="0"
y="0"
xlink:href="#neonTubing"
id="use30"
style="display:inline;mix-blend-mode:normal;filter:url(#filter30)"
transform="translate(-18.455527)" />
<use
x="0"
y="0"
xlink:href="#neonTubing"
id="use29"
style="display:inline;mix-blend-mode:normal;filter:url(#filter29)"
transform="translate(-18.455527)" />
<use
x="0"
y="0"
xlink:href="#neonTubing"
id="use9"
style="opacity:1;filter:url(#filter9)"
transform="translate(-18.455527)" />
</g>
<g
id="neonTubing"
style="fill:none;fill-opacity:1;stroke:#f8e0c3;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
transform="translate(18.455527)">
<g
id="g1"
style="display:none;stroke:#f8e0c3;stroke-width:3;stroke-dasharray:none;stroke-opacity:1">
<path
style="fill:none;fill-opacity:1;stroke:#f8e0c3;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 129.66155,74.179932 H 61.78218 a 9.6572028,9.6572028 135 0 0 -9.657203,9.657203 v 39.314015"
id="path1"
inkscape:path-effect="#path-effect9"
inkscape:original-d="M 129.66155,74.179932 H 52.124977 v 48.971218" />
<path
style="fill:none;fill-opacity:1;stroke:#f8e0c3;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 114.02977,84.567913 H 72.042986 a 7.4885845,7.4885845 134.76763 0 0 -7.488338,7.549325 l 0.186961,23.049192"
id="path2"
inkscape:path-effect="#path-effect8"
inkscape:original-d="M 114.02977,84.567913 H 64.493412 l 0.248197,30.598517"
transform="translate(-0.40337459,1.3209286)" />
<path
style="fill:none;fill-opacity:1;stroke:#f8e0c3;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 129.66155,74.179932 H 61.78218 a 9.6572028,9.6572028 135 0 0 -9.657203,9.657203 v 39.314015"
id="path1-2"
inkscape:path-effect="#path-effect9-9"
inkscape:original-d="M 129.66155,74.179932 H 52.124977 v 48.971218"
transform="rotate(180,96.802685,134.07745)" />
<path
style="fill:none;fill-opacity:1;stroke:#f8e0c3;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 114.02977,84.567913 H 72.042986 a 7.4885845,7.4885845 134.76763 0 0 -7.488338,7.549325 l 0.186961,23.049192"
id="path2-7"
inkscape:path-effect="#path-effect8-2"
inkscape:original-d="M 114.02977,84.567913 H 64.493412 l 0.248197,30.598517"
transform="rotate(180,97.004375,133.41698)" />
</g>
<g
id="g33"
transform="translate(-7.6804235,-33.343613)"
style="display:inline;stroke:#f8e0c3;stroke-width:3;stroke-dasharray:none;stroke-opacity:1">
<path
style="fill:none;stroke:#ededed;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 142.67883,102.44998 H 73.832338 a 10.424106,10.424106 142.5 0 0 -10.068913,7.72615 l -6.688692,24.96254"
id="path10"
inkscape:path-effect="#path-effect17"
inkscape:original-d="M 142.67883,102.44998 H 65.83364 l -8.758907,32.68869"
transform="translate(-6.0989631,2.1674006)" />
<path
style="fill:none;stroke:#ededed;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 156.95436,91.412872 H 56.426396 A 10.527561,10.527561 142.5 0 0 46.257553,99.2157 l -11.945382,44.58078"
id="path11"
inkscape:path-effect="#path-effect16"
inkscape:original-d="M 156.95436,91.412872 H 48.348314 L 34.312171,143.79648" />
<path
style="display:inline;fill:none;fill-opacity:1;stroke:#ededed;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 142.67883,102.44998 H 73.832338 a 10.424106,10.424106 142.5 0 0 -10.068913,7.72615 l -6.688692,24.96254"
id="path10-6"
inkscape:path-effect="#path-effect17-5"
inkscape:original-d="M 142.67883,102.44998 H 65.83364 l -8.758907,32.68869"
transform="rotate(180,105.40367,165.67111)" />
<path
style="display:inline;fill:none;fill-opacity:1;stroke:#ededed;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 156.95436,91.412872 H 56.426396 a 10.496229,10.496229 142.41749 0 0 -10.146361,7.808817 l -13.97988,52.781241"
id="path11-2"
inkscape:path-effect="#path-effect16-7"
inkscape:original-d="M 156.95436,91.412872 H 48.348314 L 32.300155,152.00293"
transform="rotate(180,103.61221,167.4007)"
sodipodi:nodetypes="ccc" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

BIN
src/static/icons/rasterLogo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

View File

@@ -0,0 +1,432 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="216.75919mm"
height="216.7592mm"
viewBox="0 0 216.75919 216.7592"
version="1.1"
id="svg1"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
sodipodi:docname="workingModel.svg"
inkscape:export-filename="neonfinal.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#1a1a1a"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.61646116"
inkscape:cx="438.79488"
inkscape:cy="524.76947"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<linearGradient
id="linearGradient1"
inkscape:collect="always">
<stop
style="stop-color:#ffc919;stop-opacity:1;"
offset="0"
id="stop1" />
<stop
style="stop-color:#d2ae2d;stop-opacity:1;"
offset="1"
id="stop2" />
</linearGradient>
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,7.9986979,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect16"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,8.0780818,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,9.6572028,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect8"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,7.5495738,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect8-2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,7.5495738,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect9-9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,9.6572028,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter9"
x="-0.030747933"
y="-0.028448841"
width="1.0614959"
height="1.0568977">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="1.1764713"
id="feGaussianBlur9" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter29"
x="-0.090988795"
y="-0.084185358"
width="1.1819776"
height="1.1683707">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="4.7058852"
id="feGaussianBlur29" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter30"
x="-0.23378194"
y="-0.21630154"
width="1.4675639"
height="1.4326031">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="13.071903"
id="feGaussianBlur30" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter5"
x="-0.041894038"
y="-0.041893957"
width="1.083788"
height="1.0837879">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="1.0664778"
id="feGaussianBlur5" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter6"
x="-0.23389403"
y="-0.23389395"
width="1.467788"
height="1.4677879">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="9.2941268"
id="feGaussianBlur6" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter8"
x="-0.44132139"
y="-0.4413213"
width="1.8826427"
height="1.8826426">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="10.040917"
id="feGaussianBlur8" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter10"
x="-0.01629857"
y="-0.01629849"
width="1.0325971"
height="1.032597">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.49999998"
id="feGaussianBlur10" />
</filter>
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect16-7"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,8.0780818,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17-5"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,7.9986979,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="radialGradient2"
cx="68.084846"
cy="82.277237"
fx="68.084846"
fy="82.277237"
r="51.629848"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-6.8045576,-27.991731)">
<circle
style="opacity:1;fill:#1a1a1a;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.09141;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0;paint-order:markers stroke fill"
id="background"
cx="115.18415"
cy="136.37132"
r="108.37959"
inkscape:export-filename="withBackground.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<use
x="0"
y="0"
xlink:href="#use6"
id="use7"
style="display:inline;opacity:1;filter:url(#filter8)"
inkscape:export-filename="orangOutline.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<use
x="0"
y="0"
xlink:href="#star"
id="use6"
style="display:inline;opacity:0.7;mix-blend-mode:normal;filter:url(#filter6)"
inkscape:export-filename="../../Documents/GitHub/asimonson1125.github.io/src/static/icons/neonfinal3.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<g
id="star"
style="display:inline;opacity:1;filter:url(#filter5)"
transform="matrix(1.1617659,0,0,1.1617659,37.635304,39.028635)"
inkscape:export-filename="../../Documents/GitHub/asimonson1125.github.io/src/static/icons/neonfinal2.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<path
style="opacity:1;mix-blend-mode:normal;fill:url(#radialGradient2);fill-opacity:1;fill-rule:evenodd;stroke:#ff9c00;stroke-width:0.860759;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter10)"
d="m 65.084856,67.277241 c -1.287779,9.10845 -5.183319,11.28276 -12.694774,12 -7.511459,0.71725 -34.305226,3 -34.305226,3 0,0 25.891549,1.71223 35,3 9.108454,1.28778 11.282748,4.48855 12,12 0.717252,7.511459 3,34.999999 3,34.999999 0,0 1.712221,-25.89154 3,-34.999999 1.28778,-9.10844 4.488549,-11.28275 11.999999,-12 7.51146,-0.71724 34.999995,-3 34.999995,-3 0,0 -25.891545,-1.71222 -34.999995,-3 -9.10845,-1.28777 -11.282748,-4.48854 -11.999999,-12 -0.717252,-7.51145 -3,-34.999998 -3,-34.999998 0,0 -1.71222,25.891548 -3,34.999998 z"
id="path3-6-8"
sodipodi:nodetypes="sscsscsscsscs" />
</g>
<g
id="neonHighlights"
style="display:inline;opacity:0.75"
transform="translate(18.455527)">
<use
x="0"
y="0"
xlink:href="#neonTubing"
id="use30"
style="display:inline;mix-blend-mode:normal;filter:url(#filter30)"
transform="translate(-18.455527)" />
<use
x="0"
y="0"
xlink:href="#neonTubing"
id="use29"
style="display:inline;mix-blend-mode:normal;filter:url(#filter29)"
transform="translate(-18.455527)" />
<use
x="0"
y="0"
xlink:href="#neonTubing"
id="use9"
style="opacity:1;filter:url(#filter9)"
transform="translate(-18.455527)" />
</g>
<g
id="neonTubing"
style="fill:none;fill-opacity:1;stroke:#f8e0c3;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
transform="translate(18.455527)">
<g
id="g1"
style="display:none;stroke:#f8e0c3;stroke-width:3;stroke-dasharray:none;stroke-opacity:1">
<path
style="fill:none;fill-opacity:1;stroke:#f8e0c3;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 129.66155,74.179932 H 61.78218 a 9.6572028,9.6572028 135 0 0 -9.657203,9.657203 v 39.314015"
id="path1"
inkscape:path-effect="#path-effect9"
inkscape:original-d="M 129.66155,74.179932 H 52.124977 v 48.971218" />
<path
style="fill:none;fill-opacity:1;stroke:#f8e0c3;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 114.02977,84.567913 H 72.042986 a 7.4885845,7.4885845 134.76763 0 0 -7.488338,7.549325 l 0.186961,23.049192"
id="path2"
inkscape:path-effect="#path-effect8"
inkscape:original-d="M 114.02977,84.567913 H 64.493412 l 0.248197,30.598517"
transform="translate(-0.40337459,1.3209286)" />
<path
style="fill:none;fill-opacity:1;stroke:#f8e0c3;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 129.66155,74.179932 H 61.78218 a 9.6572028,9.6572028 135 0 0 -9.657203,9.657203 v 39.314015"
id="path1-2"
inkscape:path-effect="#path-effect9-9"
inkscape:original-d="M 129.66155,74.179932 H 52.124977 v 48.971218"
transform="rotate(180,96.802685,134.07745)" />
<path
style="fill:none;fill-opacity:1;stroke:#f8e0c3;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 114.02977,84.567913 H 72.042986 a 7.4885845,7.4885845 134.76763 0 0 -7.488338,7.549325 l 0.186961,23.049192"
id="path2-7"
inkscape:path-effect="#path-effect8-2"
inkscape:original-d="M 114.02977,84.567913 H 64.493412 l 0.248197,30.598517"
transform="rotate(180,97.004375,133.41698)" />
</g>
<g
id="g33"
transform="translate(-7.6804235,-33.343613)"
style="display:inline;stroke:#f8e0c3;stroke-width:3;stroke-dasharray:none;stroke-opacity:1">
<path
style="fill:none;stroke:#ededed;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 142.67883,102.44998 H 73.832338 a 10.424106,10.424106 142.5 0 0 -10.068913,7.72615 l -6.688692,24.96254"
id="path10"
inkscape:path-effect="#path-effect17"
inkscape:original-d="M 142.67883,102.44998 H 65.83364 l -8.758907,32.68869"
transform="translate(-6.0989631,2.1674006)" />
<path
style="fill:none;stroke:#ededed;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 156.95436,91.412872 H 56.426396 A 10.527561,10.527561 142.5 0 0 46.257553,99.2157 l -11.945382,44.58078"
id="path11"
inkscape:path-effect="#path-effect16"
inkscape:original-d="M 156.95436,91.412872 H 48.348314 L 34.312171,143.79648" />
<path
style="display:inline;fill:none;fill-opacity:1;stroke:#ededed;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 142.67883,102.44998 H 73.832338 a 10.424106,10.424106 142.5 0 0 -10.068913,7.72615 l -6.688692,24.96254"
id="path10-6"
inkscape:path-effect="#path-effect17-5"
inkscape:original-d="M 142.67883,102.44998 H 65.83364 l -8.758907,32.68869"
transform="rotate(180,105.40367,165.67111)" />
<path
style="display:inline;fill:none;fill-opacity:1;stroke:#ededed;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 156.95436,91.412872 H 56.426396 a 10.496229,10.496229 142.41749 0 0 -10.146361,7.808817 l -13.97988,52.781241"
id="path11-2"
inkscape:path-effect="#path-effect16-7"
inkscape:original-d="M 156.95436,91.412872 H 48.348314 L 32.300155,152.00293"
transform="rotate(180,103.61221,167.4007)"
sodipodi:nodetypes="ccc" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,27 +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;
}
}
}
}
}

26
src/static/js/chessbed.js Normal file → Executable file
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();
ratings = {
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,
};
setChess({ cName: user["username"], pic: user.avatar, ratings: ratings });
} else if (user === null || user.status === 403 || user.status === null) {
},
});
} 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)");
}
}

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

@@ -1,8 +1,11 @@
const balls = [];
const density = 0.00003;
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);
}
@@ -72,21 +74,31 @@ function draw() {
for (let i = 0; i < balls.length; i++) {
balls[i].update();
}
// Draw connection lines with additive blending so overlaps brighten
blendMode(ADD);
strokeWeight(2);
for (let i = 0; i < balls.length - 1; i++) {
const a = balls[i];
for (let j = i + 1; j < balls.length; j++) {
let distance = dist(balls[i].x, balls[i].y, balls[j].x, balls[j].y);
if (distance < 100){
stroke(150);
line(balls[i].x, balls[i].y, balls[j].x, balls[j].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 < MAX_DIST_SQUARED) {
const distance = Math.sqrt(distSquared);
if (distance < 75) {
stroke(255, 85);
} else {
const chance = 0.3 ** (((random(0.2) + 0.8) * distance) / MAX_DIST);
stroke(255, chance < 0.5 ? 40 : 75);
}
else if (distance < 150) {
stroke(100);
let chance = 0.3 ** (((random(0.2) + 0.8) * distance) / 150);
if (chance < 0.5) {
stroke(50);
}
line(balls[i].x, balls[i].y, balls[j].x, balls[j].y);
line(a.x, a.y, b.x, b.y);
}
}
}
blendMode(BLEND);
}

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

@@ -1,111 +1,79 @@
window.onload = function () {
onLoaded();
};
function onLoaded() {
document.body.scrollTop = 0; // For Safari
document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
window.onresize = function () {
resizer();
};
resizer();
if (window.innerWidth < 1200) {
const e = document.querySelector(".navControl");
e.style.maxHeight = "0px";
}
}
function resizer() {
const e = document.querySelector(".navControl");
if (window.innerWidth > 1200) {
// desktop view
scrollFunction();
window.onscroll = function () {
scrollFunction();
};
e.style.maxHeight = `${e.scrollHeight + 10}px`;
} else {
// mobile view
window.onscroll = "";
document.querySelector(".header").style.backgroundColor = "#1a1a1a";
document.querySelectorAll(".header .name h1").forEach(function (x) {
x.style.fontSize = "1.5rem";
});
// document.querySelector('.header > h1').style.color = "#ecebeb";
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 scrollFunction() {
if (document.body.scrollTop > 10 || document.documentElement.scrollTop > 10) {
document.querySelector(".header").style.backgroundColor = "#1a1a1a";
document.querySelectorAll(".header .name h1").forEach(function (x) {
x.style.fontSize = "1.5rem";
});
document.querySelectorAll(".navElement *").forEach((x) => {
x.style.paddingTop = ".3rem";
x.style.paddingBottom = ".3rem";
x.style.fontSize = "1rem";
});
} else {
document.querySelector(".header").style.backgroundColor = "rgba(0,0,0,0)";
document.querySelectorAll(".header .name h1").forEach(function (x) {
x.style.fontSize = "2rem";
});
// document.querySelector('.header > h1').style.color = "#ecebeb";
document.querySelectorAll(".navElement *").forEach((x) => {
x.style.paddingTop = ".5rem";
x.style.paddingBottom = ".5rem";
x.style.fontSize = "1.2rem";
});
}
}
function toggleMenu() {
if (window.innerWidth < 1200) {
const e = document.querySelector(".navControl");
function toggleMenu(collapse) {
if (window.innerWidth < 1400) {
const menu = document.querySelector(".navControl");
const bar = document.querySelector(".header");
if (e.style.maxHeight === "0px") {
e.style.maxHeight = `${e.scrollHeight + 10}px`;
const isCollapsed = !menu.style.maxHeight || menu.style.maxHeight === "0px";
if (isCollapsed && !collapse) {
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, toggle=true}={}) {
let a = await fetch("/api/goto/" + location, {
async function goto(location, { push = true } = {}) {
let response;
try {
response = await fetch("/api/goto/" + location, {
credentials: "include",
method: "GET",
mode: "cors",
});
const response = await a.json();
const metadata = response[0];
const content = response[1];
let root = document.getElementById("root");
root.innerHTML = content;
root.querySelectorAll("script").forEach((x) => {
eval(x.innerHTML);
});
document.querySelector("title").textContent = metadata['title'];
window.scrollTo(0, 0);
if(toggle){
toggleMenu();
if (!response.ok) {
console.error(`Navigation failed: HTTP ${response.status}`);
return;
}
if(push){
history.pushState(null, null, metadata['canonical']);
} catch (err) {
console.error("Navigation fetch failed:", err);
return;
}
document.dispatchEvent(new Event('beforenavigate'));
const [metadata, content] = await response.json();
const root = document.getElementById("root");
root.innerHTML = content;
// 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(function(attr) {
newScript.setAttribute(attr.name, attr.value);
});
newScript.textContent = oldScript.textContent;
oldScript.parentNode.replaceChild(newScript, oldScript);
});
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(true);
document.querySelector("title").textContent = metadata["title"];
if (push) {
history.pushState(null, null, metadata["canonical"]);
}
}
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");
}
}

213
src/static/js/status.js Normal file
View File

@@ -0,0 +1,213 @@
let statusIntervalId = null;
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.');
}
}
function updateStatusDisplay(data) {
if (data.last_check) {
const lastCheck = new Date(data.last_check);
document.getElementById('lastUpdate').textContent = `Last checked: ${lastCheck.toLocaleString()}`;
}
if (data.next_check) {
const nextCheckEl = document.getElementById('nextUpdate');
if (nextCheckEl) {
const nextCheck = new Date(data.next_check);
nextCheckEl.textContent = `Next check: ${nextCheck.toLocaleString()}`;
}
}
data.services.forEach(function(service) {
updateServiceCard(service);
});
updateOverallStatus(data.services);
const refreshBtn = document.getElementById('refreshBtn');
if (refreshBtn) {
refreshBtn.disabled = false;
refreshBtn.textContent = 'Refresh Now';
}
}
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;
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}`);
timeDisplay.textContent = service.response_time !== null ? `${service.response_time}ms` : '--';
if (service.status_code !== null) {
codeDisplay.textContent = service.status_code;
} else {
codeDisplay.textContent = service.status === 'unknown' ? 'Unknown' : 'Error';
}
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');
}
if (uptimeDisplay && service.uptime) {
uptimeDisplay.innerHTML = [
formatUptime(service.uptime['24h'], '24h'),
formatUptime(service.uptime['7d'], '7d'),
formatUptime(service.uptime['30d'], '30d'),
formatUptime(service.uptime.all_time, 'All'),
].join(' | ');
}
if (checksDisplay && service.total_checks !== undefined) {
checksDisplay.textContent = service.total_checks;
}
}
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');
const total = services.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;
onlineCount.textContent = online;
totalCount.textContent = total;
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';
}
}
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(function() { errorDiv.remove(); }, 5000);
}
}
function refreshStatus() {
const refreshBtn = document.getElementById('refreshBtn');
if (refreshBtn) {
refreshBtn.disabled = true;
refreshBtn.textContent = 'Checking...';
}
fetchStatus();
}
function initStatusPage() {
if (statusIntervalId !== null) {
clearInterval(statusIntervalId);
}
fetchStatus();
statusIntervalId = setInterval(fetchStatus, 60000);
}
// Clean up polling interval when navigating away via SPA
document.addEventListener('beforenavigate', function() {
if (statusIntervalId !== null) {
clearInterval(statusIntervalId);
statusIntervalId = null;
}
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initStatusPage);
} else {
initStatusPage();
}

168
src/static/json/books.json Executable file
View File

@@ -0,0 +1,168 @@
{
"selection": [
"The Rational Optimist",
"The End of the World is Just the Beginning",
"When to Rob a Bank",
"Freakonomics",
"The Accidental Superpower",
"Verbal Judo",
"Zero To One"
],
"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": {
"filename": "ratOpt.jpg",
"link": "https://www.amazon.com/Rational-Optimist-Prosperity-Evolves-P-s/dp/0061452068",
"review": "The story of humanity's growth with a refined variation of a lens I very much believe deserves greater publicity. Long, yes, but ultimately one of the most important books for me to have read."
},
"Freakonomics": {
"filename": "freakonomics.jpeg",
"link": "https://freakonomics.com/books/",
"review": "The original on cracked economics. Dozens of case studies heavy on the unexpected results of incentives. Mind Expanding. Very nice."
},
"Superfreakonomics": {
"filename": "superfreakonomics.jpeg",
"link": "https://freakonomics.com/books/",
"review": "More of the goods? Excellent. A number of case studies on finding simple solutions to wicked problems through changing the attack vector in brainstorming. Another fun read."
},
"When to Rob a Bank": {
"filename": "whenToRobABank.jpeg",
"link": "https://freakonomics.com/books/",
"review": "Collection of blog posts. Good for short reading sessions. I read this one first, I thought the Freakonomics mode of thinking was hilarious. Classic."
},
"Think like a Freak": {
"filename": "thinkLikeAFreak.jpg",
"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."
},
"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": {
"filename": "theAccidentalSuperpower.jpeg",
"link": "https://zeihan.com/",
"review": "My intro to geopolitics, brilliant hook that made possibly my fastest read ever. The straightforward the lines of reasoning and grounding in concrete realities that few would consider controversial is incredible, even when the conclusions seem hyper-dramatic."
},
"The Absent Superpower": {
"filename": "theAbsentSuperpower.jpeg",
"link": "https://zeihan.com/",
"review": "Hadn't realized the author was the same as Accidental when I bought it. It covered a lot of the same stuff but with a heavy emphasis on fracking technology which was a bit slow but it solidified the veracity of Zeihan's worldview."
},
"Disunited Nations": {
"filename": "disunitedNations.jpeg",
"link": "https://zeihan.com/disunited-nations/",
"review": "Chapter profiles of key countries in the world provides lots of interesting supporting details, but being the third book of its kind made the rehashing a bit of a drag."
},
"The End of the World is Just the Beginning": {
"filename": "theEndOfTheWorldIsJustTheBeginning.jpeg",
"link": "https://zeihan.com/end-of-the-world/",
"review": "Separates itself from the others by Zeihan in its breakdown of specific resources of every category, from nickel to corn. Being generalized, specific, and an enjoyable read all at the same time. Galaxy Brain stuff."
},
"The Storm Before the Calm": {
"filename": "theStormBeforeTheCalm.jpeg",
"link": "https://www.amazon.com/Storm-Before-Calm-Americas-Discord/dp/1101911786/",
"review": "Repetitive and almost faith-based. The cycles and cultural inclinations used as the backbone for predictions may be accurate, but the evidence for this was slim. Nonetheless, the cultural archetypes laid out here have stuck with me, albeit not for political fortune-telling purposes."
},
"No, They Can't": {
"filename": "no-they-cant.jpeg",
"link": "https://www.goodreads.com/book/show/13260131-no-they-can-t",
"review": "I much preferred Give Me A Break (which I read first). Sossel's writing style in this book is less developed - it feels aimless and with an intent to lecture."
},
"Give Me a Break": {
"filename": "giveMeABreak.jpeg",
"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."
},
"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": {
"filename": "zeroToOne.jpeg",
"link": "https://www.amazon.com/Zero-One-Notes-Startups-Future/dp/0804139296",
"review": "Initially very frustrating read. Took a bit of internal review to realize that this has to be read with an entrepreneurial perspective, not a consumer one. After that, it's quite eye-opening in ways that would've been obvious without the lens of life experience. The optimistic takes are very much appreciated and their justifications are solid."
},
"Courage is Calling": {
"filename": "courageIsCalling.jpeg",
"link": "https://www.amazon.com/Courage-Calling-Fortune-Favors-Brave/dp/B094PMPCBT/",
"review": "Average on the readability scale, but inspiring all the same. As a primer in stoicism it gets a 8/10 for not enough gigachad energy to match the gigachad content."
},
"Discipline Is Destiny": {
"filename": "disciplineIsDestiny.jpg",
"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."
},
"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": {
"filename": "onGrandStrategy.jpeg",
"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."
},
"David and Goliath": {
"filename": "davidAndGoliath.png",
"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."
},
"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": {
"filename": "verbalJudo.png",
"link": "https://www.amazon.com/Verbal-Judo-Second-Gentle-Persuasion-ebook/dp/B00FJ3CMI6/",
"review": "Book tries to hook you into reading it even when you're already halfway through reading it. And it works! Definitely a good book to review occasionally to keep yourself grounded during tense moments."
},
"You Can Read Anyone": {
"filename": "youCanReadAnyone.jpg",
"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."
},
"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": {
"filename": "profilesInCourage.jpeg",
"link": "https://www.amazon.com/Profiles-Courage-John-F-Kennedy/dp/0060854936",
"review": "Another book that was hard to really get into but still provided fascinating commentary on some very important figures that have faded from public memory."
},
"Where Good Ideas Come From": {
"filename": "where-good-ideas-come-from.png",
"link": "https://www.goodreads.com/book/show/8034188-where-good-ideas-come-from",
"review": "I got this book at a recycling center. I didn't want to read it or like it. Unfortnuately, it's pretty good. 200 pages of considerate review of how innovation comes to be + suggestions to expand the utility of your ideas (I've adopted several!)"
},
"Make Your Bed": {
"filename": "makeYourBed.jpg",
"link": "https://www.amazon.com/Make-Your-Bed-Little-Things/dp/1455570249",
"review": "Something small to read on a rainy day or flight. Valuable advice condensed into personal stories that stretch beyond anecdotes."
},
"12 Rules for Life": {
"filename": "12RulesForLife.jpg",
"link": "https://www.amazon.com/12-Rules-Life-Antidote-Chaos/dp/0345816021/",
"review": "Another read with challenging academic vernacular. I had a agreeable-hate relationship with the biblical storytelling that made me somewhat dread reading yet also question the nature of religion. Another example of books with good advice being not fun to get into."
},
"Beyond Order: 12 More Rules for Life": {
"filename": "BeyondOrder.jpg",
"link": "https://www.amazon.com/Beyond-Order-More-Rules-Life/dp/0593084640",
"review": "It bothers how much these books make me stop and think because it throws me out of the focus of actually reading. More solid advice with less religion than the predecessor but retaining the 'difficult to want to read' badge."
},
"The Hitchhiker's Guide to the Galaxy": {
"filename": "HitchhikersGuideToTheGalaxy.jpeg",
"link": "https://www.amazon.com/Hitchhikers-Guide-Galaxy-Douglas-Adams/dp/0345418913",
"review": "It's alright. It felt like an aimless journey without defined boundaries that reveled in that fact for irony and wit points."
}
}
}

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

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

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

@@ -1,19 +1,47 @@
{
"Lower 48 Alt. Energy Map": {
"Antietam-Conococheague Watershed Monitoring": {
"status": "complete",
"classes": "geospacial",
"bgi": "watershedTemps.png",
"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",
"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": {
"status": "incomplete",
"classes": "pinned geospacial programming",
"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.",
"links": [
["github", "https://github.com/asimonson1125/hotspotsrit", "git repo"]
]
},
"Calorimetry Analysis Engineering": {
"status": "complete",
"classes": "pinned programming",
"bgi": "calorimeterAnalysis.png",
"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",
"links": []
},
"Geography of Alternative Energy": {
"status": "complete",
"classes": "pinned geospacial",
"bgi": "geovisF.png",
"content": "ArcGIS Map of the most effective alternative energy sources in the continental United States",
"bgi": "energyGeography.png",
"content": "An ArcGIS geospacial analysis comparing the difference in effectiveness of wind, solar, and geothermal energy across the continental 48 United States.",
"links": [
[
"globe",
"https://ritarcgis.maps.arcgis.com/apps/dashboards/17d5bda01edc4a2eb6205a4922d889c9",
"ArcGIS"
"Dashboard"
]
]
},
"OccupyRIT": {
"status": "WIP",
"status": "complete",
"classes": "pinned programming",
"bgi": "occupyRIT.png",
"content": "Collects RIT Gym Occupancy data, determining busiest workout times",
@@ -21,33 +49,17 @@
["github", "https://github.com/asimonson1125/Occupy-RIT", "git repo"]
]
},
"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": {
"Portfolio Website": {
"status": "WIP",
"classes": "programming",
"bgi": "resume.png",
"content": "My Resume, made in LaTeX with a custom design derived by the AltaCV template on OverLeaf",
"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.",
"links": [
["github", "https://github.com/asimonson1125/Resume", "git repo"],
["globe", "https://asimonson.com/Resume.pdf", "Resume"]
["globe", "https://asimonson.com", "Homepage"],
[
"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": {
@@ -56,62 +68,21 @@
"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",
"links": [
["github", "https://github.com/asimonson1125/Slate", "git repo"],
["globe", "https://slate.csh.rit.edu", "site link"]
["globe", "https://slate.csh.rit.edu", "site link"],
["github", "https://github.com/asimonson1125/Slate", "git repo"]
]
},
"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": {
"Monte Carlo Engine for NationsGame": {
"status": "complete",
"classes": "programming",
"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.",
"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"]
]
}
}

35
src/static/json/skills.json Executable file
View File

@@ -0,0 +1,35 @@
{
"Data and AI": {
"ML": {
"PySpark ML": {},
"Numpy/Pandas/Polars": {},
"TensorFlow": {},
"Scikit": {}
},
"PySpark": {},
"Selenium/BS4 Web Hacking": {},
"SQL": {},
"Declarative Pipelines": {},
"ArcGIS": {}
},
"DevOps": {
"Docker": {},
"Microsoft Azure": {},
"Databricks": {},
"Kubernetes/Openshift": {},
"Cloudflare": {},
"Bash": {}
},
"Frontend": {
"Flask (Python)": {},
"REST APIs": {},
"Web Scraping": {}
},
"Offline Skills": {
"Circuitry": {},
"Skiing": {},
"Chess": {},
"Plinking": {},
"Building something with trash that solves my problems": {}
}
}

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

@@ -11,7 +11,7 @@
},
"Rochester Institute of Technology": {
"classes": "pinned education technical",
"date": "08/2021 - 05/2025",
"date": "08/2021 - 12/2024",
"content": "Studying in Rochester Institute of Technology's Computer Science BS program with a minor in International Relations."
},
"Pretzel & Pizza Creations": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 898 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

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