mirror of
https://github.com/asimonson1125/asimonson1125.github.io.git
synced 2026-04-11 07:07:12 -05:00
Compare commits
7 Commits
44948a6e9f
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d544deb70f | ||
| 6fb6fbd828 | |||
| c21f8089d4 | |||
| a478f708a2 | |||
| d21a8ec278 | |||
| 7232d1f8de | |||
| 3bd27f59d7 |
30
Dockerfile
30
Dockerfile
@@ -1,10 +1,34 @@
|
|||||||
FROM python:3.10-bullseye
|
# Use a slimmer base image to reduce image size and pull times
|
||||||
|
FROM python:3.10-slim-bullseye
|
||||||
LABEL maintainer="Andrew Simonson <asimonson1125@gmail.com>"
|
LABEL maintainer="Andrew Simonson <asimonson1125@gmail.com>"
|
||||||
|
|
||||||
|
# Set environment variables for better Python performance in Docker
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PIP_NO_CACHE_DIR=1
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Create a non-root user for security
|
||||||
|
RUN groupadd -r appuser && useradd -r -g appuser appuser
|
||||||
|
|
||||||
|
# Copy only the requirements file first to leverage Docker layer caching
|
||||||
|
COPY src/requirements.txt .
|
||||||
|
|
||||||
|
# Install dependencies as root, but then switch to the non-root user
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Copy the rest of the source code
|
||||||
COPY src/ .
|
COPY src/ .
|
||||||
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
# Ensure the appuser owns the app directory
|
||||||
|
RUN chown -R appuser:appuser /app
|
||||||
|
|
||||||
CMD [ "gunicorn", "--bind", "0.0.0.0:8080", "app:app"]
|
# Switch to the non-root user for better security
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Expose the port (Gunicorn's default or specified in CMD)
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Start Gunicorn
|
||||||
|
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"]
|
||||||
|
|||||||
97
README.md
97
README.md
@@ -1,11 +1,92 @@
|
|||||||
# I made a uhh website
|
# Personal Portfolio & Service Monitor
|
||||||
So people can see how excellent my coding standards are.
|
|
||||||
|
|
||||||
* Style: 5/10
|
A Flask-based website for my personal portfolio and a service monitoring dashboard. This project handles dynamic project showcases, automated service health tracking, and production-ready optimizations.
|
||||||
* Originality: 3/10
|
|
||||||
* Security: Yes*
|
|
||||||
* Viruses: not included
|
|
||||||
|
|
||||||
You gotta uhh `pip3 install -r requirements.txt` and `python3 app.py` that thing
|
## Features
|
||||||
|
|
||||||
Docker compose configured to expose at `localhost:8080`
|
- **Content Management**: Pages like projects, books, and skills are managed via JSON files in the `static` directory.
|
||||||
|
- **Service Monitoring**: Background health checks for external services with uptime statistics stored in PostgreSQL.
|
||||||
|
- **Optimizations**:
|
||||||
|
- HTML, CSS, and JS minification via `Flask-Minify`.
|
||||||
|
- MD5-based cache busting for static assets.
|
||||||
|
- Configurable cache-control headers.
|
||||||
|
- **Security**: Pre-configured headers for XSS protection and frame security.
|
||||||
|
- **Deployment**: Ready for containerized deployment with Docker and Gunicorn.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Backend**: Python 3.12, Flask
|
||||||
|
- **Frontend**: Vanilla CSS/JS, Jinja2
|
||||||
|
- **Database**: PostgreSQL (optional, for monitoring history)
|
||||||
|
- **Infrastructure**: Docker, docker-compose
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├── src/
|
||||||
|
│ ├── app.py # Application entry point
|
||||||
|
│ ├── monitor.py # Service monitoring logic
|
||||||
|
│ ├── config.py # Environment configuration
|
||||||
|
│ ├── templates/ # HTML templates
|
||||||
|
│ ├── static/ # CSS, JS, and JSON data
|
||||||
|
│ └── requirements.txt # Python dependencies
|
||||||
|
├── Dockerfile # Container definition
|
||||||
|
├── docker-compose.yml # Local stack orchestration
|
||||||
|
└── STATUS_MONITOR_README.md # Monitoring system documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Using Docker
|
||||||
|
|
||||||
|
To run the full stack (App + PostgreSQL):
|
||||||
|
|
||||||
|
1. **Clone the repository**:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/asimonson1125/asimonson1125.github.io.git
|
||||||
|
cd asimonson1125.github.io
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Start services**:
|
||||||
|
```bash
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Access the site**:
|
||||||
|
Visit [http://localhost:8080](http://localhost:8080).
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
To run the Flask app without Docker:
|
||||||
|
|
||||||
|
1. **Set up a virtual environment**:
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies**:
|
||||||
|
```bash
|
||||||
|
pip install -r src/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Run the application**:
|
||||||
|
```bash
|
||||||
|
cd src
|
||||||
|
python3 app.py
|
||||||
|
```
|
||||||
|
*Note: status monitor is by default disabled outside of its container cluster*
|
||||||
|
|
||||||
|
## Service Monitoring
|
||||||
|
|
||||||
|
The monitoring system in `src/monitor.py` tracks service availability. It:
|
||||||
|
- Runs concurrent health checks every hour.
|
||||||
|
- Calculates uptime for various windows (24h, 7d, 30d).
|
||||||
|
- Provides a status UI at `/status` and a JSON API at `/api/status`.
|
||||||
|
|
||||||
|
See [STATUS_MONITOR_README.md](./STATUS_MONITOR_README.md) for more details.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is personal property. All rights reserved.
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import requests
|
|||||||
|
|
||||||
SERVICES = [
|
SERVICES = [
|
||||||
{'id': 'main', 'name': 'asimonson.com', 'url': 'https://asimonson.com', 'timeout': 10},
|
{'id': 'main', 'name': 'asimonson.com', 'url': 'https://asimonson.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},
|
||||||
]
|
]
|
||||||
|
|
||||||
CHECK_INTERVAL = 60 # seconds between checks
|
CHECK_INTERVAL = 60 # seconds between checks
|
||||||
@@ -248,7 +250,7 @@ class ServiceMonitor:
|
|||||||
if not cur.fetchone()[0]:
|
if not cur.fetchone()[0]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return round((online_count / total_count) * 100, 2)
|
return round((online_count / total_count) * 100, 3)
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|||||||
36
src/requirements.txt
Executable file → Normal file
36
src/requirements.txt
Executable file → Normal file
@@ -1,23 +1,23 @@
|
|||||||
blinker==1.8.2
|
blinker==1.9.0
|
||||||
certifi==2024.7.4
|
certifi==2026.1.4
|
||||||
charset-normalizer==3.3.2
|
charset-normalizer==3.4.4
|
||||||
click==8.1.7
|
click==8.3.1
|
||||||
Flask==3.0.3
|
Flask==3.1.3
|
||||||
Flask-Minify==0.48
|
Flask-Minify==0.50
|
||||||
gunicorn==22.0.0
|
gunicorn==25.1.0
|
||||||
htmlminf==0.1.13
|
htmlminf==0.1.13
|
||||||
idna==3.7
|
idna==3.11
|
||||||
itsdangerous==2.2.0
|
itsdangerous==2.2.0
|
||||||
Jinja2==3.1.4
|
Jinja2==3.1.6
|
||||||
jsmin==3.0.1
|
jsmin==3.0.1
|
||||||
lesscpy==0.15.1
|
lesscpy==0.15.1
|
||||||
MarkupSafe==2.1.5
|
MarkupSafe==3.0.3
|
||||||
packaging==24.1
|
packaging==26.0
|
||||||
ply==3.11
|
ply==3.11
|
||||||
rcssmin==1.1.2
|
psycopg2-binary==2.9.11
|
||||||
requests==2.32.3
|
rcssmin==1.2.2
|
||||||
six==1.16.0
|
requests==2.33.0
|
||||||
urllib3==2.2.2
|
six==1.17.0
|
||||||
Werkzeug==3.0.3
|
urllib3==2.6.3
|
||||||
xxhash==3.4.1
|
Werkzeug==3.1.6
|
||||||
psycopg2-binary==2.9.9
|
xxhash==3.6.0
|
||||||
|
|||||||
@@ -219,10 +219,10 @@ tr {
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
display: block;
|
display: block;
|
||||||
/* width: 5em; */
|
/* width: 5em; */
|
||||||
width: 10em;
|
width: 8em;
|
||||||
/* height: 30em; */
|
/* height: 30em; */
|
||||||
/* max-height: 70vh; */
|
/* max-height: 70vh; */
|
||||||
max-width: 90vw;
|
max-width: 70vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
#homeName {
|
#homeName {
|
||||||
@@ -341,8 +341,8 @@ tr {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
min-width: 100vw;
|
min-width: 200vw;
|
||||||
min-height: 100vh;
|
min-height: 200vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: rgba(var(--bg-card-rgb), 0.85);
|
background-color: rgba(var(--bg-card-rgb), 0.85);
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
@@ -778,7 +778,6 @@ tr {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
border: 1px solid currentColor;
|
border: 1px solid currentColor;
|
||||||
background: rgba(0, 0, 0, 0.65);
|
background: rgba(0, 0, 0, 0.65);
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.proj-status-badge.complete { color: var(--status-online); }
|
.proj-status-badge.complete { color: var(--status-online); }
|
||||||
@@ -1512,6 +1511,10 @@ tr {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cert-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.legend-items {
|
.legend-items {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
@@ -1839,4 +1842,134 @@ tr {
|
|||||||
|
|
||||||
.text-muted {
|
.text-muted {
|
||||||
color: #888 !important;
|
color: #888 !important;
|
||||||
}
|
}
|
||||||
|
/* Certifications */
|
||||||
|
.cert-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1.5em;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-group {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-group-provider {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0 0 0.25em 0;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-program-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0.35em 0.9em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
border: 1px solid rgba(var(--accent-rgb), 0.75);
|
||||||
|
background: rgba(var(--accent-rgb), 0.18);
|
||||||
|
color: var(--text-heading) !important;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
letter-spacing: 0.09em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-program-badge:hover {
|
||||||
|
background: rgba(var(--accent-rgb), 0.32);
|
||||||
|
border-color: rgb(var(--accent-rgb));
|
||||||
|
box-shadow: 0 0 8px rgba(var(--accent-rgb), 0.35);
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0.5em 0 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-list li {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6em;
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
border-left: 3px solid rgba(var(--accent-rgb), 0.65);
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
color: var(--text-secondary) !important;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: rgba(var(--accent-rgb), 0.35);
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
transition: border-color 0.2s ease, background 0.2s ease, padding-left 0.2s ease, color 0.2s ease;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-item::before {
|
||||||
|
content: '›';
|
||||||
|
color: rgb(var(--accent-rgb));
|
||||||
|
font-size: 1.25em;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-item:hover {
|
||||||
|
border-left-color: rgb(var(--accent-rgb));
|
||||||
|
background: rgba(var(--accent-rgb), 0.12);
|
||||||
|
padding-left: 1em;
|
||||||
|
color: var(--text-heading) !important;
|
||||||
|
text-decoration-color: rgba(var(--accent-rgb), 0.6);
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-item:hover::before {
|
||||||
|
transform: translateX(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SPA Loading Bar */
|
||||||
|
#loading-bar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, var(--accent) 0%, #fff 50%, var(--accent) 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
box-shadow: 0 0 12px rgba(var(--accent-rgb), 0.6);
|
||||||
|
z-index: 2000;
|
||||||
|
width: 0%;
|
||||||
|
opacity: 0;
|
||||||
|
transition: width 0.4s cubic-bezier(0.1, 0.05, 0.1, 1), opacity 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-bar.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-bar.active {
|
||||||
|
width: 70%;
|
||||||
|
animation: loading-shimmer 2s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-bar.finish {
|
||||||
|
opacity: 0;
|
||||||
|
width: 100%;
|
||||||
|
transition: width 0.2s ease, opacity 0.4s ease 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading-shimmer {
|
||||||
|
from { background-position: 200% 0; }
|
||||||
|
to { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,50 +14,78 @@ function toggleMenu(collapse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function goto(location, { push = true } = {}) {
|
async function goto(location, { push = true } = {}) {
|
||||||
let response;
|
const loadingBar = document.getElementById('loading-bar');
|
||||||
|
|
||||||
|
if (loadingBar) {
|
||||||
|
loadingBar.style.width = ''; // Clear inline style from previous run
|
||||||
|
}
|
||||||
|
|
||||||
|
let loadingTimeout = setTimeout(() => {
|
||||||
|
if (loadingBar) {
|
||||||
|
loadingBar.classList.remove('finish');
|
||||||
|
loadingBar.classList.add('active');
|
||||||
|
loadingBar.classList.add('visible');
|
||||||
|
}
|
||||||
|
}, 150);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response = await fetch("/api/goto/" + location, {
|
const response = await fetch("/api/goto/" + location, {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
mode: "cors",
|
mode: "cors",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error(`Navigation failed: HTTP ${response.status}`);
|
throw new Error(`HTTP ${response.status}`);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error("Navigation fetch failed:", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.dispatchEvent(new Event('beforenavigate'));
|
// Wait for the full body to download - this is usually the slow part
|
||||||
|
const [metadata, content] = await response.json();
|
||||||
|
|
||||||
|
document.dispatchEvent(new Event('beforenavigate'));
|
||||||
|
|
||||||
const [metadata, content] = await response.json();
|
const root = document.getElementById("root");
|
||||||
const root = document.getElementById("root");
|
root.innerHTML = content;
|
||||||
root.innerHTML = content;
|
|
||||||
|
|
||||||
// Re-execute scripts injected via innerHTML (browser ignores them otherwise)
|
// Re-execute scripts
|
||||||
root.querySelectorAll("script").forEach(function(oldScript) {
|
root.querySelectorAll("script").forEach(function(oldScript) {
|
||||||
const newScript = document.createElement("script");
|
const newScript = document.createElement("script");
|
||||||
Array.from(oldScript.attributes).forEach(function(attr) {
|
Array.from(oldScript.attributes).forEach(function(attr) {
|
||||||
newScript.setAttribute(attr.name, attr.value);
|
newScript.setAttribute(attr.name, attr.value);
|
||||||
|
});
|
||||||
|
newScript.textContent = oldScript.textContent;
|
||||||
|
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||||
});
|
});
|
||||||
newScript.textContent = oldScript.textContent;
|
|
||||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.location.href.includes("#")) {
|
if (window.location.href.includes("#")) {
|
||||||
const id = decodeURIComponent(window.location.hash.substring(1));
|
const id = decodeURIComponent(window.location.hash.substring(1));
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) el.scrollIntoView();
|
if (el) el.scrollIntoView();
|
||||||
} else {
|
} else {
|
||||||
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleMenu(true);
|
toggleMenu(true);
|
||||||
document.querySelector("title").textContent = metadata["title"];
|
document.querySelector("title").textContent = metadata["title"];
|
||||||
if (push) {
|
if (push) {
|
||||||
history.pushState(null, null, metadata["canonical"]);
|
history.pushState(null, null, metadata["canonical"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Navigation failed:", err);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(loadingTimeout);
|
||||||
|
if (loadingBar && loadingBar.classList.contains('active')) {
|
||||||
|
loadingBar.classList.add('finish');
|
||||||
|
loadingBar.classList.remove('active');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!loadingBar.classList.contains('active')) {
|
||||||
|
loadingBar.style.width = '0%';
|
||||||
|
loadingBar.classList.remove('finish');
|
||||||
|
loadingBar.classList.remove('visible');
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
let statusIntervalId = null;
|
// Use a global to track the interval and ensure we don't stack listeners
|
||||||
|
if (window.statusIntervalId) {
|
||||||
|
clearInterval(window.statusIntervalId);
|
||||||
|
window.statusIntervalId = null;
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchStatus() {
|
async function fetchStatus() {
|
||||||
try {
|
try {
|
||||||
@@ -17,7 +21,8 @@ async function fetchStatus() {
|
|||||||
function updateStatusDisplay(data) {
|
function updateStatusDisplay(data) {
|
||||||
if (data.last_check) {
|
if (data.last_check) {
|
||||||
const lastCheck = new Date(data.last_check);
|
const lastCheck = new Date(data.last_check);
|
||||||
document.getElementById('lastUpdate').textContent = `Last checked: ${lastCheck.toLocaleString()}`;
|
const lastUpdateEl = document.getElementById('lastUpdate');
|
||||||
|
if (lastUpdateEl) lastUpdateEl.textContent = `Last checked: ${lastCheck.toLocaleString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.next_check) {
|
if (data.next_check) {
|
||||||
@@ -28,11 +33,12 @@ function updateStatusDisplay(data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data.services.forEach(function(service) {
|
if (data.services) {
|
||||||
updateServiceCard(service);
|
data.services.forEach(function(service) {
|
||||||
});
|
updateServiceCard(service);
|
||||||
|
});
|
||||||
updateOverallStatus(data.services);
|
updateOverallStatus(data.services);
|
||||||
|
}
|
||||||
|
|
||||||
const refreshBtn = document.getElementById('refreshBtn');
|
const refreshBtn = document.getElementById('refreshBtn');
|
||||||
if (refreshBtn) {
|
if (refreshBtn) {
|
||||||
@@ -65,36 +71,38 @@ function updateServiceCard(service) {
|
|||||||
const uptimeDisplay = document.getElementById(`uptime-${service.id}`);
|
const uptimeDisplay = document.getElementById(`uptime-${service.id}`);
|
||||||
const checksDisplay = document.getElementById(`checks-${service.id}`);
|
const checksDisplay = document.getElementById(`checks-${service.id}`);
|
||||||
|
|
||||||
timeDisplay.textContent = service.response_time !== null ? `${service.response_time}ms` : '--';
|
if (timeDisplay) timeDisplay.textContent = service.response_time !== null ? `${service.response_time}ms` : '--';
|
||||||
|
|
||||||
if (service.status_code !== null) {
|
if (codeDisplay) {
|
||||||
codeDisplay.textContent = service.status_code;
|
if (service.status_code !== null) {
|
||||||
} else {
|
codeDisplay.textContent = service.status_code;
|
||||||
codeDisplay.textContent = service.status === 'unknown' ? 'Unknown' : 'Error';
|
} else {
|
||||||
|
codeDisplay.textContent = service.status === 'unknown' ? 'Unknown' : 'Error';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
card.classList.remove('online', 'degraded', 'offline', 'unknown');
|
card.classList.remove('online', 'degraded', 'offline', 'unknown');
|
||||||
|
|
||||||
switch (service.status) {
|
switch (service.status) {
|
||||||
case 'online':
|
case 'online':
|
||||||
stateDot.className = 'state-dot online';
|
if (stateDot) stateDot.className = 'state-dot online';
|
||||||
stateText.textContent = 'Operational';
|
if (stateText) stateText.textContent = 'Operational';
|
||||||
card.classList.add('online');
|
card.classList.add('online');
|
||||||
break;
|
break;
|
||||||
case 'degraded':
|
case 'degraded':
|
||||||
case 'timeout':
|
case 'timeout':
|
||||||
stateDot.className = 'state-dot degraded';
|
if (stateDot) stateDot.className = 'state-dot degraded';
|
||||||
stateText.textContent = service.status === 'timeout' ? 'Timeout' : 'Degraded';
|
if (stateText) stateText.textContent = service.status === 'timeout' ? 'Timeout' : 'Degraded';
|
||||||
card.classList.add('degraded');
|
card.classList.add('degraded');
|
||||||
break;
|
break;
|
||||||
case 'offline':
|
case 'offline':
|
||||||
stateDot.className = 'state-dot offline';
|
if (stateDot) stateDot.className = 'state-dot offline';
|
||||||
stateText.textContent = 'Offline';
|
if (stateText) stateText.textContent = 'Offline';
|
||||||
card.classList.add('offline');
|
card.classList.add('offline');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
stateDot.className = 'state-dot loading';
|
if (stateDot) stateDot.className = 'state-dot loading';
|
||||||
stateText.textContent = 'Unknown';
|
if (stateText) stateText.textContent = 'Unknown';
|
||||||
card.classList.add('unknown');
|
card.classList.add('unknown');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +122,8 @@ function updateServiceCard(service) {
|
|||||||
|
|
||||||
function updateOverallStatus(services) {
|
function updateOverallStatus(services) {
|
||||||
const overallBar = document.getElementById('overallStatus');
|
const overallBar = document.getElementById('overallStatus');
|
||||||
|
if (!overallBar) return;
|
||||||
|
|
||||||
const icon = overallBar.querySelector('.summary-icon');
|
const icon = overallBar.querySelector('.summary-icon');
|
||||||
const title = overallBar.querySelector('.summary-title');
|
const title = overallBar.querySelector('.summary-title');
|
||||||
const subtitle = document.getElementById('summary-subtitle');
|
const subtitle = document.getElementById('summary-subtitle');
|
||||||
@@ -125,46 +135,52 @@ function updateOverallStatus(services) {
|
|||||||
const degraded = services.filter(function(s) { return s.status === 'degraded' || s.status === 'timeout'; }).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;
|
const offline = services.filter(function(s) { return s.status === 'offline'; }).length;
|
||||||
|
|
||||||
onlineCount.textContent = online;
|
if (onlineCount) onlineCount.textContent = online;
|
||||||
totalCount.textContent = total;
|
if (totalCount) totalCount.textContent = total;
|
||||||
|
|
||||||
overallBar.classList.remove('online', 'degraded', 'offline');
|
overallBar.classList.remove('online', 'degraded', 'offline');
|
||||||
icon.classList.remove('operational', 'partial', 'major', 'loading');
|
if (icon) icon.classList.remove('operational', 'partial', 'major', 'loading');
|
||||||
|
|
||||||
// Determine overall status
|
// Determine overall status
|
||||||
if (online === total) {
|
if (online === total) {
|
||||||
// All systems operational
|
|
||||||
overallBar.classList.add('online');
|
overallBar.classList.add('online');
|
||||||
icon.classList.add('operational');
|
if (icon) {
|
||||||
icon.textContent = '\u2713';
|
icon.classList.add('operational');
|
||||||
title.textContent = 'All Systems Operational';
|
icon.textContent = '\u2713';
|
||||||
subtitle.textContent = `All ${total} services are running normally`;
|
}
|
||||||
|
if (title) title.textContent = 'All Systems Operational';
|
||||||
|
if (subtitle) subtitle.textContent = `All ${total} services are running normally`;
|
||||||
} else if (offline >= Math.ceil(total / 2)) {
|
} else if (offline >= Math.ceil(total / 2)) {
|
||||||
// Major outage (50% or more offline)
|
|
||||||
overallBar.classList.add('offline');
|
overallBar.classList.add('offline');
|
||||||
icon.classList.add('major');
|
if (icon) {
|
||||||
icon.textContent = '\u2715';
|
icon.classList.add('major');
|
||||||
title.textContent = 'Major Outage';
|
icon.textContent = '\u2715';
|
||||||
subtitle.textContent = `${offline} service${offline !== 1 ? 's' : ''} offline, ${degraded} degraded`;
|
}
|
||||||
|
if (title) title.textContent = 'Major Outage';
|
||||||
|
if (subtitle) subtitle.textContent = `${offline} service${offline !== 1 ? 's' : ''} offline, ${degraded} degraded`;
|
||||||
} else if (offline > 0 || degraded > 0) {
|
} else if (offline > 0 || degraded > 0) {
|
||||||
// Partial outage
|
|
||||||
overallBar.classList.add('degraded');
|
overallBar.classList.add('degraded');
|
||||||
icon.classList.add('partial');
|
if (icon) {
|
||||||
icon.textContent = '\u26A0';
|
icon.classList.add('partial');
|
||||||
title.textContent = 'Partial Outage';
|
icon.textContent = '\u26A0';
|
||||||
if (offline > 0 && degraded > 0) {
|
}
|
||||||
subtitle.textContent = `${offline} offline, ${degraded} degraded`;
|
if (title) title.textContent = 'Partial Outage';
|
||||||
} else if (offline > 0) {
|
if (subtitle) {
|
||||||
subtitle.textContent = `${offline} service${offline !== 1 ? 's' : ''} offline`;
|
if (offline > 0 && degraded > 0) {
|
||||||
} else {
|
subtitle.textContent = `${offline} offline, ${degraded} degraded`;
|
||||||
subtitle.textContent = `${degraded} service${degraded !== 1 ? 's' : ''} degraded`;
|
} else if (offline > 0) {
|
||||||
|
subtitle.textContent = `${offline} service${offline !== 1 ? 's' : ''} offline`;
|
||||||
|
} else {
|
||||||
|
subtitle.textContent = `${degraded} service${degraded !== 1 ? 's' : ''} degraded`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unknown state
|
if (icon) {
|
||||||
icon.classList.add('loading');
|
icon.classList.add('loading');
|
||||||
icon.textContent = '\u25D0';
|
icon.textContent = '\u25D0';
|
||||||
title.textContent = 'Status Unknown';
|
}
|
||||||
subtitle.textContent = 'Waiting for service data';
|
if (title) title.textContent = 'Status Unknown';
|
||||||
|
if (subtitle) subtitle.textContent = 'Waiting for service data';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,23 +207,23 @@ function refreshStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initStatusPage() {
|
function initStatusPage() {
|
||||||
if (statusIntervalId !== null) {
|
if (window.statusIntervalId) {
|
||||||
clearInterval(statusIntervalId);
|
clearInterval(window.statusIntervalId);
|
||||||
}
|
}
|
||||||
fetchStatus();
|
fetchStatus();
|
||||||
statusIntervalId = setInterval(fetchStatus, 60000);
|
window.statusIntervalId = setInterval(fetchStatus, 60000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up polling interval when navigating away via SPA
|
function cleanupStatusPage() {
|
||||||
document.addEventListener('beforenavigate', function() {
|
if (window.statusIntervalId) {
|
||||||
if (statusIntervalId !== null) {
|
clearInterval(window.statusIntervalId);
|
||||||
clearInterval(statusIntervalId);
|
window.statusIntervalId = null;
|
||||||
statusIntervalId = null;
|
|
||||||
}
|
}
|
||||||
});
|
document.removeEventListener('beforenavigate', cleanupStatusPage);
|
||||||
|
}
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
document.addEventListener('beforenavigate', cleanupStatusPage);
|
||||||
document.addEventListener('DOMContentLoaded', initStatusPage);
|
|
||||||
} else {
|
if (document.getElementById('overallStatus')) {
|
||||||
initStatusPage();
|
initStatusPage();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
{
|
{
|
||||||
|
"PsyCom - Physical Combinatorics": {
|
||||||
|
"status": "WIP",
|
||||||
|
"classes": "programming",
|
||||||
|
"content": "Experimental innovation engine operating on physical attributes and limitations of proven existing technologies with further AI review."
|
||||||
|
},
|
||||||
"Antietam-Conococheague Watershed Monitoring": {
|
"Antietam-Conococheague Watershed Monitoring": {
|
||||||
"status": "complete",
|
"status": "complete",
|
||||||
"classes": "geospacial",
|
"classes": "geospacial",
|
||||||
@@ -50,7 +55,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Portfolio Website": {
|
"Portfolio Website": {
|
||||||
"status": "WIP",
|
"status": "complete",
|
||||||
"classes": "programming",
|
"classes": "programming",
|
||||||
"content": "This website is my personal sandbox where I've integrated some of my data projects via docker cluster. It is self hosted and zero-trust secure while remaining dynamic and free of the tech debt that comes with pre-designed sites and excessive framework application. Yeah, I can do E2E.",
|
"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": [
|
"links": [
|
||||||
|
|||||||
@@ -1,75 +1,49 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="foreground"></div>
|
<div class="foreground"></div>
|
||||||
<div class="foregroundContent">
|
<div class="foregroundContent">
|
||||||
<h1>Andrew's Secret Certification Page</h1>
|
<h1>Certifications</h1>
|
||||||
|
<p class="page-subtitle">
|
||||||
|
Comprehensive list verifiable on
|
||||||
|
<a href="https://www.linkedin.com/in/simonsonandrew/details/certifications/">LinkedIn</a>
|
||||||
|
</p>
|
||||||
|
<p>Computer Science BS and Data Science MS from Rochester Institute of Technology</p>
|
||||||
|
|
||||||
<strong
|
<div class="boxed cert-group">
|
||||||
>See
|
<p class="cert-group-provider">UCSanDiegoX · edX</p>
|
||||||
<a
|
<h2 class="concentratedHead">Data Science MicroMasters Program</h2>
|
||||||
href="https://www.linkedin.com/in/simonsonandrew/details/certifications/"
|
<a href="http://credentials.edx.org/credentials/4b7e78dca8154c0d88ca9abc5aedb4ac" class="cert-program-badge">
|
||||||
>here</a
|
View Program Certificate ›
|
||||||
>
|
</a>
|
||||||
for a comprehensive list of certifications that can be confirmed by
|
<ul class="cert-list">
|
||||||
LinkedIn</strong
|
<li><a href="https://courses.edx.org/certificates/b6deccc56e5344ae84cb55f9ad81fd79" class="cert-item">DSE200x — Python for Data Science</a></li>
|
||||||
>
|
<li><a href="https://courses.edx.org/certificates/f29d0e65fc024c6e95121619e329a286" class="cert-item">DSE210x — Probability and Statistics in Data Science using Python</a></li>
|
||||||
<p>Some highlights:</p>
|
<li><a href="https://courses.edx.org/certificates/cccc2bd2ed61470e8492d6da1be530c5" class="cert-item">DSE220x — Machine Learning Fundamentals</a></li>
|
||||||
<strong><a href="http://credentials.edx.org/credentials/4b7e78dca8154c0d88ca9abc5aedb4ac">UCSanDiegoX Data Science MicroMasters Program</a></strong>
|
<li><a href="https://courses.edx.org/certificates/4dfd6563a1f84caaa8922a02a5125f29" class="cert-item">DSE230x — Big Data Analytics Using Spark</a></li>
|
||||||
<ol>
|
</ul>
|
||||||
<li><a href="https://courses.edx.org/certificates/b6deccc56e5344ae84cb55f9ad81fd79">DSE200x Python for Data Science</a></li>
|
</div>
|
||||||
<li><a href="https://courses.edx.org/certificates/f29d0e65fc024c6e95121619e329a286">DSE210x Probability and Statistics in Data Science using Python</a></li>
|
|
||||||
<li><a href="https://courses.edx.org/certificates/cccc2bd2ed61470e8492d6da1be530c5">DSE220x Machine Learning Fundamentals</a></li>
|
<div class="cert-grid">
|
||||||
<li><a href="https://courses.edx.org/certificates/4dfd6563a1f84caaa8922a02a5125f29">DSE230x Big Data Analytics Using Spark</a></li>
|
<div class="boxed cert-group">
|
||||||
</ol>
|
<h3 class="concentratedHead">One-Off Courses</h3>
|
||||||
<strong>One-Off Courses</strong>
|
<ul class="cert-list">
|
||||||
<ul>
|
<li><a href="https://files.asimonson.com/u/2662_3_1303226_1772561098_Databricks%20-%20Generic.pdf" class="cert-item">Building Retrieval Agents On Databricks</a></li>
|
||||||
<li>
|
<li><a href="https://files.asimonson.com/u/2403_3_1303226_1765822061_Databricks%20-%20Generic.pdf" class="cert-item">Machine Learning Operations by Databricks</a></li>
|
||||||
<a
|
<li><a href="https://www.linkedin.com/learning/certificates/2cb69378c606fec5a6f3a107b99a896862db392b7a3692f71a6b53af5d5545c5" class="cert-item">Career Essentials in Data Analysis by Microsoft</a></li>
|
||||||
href="https://files.asimonson.com/u/2403_3_1303226_1765822061_Databricks%20-%20Generic.pdf"
|
<li><a href="https://www.linkedin.com/learning/certificates/7facc28a13405134b3b7fa785303e9b1cf697f32d67f759e89960fbdc8a044d9" class="cert-item">Career Essentials in GitHub Professional Certificate</a></li>
|
||||||
>Machine Learning Operations by Databricks</a
|
<li><a href="https://www.linkedin.com/learning/certificates/7b952323152e258ca468c33ddc9ebcf3c55036f58a5cfb3fb9c1410da655aaa5" class="cert-item">Docker Foundations Professional Certificate</a></li>
|
||||||
>
|
<li><a href="https://www.linkedin.com/learning/certificates/7017147ac73af5bc26fdab9b3c43671fb8105a0de59d4689d5f0f71c549c150f" class="cert-item">Data Science Foundations: Fundamentals</a></li>
|
||||||
</li>
|
</ul>
|
||||||
<li>
|
</div>
|
||||||
<a
|
|
||||||
href="https://www.linkedin.com/learning/certificates/2cb69378c606fec5a6f3a107b99a896862db392b7a3692f71a6b53af5d5545c5"
|
<div class="boxed cert-group">
|
||||||
>Career Essentials in Data Analysis by Microsoft</a
|
<p class="cert-group-provider">Rochester Institute of Technology</p>
|
||||||
>
|
<h3 class="concentratedHead">Entrepreneurial Certifications</h3>
|
||||||
</li>
|
<ul class="cert-list">
|
||||||
<li>
|
<li><a href="https://files.asimonson.com/u/designThinkingCert.pdf" class="cert-item">Design Thinking Certification</a></li>
|
||||||
<a
|
<li><a href="https://files.asimonson.com/u/ideationCert.pdf" class="cert-item">Ideation Certification</a></li>
|
||||||
href="https://www.linkedin.com/learning/certificates/7facc28a13405134b3b7fa785303e9b1cf697f32d67f759e89960fbdc8a044d9"
|
<li><a href="https://files.asimonson.com/u/toolsForInnovatorsCert.pdf" class="cert-item">Tools for Innovators Certification</a></li>
|
||||||
>Career Essentials in GitHub Professional Certificate</a
|
</ul>
|
||||||
>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://www.linkedin.com/learning/certificates/7b952323152e258ca468c33ddc9ebcf3c55036f58a5cfb3fb9c1410da655aaa5"
|
|
||||||
>Docker Foundations Professional Certificate</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://www.linkedin.com/learning/certificates/7017147ac73af5bc26fdab9b3c43671fb8105a0de59d4689d5f0f71c549c150f"
|
|
||||||
>Data Science Foundations: Fundamentals</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<strong>RIT Entrepreneurial Certifications</strong>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://files.asimonson.com/u/designThinkingCert.pdf"
|
|
||||||
>Design Thinking Certification</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://files.asimonson.com/u/ideationCert.pdf"
|
|
||||||
>Ideation Certification</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://files.asimonson.com/u/toolsForInnovatorsCert.pdf"
|
|
||||||
>Tools for Innovators Certification</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
</head>
|
</head>
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<body onpopstate="backButton()">
|
<body onpopstate="backButton()">
|
||||||
|
<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'></main>
|
||||||
<div id="contentStuffer">
|
<div id="contentStuffer">
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
{% block content %} {% macro nameplate() %}
|
{% block content %} {% macro nameplate() %}
|
||||||
<div>
|
<div>
|
||||||
<h1 id="homeName" class="textGrad">Andrew's Definitely Active Website</h1>
|
<h1 id="homeName" class="textGrad">oh no.</h1>
|
||||||
|
<h2>I did not plan for visitors.</h2>
|
||||||
|
|
||||||
|
<br /> <hr> <br />
|
||||||
|
|
||||||
<div class="flex vertOnMobile">
|
<div class="flex vertOnMobile">
|
||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -22,7 +22,15 @@
|
|||||||
industry obsessed with implicit rules and exclusive empiricism.
|
industry obsessed with implicit rules and exclusive empiricism.
|
||||||
As the analysis grew more sophisticated, so too did the tech
|
As the analysis grew more sophisticated, so too did the tech
|
||||||
stack - to the point that I now manage most services, like this
|
stack - to the point that I now manage most services, like this
|
||||||
website, end to end, container image to insight visual. -->
|
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 />
|
||||||
<br />
|
<br />
|
||||||
I get bored and throw random stuff on this website.<br/>
|
I get bored and throw random stuff on this website.<br/>
|
||||||
@@ -33,6 +41,8 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<h4 class='concentratedHead'>
|
<h4 class='concentratedHead'>
|
||||||
I also have a
|
I also have a
|
||||||
|
<a href="certs">certifications page</a>
|
||||||
|
and a
|
||||||
<a href="Resume_Simonson_Andrew.pdf" target="_blank">resume</a>
|
<a href="Resume_Simonson_Andrew.pdf" target="_blank">resume</a>
|
||||||
for unexplained reasons.
|
for unexplained reasons.
|
||||||
</h4>
|
</h4>
|
||||||
|
|||||||
Reference in New Issue
Block a user