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

About

-

My story, in order

+

Bio, timeline & credentials

+ + +
+

+ 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. +

+
+

Career & Education Timeline

@@ -26,7 +50,7 @@
{{ item.date }} - {{ name }} + {{ item.title }}
{{ item.content | safe }} @@ -42,32 +66,146 @@
{% endfor %}
+ +
+ +
+ +
+

Skills & Certifications

+
+ +
{% endblock %} diff --git a/src/templates/certs.html b/src/templates/certs.html deleted file mode 100755 index db031b3..0000000 --- a/src/templates/certs.html +++ /dev/null @@ -1,50 +0,0 @@ -{% block content %} -
-
-

Certifications

-

- Comprehensive list verifiable on - LinkedIn -

-

Computer Science BS and Data Science MS from Rochester Institute of Technology

- -
-

UCSanDiegoX · edX

-

Data Science MicroMasters Program

- - View Program Certificate › - - -
- -
- - -
-

Rochester Institute of Technology

-

Entrepreneurial Certifications

- -
-
-
-{% endblock %} diff --git a/src/templates/header.html b/src/templates/header.html index bbc5e63..3125692 100755 --- a/src/templates/header.html +++ b/src/templates/header.html @@ -110,6 +110,9 @@ + diff --git a/src/templates/projects.html b/src/templates/projects.html index 6416ad6..66354f3 100755 --- a/src/templates/projects.html +++ b/src/templates/projects.html @@ -10,38 +10,16 @@
- +
-

- 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

- -
-

Technologies

- {% from 'partials/skills.html' import skills %} {{ - skills(var['skillList']) }} -
-

Projects