Compare commits

..

4 Commits

Author SHA1 Message Date
8432cc0bf6 content screen height fix 2026-08-23 00:53:32 -05:00
21ebd13a07 remove old service from list 2026-08-23 00:43:58 -05:00
78aa49c0f3 local review resolutions, no content change 2026-08-23 00:42:42 -05:00
9fc5bfe9c7 test 2026-08-22 22:00:28 -05:00
15 changed files with 538 additions and 318 deletions

View File

@@ -62,7 +62,6 @@ def load_json(path):
projects = load_json("./static/json/projects.json") projects = load_json("./static/json/projects.json")
books = load_json("./static/json/books.json") books = load_json("./static/json/books.json")
skills = load_json("./static/json/skills.json") skills = load_json("./static/json/skills.json")
timeline = load_json("./static/json/timeline.json")
pages = load_json("./static/json/pages.json") pages = load_json("./static/json/pages.json")
pages['projects']['skillList'] = skills pages['projects']['skillList'] = skills

View File

@@ -16,7 +16,7 @@ SERVICES = [
# {'id': 'EternalRelays', 'name': 'eternalrelays.com', 'url': 'https://eternalrelays.com', 'timeout': 10}, # {'id': 'EternalRelays', 'name': 'eternalrelays.com', 'url': 'https://eternalrelays.com', 'timeout': 10},
{'id': 'files', 'name': 'files.asimonson.com', 'url': 'https://files.asimonson.com', 'timeout': 10}, {'id': 'files', 'name': 'files.asimonson.com', 'url': 'https://files.asimonson.com', 'timeout': 10},
{'id': 'git', 'name': 'git.asimonson.com', 'url': 'https://git.asimonson.com', 'timeout': 10}, {'id': 'git', 'name': 'git.asimonson.com', 'url': 'https://git.asimonson.com', 'timeout': 10},
{'id': 'cascadalyst', 'name': 'cascadalyst.com', 'url': 'https://cascadalyst.com', 'timeout': 10}, # {'id': 'cascadalyst', 'name': 'cascadalyst.com', 'url': 'https://cascadalyst.com', 'timeout': 10},
] ]
CHECK_INTERVAL = 60 # seconds between checks CHECK_INTERVAL = 60 # seconds between checks

View File

