You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
376 lines
15 KiB
376 lines
15 KiB
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>System Health - OTB Billing</title> |
|
<link rel="stylesheet" href="/static/css/style.css"> |
|
<style> |
|
.page-wrap { |
|
padding: 1.5rem; |
|
} |
|
|
|
.page-header h1 { |
|
margin-bottom: 0.35rem; |
|
} |
|
|
|
.page-header p { |
|
margin-top: 0; |
|
opacity: 0.9; |
|
} |
|
|
|
.health-links { |
|
margin: 1rem 0 1.25rem 0; |
|
} |
|
|
|
.health-links a { |
|
margin-right: 1rem; |
|
text-decoration: underline; |
|
} |
|
|
|
.health-grid { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); |
|
gap: 1rem; |
|
margin-top: 1rem; |
|
} |
|
|
|
.health-card { |
|
border: 1px solid rgba(255,255,255,0.16); |
|
border-radius: 14px; |
|
padding: 1rem 1.1rem; |
|
background: rgba(255,255,255,0.03); |
|
box-shadow: 0 6px 18px rgba(0,0,0,0.16); |
|
} |
|
|
|
.health-card h3 { |
|
margin-top: 0; |
|
margin-bottom: 0.85rem; |
|
} |
|
|
|
.health-card a { |
|
color: inherit; |
|
text-decoration: underline; |
|
text-underline-offset: 2px; |
|
} |
|
|
|
.ok { |
|
color: #69db7c; |
|
font-weight: 700; |
|
} |
|
|
|
.warn { |
|
color: #ffd43b; |
|
font-weight: 700; |
|
} |
|
|
|
.bad { |
|
color: #ff8787; |
|
font-weight: 700; |
|
} |
|
|
|
.health-action-button { |
|
margin-top: 0.65rem; |
|
padding: 0.45rem 0.7rem; |
|
border: 1px solid rgba(255,255,255,0.25); |
|
border-radius: 0.45rem; |
|
background: rgba(255,255,255,0.08); |
|
color: inherit; |
|
cursor: pointer; |
|
} |
|
|
|
.health-action-button:hover { |
|
background: rgba(255,255,255,0.14); |
|
} |
|
|
|
.health-note { |
|
margin-top: 0.55rem; |
|
opacity: 0.9; |
|
} |
|
|
|
.health-card p { |
|
margin: 0.45rem 0; |
|
} |
|
|
|
.status-good { |
|
color: #63d471; |
|
font-weight: 700; |
|
} |
|
|
|
.status-bad { |
|
color: #ff7b7b; |
|
font-weight: 700; |
|
} |
|
|
|
.monoish { |
|
word-break: break-word; |
|
} |
|
|
|
.health-toast { |
|
position: fixed; |
|
right: 1.25rem; |
|
bottom: 1.25rem; |
|
z-index: 9999; |
|
min-width: 220px; |
|
max-width: 360px; |
|
padding: 0.8rem 1rem; |
|
border: 1px solid rgba(255,255,255,0.22); |
|
border-radius: 0.55rem; |
|
background: rgba(20, 24, 38, 0.96); |
|
color: inherit; |
|
box-shadow: 0 10px 28px rgba(0,0,0,0.35); |
|
opacity: 0; |
|
transform: translateY(12px); |
|
transition: opacity 180ms ease, transform 180ms ease; |
|
pointer-events: none; |
|
} |
|
|
|
.health-toast.show { |
|
opacity: 1; |
|
transform: translateY(0); |
|
} |
|
|
|
.health-toast.ok { |
|
border-color: rgba(105, 219, 124, 0.55); |
|
} |
|
|
|
.health-toast.bad { |
|
border-color: rgba(255, 135, 135, 0.55); |
|
} |
|
|
|
|
|
</style> |
|
<link rel="icon" type="image/png" href="/static/favicon.png"> |
|
</head> |
|
<body> |
|
<div class="page-wrap"> |
|
<div class="page-header"> |
|
<h1>System Health</h1> |
|
<p>Application and server status for OTB Billing.</p> |
|
</div> |
|
|
|
<div class="health-links"> |
|
<a href="/">Home</a> |
|
<a href="/health.json">Raw JSON</a> |
|
</div> |
|
|
|
<div class="health-grid"> |
|
<div class="health-card"> |
|
<h3>Status</h3> |
|
{% if health.status == "ok" %} |
|
<p class="status-good">Healthy</p> |
|
{% else %} |
|
<p class="status-bad">Degraded</p> |
|
{% endif %} |
|
<p><strong>App:</strong> {{ health.app_name }}</p> |
|
<p><strong>Host:</strong> {{ health.hostname }}</p> |
|
<p class="monoish"><strong>Toronto Time:</strong> {{ health.server_time_toronto }}</p> |
|
<p class="monoish"><strong>UTC Time:</strong> {{ health.server_time_utc }}</p> |
|
</div> |
|
|
|
<div class="health-card"> |
|
<h3>Database</h3> |
|
{% if health.database.ok %} |
|
<p class="status-good">Connected</p> |
|
{% else %} |
|
<p class="status-bad">Connection Error</p> |
|
{% endif %} |
|
<p class="monoish"><strong>Error:</strong> {{ health.database.error or "None" }}</p> |
|
</div> |
|
|
|
<div class="health-card"> |
|
<h3>Uptime</h3> |
|
<p><strong>Application:</strong> {{ health.app_uptime_human }}</p> |
|
<p><strong>Server:</strong> {{ health.server_uptime_human }}</p> |
|
</div> |
|
|
|
<div class="health-card"> |
|
<h3>Load Average</h3> |
|
<p><strong>1 min:</strong> {{ health.load_average["1m"] }}</p> |
|
<p><strong>5 min:</strong> {{ health.load_average["5m"] }}</p> |
|
<p><strong>15 min:</strong> {{ health.load_average["15m"] }}</p> |
|
</div> |
|
|
|
<div class="health-card"> |
|
<h3>Memory</h3> |
|
<p><strong>Total:</strong> {{ health.memory.total_mb }} MB</p> |
|
<p><strong>Available:</strong> {{ health.memory.available_mb }} MB</p> |
|
<p><strong>Used:</strong> {{ health.memory.used_mb }} MB</p> |
|
<p><strong>Used %:</strong> {{ health.memory.used_percent }}%</p> |
|
</div> |
|
|
|
<div class="health-card"> |
|
<h3>Disk /</h3> |
|
<p><strong>Total:</strong> {{ health.disk_root.total_gb }} GB</p> |
|
<p><strong>Used:</strong> {{ health.disk_root.used_gb }} GB</p> |
|
<p><strong>Free:</strong> {{ health.disk_root.free_gb }} GB</p> |
|
<p><strong>Used %:</strong> {{ health.disk_root.used_percent }}%</p> |
|
</div> |
|
|
|
|
|
<!-- OTB_HEALTH_REVENUE_PANELS_V0_6_0 --> |
|
<div class="health-card"> |
|
<h3>Square / Revenue Health</h3> |
|
{% if health.revenue_health and health.revenue_health.available %} |
|
<p><strong>Square confirmed payments</strong></p> |
|
<p><strong>Today:</strong> {{ health.revenue_health.square.today.count }} payment{% if health.revenue_health.square.today.count != 1 %}s{% endif %} | {{ health.revenue_health.square.today.cad_total_display }}</p> |
|
<p><strong>This Month:</strong> {{ health.revenue_health.square.month.count }} payment{% if health.revenue_health.square.month.count != 1 %}s{% endif %} | {{ health.revenue_health.square.month.cad_total_display }}</p> |
|
<p><strong>This Year:</strong> {{ health.revenue_health.square.year.count }} payment{% if health.revenue_health.square.year.count != 1 %}s{% endif %} | {{ health.revenue_health.square.year.cad_total_display }}</p> |
|
|
|
<hr> |
|
|
|
<p><strong>All confirmed payments</strong></p> |
|
<p><strong>Today:</strong> {{ health.revenue_health.all_confirmed.today.count }} payment{% if health.revenue_health.all_confirmed.today.count != 1 %}s{% endif %} | {{ health.revenue_health.all_confirmed.today.cad_total_display }}</p> |
|
<p><strong>This Month:</strong> {{ health.revenue_health.all_confirmed.month.count }} payment{% if health.revenue_health.all_confirmed.month.count != 1 %}s{% endif %} | {{ health.revenue_health.all_confirmed.month.cad_total_display }}</p> |
|
<p><strong>This Year:</strong> {{ health.revenue_health.all_confirmed.year.count }} payment{% if health.revenue_health.all_confirmed.year.count != 1 %}s{% endif %} | {{ health.revenue_health.all_confirmed.year.cad_total_display }}</p> |
|
|
|
{% if health.revenue_health.last_payment %} |
|
<hr> |
|
<p><strong>Last Payment:</strong> {{ health.revenue_health.last_payment.cad_total_display }} {{ health.revenue_health.last_payment.currency }}</p> |
|
<p class="monoish"><strong>When:</strong> {{ health.revenue_health.last_payment.payment_time }}</p> |
|
{% endif %} |
|
{% elif health.revenue_health %} |
|
<p class="status-bad">Unavailable</p> |
|
<p class="monoish"><strong>Error:</strong> {{ health.revenue_health.error }}</p> |
|
{% else %} |
|
<p class="status-bad">Unavailable</p> |
|
{% endif %} |
|
</div> |
|
|
|
<div class="health-card"> |
|
<h3>Receivables Aging</h3> |
|
{% if health.receivables_aging and health.receivables_aging.available %} |
|
<p><strong>Total Unpaid:</strong> {{ health.receivables_aging.total_unpaid_display }}</p> |
|
<p><strong>Open Invoices:</strong> {{ health.receivables_aging.invoice_count }}</p> |
|
<hr> |
|
<p><strong>Current:</strong> {{ health.receivables_aging.current.count }} invoice{% if health.receivables_aging.current.count != 1 %}s{% endif %} | {{ health.receivables_aging.current.cad_total_display }}</p> |
|
<p><strong>1-30 Days:</strong> {{ health.receivables_aging.d1_30.count }} invoice{% if health.receivables_aging.d1_30.count != 1 %}s{% endif %} | {{ health.receivables_aging.d1_30.cad_total_display }}</p> |
|
<p><strong>31-60 Days:</strong> {{ health.receivables_aging.d31_60.count }} invoice{% if health.receivables_aging.d31_60.count != 1 %}s{% endif %} | {{ health.receivables_aging.d31_60.cad_total_display }}</p> |
|
<p><strong>61-90 Days:</strong> {{ health.receivables_aging.d61_90.count }} invoice{% if health.receivables_aging.d61_90.count != 1 %}s{% endif %} | {{ health.receivables_aging.d61_90.cad_total_display }}</p> |
|
<p><strong>90+ Days:</strong> {{ health.receivables_aging.d90_plus.count }} invoice{% if health.receivables_aging.d90_plus.count != 1 %}s{% endif %} | {{ health.receivables_aging.d90_plus.cad_total_display }}</p> |
|
{% elif health.receivables_aging %} |
|
<p class="status-bad">Unavailable</p> |
|
<p class="monoish"><strong>Error:</strong> {{ health.receivables_aging.error }}</p> |
|
{% else %} |
|
<p class="status-bad">Unavailable</p> |
|
{% endif %} |
|
</div> |
|
|
|
<div class="health-card"> |
|
<h3>Operations Balance</h3> |
|
{% if health.operations_balances %} |
|
{% for asset in health.operations_balances.assets %} |
|
<p> |
|
<strong>{% if asset.explorer_url %}<a href="{{ asset.explorer_url }}" target="_blank" rel="noopener">{{ asset.coin }}</a>{% else %}{{ asset.coin }}{% endif %}:</strong> |
|
{% if asset.ok %} |
|
{{ asset.balance }} |
|
{% else %} |
|
error |
|
{% endif %} |
|
</p> |
|
{% endfor %} |
|
<p class="monoish"><strong>Wallet:</strong> {{ health.operations_balances.wallet }}</p> |
|
{% else %} |
|
<p>Not available</p> |
|
{% endif %} |
|
</div> |
|
|
|
<div class="health-card"> |
|
<h3>Treasury Balance</h3> |
|
{% if health.treasury_balances %} |
|
{% for asset in health.treasury_balances.assets %} |
|
<p> |
|
<strong>{% if asset.explorer_url %}<a href="{{ asset.explorer_url }}" target="_blank" rel="noopener">{{ asset.coin }}</a>{% else %}{{ asset.coin }}{% endif %}:</strong> |
|
{% if asset.ok %} |
|
{{ asset.balance }} |
|
{% else %} |
|
error |
|
{% endif %} |
|
</p> |
|
{% endfor %} |
|
<p class="monoish"><strong>Wallet:</strong> {{ health.treasury_balances.wallet }}</p> |
|
{% else %} |
|
<p>Not available</p> |
|
{% endif %} |
|
</div> |
|
|
|
<div class="health-card"> |
|
<h3>Crypto Reconcile</h3> |
|
{% if health.crypto_reconcile %} |
|
<p><strong>Timer:</strong> |
|
{% if health.crypto_reconcile.timer_active == "active" %} |
|
<span class="ok">{{ health.crypto_reconcile.timer_active }}</span> |
|
{% else %} |
|
<span class="warn">{{ health.crypto_reconcile.timer_active }}</span> |
|
{% endif %} |
|
</p> |
|
<p><strong>Service:</strong> {{ health.crypto_reconcile.service_active }}</p> |
|
<p class="monoish"><strong>Last Run:</strong> {{ health.crypto_reconcile.last_run }}</p> |
|
<p><strong>Last Result:</strong> {{ health.crypto_reconcile.last_result }}</p> |
|
|
|
{% if health.crypto_reconcile.payment_stats and health.crypto_reconcile.payment_stats.available %} |
|
<p><strong>Pending:</strong> {{ health.crypto_reconcile.payment_stats.pending }}</p> |
|
<p><strong>Confirmed Today:</strong> {{ health.crypto_reconcile.payment_stats.confirmed_today }}</p> |
|
<p><strong>Stale Pending:</strong> |
|
{% if health.crypto_reconcile.payment_stats.stale_pending and health.crypto_reconcile.payment_stats.stale_pending > 0 %} |
|
<span class="warn">{{ health.crypto_reconcile.payment_stats.stale_pending }}</span> |
|
{% else %} |
|
{{ health.crypto_reconcile.payment_stats.stale_pending }} |
|
{% endif %} |
|
</p> |
|
{% elif health.crypto_reconcile.payment_stats %} |
|
<p><strong>Stats:</strong> <span class="warn">unavailable</span></p> |
|
<p class="monoish"><strong>Error:</strong> {{ health.crypto_reconcile.payment_stats.error }}</p> |
|
{% endif %} |
|
|
|
<form method="post" action="/health/reconcile-now"> |
|
<button class="health-action-button" type="submit">Reconcile Now</button> |
|
</form> |
|
{% else %} |
|
<p>Not available</p> |
|
{% endif %} |
|
</div> |
|
|
|
</div> |
|
</div> |
|
|
|
{% include "footer.html" %} |
|
|
|
<div id="health-toast" class="health-toast" role="status" aria-live="polite"></div> |
|
|
|
<script> |
|
(function () { |
|
const params = new URLSearchParams(window.location.search); |
|
const result = params.get("reconcile"); |
|
if (!result) return; |
|
|
|
const messages = { |
|
"started": { text: "Manual reconcile started.", cls: "ok" }, |
|
"failed": { text: "Manual reconcile failed to start.", cls: "bad" }, |
|
"missing-worker": { text: "Worker script missing.", cls: "bad" } |
|
}; |
|
|
|
const reconcileToastMessage = messages[result]; |
|
if (!reconcileToastMessage) return; |
|
|
|
const toast = document.getElementById("health-toast"); |
|
if (!toast) return; |
|
|
|
toast.textContent = reconcileToastMessage.text; |
|
toast.classList.add(reconcileToastMessage.cls); |
|
|
|
requestAnimationFrame(function () { |
|
toast.classList.add("show"); |
|
}); |
|
|
|
setTimeout(function () { |
|
toast.classList.remove("show"); |
|
|
|
const cleanUrl = window.location.pathname + window.location.hash; |
|
window.history.replaceState({}, document.title, cleanUrl); |
|
}, 3000); |
|
})(); |
|
</script> |
|
|
|
</body> |
|
</html>
|
|
|