Single-file HTML with vanilla CSS, monospace terminal aesthetic, and live health check indicators for backend services. Includes tech stack statement and cost footer.
146 lines
4.0 KiB
HTML
146 lines
4.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Portfolio</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Courier New', monospace;
|
|
line-height: 1.6;
|
|
padding: 2rem;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
background: #0a0a0a;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.stack {
|
|
margin: 2rem 0 3rem 0;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.status {
|
|
margin: 3rem 0;
|
|
}
|
|
|
|
.service {
|
|
margin: 0.5rem 0;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.indicator {
|
|
display: inline-block;
|
|
width: 8px;
|
|
height: 8px;
|
|
margin-right: 8px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.running {
|
|
background: #4a9e5f;
|
|
}
|
|
|
|
.failed {
|
|
background: #c75450;
|
|
}
|
|
|
|
.checking {
|
|
background: #666;
|
|
animation: blink 1s infinite;
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 50% { opacity: 1; }
|
|
51%, 100% { opacity: 0.3; }
|
|
}
|
|
|
|
.cost {
|
|
margin-top: 3rem;
|
|
font-size: 0.85rem;
|
|
color: #888;
|
|
}
|
|
|
|
footer {
|
|
margin-top: 5rem;
|
|
padding-top: 2rem;
|
|
border-top: 1px solid #222;
|
|
font-size: 0.85rem;
|
|
color: #666;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="stack">
|
|
Go for the backend, net/http for the API, SQLite for data, goquery for scraping, Docker to package it, self-hosted on Hetzner.
|
|
</div>
|
|
|
|
<div class="status">
|
|
<div class="service">
|
|
<span class="indicator checking" id="api-indicator"></span>
|
|
<span id="api-status">API</span>
|
|
</div>
|
|
<div class="service">
|
|
<span class="indicator checking" id="scraper-indicator"></span>
|
|
<span id="scraper-status">Scraper</span>
|
|
</div>
|
|
<div class="service">
|
|
<span class="indicator checking" id="db-indicator"></span>
|
|
<span id="db-status">Database</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cost">
|
|
All code on this page runs on five euros a month.
|
|
</div>
|
|
|
|
<footer>
|
|
Alps 2,034m above complaints.
|
|
</footer>
|
|
|
|
<script>
|
|
// Simulated status checks - replace with actual endpoints
|
|
const services = [
|
|
{ id: 'api', name: 'API', endpoint: '/health' },
|
|
{ id: 'scraper', name: 'Scraper', endpoint: '/scraper/status' },
|
|
{ id: 'db', name: 'Database', endpoint: '/db/ping' }
|
|
];
|
|
|
|
async function checkService(service) {
|
|
const indicator = document.getElementById(`${service.id}-indicator`);
|
|
const status = document.getElementById(`${service.id}-status`);
|
|
|
|
try {
|
|
// Replace this with actual fetch to your endpoints
|
|
// const response = await fetch(service.endpoint);
|
|
// const isRunning = response.ok;
|
|
|
|
// Simulated check - shows running after 1 second
|
|
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000));
|
|
const isRunning = Math.random() > 0.1; // 90% uptime simulation
|
|
|
|
indicator.className = `indicator ${isRunning ? 'running' : 'failed'}`;
|
|
status.textContent = `${service.name} ${isRunning ? 'running' : 'failed'}`;
|
|
} catch (error) {
|
|
indicator.className = 'indicator failed';
|
|
status.textContent = `${service.name} failed`;
|
|
}
|
|
}
|
|
|
|
function checkAll() {
|
|
services.forEach(checkService);
|
|
}
|
|
|
|
// Check on load and every 30 seconds
|
|
checkAll();
|
|
setInterval(checkAll, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|