995 lines
30 KiB
HTML
995 lines
30 KiB
HTML
|
|
<div class="health-container">
|
||
|
|
<!-- Health Overview -->
|
||
|
|
<div class="health-overview"
|
||
|
|
hx-get="/api/monitoring/health/overview"
|
||
|
|
hx-trigger="load, every 10s"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<div class="health-status healthy">
|
||
|
|
<div class="status-icon">
|
||
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||
|
|
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
<div class="status-info">
|
||
|
|
<h2>All Systems Operational</h2>
|
||
|
|
<p>All health checks are passing</p>
|
||
|
|
</div>
|
||
|
|
<div class="status-badge healthy">Healthy</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Uptime Stats -->
|
||
|
|
<div class="uptime-stats">
|
||
|
|
<div class="stat-card"
|
||
|
|
hx-get="/api/monitoring/health/uptime"
|
||
|
|
hx-trigger="load, every 60s"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<div class="stat-icon">
|
||
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<circle cx="12" cy="12" r="10"></circle>
|
||
|
|
<polyline points="12 6 12 12 16 14"></polyline>
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
<div class="stat-content">
|
||
|
|
<span class="stat-value">--</span>
|
||
|
|
<span class="stat-label">Uptime</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-card"
|
||
|
|
hx-get="/api/monitoring/health/uptime-percent"
|
||
|
|
hx-trigger="load, every 60s"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<div class="stat-icon success">
|
||
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
<div class="stat-content">
|
||
|
|
<span class="stat-value">--%</span>
|
||
|
|
<span class="stat-label">Uptime (30 days)</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-card"
|
||
|
|
hx-get="/api/monitoring/health/last-incident"
|
||
|
|
hx-trigger="load, every 60s"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<div class="stat-icon warning">
|
||
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
||
|
|
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||
|
|
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
<div class="stat-content">
|
||
|
|
<span class="stat-value">--</span>
|
||
|
|
<span class="stat-label">Last Incident</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-card"
|
||
|
|
hx-get="/api/monitoring/health/response-time"
|
||
|
|
hx-trigger="load, every 30s"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<div class="stat-icon info">
|
||
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path>
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
<div class="stat-content">
|
||
|
|
<span class="stat-value">-- ms</span>
|
||
|
|
<span class="stat-label">Avg Response Time</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Health Checks Grid -->
|
||
|
|
<div class="health-section">
|
||
|
|
<div class="section-header">
|
||
|
|
<h3></h3>
|
||
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
|
||
|
|
</svg>
|
||
|
|
Health Check Endpoints
|
||
|
|
</h3>
|
||
|
|
<div class="section-actions">
|
||
|
|
<button class="action-btn secondary" onclick="refreshAllChecks()">
|
||
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<polyline points="23 4 23 10 17 10"></polyline>
|
||
|
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||
|
|
</svg>
|
||
|
|
Refresh All
|
||
|
|
</button>
|
||
|
|
<a href="/health" target="_blank" class="action-btn">
|
||
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
||
|
|
<polyline points="15 3 21 3 21 9"></polyline>
|
||
|
|
<line x1="10" y1="14" x2="21" y2="3"></line>
|
||
|
|
</svg>
|
||
|
|
View Raw
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="health-checks-grid" id="health-checks"
|
||
|
|
hx-get="/api/monitoring/health/checks"
|
||
|
|
hx-trigger="load, every 10s"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<!-- Health checks loaded via HTMX -->
|
||
|
|
<div class="health-check-card">
|
||
|
|
<div class="check-header">
|
||
|
|
<span class="check-status healthy"></span>
|
||
|
|
<span class="check-name">Database</span>
|
||
|
|
<span class="check-badge healthy">Healthy</span>
|
||
|
|
</div>
|
||
|
|
<div class="check-details">
|
||
|
|
<div class="check-row">
|
||
|
|
<span class="check-label">Response Time</span>
|
||
|
|
<span class="check-value">-- ms</span>
|
||
|
|
</div>
|
||
|
|
<div class="check-row">
|
||
|
|
<span class="check-label">Last Check</span>
|
||
|
|
<span class="check-value">--</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="health-check-card">
|
||
|
|
<div class="check-header">
|
||
|
|
<span class="check-status healthy"></span>
|
||
|
|
<span class="check-name">Cache</span>
|
||
|
|
<span class="check-badge healthy">Healthy</span>
|
||
|
|
</div>
|
||
|
|
<div class="check-details">
|
||
|
|
<div class="check-row">
|
||
|
|
<span class="check-label">Response Time</span>
|
||
|
|
<span class="check-value">-- ms</span>
|
||
|
|
</div>
|
||
|
|
<div class="check-row">
|
||
|
|
<span class="check-label">Last Check</span>
|
||
|
|
<span class="check-value">--</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="health-check-card">
|
||
|
|
<div class="check-header">
|
||
|
|
<span class="check-status healthy"></span>
|
||
|
|
<span class="check-name">Message Queue</span>
|
||
|
|
<span class="check-badge healthy">Healthy</span>
|
||
|
|
</div>
|
||
|
|
<div class="check-details">
|
||
|
|
<div class="check-row">
|
||
|
|
<span class="check-label">Response Time</span>
|
||
|
|
<span class="check-value">-- ms</span>
|
||
|
|
</div>
|
||
|
|
<div class="check-row">
|
||
|
|
<span class="check-label">Last Check</span>
|
||
|
|
<span class="check-value">--</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="health-check-card">
|
||
|
|
<div class="check-header">
|
||
|
|
<span class="check-status healthy"></span>
|
||
|
|
<span class="check-name">Storage</span>
|
||
|
|
<span class="check-badge healthy">Healthy</span>
|
||
|
|
</div>
|
||
|
|
<div class="check-details">
|
||
|
|
<div class="check-row">
|
||
|
|
<span class="check-label">Response Time</span>
|
||
|
|
<span class="check-value">-- ms</span>
|
||
|
|
</div>
|
||
|
|
<div class="check-row">
|
||
|
|
<span class="check-label">Last Check</span>
|
||
|
|
<span class="check-value">--</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Dependencies Section -->
|
||
|
|
<div class="health-section">
|
||
|
|
<div class="section-header">
|
||
|
|
<h3>
|
||
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<polyline points="16 18 22 12 16 6"></polyline>
|
||
|
|
<polyline points="8 6 2 12 8 18"></polyline>
|
||
|
|
</svg>
|
||
|
|
External Dependencies
|
||
|
|
</h3>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="dependencies-list" id="dependencies"
|
||
|
|
hx-get="/api/monitoring/health/dependencies"
|
||
|
|
hx-trigger="load, every 30s"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<div class="dependency-row">
|
||
|
|
<div class="dependency-info">
|
||
|
|
<span class="dependency-status healthy"></span>
|
||
|
|
<span class="dependency-name">OpenAI API</span>
|
||
|
|
<span class="dependency-url">api.openai.com</span>
|
||
|
|
</div>
|
||
|
|
<div class="dependency-stats">
|
||
|
|
<span class="stat">
|
||
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<circle cx="12" cy="12" r="10"></circle>
|
||
|
|
<polyline points="12 6 12 12 16 14"></polyline>
|
||
|
|
</svg>
|
||
|
|
-- ms
|
||
|
|
</span>
|
||
|
|
<span class="dependency-badge healthy">Online</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="dependency-row">
|
||
|
|
<div class="dependency-info">
|
||
|
|
<span class="dependency-status healthy"></span>
|
||
|
|
<span class="dependency-name">WhatsApp Business API</span>
|
||
|
|
<span class="dependency-url">graph.facebook.com</span>
|
||
|
|
</div>
|
||
|
|
<div class="dependency-stats">
|
||
|
|
<span class="stat">
|
||
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<circle cx="12" cy="12" r="10"></circle>
|
||
|
|
<polyline points="12 6 12 12 16 14"></polyline>
|
||
|
|
</svg>
|
||
|
|
-- ms
|
||
|
|
</span>
|
||
|
|
<span class="dependency-badge healthy">Online</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="dependency-row">
|
||
|
|
<div class="dependency-info">
|
||
|
|
<span class="dependency-status healthy"></span>
|
||
|
|
<span class="dependency-name">Email Service</span>
|
||
|
|
<span class="dependency-url">smtp.sendgrid.net</span>
|
||
|
|
</div>
|
||
|
|
<div class="dependency-stats">
|
||
|
|
<span class="stat">
|
||
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<circle cx="12" cy="12" r="10"></circle>
|
||
|
|
<polyline points="12 6 12 12 16 14"></polyline>
|
||
|
|
</svg>
|
||
|
|
-- ms
|
||
|
|
</span>
|
||
|
|
<span class="dependency-badge healthy">Online</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Uptime History -->
|
||
|
|
<div class="health-section">
|
||
|
|
<div class="section-header">
|
||
|
|
<h3>
|
||
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||
|
|
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||
|
|
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||
|
|
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||
|
|
</svg>
|
||
|
|
Uptime History (Last 90 Days)
|
||
|
|
</h3>
|
||
|
|
<div class="uptime-legend">
|
||
|
|
<span class="legend-item"><span class="legend-dot healthy"></span>Operational</span>
|
||
|
|
<span class="legend-item"><span class="legend-dot degraded"></span>Degraded</span>
|
||
|
|
<span class="legend-item"><span class="legend-dot outage"></span>Outage</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="uptime-chart" id="uptime-chart"
|
||
|
|
hx-get="/api/monitoring/health/uptime-history"
|
||
|
|
hx-trigger="load"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<div class="uptime-bars">
|
||
|
|
<!-- 90 days of uptime bars -->
|
||
|
|
<div class="uptime-bar healthy" title="Jan 15 - 100%"></div>
|
||
|
|
<div class="uptime-bar healthy" title="Jan 14 - 100%"></div>
|
||
|
|
<div class="uptime-bar healthy" title="Jan 13 - 100%"></div>
|
||
|
|
<div class="uptime-bar healthy" title="Jan 12 - 100%"></div>
|
||
|
|
<div class="uptime-bar healthy" title="Jan 11 - 100%"></div>
|
||
|
|
<div class="uptime-bar healthy" title="Jan 10 - 100%"></div>
|
||
|
|
<div class="uptime-bar degraded" title="Jan 9 - 99.5%"></div>
|
||
|
|
<div class="uptime-bar healthy" title="Jan 8 - 100%"></div>
|
||
|
|
<div class="uptime-bar healthy" title="Jan 7 - 100%"></div>
|
||
|
|
<div class="uptime-bar healthy" title="Jan 6 - 100%"></div>
|
||
|
|
<div class="uptime-bar healthy" title="Jan 5 - 100%"></div>
|
||
|
|
<div class="uptime-bar healthy" title="Jan 4 - 100%"></div>
|
||
|
|
<div class="uptime-bar healthy" title="Jan 3 - 100%"></div>
|
||
|
|
<div class="uptime-bar healthy" title="Jan 2 - 100%"></div>
|
||
|
|
<div class="uptime-bar healthy" title="Jan 1 - 100%"></div>
|
||
|
|
<!-- ... more bars ... -->
|
||
|
|
</div>
|
||
|
|
<div class="uptime-labels">
|
||
|
|
<span>90 days ago</span>
|
||
|
|
<span>Today</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Recent Incidents -->
|
||
|
|
<div class="health-section">
|
||
|
|
<div class="section-header">
|
||
|
|
<h3>
|
||
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
||
|
|
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||
|
|
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||
|
|
</svg>
|
||
|
|
Recent Incidents
|
||
|
|
</h3>
|
||
|
|
<a href="#" class="view-all-link">View All</a>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="incidents-list" id="incidents"
|
||
|
|
hx-get="/api/monitoring/health/incidents"
|
||
|
|
hx-trigger="load"
|
||
|
|
hx-swap="innerHTML">
|
||
|
|
<div class="incident-placeholder">
|
||
|
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
|
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||
|
|
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||
|
|
</svg>
|
||
|
|
<p>No recent incidents</p>
|
||
|
|
<span>System</span> has been stable</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
.health-container {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 1.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Health Overview */
|
||
|
|
.health-overview {
|
||
|
|
background: var(--surface);
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 1.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.health-status {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 1.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-icon {
|
||
|
|
width: 64px;
|
||
|
|
height: 64px;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
border-radius: 16px;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.health-status.healthy .status-icon {
|
||
|
|
background: rgba(34, 197, 94, 0.1);
|
||
|
|
color: var(--success);
|
||
|
|
}
|
||
|
|
|
||
|
|
.health-status.degraded .status-icon {
|
||
|
|
background: rgba(245, 158, 11, 0.1);
|
||
|
|
color: var(--warning);
|
||
|
|
}
|
||
|
|
|
||
|
|
.health-status.unhealthy .status-icon {
|
||
|
|
background: rgba(239, 68, 68, 0.1);
|
||
|
|
color: var(--error);
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-info {
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-info h2 {
|
||
|
|
font-size: 1.25rem;
|
||
|
|
font-weight: 600;
|
||
|
|
color: var(--text);
|
||
|
|
margin-bottom: 0.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-info p {
|
||
|
|
font-size: 0.875rem;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-badge {
|
||
|
|
padding: 0.5rem 1rem;
|
||
|
|
border-radius: 20px;
|
||
|
|
font-size: 0.875rem;
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-badge.healthy {
|
||
|
|
background: rgba(34, 197, 94, 0.1);
|
||
|
|
color: var(--success);
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-badge.degraded {
|
||
|
|
background: rgba(245, 158, 11, 0.1);
|
||
|
|
color: var(--warning);
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-badge.unhealthy {
|
||
|
|
background: rgba(239, 68, 68, 0.1);
|
||
|
|
color: var(--error);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Uptime Stats */
|
||
|
|
.uptime-stats {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
|
|
gap: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-card {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 1rem;
|
||
|
|
padding: 1.25rem;
|
||
|
|
background: var(--surface);
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
border-radius: 10px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-icon {
|
||
|
|
width: 44px;
|
||
|
|
height: 44px;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
background: var(--primary-light);
|
||
|
|
color: var(--primary);
|
||
|
|
border-radius: 10px;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-icon.success {
|
||
|
|
background: rgba(34, 197, 94, 0.1);
|
||
|
|
color: var(--success);
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-icon.warning {
|
||
|
|
background: rgba(245, 158, 11, 0.1);
|
||
|
|
color: var(--warning);
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-icon.info {
|
||
|
|
background: rgba(139, 92, 246, 0.1);
|
||
|
|
color: #8b5cf6;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-content {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-value {
|
||
|
|
font-size: 1.375rem;
|
||
|
|
font-weight: 700;
|
||
|
|
color: var(--text);
|
||
|
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-label {
|
||
|
|
font-size: 0.75rem;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Health Section */
|
||
|
|
.health-section {
|
||
|
|
background: var(--surface);
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
border-radius: 12px;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.section-header {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
padding: 1rem 1.25rem;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 0.75rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.section-header h3 {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
font-size: 0.9375rem;
|
||
|
|
font-weight: 600;
|
||
|
|
color: var(--text);
|
||
|
|
}
|
||
|
|
|
||
|
|
.section-header h3 svg {
|
||
|
|
color: var(--primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.section-actions {
|
||
|
|
display: flex;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.action-btn {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.375rem;
|
||
|
|
padding: 0.375rem 0.75rem;
|
||
|
|
background: var(--primary);
|
||
|
|
color: white;
|
||
|
|
border: none;
|
||
|
|
border-radius: 6px;
|
||
|
|
font-size: 0.75rem;
|
||
|
|
font-weight: 500;
|
||
|
|
cursor: pointer;
|
||
|
|
text-decoration: none;
|
||
|
|
transition: all 0.2s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.action-btn:hover {
|
||
|
|
background: var(--primary-hover);
|
||
|
|
}
|
||
|
|
|
||
|
|
.action-btn.secondary {
|
||
|
|
background: var(--bg);
|
||
|
|
color: var(--text);
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
}
|
||
|
|
|
||
|
|
.action-btn.secondary:hover {
|
||
|
|
background: var(--surface-hover);
|
||
|
|
border-color: var(--primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.view-all-link {
|
||
|
|
font-size: 0.8125rem;
|
||
|
|
color: var(--primary);
|
||
|
|
text-decoration: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.view-all-link:hover {
|
||
|
|
text-decoration: underline;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Health Checks Grid */
|
||
|
|
.health-checks-grid {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
|
|
gap: 1rem;
|
||
|
|
padding: 1.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.health-check-card {
|
||
|
|
background: var(--bg);
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
border-radius: 8px;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-header {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
padding: 0.875rem 1rem;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-status {
|
||
|
|
width: 10px;
|
||
|
|
height: 10px;
|
||
|
|
border-radius: 50%;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-status.healthy {
|
||
|
|
background: var(--success);
|
||
|
|
box-shadow: 0 0 8px var(--success);
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-status.degraded {
|
||
|
|
background: var(--warning);
|
||
|
|
box-shadow: 0 0 8px var(--warning);
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-status.unhealthy {
|
||
|
|
background: var(--error);
|
||
|
|
box-shadow: 0 0 8px var(--error);
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-name {
|
||
|
|
flex: 1;
|
||
|
|
font-size: 0.9375rem;
|
||
|
|
font-weight: 600;
|
||
|
|
color: var(--text);
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-badge {
|
||
|
|
padding: 0.125rem 0.5rem;
|
||
|
|
border-radius: 4px;
|
||
|
|
font-size: 0.6875rem;
|
||
|
|
font-weight: 600;
|
||
|
|
text-transform: uppercase;
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-badge.healthy {
|
||
|
|
background: rgba(34, 197, 94, 0.1);
|
||
|
|
color: var(--success);
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-badge.degraded {
|
||
|
|
background: rgba(245, 158, 11, 0.1);
|
||
|
|
color: var(--warning);
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-badge.unhealthy {
|
||
|
|
background: rgba(239, 68, 68, 0.1);
|
||
|
|
color: var(--error);
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-details {
|
||
|
|
padding: 0.75rem 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-row {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
padding: 0.375rem 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-row:not(:last-child) {
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-label {
|
||
|
|
font-size: 0.8125rem;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-value {
|
||
|
|
font-size: 0.8125rem;
|
||
|
|
color: var(--text);
|
||
|
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Dependencies */
|
||
|
|
.dependencies-list {
|
||
|
|
padding: 0.5rem 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dependency-row {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
padding: 0.875rem 1.25rem;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dependency-row:last-child {
|
||
|
|
border-bottom: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dependency-info {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.75rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dependency-status {
|
||
|
|
width: 8px;
|
||
|
|
height: 8px;
|
||
|
|
border-radius: 50%;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dependency-status.healthy { background: var(--success); }
|
||
|
|
.dependency-status.degraded { background: var(--warning); }
|
||
|
|
.dependency-status.unhealthy { background: var(--error); }
|
||
|
|
|
||
|
|
.dependency-name {
|
||
|
|
font-size: 0.9375rem;
|
||
|
|
font-weight: 500;
|
||
|
|
color: var(--text);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dependency-url {
|
||
|
|
font-size: 0.75rem;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dependency-stats {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dependency-stats .stat {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.25rem;
|
||
|
|
font-size: 0.8125rem;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dependency-stats .stat svg {
|
||
|
|
color: var(--primary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dependency-badge {
|
||
|
|
padding: 0.125rem 0.5rem;
|
||
|
|
border-radius: 4px;
|
||
|
|
font-size: 0.6875rem;
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dependency-badge.healthy {
|
||
|
|
background: rgba(34, 197, 94, 0.1);
|
||
|
|
color: var(--success);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dependency-badge.unhealthy {
|
||
|
|
background: rgba(239, 68, 68, 0.1);
|
||
|
|
color: var(--error);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Uptime Chart */
|
||
|
|
.uptime-legend {
|
||
|
|
display: flex;
|
||
|
|
gap: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.375rem;
|
||
|
|
font-size: 0.75rem;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend-dot {
|
||
|
|
width: 12px;
|
||
|
|
height: 12px;
|
||
|
|
border-radius: 2px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend-dot.healthy { background: var(--success); }
|
||
|
|
.legend-dot.degraded { background: var(--warning); }
|
||
|
|
.legend-dot.outage { background: var(--error); }
|
||
|
|
|
||
|
|
.uptime-chart {
|
||
|
|
padding: 1.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.uptime-bars {
|
||
|
|
display: flex;
|
||
|
|
gap: 2px;
|
||
|
|
height: 32px;
|
||
|
|
margin-bottom: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.uptime-bar {
|
||
|
|
flex: 1;
|
||
|
|
border-radius: 2px;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: opacity 0.2s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.uptime-bar:hover {
|
||
|
|
opacity: 0.8;
|
||
|
|
}
|
||
|
|
|
||
|
|
.uptime-bar.healthy { background: var(--success); }
|
||
|
|
.uptime-bar.degraded { background: var(--warning); }
|
||
|
|
.uptime-bar.outage { background: var(--error); }
|
||
|
|
|
||
|
|
.uptime-labels {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
font-size: 0.6875rem;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Incidents */
|
||
|
|
.incidents-list {
|
||
|
|
padding: 1rem 1.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.incident-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: flex-start;
|
||
|
|
gap: 1rem;
|
||
|
|
padding: 1rem;
|
||
|
|
background: var(--bg);
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
border-radius: 8px;
|
||
|
|
margin-bottom: 0.75rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.incident-item:last-child {
|
||
|
|
margin-bottom: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.incident-icon {
|
||
|
|
width: 36px;
|
||
|
|
height: 36px;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
border-radius: 8px;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.incident-item.resolved .incident-icon {
|
||
|
|
background: rgba(34, 197, 94, 0.1);
|
||
|
|
color: var(--success);
|
||
|
|
}
|
||
|
|
|
||
|
|
.incident-item.ongoing .incident-icon {
|
||
|
|
background: rgba(239, 68, 68, 0.1);
|
||
|
|
color: var(--error);
|
||
|
|
}
|
||
|
|
|
||
|
|
.incident-content {
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.incident-title {
|
||
|
|
font-size: 0.9375rem;
|
||
|
|
font-weight: 600;
|
||
|
|
color: var(--text);
|
||
|
|
margin-bottom: 0.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.incident-description {
|
||
|
|
font-size: 0.8125rem;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
margin-bottom: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.incident-meta {
|
||
|
|
display: flex;
|
||
|
|
gap: 1rem;
|
||
|
|
font-size: 0.75rem;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.incident-placeholder {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
padding: 2rem;
|
||
|
|
text-align: center;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.incident-placeholder svg {
|
||
|
|
margin-bottom: 0.75rem;
|
||
|
|
color: var(--success);
|
||
|
|
opacity: 0.6;
|
||
|
|
}
|
||
|
|
|
||
|
|
.incident-placeholder p {
|
||
|
|
font-size: 0.9375rem;
|
||
|
|
color: var(--text);
|
||
|
|
margin-bottom: 0.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.incident-placeholder span {
|
||
|
|
font-size: 0.8125rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Responsive */
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.health-status {
|
||
|
|
flex-direction: column;
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.uptime-stats {
|
||
|
|
grid-template-columns: repeat(2, 1fr);
|
||
|
|
}
|
||
|
|
|
||
|
|
.health-checks-grid {
|
||
|
|
grid-template-columns: 1fr;
|
||
|
|
}
|
||
|
|
|
||
|
|
.section-header {
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: flex-start;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dependency-row {
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: flex-start;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dependency-stats {
|
||
|
|
width: 100%;
|
||
|
|
justify-content: space-between;
|
||
|
|
}
|
||
|
|
|
||
|
|
.uptime-legend {
|
||
|
|
flex-wrap: wrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.uptime-bars {
|
||
|
|
gap: 1px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 480px) {
|
||
|
|
.uptime-stats {
|
||
|
|
grid-template-columns: 1fr;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
function refreshAllChecks() {
|
||
|
|
htmx.trigger('#health-checks', 'refresh');
|
||
|
|
htmx.trigger('#dependencies', 'refresh');
|
||
|
|
htmx.trigger('.health-overview', 'refresh');
|
||
|
|
|
||
|
|
// Visual feedback
|
||
|
|
const btn = event.currentTarget;
|
||
|
|
const originalText = btn.innerHTML;
|
||
|
|
btn.innerHTML = `
|
||
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin">
|
||
|
|
<polyline points="23 4 23 10 17 10"></polyline>
|
||
|
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||
|
|
</svg>
|
||
|
|
Refreshing...
|
||
|
|
`;
|
||
|
|
btn.disabled = true;
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
btn.innerHTML = originalText;
|
||
|
|
btn.disabled = false;
|
||
|
|
}, 1000);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add tooltip functionality for uptime bars
|
||
|
|
document.querySelectorAll('.uptime-bar').forEach(bar => {
|
||
|
|
bar.addEventListener('mouseenter', function(e) {
|
||
|
|
const tooltip = document.createElement('div');
|
||
|
|
tooltip.className = 'uptime-tooltip';
|
||
|
|
tooltip.textContent = this.title;
|
||
|
|
tooltip.style.cssText = `
|
||
|
|
position: fixed;
|
||
|
|
background: var(--surface);
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
padding: 0.375rem 0.625rem;
|
||
|
|
border-radius: 4px;
|
||
|
|
font-size: 0.75rem;
|
||
|
|
color: var(--text);
|
||
|
|
pointer-events: none;
|
||
|
|
z-index: 1000;
|
||
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
|
|
`;
|
||
|
|
document.body.appendChild(tooltip);
|
||
|
|
|
||
|
|
const rect = this.getBoundingClientRect();
|
||
|
|
tooltip.style.left = `${rect.left + rect.width/2 - tooltip.offsetWidth/2}px`;
|
||
|
|
tooltip.style.top = `${rect.top - tooltip.offsetHeight - 8}px`;
|
||
|
|
|
||
|
|
this._tooltip = tooltip;
|
||
|
|
});
|
||
|
|
|
||
|
|
bar.addEventListener('mouseleave', function() {
|
||
|
|
if (this._tooltip) {
|
||
|
|
this._tooltip.remove();
|
||
|
|
this._tooltip = null;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
</script>
|