@@ -193,6 +193,16 @@ tr {
position: unset !important; position: unset !important;
} }
.growBottom {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.growBottom :last-child {
flex-grow: 1;
}
.header h1 { .header h1 {
display: inline-block; display: inline-block;
transition: .4s; transition: .4s;
@@ -551,17 +561,15 @@ tr {
} }
#skillTree { #skillTree {
border-left: var(--accent) solid 3px;
border-radius: 0.5em; border-radius: 0.5em;
background: rgba(var(--bg-card-rgb), 0.85); background: rgba(var(--bg-card-rgb), 0.6);
font-size: x-large; font-size: x-large;
margin-bottom: 10px; margin-bottom: 10px;
padding: 1em; padding: 1em 1.25em;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
} }
.skill { .skill {
padding: .2em; padding: 0.2em 0;
color: var(--text-heading); color: var(--text-heading);
} }
@@ -594,21 +602,26 @@ tr {
.skill > .skillname { .skill > .skillname {
text-decoration: underline; text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer; cursor: pointer;
transition: color 0.2s ease, padding-left 0.2s ease; transition: color 0.2s ease;
padding: 0.2em 0; padding: 0.2em 0;
display: inline-block;
} }
.skill > .skillname:hover { .skill > .skillname:hover {
padding-left: 0.3em; color: #ddd;
} }
.skill > .skillname::after { .skill > .skillname::after {
content: " >"; content: " \2192";
font-size: 0.85em;
margin-left: 0.2em;
text-decoration: none;
} }
.skill.activeSkill > .skillname::after { .skill.activeSkill > .skillname::after {
content: " <"; content: " \2190";
} }
.skill[data-length="0"].activeSkill > .skillname, .skill[data-length="0"] > .skillname:hover, .skill.activeSkill > .skillname:hover { .skill[data-length="0"].activeSkill > .skillname, .skill[data-length="0"] > .skillname:hover, .skill.activeSkill > .skillname:hover {
@@ -628,8 +641,8 @@ tr {
/* Project List Grid */ /* Project List Grid */
.projectList { .projectList {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 480px)); grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5em; gap: 1.25em;
width: 100%; width: 100%;
} }
@@ -637,61 +650,26 @@ tr {
.project { .project {
position: relative; position: relative;
background: rgba(var(--bg-card-rgb), 0.92); background: rgba(var(--bg-card-rgb), 0.92);
border: 1px solid rgba(var(--accent-rgb), 0.15); border: 1px solid var(--border-subtle);
border-left: 3px solid rgba(var(--accent-rgb), 0.75); border-radius: 0.75em;
overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; transition: border-color 0.25s ease, box-shadow 0.25s ease, transform 0.2s ease;
transition: border-color 0.3s ease, box-shadow 0.3s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
}
/* Corner bracket: top-right */
.project::before {
content: '';
position: absolute;
top: -1px;
right: -1px;
width: 18px;
height: 18px;
border-top: 2px solid rgba(var(--accent-rgb), 0.5);
border-right: 2px solid rgba(var(--accent-rgb), 0.5);
z-index: 5;
pointer-events: none;
transition: border-color 0.3s ease;
}
/* Corner bracket: bottom-left */
.project::after {
content: '';
position: absolute;
bottom: -1px;
left: 2px;
width: 18px;
height: 18px;
border-bottom: 2px solid rgba(var(--accent-rgb), 0.5);
border-left: 2px solid rgba(var(--accent-rgb), 0.5);
z-index: 5;
pointer-events: none;
transition: border-color 0.3s ease;
} }
.project:hover { .project:hover {
border-color: rgba(var(--accent-rgb), 0.45); border-color: rgba(var(--accent-rgb), 0.35);
border-left-color: rgb(var(--accent-rgb)); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5), inset 0 0 0 1px rgba(var(--accent-rgb), 0.08); transform: translateY(-2px);
}
.project:hover::before,
.project:hover::after {
border-color: rgba(var(--accent-rgb), 0.9);
} }
/* Project Image */ /* Project Image */
.projImageWrap { .projImageWrap {
position: relative; position: relative;
width: 100%; width: 100%;
height: 200px; height: 180px;
overflow: hidden; overflow: hidden;
background: #0c0c0c; background: #0c0c0c;
flex-shrink: 0; flex-shrink: 0;
@@ -702,140 +680,89 @@ tr {
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
object-position: center top; object-position: center top;
filter: saturate(0.6) brightness(0.7); filter: brightness(0.88);
transition: filter 0.5s ease, transform 0.5s ease; transition: filter 0.3s ease;
display: block; display: block;
} }
.project:hover .projImageWrap img { .project:hover .projImageWrap img {
filter: saturate(0.85) brightness(0.85); filter: brightness(1);
transform: scale(1.04);
}
/* Scanlines overlay */
.projImageWrap::before {
content: '';
position: absolute;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 3px,
rgba(0, 0, 0, 0.1) 3px,
rgba(0, 0, 0, 0.1) 4px
);
z-index: 2;
pointer-events: none;
}
/* Gradient fade into card body */
.projImageWrap::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 55%;
background: linear-gradient(to bottom, transparent, rgba(var(--bg-card-rgb), 0.98));
z-index: 3;
} }
/* Placeholder for projects without an image */ /* Placeholder for projects without an image */
.projImagePlaceholder { .projImagePlaceholder {
width: 100%; width: 100%;
height: 100%; height: 100%;
background-image: background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.06) 25%, transparent 25%) -4px 0,
repeating-linear-gradient( linear-gradient(225deg, rgba(var(--accent-rgb), 0.06) 25%, transparent 25%) -4px 0,
45deg, linear-gradient(315deg, rgba(var(--accent-rgb), 0.06) 25%, transparent 25%) 4px 0,
rgba(var(--accent-rgb), 0.04) 0px, linear-gradient(45deg, rgba(var(--accent-rgb), 0.06) 25%, transparent 25%) 4px 0;
rgba(var(--accent-rgb), 0.04) 1px, background-size: 8px 8px;
transparent 1px, background-color: rgba(30, 30, 30, 0.6);
transparent 8px
),
repeating-linear-gradient(
-45deg,
rgba(var(--accent-rgb), 0.04) 0px,
rgba(var(--accent-rgb), 0.04) 1px,
transparent 1px,
transparent 8px
);
} }
/* Status Badge */ /* Status indicator (inline, in card body) */
.proj-status-badge { .proj-status {
position: absolute;
top: 0.75em;
right: 0.75em;
z-index: 4;
display: inline-flex;
align-items: center;
gap: 0.45em;
padding: 0.25em 0.65em 0.25em 0.5em;
font-family: 'Courier New', Courier, monospace;
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
border: 1px solid currentColor;
background: rgba(0, 0, 0, 0.65);
}
.proj-status-badge.complete { color: var(--status-online); }
.proj-status-badge.WIP { color: var(--status-degraded); }
.proj-status-badge.incomplete { color: var(--status-offline); }
.status-indicator {
display: inline-block; display: inline-block;
width: 6px; font-size: 0.68rem;
height: 6px; font-weight: 600;
border-radius: 50%; letter-spacing: 0.06em;
color: inherit; text-transform: uppercase;
background: currentColor; padding: 0.15em 0.5em;
box-shadow: 0 0 6px currentColor; border-radius: 0.2em;
flex-shrink: 0; flex-shrink: 0;
} }
.proj-status-badge.complete .status-indicator { .proj-status.complete { color: var(--status-online); background: rgba(76, 175, 80, 0.1); }
animation: statusPulse 2.5s ease-in-out infinite; .proj-status.WIP { color: var(--status-degraded); background: rgba(255, 193, 7, 0.1); }
} .proj-status.incomplete { color: var(--status-offline); background: rgba(244, 67, 54, 0.1); }
.proj-status-badge.WIP .status-indicator {
animation: statusPulse 0.9s ease-in-out infinite;
}
.proj-status-badge.incomplete .status-indicator {
opacity: 0.45;
}
@keyframes statusPulse {
0%, 100% { opacity: 1; box-shadow: 0 0 6px currentColor; }
50% { opacity: 0.2; box-shadow: none; }
}
/* Project Content */ /* Project Content */
.projContent { .projContent {
padding: 1.1em 1.4em 1.4em 1.2em; padding: 1.2em 1.4em 1.4em;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
gap: 0.6em; gap: 0.5em;
}
.projTitleRow {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.75em;
} }
.project h3 { .project h3 {
margin: 0; margin: 0;
font-size: 1.05rem; font-size: 1rem;
font-weight: 700; font-weight: 600;
color: var(--text-heading); color: var(--text-heading);
letter-spacing: 0.02em; line-height: 1.35;
line-height: 1.3; }
display: block;
border-bottom: none; /* Project category tags */
.projTags {
display: flex;
flex-wrap: wrap;
gap: 0.35em;
}
.proj-tag {
font-size: 0.65rem;
font-weight: 500;
letter-spacing: 0.04em;
text-transform: capitalize;
padding: 0.1em 0.45em;
border-radius: 0.2em;
color: var(--text-secondary);
background: rgba(var(--accent-rgb), 0.06);
} }
.projDesc { .projDesc {
font-size: 0.83rem; font-size: 0.82rem;
line-height: 1.65; line-height: 1.6;
color: rgba(236, 235, 235, 0.65); color: var(--text-secondary);
margin: 0; margin: 0;
flex: 1; flex: 1;
} }
@@ -852,47 +779,29 @@ tr {
.proj-link { .proj-link {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.45em; gap: 0.4em;
padding: 0.3em 0.8em 0.3em 0.6em; color: var(--text-secondary);
border: 1px solid rgba(var(--accent-rgb), 0.35);
background: rgba(var(--accent-rgb), 0.07);
color: rgba(236, 235, 235, 0.75);
text-decoration: none; text-decoration: none;
font-family: 'Courier New', Courier, monospace; font-size: 0.8rem;
font-size: 0.68rem;
letter-spacing: 0.1em;
text-transform: uppercase;
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
.proj-link::before {
content: '>';
color: rgba(var(--accent-rgb), 0.7);
font-weight: 700;
transition: color 0.2s ease; transition: color 0.2s ease;
} }
.proj-link:hover { .proj-link:hover {
background: rgba(var(--accent-rgb), 0.18); color: var(--text-heading);
border-color: rgba(var(--accent-rgb), 0.7); text-decoration: underline;
color: #ecebeb;
}
.proj-link:hover::before {
color: rgb(var(--accent-rgb));
} }
.proj-link img.projectLink { .proj-link img.projectLink {
height: 0.9em; height: 1em;
width: auto; width: auto;
filter: invert(1) opacity(0.75); filter: invert(1) opacity(0.65);
padding: 0; padding: 0;
margin: 0; margin: 0;
transition: filter 0.2s ease; transition: filter 0.2s ease, opacity 0.2s ease;
} }
.proj-link:hover img.projectLink { .proj-link:hover img.projectLink {
filter: invert(1); filter: invert(1) opacity(0.9);
} }
.grow { .grow {
@@ -1004,8 +913,8 @@ tr {
.homeSubContent { .homeSubContent {
background-color: rgba(var(--bg-card-rgb), 0.85); background-color: rgba(var(--bg-card-rgb), 0.85);
padding: 2em 8em; padding: 2.5em 6em;
border-top: solid 4px rgba(var(--accent-rgb), 0.5); border-top: solid 2px rgba(var(--accent-rgb), 0.25);
border-radius: 0.5em 0.5em 0 0; border-radius: 0.5em 0.5em 0 0;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.3); box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.3);
} }
@@ -1327,7 +1236,7 @@ tr {
@media screen and (max-width: 1400px) { @media screen and (max-width: 1400px) {
.projectList { .projectList {
grid-template-columns: repeat(auto-fill, minmax(280px, 420px)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
} }
#homepage { #homepage {
@@ -1459,9 +1368,6 @@ tr {
margin: auto; margin: auto;
} }
.projDesc {
margin: 0;
}
.col { .col {
width: 100%; width: 100%;
margin-bottom: 0; margin-bottom: 0;
@@ -1550,6 +1456,145 @@ tr {
font-size: 1rem; font-size: 1rem;
} }
/* Timeline */
.timeline {
position: relative;
padding-left: 2em;
}
.timeline::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 2px;
background: linear-gradient(to bottom, rgba(var(--accent-rgb), 0.05), rgba(var(--accent-rgb), 0.7) 10%, rgba(var(--accent-rgb), 0.7) 90%, rgba(var(--accent-rgb), 0.05));
}
.timeitem {
position: relative;
margin-bottom: 1.5em;
transition: .4s;
overflow-y: clip;
height: auto;
max-height: 500px;
}
.timeitem::before {
content: '';
position: absolute;
left: -2.15em;
top: 0.5em;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--bg-base);
border: 2px solid rgba(var(--accent-rgb), 0.6);
z-index: 1;
transition: background 0.25s ease, border-color 0.25s ease;
}
.timeitem:hover::before {
background: rgba(var(--accent-rgb), 0.4);
border-color: var(--accent);
}
.timeitem-inner {
position: relative;
padding: 1em 1.25em;
}
.timeitem-header {
display: flex;
flex-direction: column;
gap: 0.25em;
margin-bottom: 0.5em;
}
.timeitem-header strong {
color: var(--text-heading);
font-size: 1.05rem;
}
.timeitem-header .datetext {
font-size: 0.8rem;
color: var(--text-secondary);
font-family: 'Courier New', Courier, monospace;
}
.timeitem-content {
color: var(--text-body);
font-size: 0.9rem;
line-height: 1.6;
margin-bottom: 0.75em;
}
.timeitem-tags {
display: flex;
flex-wrap: wrap;
gap: 0.4em;
}
.tag {
display: inline-block;
padding: 0.15em 0.6em;
border-radius: 0.25em;
font-size: 0.7rem;
font-family: 'Courier New', Courier, monospace;
letter-spacing: 0.05em;
text-transform: uppercase;
border: 1px solid rgba(var(--accent-rgb), 0.35);
color: var(--text-secondary);
background: rgba(var(--accent-rgb), 0.07);
}
.tag.pinned {
border-color: rgba(var(--accent-rgb), 0.7);
background: rgba(var(--accent-rgb), 0.15);
color: var(--text-heading);
}
.tag.experience { color: #6cb4ee; }
.tag.education { color: #8bc34a; }
.tag.technical { color: #ce93d8; }
/* Timeline filter buttons */
.timeline-filters {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-bottom: 1.5em;
}
.filter-btn {
padding: 0.35em 0.9em;
border: 1px solid rgba(var(--accent-rgb), 0.3);
border-radius: 0.25em;
background: transparent;
color: var(--text-secondary);
font-family: 'Courier New', Courier, monospace;
font-size: 0.78rem;
letter-spacing: 0.05em;
text-transform: uppercase;
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
.filter-btn:hover, .filter-btn.active {
border-color: rgba(var(--accent-rgb), 0.7);
background: rgba(var(--accent-rgb), 0.12);
color: var(--text-heading);
}
.filter-btn.active {
border-color: var(--accent);
}
.timeitem.hidden {
display: none;
}
/* Summary Card */ /* Summary Card */
.summary-card { .summary-card {
padding: 2em; padding: 2em;
@@ -1973,3 +2018,28 @@ tr {
from { background-position: 200% 0; } from { background-position: 200% 0; }
to { background-position: -200% 0; } to { background-position: -200% 0; }
} }
/* Skip-to-content link (accessible navigation) */
.skip-link {
position: fixed;
top: -100%;
left: 50%;
transform: translateX(-50%);
z-index: 10000;
background: #000;
color: #fff;
padding: 0.75rem 1.5rem;
font-size: 0.95rem;
font-weight: 600;
text-decoration: none;
border: 2px solid currentColor;
border-radius: 0 0 4px 4px;
white-space: nowrap;
transition: top 0.15s ease;
}
.skip-link:focus {
top: 0;
outline: 2px solid #4E9A26;
outline-offset: 0;
}

View File

@@ -2,13 +2,17 @@ function toggleMenu(collapse) {
if (window.innerWidth < 1400) { if (window.innerWidth < 1400) {
const menu = document.querySelector(".navControl"); const menu = document.querySelector(".navControl");
const bar = document.querySelector(".header"); const bar = document.querySelector(".header");
const btn = document.getElementById("menu");
const isCollapsed = !menu.style.maxHeight || menu.style.maxHeight === "0px"; const isCollapsed = !menu.style.maxHeight || menu.style.maxHeight === "0px";
if (isCollapsed && !collapse) { if (isCollapsed && !collapse) {
menu.style.maxHeight = `${menu.scrollHeight + 10}px`; menu.style.maxHeight = `${menu.scrollHeight + 10}px`;
bar.style.borderBottomWidth = "0px"; bar.style.borderBottomWidth = "0px";
if (btn) btn.setAttribute("aria-expanded", "true");
} else { } else {
menu.style.maxHeight = "0px"; menu.style.maxHeight = "0px";
bar.style.borderBottomWidth = "3px"; bar.style.borderBottomWidth = "3px";
if (btn) btn.setAttribute("aria-expanded", "false");
} }
} }
} }
@@ -98,10 +102,14 @@ function activeSkill(obj) {
let skill = obj.closest(".skill"); let skill = obj.closest(".skill");
if (skill.classList.contains("activeSkill")) { if (skill.classList.contains("activeSkill")) {
skill.classList.remove("activeSkill"); skill.classList.remove("activeSkill");
const btn = obj.closest('.skillname') || obj.querySelector?.('.skillname') || obj;
if (btn && btn.setAttribute) btn.setAttribute("aria-expanded", "false");
return; return;
} }
while (skill) { while (skill) {
skill.classList.add("activeSkill"); skill.classList.add("activeSkill");
const nameEl = skill.querySelector?.(':scope > .skillname') || skill.querySelector('.skillname');
if (nameEl) nameEl.setAttribute("aria-expanded", "true");
skill = skill.parentElement.closest(".skill"); skill = skill.parentElement.closest(".skill");
} }
} }

View File

@@ -1,8 +1,8 @@
{ {
"home": { "home": {
"template": "home.html", "template": "home.html",
"title": "Andrew Simonson - Portfolio Home", "title": "Andrew Simonson - Data Scientist & Researcher",
"description": "Andrew Simonson's Digital Portfolio home", "description": "Andrew Simonson - Data Scientist at Ecolab, graduate student at RIT. Projects, research papers, and more.",
"canonical": "/" "canonical": "/"
}, },
"status": { "status": {
@@ -13,8 +13,8 @@
}, },
"projects": { "projects": {
"template": "projects.html", "template": "projects.html",
"title": "Andrew Simonson - Projects", "title": "Projects & Research - Andrew Simonson",
"description": "Recent projects by Andrew Simonson on his lovely portfolio website :)", "description": "Data science projects, geospatial analysis, research papers, and software experiments by Andrew Simonson.",
"canonical": "/projects" "canonical": "/projects"
}, },
"books": { "books": {
@@ -31,8 +31,8 @@
}, },
"certificates": { "certificates": {
"template": "certs.html", "template": "certs.html",
"title": "Certificates and Awards", "title": "Certifications & Awards - Andrew Simonson",
"description": "Certificates and Awards Listing", "description": "Data science, DevOps, and entrepreneurship certifications earned by Andrew Simonson.",
"canonical": "/certs" "canonical": "/certs"
} }
} }

View File

@@ -1,18 +1,53 @@
{ {
"Co-op @ Dow Chemical": { "Data Scientist @ Ecolab": {
"classes": "pinned experience technical",
"date": "01/2024 - Present",
"content": "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. Built explainable anomaly detection with XGBoost/SHAP attribution, increasing speed of service by 20%. Represented Ecolab in Microsoft's 100-member AI accelerator program."
},
"Data Scientist / Data Engineer": {
"classes": "pinned experience technical", "classes": "pinned experience technical",
"date": "01/2023 - 05/2023", "date": "01/2023 - 05/2023",
"content": "Spring 2023 Semester Co-op under Dow Chemical's Global Reactive Chemicals team in Analytical Sciences. Responsibilities included management of chemical compatability data and tool creation for parsing, generating, and submitting reports." "content": "Co-op under Dow Chemical's Global Reactive Chemicals team in Analytical Sciences. Independently built reactive chemistry analysis Flask app encoding adiabatic correction and exotherm-detection logic. Replaced 4 legacy VBA tools with an object-model abstraction architecture. Product became the interdepartmental data standard, saving >1,020 hours (~0.65 FTE) annually."
},
"Rochester Institute of Technology — M.S.": {
"classes": "pinned education technical",
"date": "01/2023 - 12/2026",
"content": "Data Science M.S. with dual cluster in Data Science and AI. Capstone Fall 2026. Certified by Databricks in ML Model Deployment, Retrieval Agents, and MLOps. Certified by Microsoft in Data Analysis Career Essentials and Docker Foundations."
},
"Rochester Institute of Technology — B.S.": {
"classes": "pinned education technical",
"date": "08/2021 - 12/2024",
"content": "Computer Science B.S. (GPA 3.63, Dean's List) with a minor in International Relations. Active member of Computer Science House — built and maintained web services for 80+ members, presented seminars on web scraping, analytics, and GIS."
},
"UCSanDiegoX Data Science MicroMasters": {
"classes": "education technical",
"date": "2024",
"content": "Completed MicroMasters program in Data Science through UC San Diego on edX."
}, },
"Started Portfolio": { "Started Portfolio": {
"classes": "", "classes": "",
"date": "08/26/2021", "date": "08/26/2021",
"content": "I started building this website on this day. I wish I could say I was farther along than I am." "content": "I started building this website on this day. I wish I could say I was farther along than I am."
}, },
"Rochester Institute of Technology": { "Human vs AI Code Smell Study": {
"classes": "pinned education technical", "classes": "technical",
"date": "08/2021 - 12/2024", "date": "2024 - 2025",
"content": "Studying in Rochester Institute of Technology's Computer Science BS program with a minor in International Relations." "content": "Analyzed 85K+ code definitions comparing AI vs human code maintainability via static analysis and non-parametric statistics. Found AI code carries 3-4x more structural debt invisible to reviewers."
},
"Generalized Trust Review": {
"classes": "technical",
"date": "2023 - 2024",
"content": "Cross-national correlation analysis of media and science trust across 128 countries with subgroup OLS regression. Found trust transfer attenuates 5.7x in low press-freedom regimes."
},
"Antietam Watershed Monitoring": {
"classes": "technical",
"date": "2023",
"content": "ArcGIS choropleth dashboard monitoring stream temps and biotic indices across Upper Potomac sub-watersheds. Flags exceedances of healthy stream conditions for state environmental review."
},
"Hackathon Awards": {
"classes": "technical",
"date": "2023 - 2024",
"content": "CSHacks - Best Use of AI (Paychex). Tiger Games - Social Impact Winner. HACK.COMS '24 - Accessibility Winner."
}, },
"Pretzel & Pizza Creations": { "Pretzel & Pizza Creations": {
"classes": "experience", "classes": "experience",
@@ -27,11 +62,11 @@
"Boonsboro High School": { "Boonsboro High School": {
"classes": "education", "classes": "education",
"date": "09/2016 - 06/2021", "date": "09/2016 - 06/2021",
"content": "Graduated high school with highest honors.\nMember of National Honor Society, Academic Team County Champions. Participated in Physics Olympics, Robotics Club, and scored at state championships in Cross Country and Track and Field (4x800, 800)." "content": "Graduated high school with highest honors. Member of National Honor Society, Academic Team County Champions. Participated in Physics Olympics, Robotics Club, and scored at state championships in Cross Country and Track and Field (4x800, 800)."
}, },
"Vex Robotics Team Lead/Club Preisdent": { "Vex Robotics Team Lead/Club President": {
"classes": "technical", "classes": "technical",
"date": "10/2015 - 04/2021", "date": "10/2015 - 04/2021",
"content": "Led 5 teams through middle and high school to VEX Robotics Competitions, elevating Boonsboro from county group-stage elimination to its first state championship participation. Reorganized club and set up its first interface with the community + sponsors" "content": "Led 5 teams through middle and high school to VEX Robotics Competitions, elevating Boonsboro from county group-stage elimination to its first state championship participation. Reorganized club and set up its first interface with the community + sponsors."
} }
} }

73
src/templates/about.html Normal file
View File

@@ -0,0 +1,73 @@
{% block content %}
<div class="foreground"></div>
<div class="relative growBottom">
<div id="nametag" class="flex" data-aos="fade-up">
<div>
<h1 class="textGrad">About</h1>
<h2>My story, in order</h2>
</div>
</div>
<div class="homeSubContent">
<div class="foregroundContent">
<div class="concentratedHead">
<h3>Career &amp; Education Timeline</h3>
</div>
<div class="timeline-filters" role="toolbar" aria-label="Filter timeline entries">
<button class="filter-btn active" data-filter="all" aria-pressed="true">All</button>
<button class="filter-btn" data-filter="experience" aria-pressed="false">Experience</button>
<button class="filter-btn" data-filter="education" aria-pressed="false">Education</button>
<button class="filter-btn" data-filter="technical" aria-pressed="false">Technical</button>
</div>
<div class="timeline" id="timeline">
{% for name, item in var['timeline'].items() %}
<div class="timeitem" data-categories="{{ item.classes }}">
<div class="timeitem-inner boxed">
<div class="timeitem-header">
<span class="datetext">{{ item.date }}</span>
<strong>{{ name }}</strong>
</div>
<div class="timeitem-content">
{{ item.content | safe }}
</div>
{% if item.classes %}
<div class="timeitem-tags">
{% for cls in item.classes.split() %}
<span class="tag tag-{{ cls }}">{{ cls | replace('_', ' ') | title }}</span>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const buttons = document.querySelectorAll('.filter-btn');
const items = document.querySelectorAll('.timeitem');
buttons.forEach(function(btn) {
btn.addEventListener('click', function() {
var filter = this.getAttribute('data-filter');
buttons.forEach(function(b) { b.classList.remove('active'); b.setAttribute('aria-pressed', 'false'); });
this.classList.add('active');
this.setAttribute('aria-pressed', 'true');
items.forEach(function(item) {
var cats = item.getAttribute('data-categories') || '';
if (filter === 'all' || cats.indexOf(filter) !== -1) {
item.classList.remove('hidden');
} else {
item.classList.add('hidden');
}
});
});
});
});
</script>
{% endblock %}

View File

@@ -3,14 +3,15 @@
<div class="foregroundContent"> <div class="foregroundContent">
<h2>"You can't judge a book by its cover but you can judge a person by their bookshelf" - Me, today.</h2> <h2>"You can't judge a book by its cover but you can judge a person by their bookshelf" - Me, today.</h2>
<h3>My Favorites</h3> <h3>My Favorites</h3>
<div class='flex wrap boxed'> <div class='flex wrap boxed' role="list" aria-label="Favorite books">
{% for i in var.books.selection %} {% for i in var.books.selection %}
<div> <div role="listitem">
<a href="#'{{i}}'"> <a href="#'{{i}}'" aria-label="View review of {{ i }}">
<img <img
class="bookcover" class="bookcover"
alt="{{i}} cover" alt="{{ i }} cover"
src="{{ url_for('static', filename=('photos/books/' + var.books.books[i].filename))}}" src="{{ url_for('static', filename=('photos/books/' + var.books.books[i].filename))}}"
loading="lazy"
/> />
</a> </a>
</div> </div>
@@ -18,16 +19,17 @@
</div> </div>
<h3>All Reviews</h3> <h3>All Reviews</h3>
<p>Hover to reveal review<br />Read to reveal KNOWLEDGE</p> <p>Hover to reveal review<br />Read to reveal KNOWLEDGE</p>
<div class="booklist flex wrap"> <div class="booklist flex wrap" role="list" aria-label="Book reviews">
{% for i in var.books.books %} {% for i in var.books.books %}
<div id="'{{i}}'" class="bookReview"> <div id="'{{i}}'" class="bookReview" role="listitem" aria-label="Review of {{ i }}">
<div class='fullHeight flex'> <div class='fullHeight flex'>
<div> <div>
<div class='heightBox'> <div class='heightBox'>
<img <img
class="bookcover" class="bookcover"
alt="{{i}} cover" alt="{{ i }} cover"
src="{{ url_for('static', filename=('photos/books/' + var.books.books[i].filename))}}" src="{{ url_for('static', filename=('photos/books/' + var.books.books[i].filename))}}"
loading="lazy"
/> />
</div> </div>
</div> </div>
@@ -36,7 +38,7 @@
<h4 class='nomargin'>{{ i }}</h4> <h4 class='nomargin'>{{ i }}</h4>
<p class='nomargin'>{{ var.books.books[i].review }}</p> <p class='nomargin'>{{ var.books.books[i].review }}</p>
</div> </div>
<a href="{{ var.books.books[i].link }}"><p>Book Source</p></a> <a href="{{ var.books.books[i].link }}" rel="noopener noreferrer" aria-label="Buy or learn more about {{ i }}">Book Source</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -14,7 +14,7 @@
<link rel="preconnect" href="https://cdn.jsdelivr.net" /> <link rel="preconnect" href="https://cdn.jsdelivr.net" />
<meta name="description" content="{{ var['description'] }}" /> <meta name="description" content="{{ var['description'] }}" />
<meta property="og:title" content="Andrew Simonson" /> <meta property="og:title" content="Andrew Simonson" />
<meta name="og:description" content="{{ var['description'] }}" /> <meta property="og:description" content="{{ var['description'] }}" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta <meta
property="og:image" property="og:image"
@@ -32,6 +32,29 @@
<meta name="twitter:image:alt" content="some example picture idk" /> <meta name="twitter:image:alt" content="some example picture idk" />
<meta name="twitter:site" content="@asimonson1125" /> <meta name="twitter:site" content="@asimonson1125" />
<!-- Structured data for SEO -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Andrew Simonson",
"url": "https://asimonson.com",
"sameAs": [
"https://github.com/asimonson1125",
"https://www.linkedin.com/in/simonsonandrew"
],
"jobTitle": "Data Scientist",
"worksFor": {
"@type": "Organization",
"name": "Ecolab"
},
"alumniOf": {
"@type": "CollegeOrUniversity",
"name": "Rochester Institute of Technology"
}
}
</script>
<!-- Global site tag (gtag.js) - Google Analytics (deferred for performance) --> <!-- Global site tag (gtag.js) - Google Analytics (deferred for performance) -->
<script <script
async async
@@ -59,12 +82,13 @@
</head> </head>
{% block header %} {% block header %}
<body onpopstate="backButton()"> <body onpopstate="backButton()">
<a class="skip-link" href="#root">Skip to content</a>
<div id="loading-bar"></div> <div id="loading-bar"></div>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<main id='map'></main> <main id='map' aria-label="Main content"></main>
<div id="contentStuffer"> <div id="contentStuffer">
<div class="header"> <header class="header">
<div id="name-container" onclick="goto('home')"> <div id="name-container" onclick="goto('home')" onKeyDown="if(event.key==='Enter'||event.key===' ')goto('home')" role="button" tabindex="0" aria-label="Go to home">
<div class="line name"> <div class="line name">
<span class="textGrad">{# Andrew Simonoson #}</span> <span class="textGrad">{# Andrew Simonoson #}</span>
</div> </div>
@@ -74,27 +98,32 @@
src="{{ url_for('static', filename='icons/menu.svg')}}" src="{{ url_for('static', filename='icons/menu.svg')}}"
alt="menu" alt="menu"
id="menu" id="menu"
role="button"
aria-label="Toggle navigation menu"
aria-expanded="false"
aria-controls="navBar"
onClick="toggleMenu()" onClick="toggleMenu()"
onKeyDown="if(event.key==='Enter'||event.key===' ')toggleMenu()"
/> />
<div class="navControl"> <div class="navControl">
<div class="navBar"> <nav id="navBar" class="navBar" role="menubar" aria-label="Main navigation">
<div onClick="goto('home')" class="navElement"> <div onClick="goto('home')" onKeyDown="if(event.key==='Enter'||event.key===' ')goto('home')" role="menuitem" tabindex="0" class="navElement">
<p>Home</p> <p>Home</p>
</div> </div>
<div onclick="goto('status')" class="navElement"> <div onClick="goto('projects')" onKeyDown="if(event.key==='Enter'||event.key===' ')goto('projects')" role="menuitem" tabindex="0" class="navElement">
<p>Status</p>
</div>
<div onclick="goto('projects')" class="navElement">
<p>Work</p> <p>Work</p>
</div> </div>
</div> <div onclick="goto('status')" onKeyDown="if(event.key==='Enter'||event.key===' ')goto('status')" role="menuitem" tabindex="0" class="navElement">
<p>Status</p>
</div>
</nav>
</div> </div>
</div> </header>
{% endblock %} {% block content%} {% endblock %} {% block content%}
<div id="root">{% include var['template'] %}</div> <div id="root">{% include var['template'] %}</div>
{% endblock %} {% block footer %} {% endblock %} {% block footer %}
<div id="footerSpacer"></div> <div id="footerSpacer"></div>
<div class="footer">{% include 'partials/socials.html' %}</div> <footer class="footer" role="contentinfo">{% include 'partials/socials.html' %}</footer>
{% endblock %} {% endblock %}
</div> </div>
</body> </body>

View File

@@ -19,7 +19,7 @@
{% endmacro %} {% endmacro %}
<div class="foreground homeground"></div> <div class="foreground homeground"></div>
<div class="relative"> <div class="relative growBottom">
<div id="nametag" class="flex" data-aos="fade-up">{{ nameplate() }}</div> <div id="nametag" class="flex" data-aos="fade-up">{{ nameplate() }}</div>
<!--<INSERT SMALL BANNER HERE FOR PROJECT IMAGECARD CAROUSEL>--> <!--<INSERT SMALL BANNER HERE FOR PROJECT IMAGECARD CAROUSEL>-->
<div id="desktopSpacer"></div> <div id="desktopSpacer"></div>

View File

@@ -7,12 +7,19 @@
{% else %} {% else %}
<div class="projImagePlaceholder"></div> <div class="projImagePlaceholder"></div>
{% endif %} {% endif %}
<div class="proj-status-badge {{ status }}">
<span class="status-indicator"></span>{{ status }}
</div>
</div> </div>
<div class="projContent"> <div class="projContent">
<h3>{{ title }}</h3> <div class="projTitleRow">
<h3>{{ title }}</h3>
<span class="proj-status {{ status }}">{{ status }}</span>
</div>
{% if classes %}
<div class="projTags">
{% for cls in classes.split() %}
{% if cls != 'pinned' %}<span class="proj-tag">{{ cls }}</span>{% endif %}
{% endfor %}
</div>
{% endif %}
<p class="projDesc">{{ content }}</p> <p class="projDesc">{{ content }}</p>
{% if links %} {% if links %}
<div class="projLinks"> <div class="projLinks">

View File

@@ -1,6 +1,6 @@
{% macro expandSkill(dict, name, classes="") %} {% macro expandSkill(dict, name, classes="") %}
<div class='skill {{ classes }}' data-length='{{ dict[name]|length }}'> <div class='skill {{ classes }}' data-length='{{ dict[name]|length }}'>
<div onclick='activeSkill(this)' class='skillname'>{{ name }}</div> <div onclick='activeSkill(this)' onKeyDown="if(event.key==='Enter'||event.key===' ')activeSkill(this)" role='button' tabindex='0' aria-expanded='false' class='skillname'>{{ name }}</div>
{% if dict[name]|length > 0 %} {% if dict[name]|length > 0 %}
<div class='skill-children'> <div class='skill-children'>
<div class='skill-children-inner'> <div class='skill-children-inner'>
@@ -14,7 +14,7 @@
{% endmacro %} {% endmacro %}
{% macro skills(skills) %} {% macro skills(skills) %}
<div id="skillTree"> <div id="skillTree" role='tree' aria-label='Skills'>
{% for skill in skills %} {% for skill in skills %}
{% set classes = "" %} {% set classes = "" %}
{% if skill == skills|first %} {% if skill == skills|first %}

View File

@@ -1,6 +1,12 @@
<div class='socials'> <div class='socials' role='contentinfo' aria-label='Social links'>
<a href='https://github.com/asimonson1125'><img alt='Github' src="{{ url_for('static', filename='icons/github.svg') }}" /></a> <a href='https://github.com/asimonson1125' aria-label='GitHub profile' rel='noopener noreferrer'>
<a href='https://www.linkedin.com/in/simonsonandrew/'><img alt='LinkedIn' src="{{ url_for('static', filename='icons/linkedin.svg') }}" /></a> <img alt='' src="{{ url_for('static', filename='icons/github.svg') }}" />
<a href='mailto:asimonson1125@gmail.com'><img alt='E-mail' src="{{ url_for('static', filename='icons/email.svg') }}" /></a> </a>
<div id='vertLine'></div> <a href='https://www.linkedin.com/in/simonsonandrew/' aria-label='LinkedIn profile' rel='noopener noreferrer'>
<img alt='' src="{{ url_for('static', filename='icons/linkedin.svg') }}" />
</a>
<a href='mailto:asimonson1125@gmail.com' aria-label='Send an email'>
<img alt='' src="{{ url_for('static', filename='icons/email.svg') }}" />
</a>
<div id='vertLine' aria-hidden='true'></div>
</div> </div>

View File

@@ -1,68 +1,59 @@
{% block content %} {% block content %}
<div class="foreground"></div> <div class="foreground"></div>
<div class="foregroundContent"> <div class="relative growBottom">
<div class="flex equalitems vertOnMobile"> <div id="nametag" class="flex" data-aos="fade-up">
<div> <div>
<div> <h1 class="textGrad">Work</h1>
<h2 class="concentratedHead">About Me<p><sup>Data Scientist, Amateur SysAdmin, Polymath</sup></p></h2> <h2>Projects &amp; experiments</h2>
<p>
I'm Andrew Simonson<!--, CEO of the anti-thermodynamics syndicate.-->,
a <strong>Data Scientist at Ecolab</strong> and a graduate Data
Science student at
<strong>Rochester Institute of Technology</strong>, having
recently completed the <b>Computer Science BS</b> program
(international relations minor) with a focus on probability
theory.
<!-- <br />
<br />
I started in ~2017, reverse engineering probablistic logic
models in games and developing interfaces to recreate my
findings for friends. Now I develop tracable AI built on
deductive reasoning, maintaning scientific methodology in an
industry obsessed with implicit rules and exclusive empiricism.
As the analysis grew more sophisticated, so too did the tech
stack - to the point that I now manage most services, like this
website, end to end, container image to insight visual.
With substantial development and systems operation expertise, I
am capable of working with small teams that require many critical
functions, from devops to analytical platforms, to artificial
intelligence implementations, to all be handled by the same person.
Looking for an analyst who can create and operate the full digital
stack on your crack team of scientists? You've come to the right place.
-->
<br />
<br />
I get bored and throw random stuff on this website.<br/>
This is what unprofessional development looks like.
</p>
</div>
<br/>
<br/>
<h4 class='concentratedHead'>
I also have a
<a href="certs">certifications page</a>
and a
<a href="Resume_Simonson_Andrew.pdf" target="_blank">resume</a>
for unexplained reasons.
</h4>
</div>
<div id="skills">
<h2 id="skillstag">Technologies</h2>
{% from 'partials/skills.html' import skills %} {{
skills(var['skillList']) }}
</div>
</div> </div>
</div>
<div class="homeSubContent">
<div class="foregroundContent">
<!-- About / Intro -->
<div style="max-width: 52em; margin-bottom: 3em;">
<p>
I'm Andrew Simonson<!--, CEO of the anti-thermodynamics syndicate.-->,
a <strong>Data Scientist at Ecolab</strong> and a graduate Data
Science student at
<strong>Rochester Institute of Technology</strong>, having
recently completed the <b>Computer Science BS</b> program
(international relations minor) with a focus on probability
theory.
<br/>
<br/>
I get bored and throw random stuff on this website.<br/>
This is what unprofessional development looks like.
</p>
<br/>
<p style="color: var(--text-secondary); font-size: 0.9rem;">
I also have a
<a href="certs">certifications page</a>
and a
<a href="Resume_Simonson_Andrew.pdf" target="_blank">resume</a>
for unexplained reasons.
</p>
</div>
<!-- Skills -->
<div id="skills" style="margin-bottom: 3.5em;">
<h2>Technologies</h2>
{% from 'partials/skills.html' import skills %} {{
skills(var['skillList']) }}
</div>
<!-- Projects -->
<h2>Projects</h2>
<div class="projectList">
{% from 'partials/project.html' import project %} {% for i in
var["projects"] %} {{ project(i, var["projects"][i]["classes"],
var["projects"][i]["status"], var["projects"][i]["bgi"],
var["projects"][i]["content"], var["projects"][i]["links"]) }} {% endfor
%}
</div>
<br />
<h2 class="concentratedHead">Projects</h2>
<div class="projectList">
{% from 'partials/project.html' import project %} {% for i in
var["projects"] %} {{ project(i, var["projects"][i]["classes"],
var["projects"][i]["status"], var["projects"][i]["bgi"],
var["projects"][i]["content"], var["projects"][i]["links"]) }} {% endfor
%}
</div> </div>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -3,14 +3,14 @@
<div class="foregroundContent"> <div class="foregroundContent">
<h2 class='concentratedHead'>Service Status Monitor</h2> <h2 class='concentratedHead'>Service Status Monitor</h2>
<div class="info-box"> <div class="info-box" role="region" aria-label="Status overview">
<div class='flex spaceBetween'> <div class='flex spaceBetween'>
<div> <div>
<span id="lastUpdate">Last checked: Loading...</span> <span id="lastUpdate" aria-live="polite">Last checked: Loading...</span>
<br /> <br />
<span id="nextUpdate">Next check: --</span> <span id="nextUpdate" aria-live="polite">Next check: --</span>
</div> </div>
<button id="refreshBtn" onclick="refreshStatus()">Refresh Now</button> <button id="refreshBtn" onclick="refreshStatus()" aria-label="Refresh service status now">Refresh Now</button>
</div> </div>
<br/> <br/>
@@ -32,10 +32,10 @@
</div> </div>
<!-- Overall Status Bar --> <!-- Overall Status Bar -->
<div class="info-card summary-card" id="overallStatus"> <div class="info-card summary-card" id="overallStatus" role="status" aria-live="polite">
<div class="summary-content"> <div class="summary-content">
<div class="summary-indicator"> <div class="summary-indicator">
<span class="summary-icon loading">&#x25D0;</span> <span class="summary-icon loading" aria-hidden="true">&#x25D0;</span>
<div> <div>
<h3 class="summary-title">Checking Systems...</h3> <h3 class="summary-title">Checking Systems...</h3>
<h5 id="summary-subtitle">Loading service status</h5> <h5 id="summary-subtitle">Loading service status</h5>
@@ -43,45 +43,45 @@
</div> </div>
<div class="flex spaceBetween"> <div class="flex spaceBetween">
<div class="metric-box"> <div class="metric-box">
<span class="metric-number" id="onlineCount">--</span> <span class="metric-number" id="onlineCount" aria-label="Online services">--</span>
<span class="metric-label">Online</span> <span class="metric-label">Online</span>
</div> </div>
<div class="metric-box"> <div class="metric-box">
<span class="metric-number" id="totalCount">--</span> <span class="metric-number" id="totalCount" aria-label="Total services">--</span>
<span class="metric-label">Total</span> <span class="metric-label">Total</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="card-grid"> <div class="card-grid" role="list" aria-label="Service status cards">
{% for service in var.services %} {% for service in var.services %}
<a href="{{ service.url }}"> <a href="{{ service.url }}" class="info-card-link" id="status-{{ service.id }}" role="listitem">
<div class="info-card" id="status-{{ service.id }}"> <div class="info-card">
<div class="card-header"> <div class="card-header">
<h3>{{ service.name }}</h3> <h3>{{ service.name }}</h3>
<div class="state-indicator"> <div class="state-indicator">
<span class="state-dot loading"></span> <span class="state-dot loading" aria-label="Status: Loading..."></span>
<span class="state-text">Loading...</span> <span class="state-text">Loading...</span>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="metric-row"> <div class="metric-row">
<span class="metric-label">Response Time:</span> <span class="metric-label">Response Time:</span>
<span class="metric-value" id="time-{{ service.id }}">--</span> <span class="metric-value" id="time-{{ service.id }}" aria-live="polite">--</span>
</div> </div>
<div class="metric-row"> <div class="metric-row">
<span class="metric-label">Status Code:</span> <span class="metric-label">Status Code:</span>
<span class="metric-value" id="code-{{ service.id }}">--</span> <span class="metric-value" id="code-{{ service.id }}" aria-live="polite">--</span>
</div> </div>
<div class="metric-row"> <div class="metric-row">
<span class="metric-label">Total Checks:</span> <span class="metric-label">Total Checks:</span>
<span class="metric-value" id="checks-{{ service.id }}">--</span> <span class="metric-value" id="checks-{{ service.id }}" aria-live="polite">--</span>
</div> </div>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<div class="detail-label">Uptime:</div> <div class="detail-label">Uptime:</div>
<div class="detail-values" id="uptime-{{ service.id }}">Loading...</div> <div class="detail-values" id="uptime-{{ service.id }}" aria-live="polite">Loading...</div>
</div> </div>
</div> </div>
</a> </a>