botui/ui/suite/monitoring/health.html

995 lines
30 KiB
HTML
Raw Normal View History

2025-12-06 11:09:12 -03:00
<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>