feat: indie status page MVP -- FastAPI + SQLite

- 8 DB models (services, incidents, monitors, subscribers, etc.)
- Full CRUD API for services, incidents, monitors
- Public status page with live data
- Incident detail page with timeline
- API key authentication
- Uptime monitoring scheduler
- 13 tests passing
- TECHNICAL_DESIGN.md with full spec
This commit is contained in:
IndieStatusBot 2026-04-25 05:00:00 +00:00
commit 902133edd3
4655 changed files with 1342691 additions and 0 deletions

172
app/static/css/style.css Normal file
View file

@ -0,0 +1,172 @@
/* Indie Status Page — Minimal responsive styles */
:root {
--accent: #4f46e5;
--up: #16a34a;
--down: #dc2626;
--degraded: #f59e0b;
--bg: #f9fafb;
--text: #111827;
--border: #e5e7eb;
--card-bg: #ffffff;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
.container {
max-width: 720px;
margin: 0 auto;
padding: 0 1.5rem;
}
header {
border-bottom: 1px solid var(--border);
padding: 1.5rem 0;
}
header h1 a {
color: var(--text);
text-decoration: none;
font-size: 1.5rem;
}
main {
padding: 2rem 0;
min-height: 70vh;
}
footer {
border-top: 1px solid var(--border);
padding: 1.5rem 0;
color: #6b7280;
font-size: 0.85rem;
}
/* Status banners */
.status-banner {
padding: 1rem;
border-radius: 8px;
text-align: center;
font-weight: 600;
margin-bottom: 2rem;
}
.status-banner--operational {
background: #d1fae5;
color: #065f46;
}
.status-banner--major {
background: #fee2e2;
color: #991b1b;
}
/* Service rows */
.service-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 6px;
margin-bottom: 0.5rem;
}
.service-name {
font-weight: 500;
}
.service-status {
font-size: 0.8rem;
font-weight: 600;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
}
.status-up { background: #d1fae5; color: #065f46; }
.status-down { background: #fee2e2; color: #991b1b; }
.status-degraded { background: #fef3c7; color: #92400e; }
/* Severity badges */
.severity {
font-size: 0.75rem;
font-weight: 600;
padding: 0.15rem 0.5rem;
border-radius: 4px;
margin-right: 0.5rem;
}
.severity-minor { background: #dbeafe; color: #1e40af; }
.severity-major { background: #fef3c7; color: #92400e; }
.severity-outage { background: #fee2e2; color: #991b1b; }
/* Incident cards */
.incident-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 1rem;
margin-bottom: 1rem;
}
.incident-card h3 a { color: var(--text); }
.incident-card h3 a:hover { color: var(--accent); }
.incident-card .timestamp { color: #6b7280; font-size: 0.85rem; }
/* Subscribe form */
.subscribe, .subscribe-page {
margin-top: 2rem;
padding: 1.5rem;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 8px;
}
form {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
}
input[type="email"] {
flex: 1;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
border-radius: 4px;
font-size: 0.95rem;
}
button[type="submit"] {
padding: 0.5rem 1.25rem;
background: var(--accent);
color: white;
border: none;
border-radius: 4px;
font-weight: 600;
cursor: pointer;
}
button[type="submit"]:hover { opacity: 0.9; }
/* Timeline */
.timeline-entry {
padding: 1rem 0;
border-left: 3px solid var(--border);
padding-left: 1.5rem;
margin-left: 0.5rem;
}
.timeline-status { font-weight: 600; }
.timeline-body { margin: 0.25rem 0; }
.timeline-time { color: #6b7280; font-size: 0.85rem; }
/* Confirm page */
.confirm-page { text-align: center; padding: 3rem 0; }

22
app/static/js/status.js Normal file
View file

@ -0,0 +1,22 @@
/* Minimal JS for auto-refreshing the status page every 60 seconds */
(function () {
const REFRESH_INTERVAL = 60000;
function autoRefresh() {
setTimeout(function () {
fetch(window.location.href, { headers: { "X-Requested-With": "XMLHttpRequest" } })
.then(function () {
window.location.reload();
})
.catch(function () {
// Silently fail — the page will try again next interval
});
}, REFRESH_INTERVAL);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", autoRefresh);
} else {
autoRefresh();
}
})();