Spaces:
Sleeping
Sleeping
Claude Code Claude Opus 4.6 commited on
Commit ·
774e62c
1
Parent(s): f5415ca
Claude Code: Add /api/health endpoint with uptime and active_agents, and footer status polling in dashboard
Browse files- app.py +9 -0
- frontend/agent-dashboard.html +112 -0
app.py
CHANGED
|
@@ -56,6 +56,15 @@ def get_app():
|
|
| 56 |
"""Health check endpoint for Docker logs."""
|
| 57 |
return {"status": "ok"}
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
@app.exception_handler(Exception)
|
| 60 |
async def global_exception_handler(request: Request, exc: Exception):
|
| 61 |
"""Global exception handler to catch all errors."""
|
|
|
|
| 56 |
"""Health check endpoint for Docker logs."""
|
| 57 |
return {"status": "ok"}
|
| 58 |
|
| 59 |
+
@app.get("/api/health")
|
| 60 |
+
async def api_health():
|
| 61 |
+
"""Health check API endpoint with uptime and agent info."""
|
| 62 |
+
return {
|
| 63 |
+
"status": "ok",
|
| 64 |
+
"uptime_seconds": time.time() - START_TIME,
|
| 65 |
+
"active_agents": 1 # Cain is always active
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
@app.exception_handler(Exception)
|
| 69 |
async def global_exception_handler(request: Request, exc: Exception):
|
| 70 |
"""Global exception handler to catch all errors."""
|
frontend/agent-dashboard.html
CHANGED
|
@@ -409,6 +409,53 @@
|
|
| 409 |
font-size: 14px;
|
| 410 |
color: #666;
|
| 411 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
</style>
|
| 413 |
</head>
|
| 414 |
<body>
|
|
@@ -560,6 +607,15 @@
|
|
| 560 |
<!-- Transfer notification container -->
|
| 561 |
<div id="notificationContainer"></div>
|
| 562 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
<script>
|
| 564 |
// Configuration
|
| 565 |
const API_BASE = window.location.origin;
|
|
@@ -579,8 +635,10 @@
|
|
| 579 |
setInterval(updateStats, 1000);
|
| 580 |
setInterval(fetchResources, RESOURCE_INTERVAL);
|
| 581 |
setInterval(fetchDebugHealth, 5000); // Poll /debug/health every 5 seconds
|
|
|
|
| 582 |
fetchResources(); // Initial fetch
|
| 583 |
fetchDebugHealth(); // Initial health fetch
|
|
|
|
| 584 |
});
|
| 585 |
|
| 586 |
// Connect to event bus
|
|
@@ -889,6 +947,60 @@
|
|
| 889 |
if (uptimeEl) uptimeEl.textContent = statusCode;
|
| 890 |
if (memoryEl) memoryEl.textContent = statusCode;
|
| 891 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 892 |
</script>
|
| 893 |
</body>
|
| 894 |
</html>
|
|
|
|
| 409 |
font-size: 14px;
|
| 410 |
color: #666;
|
| 411 |
}
|
| 412 |
+
|
| 413 |
+
/* Footer Styles */
|
| 414 |
+
.footer {
|
| 415 |
+
background: white;
|
| 416 |
+
border-radius: 12px;
|
| 417 |
+
padding: 16px 24px;
|
| 418 |
+
margin-top: 20px;
|
| 419 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 420 |
+
display: flex;
|
| 421 |
+
justify-content: space-between;
|
| 422 |
+
align-items: center;
|
| 423 |
+
flex-wrap: wrap;
|
| 424 |
+
gap: 16px;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.footer-status {
|
| 428 |
+
display: flex;
|
| 429 |
+
align-items: center;
|
| 430 |
+
gap: 12px;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.footer-status-dot {
|
| 434 |
+
width: 10px;
|
| 435 |
+
height: 10px;
|
| 436 |
+
border-radius: 50%;
|
| 437 |
+
background: #28a745;
|
| 438 |
+
animation: pulse 2s infinite;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
.footer-status-dot.offline {
|
| 442 |
+
background: #dc3545;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
.footer-status-dot.warning {
|
| 446 |
+
background: #ffc107;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.footer-info {
|
| 450 |
+
font-size: 14px;
|
| 451 |
+
color: #666;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.footer-uptime {
|
| 455 |
+
font-size: 13px;
|
| 456 |
+
color: #999;
|
| 457 |
+
font-family: monospace;
|
| 458 |
+
}
|
| 459 |
</style>
|
| 460 |
</head>
|
| 461 |
<body>
|
|
|
|
| 607 |
<!-- Transfer notification container -->
|
| 608 |
<div id="notificationContainer"></div>
|
| 609 |
|
| 610 |
+
<!-- Footer -->
|
| 611 |
+
<div class="footer">
|
| 612 |
+
<div class="footer-status">
|
| 613 |
+
<div class="footer-status-dot" id="footerStatusDot"></div>
|
| 614 |
+
<span class="footer-info" id="footerStatusText">Connecting...</span>
|
| 615 |
+
</div>
|
| 616 |
+
<div class="footer-uptime" id="footerUptime">Uptime: --</div>
|
| 617 |
+
</div>
|
| 618 |
+
|
| 619 |
<script>
|
| 620 |
// Configuration
|
| 621 |
const API_BASE = window.location.origin;
|
|
|
|
| 635 |
setInterval(updateStats, 1000);
|
| 636 |
setInterval(fetchResources, RESOURCE_INTERVAL);
|
| 637 |
setInterval(fetchDebugHealth, 5000); // Poll /debug/health every 5 seconds
|
| 638 |
+
setInterval(fetchApiHealth, 3000); // Poll /api/health every 3 seconds
|
| 639 |
fetchResources(); // Initial fetch
|
| 640 |
fetchDebugHealth(); // Initial health fetch
|
| 641 |
+
fetchApiHealth(); // Initial API health fetch
|
| 642 |
});
|
| 643 |
|
| 644 |
// Connect to event bus
|
|
|
|
| 947 |
if (uptimeEl) uptimeEl.textContent = statusCode;
|
| 948 |
if (memoryEl) memoryEl.textContent = statusCode;
|
| 949 |
}
|
| 950 |
+
|
| 951 |
+
// ========== API Health Monitoring ==========
|
| 952 |
+
|
| 953 |
+
// Fetch API health status
|
| 954 |
+
async function fetchApiHealth() {
|
| 955 |
+
try {
|
| 956 |
+
const response = await fetch(`${API_BASE}/api/health`);
|
| 957 |
+
if (response.ok) {
|
| 958 |
+
const data = await response.json();
|
| 959 |
+
updateFooterStatus(true, data);
|
| 960 |
+
} else {
|
| 961 |
+
updateFooterStatus(false, null);
|
| 962 |
+
}
|
| 963 |
+
} catch (error) {
|
| 964 |
+
console.error('Error fetching API health:', error);
|
| 965 |
+
updateFooterStatus(false, null);
|
| 966 |
+
}
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
// Update footer status display
|
| 970 |
+
function updateFooterStatus(isOnline, data) {
|
| 971 |
+
const dot = document.getElementById('footerStatusDot');
|
| 972 |
+
const text = document.getElementById('footerStatusText');
|
| 973 |
+
const uptimeEl = document.getElementById('footerUptime');
|
| 974 |
+
|
| 975 |
+
if (isOnline && data && data.status === 'ok') {
|
| 976 |
+
dot.className = 'footer-status-dot';
|
| 977 |
+
text.textContent = `Online (${data.active_agents || 1} agent${(data.active_agents || 1) !== 1 ? 's' : ''} active)`;
|
| 978 |
+
if (data.uptime_seconds !== undefined) {
|
| 979 |
+
uptimeEl.textContent = `Uptime: ${formatUptime(data.uptime_seconds)}`;
|
| 980 |
+
}
|
| 981 |
+
} else {
|
| 982 |
+
dot.className = 'footer-status-dot offline';
|
| 983 |
+
text.textContent = 'Offline - Reconnecting...';
|
| 984 |
+
}
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
// Format uptime seconds to readable string
|
| 988 |
+
function formatUptime(seconds) {
|
| 989 |
+
const days = Math.floor(seconds / 86400);
|
| 990 |
+
const hours = Math.floor((seconds % 86400) / 3600);
|
| 991 |
+
const minutes = Math.floor((seconds % 3600) / 60);
|
| 992 |
+
const secs = Math.floor(seconds % 60);
|
| 993 |
+
|
| 994 |
+
if (days > 0) {
|
| 995 |
+
return `${days}d ${hours}h ${minutes}m`;
|
| 996 |
+
} else if (hours > 0) {
|
| 997 |
+
return `${hours}h ${minutes}m ${secs}s`;
|
| 998 |
+
} else if (minutes > 0) {
|
| 999 |
+
return `${minutes}m ${secs}s`;
|
| 1000 |
+
} else {
|
| 1001 |
+
return `${secs}s`;
|
| 1002 |
+
}
|
| 1003 |
+
}
|
| 1004 |
</script>
|
| 1005 |
</body>
|
| 1006 |
</html>
|