mirror of
https://github.com/asimonson1125/asimonson1125.github.io.git
synced 2026-02-24 21:09:49 -06:00
feat: implement SPA loading bar, harden status page logic, and optimize Dockerfile
This commit is contained in:
28
Dockerfile
28
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
|
||||||
|
|
||||||
|
# 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"]
|
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"]
|
||||||
|
|||||||
@@ -1840,3 +1840,38 @@ tr {
|
|||||||
.text-muted {
|
.text-muted {
|
||||||
color: #888 !important;
|
color: #888 !important;
|
||||||
}
|
}
|
||||||
|
/* 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,29 +14,42 @@ function toggleMenu(collapse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function goto(location, { push = true } = {}) {
|
async function goto(location, { push = true } = {}) {
|
||||||
let response;
|
const loadingBar = document.getElementById('loading-bar');
|
||||||
|
console.log(`Navigating to ${location}`);
|
||||||
|
|
||||||
|
if (loadingBar) {
|
||||||
|
loadingBar.style.width = ''; // Clear inline style from previous run
|
||||||
|
}
|
||||||
|
|
||||||
|
let loadingTimeout = setTimeout(() => {
|
||||||
|
if (loadingBar) {
|
||||||
|
console.log("Navigation taking > 150ms, showing bar");
|
||||||
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for the full body to download - this is usually the slow part
|
||||||
|
const [metadata, content] = await response.json();
|
||||||
|
|
||||||
document.dispatchEvent(new Event('beforenavigate'));
|
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) {
|
||||||
@@ -59,6 +72,24 @@ async function goto(location, { push = true } = {}) {
|
|||||||
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')) {
|
||||||
|
console.log("Navigation finished, hiding bar");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function backButton() {
|
function backButton() {
|
||||||
|
|||||||
@@ -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) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.services) {
|
||||||
data.services.forEach(function(service) {
|
data.services.forEach(function(service) {
|
||||||
updateServiceCard(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 (codeDisplay) {
|
||||||
if (service.status_code !== null) {
|
if (service.status_code !== null) {
|
||||||
codeDisplay.textContent = service.status_code;
|
codeDisplay.textContent = service.status_code;
|
||||||
} else {
|
} else {
|
||||||
codeDisplay.textContent = service.status === 'unknown' ? 'Unknown' : 'Error';
|
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,33 +135,37 @@ 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');
|
||||||
|
if (icon) {
|
||||||
icon.classList.add('operational');
|
icon.classList.add('operational');
|
||||||
icon.textContent = '\u2713';
|
icon.textContent = '\u2713';
|
||||||
title.textContent = 'All Systems Operational';
|
}
|
||||||
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');
|
||||||
|
if (icon) {
|
||||||
icon.classList.add('major');
|
icon.classList.add('major');
|
||||||
icon.textContent = '\u2715';
|
icon.textContent = '\u2715';
|
||||||
title.textContent = 'Major Outage';
|
}
|
||||||
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');
|
||||||
|
if (icon) {
|
||||||
icon.classList.add('partial');
|
icon.classList.add('partial');
|
||||||
icon.textContent = '\u26A0';
|
icon.textContent = '\u26A0';
|
||||||
title.textContent = 'Partial Outage';
|
}
|
||||||
|
if (title) title.textContent = 'Partial Outage';
|
||||||
|
if (subtitle) {
|
||||||
if (offline > 0 && degraded > 0) {
|
if (offline > 0 && degraded > 0) {
|
||||||
subtitle.textContent = `${offline} offline, ${degraded} degraded`;
|
subtitle.textContent = `${offline} offline, ${degraded} degraded`;
|
||||||
} else if (offline > 0) {
|
} else if (offline > 0) {
|
||||||
@@ -159,12 +173,14 @@ function updateOverallStatus(services) {
|
|||||||
} else {
|
} else {
|
||||||
subtitle.textContent = `${degraded} service${degraded !== 1 ? 's' : ''} degraded`;
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user