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:
commit
902133edd3
4655 changed files with 1342691 additions and 0 deletions
172
app/static/css/style.css
Normal file
172
app/static/css/style.css
Normal 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
22
app/static/js/status.js
Normal 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();
|
||||
}
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue