diff --git a/src/app.py b/src/app.py
index 29b8e6d..e198f99 100755
--- a/src/app.py
+++ b/src/app.py
@@ -4,12 +4,17 @@ import os
import flask
from flask_minify import Minify
+from werkzeug.middleware.proxy_fix import ProxyFix
import werkzeug.exceptions as HTTPerror
import config # noqa: F401 — side-effect: loads dev env vars
from monitor import monitor, SERVICES
app = flask.Flask(__name__)
+# Trust the scheme/host Cloudflare sets, so redirects Werkzeug builds itself
+# (e.g. the trailing-slash redirect on /api/goto) use https instead of the
+# plaintext scheme it sees on its own socket.
+app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
# ── Static file fingerprinting ────────────────────────────────────────
@@ -42,6 +47,9 @@ def add_headers(response):
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
+ if flask.request.is_secure:
+ response.headers['Strict-Transport-Security'] = 'max-age=31536000'
+
if flask.request.path.startswith('/static/'):
response.headers['Cache-Control'] = 'public, max-age=31536000, immutable'
elif flask.request.path in ['/sitemap.xml', '/robots.txt']:
@@ -55,7 +63,7 @@ def add_headers(response):
# ── Load page data ────────────────────────────────────────────────────
def load_json(path):
- with open(path, "r") as f:
+ with open(path, "r", encoding="utf-8") as f:
return json.load(f)
@@ -64,7 +72,7 @@ books = load_json("./static/json/books.json")
skills = load_json("./static/json/skills.json")
pages = load_json("./static/json/pages.json")
-pages['projects']['skillList'] = skills
+pages['about']['skillList'] = skills
pages['projects']['projects'] = projects
pages['home']['books'] = books
pages['books']['books'] = books
diff --git a/src/monitor.py b/src/monitor.py
index b3191e7..80e391e 100644
--- a/src/monitor.py
+++ b/src/monitor.py
@@ -51,6 +51,26 @@ class ServiceMonitor:
for svc in SERVICES
}
self._last_check = None
+ # Cheap placeholder (no DB access) until the first check_all_services()
+ # run replaces it with the real, uptime-backed summary.
+ self._cached_summary = {
+ 'last_check': None,
+ 'next_check': None,
+ 'services': [
+ {
+ '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': None, '7d': None, '30d': None, 'all_time': None},
+ 'total_checks': 0,
+ }
+ for service_id, cached in self._current.items()
+ ],
+ }
self._ensure_schema()
# ── Database helpers ──────────────────────────────────────────
@@ -206,6 +226,7 @@ class ServiceMonitor:
if result['status'] == 'online':
cached['last_online'] = result['timestamp']
self._last_check = datetime.now().isoformat()
+ self._cached_summary = self._build_summary_locked()
# ── Uptime calculations ───────────────────────────────────────
@@ -271,38 +292,48 @@ class ServiceMonitor:
# ── Status summary ────────────────────────────────────────────
+ def _build_summary_locked(self):
+ """Build a JSON-serializable status summary with uptime statistics.
+
+ Hits the database (multiple queries per service) -- only call this
+ from within check_all_services() while self.lock is held, so it runs
+ once per check cycle rather than once per /api/status request.
+ """
+ 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
+
def get_status_summary(self):
- """Build a JSON-serializable status summary with uptime statistics."""
+ """Return the cached status summary, refreshed once per check cycle
+ (see check_all_services), so this never itself touches the database."""
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
+ return self._cached_summary
# ── Background loop ───────────────────────────────────────────
diff --git a/src/requirements.txt b/src/requirements.txt
index 4359593..55caa9e 100644
--- a/src/requirements.txt
+++ b/src/requirements.txt
@@ -5,7 +5,6 @@ click==8.4.1
Flask==3.1.3
Flask-Minify==0.50
gunicorn==26.0.0
-htmlminf==0.1.13
idna==3.16
itsdangerous==2.2.0
Jinja2==3.1.6
diff --git a/src/static/css/App.css b/src/static/css/App.css
index f6b651f..1e4db9e 100755
--- a/src/static/css/App.css
+++ b/src/static/css/App.css
@@ -199,7 +199,7 @@ tr {
min-height: 100vh;
}
-.growBottom :last-child {
+.growBottom > :last-child {
flex-grow: 1;
}
@@ -1595,6 +1595,57 @@ tr {
display: none;
}
+.timeline.collapsed::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ height: 8em;
+ background: linear-gradient(to bottom, transparent, rgba(var(--bg-card-rgb), 0.97));
+ pointer-events: none;
+}
+
+.timeline-expand-wrap {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ justify-content: center;
+ margin-top: 1em;
+ margin-bottom: 2.5em;
+}
+
+.timeline-expand-wrap.overlap {
+ margin-top: -3.75em;
+}
+
+.timeline-expand-btn {
+ padding: 0.65em 2em;
+ border: 1px solid rgba(var(--accent-rgb), 0.5);
+ border-radius: 2em;
+ background: rgba(var(--accent-rgb), 0.2);
+ color: var(--text-heading);
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 0.85rem;
+ font-weight: bold;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ flex-grow: 1;
+ cursor: pointer;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
+ transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
+}
+
+.timeline-expand-btn:hover {
+ background: rgba(var(--accent-rgb), 0.35);
+ border-color: var(--accent);
+ transform: translateY(-1px);
+}
+
+.timeline-expand-btn.hidden {
+ display: none;
+}
+
/* Summary Card */
.summary-card {
padding: 2em;
@@ -1896,6 +1947,23 @@ tr {
margin-top: 1.5em;
}
+.skills-certs-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2em;
+ align-items: start;
+}
+
+.skills-certs-grid .skills-col #skillTree {
+ font-size: large;
+}
+
+@media screen and (max-width: 900px) {
+ .skills-certs-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
.cert-group {
margin-bottom: 1.5em;
min-width: 0;
diff --git a/src/static/json/pages.json b/src/static/json/pages.json
index 93d75a3..7fdbcd3 100755
--- a/src/static/json/pages.json
+++ b/src/static/json/pages.json
@@ -29,10 +29,42 @@
"description": "Face it, you've been ducked",
"canonical": "/duck"
},
- "certificates": {
- "template": "certs.html",
- "title": "Certifications & Awards - Andrew Simonson",
- "description": "Data science, DevOps, and entrepreneurship certifications earned by Andrew Simonson.",
- "canonical": "/certs"
+ "about": {
+ "template": "about.html",
+ "title": "About, Timeline & Certifications - Andrew Simonson",
+ "description": "Bio, career/education timeline, and certifications for Andrew Simonson - Data Scientist at Ecolab.",
+ "canonical": "/about",
+ "timeline": {
+ "ecolab": {
+ "title": "Data Scientist / Data Engineer — Ecolab",
+ "date": "January 2024 – Present",
+ "content": "Data Scientist / Data Engineer
Ecolab · Saint Paul, MN
• Primary model developer of RushReady from inception through multimillion-dollar product commercialization
• Engineered near-real-time data platform with Delta Live Tables and automated MLOps lifecycle across QSR brands
• Built explainable anomaly detection with XGBoost/SHAP attribution and brand-configurable recommendations, increasing speed of service by 20%
• Represented Ecolab as liaison in Microsoft's 100-member AI accelerator program",
+ "classes": "experience"
+ },
+ "rit_ms": {
+ "title": "M.S. Data Science — RIT",
+ "date": "Dec 2026 (expected)",
+ "content": "Master of Science in Data Science
Rochester Institute of Technology · Rochester, NY
• Focus on probability theory, statistical learning, and Bayesian methods
• Capstone: Fall 2026",
+ "classes": "education"
+ },
+ "dow_chemical": {
+ "title": "Data Engineer — Dow Chemical",
+ "date": "January 2023 – May 2023",
+ "content": "Data Engineer
Dow Chemical · Freeport, TX
• Independently built reactive chemistry analysis Flask app encoding adiabatic correction and exotherm-detection logic across 9 test types
• Architected object-model abstraction, decoupling test-type parsers from report generation and replacing 4 legacy VBA tools
• Product became the interdepartmental data standard, saving >1,020 hours (~0.65 FTE) annually",
+ "classes": "experience"
+ },
+ "rit_bs": {
+ "title": "B.S. Computer Science & Data Science — RIT",
+ "date": "Dec 2024",
+ "content": "B.S. in Computer Science · B.S. in Data Science (Dual Cluster)
Rochester Institute of Technology · Rochester, NY
• GPA 3.63 · Dean's List
• Minor in International Relations
• MicroMasters in Data Science – UC San Diego (edX)",
+ "classes": "education"
+ },
+ "csh": {
+ "title": "E-Board Member & Presenter — CSH",
+ "date": "Aug 2021 – Present",
+ "content": "E-Board Member & Presenter
Computer Science House · Rochester, NY
• Presented seminars on web scraping, analytics, and GIS
• Built and maintained web services for 80+ members",
+ "classes": "experience technical"
+ }
+ }
}
}
diff --git a/src/templates/about.html b/src/templates/about.html
index 8c5e847..c1d7078 100644
--- a/src/templates/about.html
+++ b/src/templates/about.html
@@ -4,11 +4,35 @@
+ I'm Andrew Simonson,
+ a Data Scientist at Ecolab and a graduate Data
+ Science student at
+ Rochester Institute of Technology, having
+ recently completed the Computer Science BS program
+ (international relations minor) with a focus on probability
+ theory.
+
+
+ I get bored and throw random stuff on this website.
+ This is what unprofessional development looks like.
+
+ I also have a + resume + for unexplained reasons. +
++ Comprehensive list verifiable on + LinkedIn +
+ + + +Rochester Institute of Technology
+- Comprehensive list verifiable on - LinkedIn -
-Computer Science BS and Data Science MS from Rochester Institute of Technology
- - - -Rochester Institute of Technology
-
- I'm Andrew Simonson,
- a Data Scientist at Ecolab and a graduate Data
- Science student at
- Rochester Institute of Technology, having
- recently completed the Computer Science BS program
- (international relations minor) with a focus on probability
- theory.
-
-
- I get bored and throw random stuff on this website.
- This is what unprofessional development looks like.
-
- I also have a - certifications page - and a - resume - for unexplained reasons. + Curious who's behind these? + About & certifications + · + Resume