Add styled /status page, update footer/terms links from /health to /status
All checks were successful
Deploy to Production / Deploy to Server (push) Successful in 2m49s
All checks were successful
Deploy to Production / Deploy to Server (push) Successful in 2m49s
This commit is contained in:
parent
59cc8f3d0e
commit
6cc30db5c6
11 changed files with 219 additions and 11 deletions
4
dist/index.js
vendored
4
dist/index.js
vendored
|
|
@ -195,6 +195,10 @@ app.get("/terms", (_req, res) => {
|
||||||
res.setHeader('Cache-Control', 'public, max-age=86400');
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||||
res.sendFile(path.join(__dirname, "../public/terms.html"));
|
res.sendFile(path.join(__dirname, "../public/terms.html"));
|
||||||
});
|
});
|
||||||
|
app.get("/status", (_req, res) => {
|
||||||
|
res.setHeader("Cache-Control", "public, max-age=60");
|
||||||
|
res.sendFile(path.join(__dirname, "../public/status.html"));
|
||||||
|
});
|
||||||
// API root
|
// API root
|
||||||
app.get("/api", (_req, res) => {
|
app.get("/api", (_req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
|
|
|
||||||
|
|
@ -96,9 +96,8 @@ footer .container { display: flex; justify-content: space-between; align-items:
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/docs">Docs</a>
|
<a href="/docs">Docs</a>
|
||||||
<a href="/health">API Status</a>
|
<a href="/status">API Status</a>
|
||||||
<a href="/#change-email" class="open-email-change">Change Email</a>
|
<a href="/#change-email" class="open-email-change">Change Email</a>
|
||||||
<a href="mailto:support@docfast.dev">Support</a>
|
|
||||||
<a href="/impressum">Impressum</a>
|
<a href="/impressum">Impressum</a>
|
||||||
<a href="/privacy">Privacy Policy</a>
|
<a href="/privacy">Privacy Policy</a>
|
||||||
<a href="/terms">Terms of Service</a>
|
<a href="/terms">Terms of Service</a>
|
||||||
|
|
|
||||||
|
|
@ -421,9 +421,8 @@ html, body {
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/docs">Docs</a>
|
<a href="/docs">Docs</a>
|
||||||
<a href="/health">API Status</a>
|
<a href="/status">API Status</a>
|
||||||
<a href="/#change-email" class="open-email-change">Change Email</a>
|
<a href="/#change-email" class="open-email-change">Change Email</a>
|
||||||
<a href="mailto:support@docfast.dev">Support</a>
|
|
||||||
<a href="/impressum">Impressum</a>
|
<a href="/impressum">Impressum</a>
|
||||||
<a href="/privacy">Privacy Policy</a>
|
<a href="/privacy">Privacy Policy</a>
|
||||||
<a href="/terms">Terms of Service</a>
|
<a href="/terms">Terms of Service</a>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/docs">Docs</a>
|
<a href="/docs">Docs</a>
|
||||||
<a href="/health">API Status</a>
|
<a href="/status">API Status</a>
|
||||||
<a href="/#change-email" class="open-email-change">Change Email</a>
|
<a href="/#change-email" class="open-email-change">Change Email</a>
|
||||||
<a href="/impressum">Impressum</a>
|
<a href="/impressum">Impressum</a>
|
||||||
<a href="/privacy">Privacy Policy</a>
|
<a href="/privacy">Privacy Policy</a>
|
||||||
|
|
|
||||||
|
|
@ -178,9 +178,8 @@ footer .container { display: flex; justify-content: space-between; align-items:
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/docs">Docs</a>
|
<a href="/docs">Docs</a>
|
||||||
<a href="/health">API Status</a>
|
<a href="/status">API Status</a>
|
||||||
<a href="/#change-email" class="open-email-change">Change Email</a>
|
<a href="/#change-email" class="open-email-change">Change Email</a>
|
||||||
<a href="mailto:support@docfast.dev">Support</a>
|
|
||||||
<a href="/impressum">Impressum</a>
|
<a href="/impressum">Impressum</a>
|
||||||
<a href="/privacy">Privacy Policy</a>
|
<a href="/privacy">Privacy Policy</a>
|
||||||
<a href="/terms">Terms of Service</a>
|
<a href="/terms">Terms of Service</a>
|
||||||
|
|
|
||||||
48
public/src/status.html
Normal file
48
public/src/status.html
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>System Status — DocFast</title>
|
||||||
|
<meta name="description" content="Real-time system status for the DocFast PDF API.">
|
||||||
|
<link rel="canonical" href="https://docfast.dev/status">
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
{{> styles_base}}
|
||||||
|
<style>
|
||||||
|
.status-hero { text-align: center; padding: 48px 0 32px; }
|
||||||
|
.status-indicator { display: inline-flex; align-items: center; gap: 12px; font-size: 1.5rem; font-weight: 700; margin-bottom: 8px; }
|
||||||
|
.status-dot { width: 16px; height: 16px; border-radius: 50%; display: inline-block; }
|
||||||
|
.status-dot.ok { background: var(--accent); box-shadow: 0 0 12px var(--accent); }
|
||||||
|
.status-dot.degraded { background: #f59e0b; box-shadow: 0 0 12px #f59e0b; }
|
||||||
|
.status-dot.error { background: #ef4444; box-shadow: 0 0 12px #ef4444; }
|
||||||
|
.status-meta { color: var(--muted); font-size: 0.85rem; margin-top: 8px; }
|
||||||
|
.status-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 32px 0; }
|
||||||
|
@media (max-width: 600px) { .status-grid { grid-template-columns: 1fr; } }
|
||||||
|
.status-card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; }
|
||||||
|
.status-card h3 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); margin-bottom: 16px; }
|
||||||
|
.status-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid var(--border); }
|
||||||
|
.status-row:last-child { border-bottom: none; }
|
||||||
|
.status-label { color: var(--muted); font-size: 0.9rem; }
|
||||||
|
.status-value { font-weight: 600; font-size: 0.9rem; }
|
||||||
|
.status-value.ok { color: var(--accent); }
|
||||||
|
.status-value.warn { color: #f59e0b; }
|
||||||
|
.status-value.err { color: #ef4444; }
|
||||||
|
.status-loading { text-align: center; padding: 60px 0; color: var(--muted); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
{{> nav}}
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="container">
|
||||||
|
<div id="status-content" class="status-loading">Loading system status…</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{{> footer}}
|
||||||
|
|
||||||
|
<script src="/status.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -93,7 +93,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Target:</strong> 99.5% uptime (best effort, no SLA for free tier)</li>
|
<li><strong>Target:</strong> 99.5% uptime (best effort, no SLA for free tier)</li>
|
||||||
<li><strong>Maintenance:</strong> Scheduled maintenance with advance notice</li>
|
<li><strong>Maintenance:</strong> Scheduled maintenance with advance notice</li>
|
||||||
<li><strong>Status page:</strong> <a href="/health">https://docfast.dev/health</a></li>
|
<li><strong>Status page:</strong> <a href="/status">https://docfast.dev/status</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3>5.2 Performance</h3>
|
<h3>5.2 Performance</h3>
|
||||||
|
|
|
||||||
106
public/status.html
Normal file
106
public/status.html
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>System Status — DocFast</title>
|
||||||
|
<meta name="description" content="Real-time system status for the DocFast PDF API.">
|
||||||
|
<link rel="canonical" href="https://docfast.dev/status">
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
:root {
|
||||||
|
--bg: #0b0d11; --bg2: #12151c; --fg: #e4e7ed; --muted: #7a8194;
|
||||||
|
--accent: #34d399; --accent-hover: #5eead4; --accent-glow: rgba(52,211,153,0.12);
|
||||||
|
--card: #151922; --border: #1e2433;
|
||||||
|
--radius: 12px; --radius-lg: 16px;
|
||||||
|
}
|
||||||
|
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--fg); line-height: 1.65; -webkit-font-smoothing: antialiased; }
|
||||||
|
a { color: var(--accent); text-decoration: none; transition: color 0.2s; }
|
||||||
|
a:hover { color: var(--accent-hover); }
|
||||||
|
.container { max-width: 800px; margin: 0 auto; padding: 0 24px; }
|
||||||
|
nav { padding: 20px 0; border-bottom: 1px solid var(--border); }
|
||||||
|
nav .container { display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.logo { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.5px; color: var(--fg); display: flex; align-items: center; gap: 8px; text-decoration: none; }
|
||||||
|
.logo span { color: var(--accent); }
|
||||||
|
.nav-links { display: flex; gap: 28px; align-items: center; }
|
||||||
|
.nav-links a { color: var(--muted); font-size: 0.9rem; font-weight: 500; }
|
||||||
|
.nav-links a:hover { color: var(--fg); }
|
||||||
|
.content { padding: 60px 0; min-height: 60vh; }
|
||||||
|
.content h1 { font-size: 2rem; font-weight: 800; margin-bottom: 32px; letter-spacing: -1px; }
|
||||||
|
.content h2 { font-size: 1.3rem; font-weight: 700; margin: 32px 0 16px; color: var(--fg); }
|
||||||
|
.content h3 { font-size: 1.1rem; font-weight: 600; margin: 24px 0 12px; color: var(--fg); }
|
||||||
|
.content p, .content li { color: var(--muted); margin-bottom: 12px; }
|
||||||
|
.content ul, .content ol { padding-left: 24px; }
|
||||||
|
.content strong { color: var(--fg); }
|
||||||
|
footer { padding: 32px 0; border-top: 1px solid var(--border); margin-top: 60px; }
|
||||||
|
footer .container { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; }
|
||||||
|
.footer-left { color: var(--muted); font-size: 0.85rem; }
|
||||||
|
.footer-links { display: flex; gap: 20px; flex-wrap: wrap; }
|
||||||
|
.footer-links a { color: var(--muted); font-size: 0.85rem; }
|
||||||
|
.footer-links a:hover { color: var(--fg); }
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
footer .container { flex-direction: column; text-align: center; }
|
||||||
|
.nav-links { gap: 16px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
.status-hero { text-align: center; padding: 48px 0 32px; }
|
||||||
|
.status-indicator { display: inline-flex; align-items: center; gap: 12px; font-size: 1.5rem; font-weight: 700; margin-bottom: 8px; }
|
||||||
|
.status-dot { width: 16px; height: 16px; border-radius: 50%; display: inline-block; }
|
||||||
|
.status-dot.ok { background: var(--accent); box-shadow: 0 0 12px var(--accent); }
|
||||||
|
.status-dot.degraded { background: #f59e0b; box-shadow: 0 0 12px #f59e0b; }
|
||||||
|
.status-dot.error { background: #ef4444; box-shadow: 0 0 12px #ef4444; }
|
||||||
|
.status-meta { color: var(--muted); font-size: 0.85rem; margin-top: 8px; }
|
||||||
|
.status-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin: 32px 0; }
|
||||||
|
@media (max-width: 600px) { .status-grid { grid-template-columns: 1fr; } }
|
||||||
|
.status-card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; }
|
||||||
|
.status-card h3 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); margin-bottom: 16px; }
|
||||||
|
.status-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid var(--border); }
|
||||||
|
.status-row:last-child { border-bottom: none; }
|
||||||
|
.status-label { color: var(--muted); font-size: 0.9rem; }
|
||||||
|
.status-value { font-weight: 600; font-size: 0.9rem; }
|
||||||
|
.status-value.ok { color: var(--accent); }
|
||||||
|
.status-value.warn { color: #f59e0b; }
|
||||||
|
.status-value.err { color: #ef4444; }
|
||||||
|
.status-loading { text-align: center; padding: 60px 0; color: var(--muted); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav aria-label="Main navigation">
|
||||||
|
<div class="container">
|
||||||
|
<a href="/" class="logo">⚡ Doc<span>Fast</span></a>
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="/#features">Features</a>
|
||||||
|
<a href="/#pricing">Pricing</a>
|
||||||
|
<a href="/docs">Docs</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="container">
|
||||||
|
<div id="status-content" class="status-loading">Loading system status…</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer aria-label="Footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer-left">© 2026 DocFast. Fast PDF generation for developers.</div>
|
||||||
|
<div class="footer-links">
|
||||||
|
<a href="/">Home</a>
|
||||||
|
<a href="/docs">Docs</a>
|
||||||
|
<a href="/status">API Status</a>
|
||||||
|
<a href="/#change-email" class="open-email-change">Change Email</a>
|
||||||
|
<a href="/impressum">Impressum</a>
|
||||||
|
<a href="/privacy">Privacy Policy</a>
|
||||||
|
<a href="/terms">Terms of Service</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/status.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
48
public/status.js
Normal file
48
public/status.js
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
async function fetchStatus() {
|
||||||
|
const el = document.getElementById("status-content");
|
||||||
|
try {
|
||||||
|
const res = await fetch("/health");
|
||||||
|
const d = await res.json();
|
||||||
|
const isOk = d.status === "ok";
|
||||||
|
const isDegraded = d.status === "degraded";
|
||||||
|
const dotClass = isOk ? "ok" : isDegraded ? "degraded" : "error";
|
||||||
|
const label = isOk ? "All Systems Operational" : isDegraded ? "Degraded Performance" : "Service Disruption";
|
||||||
|
const now = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
|
el.innerHTML =
|
||||||
|
"<div class=\"status-hero\">" +
|
||||||
|
"<div class=\"status-indicator\"><span class=\"status-dot " + dotClass + "\"></span> " + label + "</div>" +
|
||||||
|
"<div class=\"status-meta\">Version " + d.version + " · Last checked " + now + " · Auto-refreshes every 30s</div>" +
|
||||||
|
"</div>" +
|
||||||
|
"<div class=\"status-grid\">" +
|
||||||
|
"<div class=\"status-card\">" +
|
||||||
|
"<h3>🗄️ Database</h3>" +
|
||||||
|
"<div class=\"status-row\"><span class=\"status-label\">Status</span><span class=\"status-value " + (d.database && d.database.status === "ok" ? "ok" : "err") + "\">" + (d.database && d.database.status === "ok" ? "Connected" : "Error") + "</span></div>" +
|
||||||
|
"<div class=\"status-row\"><span class=\"status-label\">Engine</span><span class=\"status-value\">" + (d.database ? d.database.version : "Unknown") + "</span></div>" +
|
||||||
|
"</div>" +
|
||||||
|
"<div class=\"status-card\">" +
|
||||||
|
"<h3>🖨️ PDF Engine</h3>" +
|
||||||
|
"<div class=\"status-row\"><span class=\"status-label\">Status</span><span class=\"status-value " + (d.pool && d.pool.available > 0 ? "ok" : "warn") + "\">" + (d.pool && d.pool.available > 0 ? "Ready" : "Busy") + "</span></div>" +
|
||||||
|
"<div class=\"status-row\"><span class=\"status-label\">Available</span><span class=\"status-value\">" + (d.pool ? d.pool.available : 0) + " / " + (d.pool ? d.pool.size : 0) + "</span></div>" +
|
||||||
|
"<div class=\"status-row\"><span class=\"status-label\">Queue</span><span class=\"status-value " + (d.pool && d.pool.queueDepth > 0 ? "warn" : "ok") + "\">" + (d.pool ? d.pool.queueDepth : 0) + " waiting</span></div>" +
|
||||||
|
"<div class=\"status-row\"><span class=\"status-label\">PDFs Generated</span><span class=\"status-value\">" + (d.pool ? d.pool.pdfCount.toLocaleString() : "0") + "</span></div>" +
|
||||||
|
"<div class=\"status-row\"><span class=\"status-label\">Uptime</span><span class=\"status-value\">" + formatUptime(d.pool ? d.pool.uptimeSeconds : 0) + "</span></div>" +
|
||||||
|
"</div>" +
|
||||||
|
"</div>" +
|
||||||
|
"<div style=\"text-align:center;margin-top:16px;\"><a href=\"/health\" style=\"font-size:0.8rem;color:var(--muted);\">Raw JSON endpoint →</a></div>";
|
||||||
|
} catch (e) {
|
||||||
|
el.innerHTML = "<div class=\"status-hero\"><div class=\"status-indicator\"><span class=\"status-dot error\"></span> Unable to reach API</div><div class=\"status-meta\">The service may be temporarily unavailable. Please try again shortly.</div></div>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUptime(s) {
|
||||||
|
if (!s && s !== 0) return "Unknown";
|
||||||
|
if (s < 60) return s + "s";
|
||||||
|
if (s < 3600) return Math.floor(s/60) + "m " + (s%60) + "s";
|
||||||
|
var h = Math.floor(s/3600);
|
||||||
|
var m = Math.floor((s%3600)/60);
|
||||||
|
return h + "h " + m + "m";
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchStatus();
|
||||||
|
setInterval(fetchStatus, 30000);
|
||||||
|
|
@ -138,7 +138,7 @@ footer .container { display: flex; justify-content: space-between; align-items:
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Target:</strong> 99.5% uptime (best effort, no SLA for free tier)</li>
|
<li><strong>Target:</strong> 99.5% uptime (best effort, no SLA for free tier)</li>
|
||||||
<li><strong>Maintenance:</strong> Scheduled maintenance with advance notice</li>
|
<li><strong>Maintenance:</strong> Scheduled maintenance with advance notice</li>
|
||||||
<li><strong>Status page:</strong> <a href="/health">https://docfast.dev/health</a></li>
|
<li><strong>Status page:</strong> <a href="/status">https://docfast.dev/status</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3>5.2 Performance</h3>
|
<h3>5.2 Performance</h3>
|
||||||
|
|
@ -250,9 +250,8 @@ footer .container { display: flex; justify-content: space-between; align-items:
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/docs">Docs</a>
|
<a href="/docs">Docs</a>
|
||||||
<a href="/health">API Status</a>
|
<a href="/status">API Status</a>
|
||||||
<a href="/#change-email" class="open-email-change">Change Email</a>
|
<a href="/#change-email" class="open-email-change">Change Email</a>
|
||||||
<a href="mailto:support@docfast.dev">Support</a>
|
|
||||||
<a href="/impressum">Impressum</a>
|
<a href="/impressum">Impressum</a>
|
||||||
<a href="/privacy">Privacy Policy</a>
|
<a href="/privacy">Privacy Policy</a>
|
||||||
<a href="/terms">Terms of Service</a>
|
<a href="/terms">Terms of Service</a>
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,12 @@ app.get("/terms", (_req, res) => {
|
||||||
res.sendFile(path.join(__dirname, "../public/terms.html"));
|
res.sendFile(path.join(__dirname, "../public/terms.html"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/status", (_req, res) => {
|
||||||
|
res.setHeader("Cache-Control", "public, max-age=60");
|
||||||
|
res.sendFile(path.join(__dirname, "../public/status.html"));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// API root
|
// API root
|
||||||
app.get("/api", (_req, res) => {
|
app.get("/api", (_req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue