896 lines
41 KiB
HTML
896 lines
41 KiB
HTML
{% extends "base.html" %}
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-1 fw-bold">System Metrics</h1>
|
|
<p class="text-muted mb-0">Real-time server performance and storage usage</p>
|
|
</div>
|
|
<div class="d-flex gap-2 align-items-center">
|
|
<span class="d-flex align-items-center gap-2 text-muted small" id="metricsLiveIndicator">
|
|
<span class="live-indicator"></span>
|
|
Auto-refresh: <span id="refreshCountdown">5</span>s
|
|
</span>
|
|
<button class="btn btn-outline-secondary btn-sm" id="refreshMetricsBtn">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-arrow-clockwise me-1" viewBox="0 0 16 16">
|
|
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
|
|
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
|
</svg>
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-md-6 col-xl-3">
|
|
<div class="card shadow-sm h-100 border-0 metric-card">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
|
<h6 class="card-subtitle text-muted text-uppercase small fw-bold mb-0">CPU Usage</h6>
|
|
<div class="icon-box bg-primary-subtle text-primary rounded-circle p-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-cpu" viewBox="0 0 16 16">
|
|
<path d="M5 0a.5.5 0 0 1 .5.5V2h1V.5a.5.5 0 0 1 1 0V2h1V.5a.5.5 0 0 1 1 0V2h1V.5a.5.5 0 0 1 1 0V2A2.5 2.5 0 0 1 14 4.5h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14a2.5 2.5 0 0 1-2.5 2.5v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14A2.5 2.5 0 0 1 2 11.5H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2A2.5 2.5 0 0 1 4.5 2V.5a.5.5 0 0 1 .5-.5zM5 4H5v8h6V4H5z"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<h2 class="display-6 fw-bold mb-2 stat-value"><span data-metric="cpu_percent">{{ cpu_percent }}</span><span class="fs-4 fw-normal text-muted">%</span></h2>
|
|
<div class="progress" style="height: 8px; border-radius: 4px;">
|
|
<div class="progress-bar bg-primary" data-metric="cpu_bar" role="progressbar" style="width: {{ cpu_percent }}%"></div>
|
|
</div>
|
|
<div class="mt-2 d-flex justify-content-between">
|
|
<small class="text-muted">Current load</small>
|
|
<small data-metric="cpu_status" class="text-success">Normal</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6 col-xl-3">
|
|
<div class="card shadow-sm h-100 border-0 metric-card">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
|
<h6 class="card-subtitle text-muted text-uppercase small fw-bold mb-0">Memory</h6>
|
|
<div class="icon-box bg-info-subtle text-info rounded-circle p-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-memory" viewBox="0 0 16 16">
|
|
<path d="M1 3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4.586a1 1 0 0 0 .707-.293l.353-.353a.5.5 0 0 1 .708 0l.353.353a1 1 0 0 0 .707.293H15a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H1Zm.5 1h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-4a.5.5 0 0 1 .5-.5Zm5 0h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-4a.5.5 0 0 1 .5-.5Zm4.5.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-4Z"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<h2 class="display-6 fw-bold mb-2 stat-value"><span data-metric="memory_percent">{{ memory.percent }}</span><span class="fs-4 fw-normal text-muted">%</span></h2>
|
|
<div class="progress" style="height: 8px; border-radius: 4px;">
|
|
<div class="progress-bar bg-info" data-metric="memory_bar" role="progressbar" style="width: {{ memory.percent }}%"></div>
|
|
</div>
|
|
<div class="mt-2 d-flex justify-content-between">
|
|
<small class="text-muted"><span data-metric="memory_used">{{ memory.used }}</span> used</small>
|
|
<small class="text-muted"><span data-metric="memory_total">{{ memory.total }}</span> total</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6 col-xl-3">
|
|
<div class="card shadow-sm h-100 border-0 metric-card">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
|
<h6 class="card-subtitle text-muted text-uppercase small fw-bold mb-0">Disk Space</h6>
|
|
<div class="icon-box bg-warning-subtle text-warning rounded-circle p-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-hdd" viewBox="0 0 16 16">
|
|
<path d="M4.5 11a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1zM3 10.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0z"/>
|
|
<path d="M16 11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V9.51c0-.418.105-.83.305-1.197l2.472-4.531A1.5 1.5 0 0 1 4.094 3h7.812a1.5 1.5 0 0 1 1.317.782l2.472 4.53c.2.368.305.78.305 1.198V11zM3.655 4.26 1.592 8.043C1.724 8.014 1.86 8 2 8h12c.14 0 .276.014.408.042L12.345 4.26a.5.5 0 0 0-.439-.26H4.094a.5.5 0 0 0-.439.26zM1 10v1a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-1a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1z"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<h2 class="display-6 fw-bold mb-2 stat-value"><span data-metric="disk_percent">{{ disk.percent }}</span><span class="fs-4 fw-normal text-muted">%</span></h2>
|
|
<div class="progress" style="height: 8px; border-radius: 4px;">
|
|
<div class="progress-bar bg-warning" data-metric="disk_bar" role="progressbar" style="width: {{ disk.percent }}%"></div>
|
|
</div>
|
|
<div class="mt-2 d-flex justify-content-between">
|
|
<small class="text-muted"><span data-metric="disk_free">{{ disk.free }}</span> free</small>
|
|
<small class="text-muted"><span data-metric="disk_total">{{ disk.total }}</span> total</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6 col-xl-3">
|
|
<div class="card shadow-sm h-100 border-0 metric-card">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
|
<h6 class="card-subtitle text-muted text-uppercase small fw-bold mb-0">Storage</h6>
|
|
<div class="icon-box bg-success-subtle text-success rounded-circle p-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-database" viewBox="0 0 16 16">
|
|
<path d="M4.318 2.687C5.234 2.271 6.536 2 8 2s2.766.27 3.682.687C12.644 3.125 13 3.627 13 4c0 .374-.356.875-1.318 1.313C10.766 5.729 9.464 6 8 6s-2.766-.27-3.682-.687C3.356 4.875 3 4.373 3 4c0-.374.356-.875 1.318-1.313ZM13 5.698V7c0 .374-.356.875-1.318 1.313C10.766 8.729 9.464 9 8 9s-2.766-.27-3.682-.687C3.356 7.875 3 7.373 3 7V5.698c.271.202.58.378.904.525C4.978 6.711 6.427 7 8 7s3.022-.289 4.096-.777A4.92 4.92 0 0 0 13 5.698ZM14 4c0-1.007-.875-1.755-1.904-2.223C11.022 1.289 9.573 1 8 1s-3.022.289-4.096.777C2.875 2.245 2 2.993 2 4v9c0 1.007.875 1.755 1.904 2.223C4.978 15.71 6.427 16 8 16s3.022-.289 4.096-.777C13.125 14.755 14 14.007 14 13V4Zm-1 4.698V10c0 .374-.356.875-1.318 1.313C10.766 11.729 9.464 12 8 12s-2.766-.27-3.682-.687C3.356 10.875 3 10.373 3 10V8.698c.271.202.58.378.904.525C4.978 9.71 6.427 10 8 10s3.022-.289 4.096-.777A4.92 4.92 0 0 0 13 8.698Zm0 3V13c0 .374-.356.875-1.318 1.313C10.766 14.729 9.464 15 8 15s-2.766-.27-3.682-.687C3.356 13.875 3 13.373 3 13v-1.302c.271.202.58.378.904.525C4.978 12.71 6.427 13 8 13s3.022-.289 4.096-.777c.324-.147.633-.323.904-.525Z"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<h2 class="display-6 fw-bold mb-2 stat-value" data-metric="storage_used">{{ app.storage_used }}</h2>
|
|
<div class="d-flex gap-3 mt-3">
|
|
<div class="text-center flex-fill">
|
|
<div class="h5 fw-bold mb-0" data-metric="buckets_count">{{ app.buckets }}</div>
|
|
<small class="text-muted">Buckets</small>
|
|
</div>
|
|
<div class="vr"></div>
|
|
<div class="text-center flex-fill">
|
|
<div class="h5 fw-bold mb-0" data-metric="objects_count">{{ app.objects }}</div>
|
|
<small class="text-muted">Objects</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
<div class="col-lg-8">
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-header bg-transparent border-0 pt-4 px-4 d-flex justify-content-between align-items-center">
|
|
<h5 class="card-title mb-0 fw-semibold">System Overview</h5>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead>
|
|
<tr class="text-muted small text-uppercase">
|
|
<th class="fw-semibold border-0 pb-3">Resource</th>
|
|
<th class="fw-semibold border-0 pb-3">Value</th>
|
|
<th class="fw-semibold border-0 pb-3">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td class="py-3">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div class="bg-secondary-subtle rounded p-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-hdd-stack text-secondary" viewBox="0 0 16 16">
|
|
<path d="M14 10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-1a1 1 0 0 1 1-1h12zM2 9a2 2 0 0 0-2 2v1a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-1a2 2 0 0 0-2-2H2z"/>
|
|
<path d="M5 11.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zm-2 0a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zM14 3a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h12zM2 2a2 2 0 0 0-2 2v1a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H2z"/>
|
|
<path d="M5 4.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zm-2 0a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0z"/>
|
|
</svg>
|
|
</div>
|
|
<span class="fw-medium">Total Disk Capacity</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 fw-semibold">{{ disk.total }}</td>
|
|
<td class="py-3"><span class="badge bg-secondary-subtle text-secondary">Hardware</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="py-3">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div class="bg-success-subtle rounded p-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-circle text-success" viewBox="0 0 16 16">
|
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
|
<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/>
|
|
</svg>
|
|
</div>
|
|
<span class="fw-medium">Available Space</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 fw-semibold">{{ disk.free }}</td>
|
|
<td class="py-3">
|
|
{% if disk.percent > 90 %}
|
|
<span class="status-badge status-badge-danger badge bg-danger-subtle text-danger">
|
|
<span class="status-badge-dot"></span>Critical
|
|
</span>
|
|
{% elif disk.percent > 75 %}
|
|
<span class="status-badge status-badge-warning badge bg-warning-subtle text-warning">
|
|
<span class="status-badge-dot"></span>Low
|
|
</span>
|
|
{% else %}
|
|
<span class="status-badge status-badge-success badge bg-success-subtle text-success">
|
|
<span class="status-badge-dot"></span>Good
|
|
</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="py-3">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div class="bg-primary-subtle rounded p-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bucket text-primary" viewBox="0 0 16 16">
|
|
<path d="M2.522 5H2a.5.5 0 0 0-.494.574l1.372 9.149A1.5 1.5 0 0 0 4.36 16h7.278a1.5 1.5 0 0 0 1.483-1.277l1.373-9.149A.5.5 0 0 0 14 5h-.522A5.5 5.5 0 0 0 2.522 5zm1.005 0a4.5 4.5 0 0 1 8.945 0H3.527z"/>
|
|
</svg>
|
|
</div>
|
|
<span class="fw-medium">MyFSIO Data</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 fw-semibold">{{ app.storage_used }}</td>
|
|
<td class="py-3"><span class="badge bg-primary-subtle text-primary">Application</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="py-3">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div class="bg-info-subtle rounded p-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark text-info" viewBox="0 0 16 16">
|
|
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
|
|
</svg>
|
|
</div>
|
|
<span class="fw-medium">Total Objects</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 fw-semibold">{{ app.objects }}</td>
|
|
<td class="py-3"><span class="badge bg-info-subtle text-info">Count</span></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-4">
|
|
{% set has_issues = (cpu_percent > 80) or (memory.percent > 85) or (disk.percent > 90) %}
|
|
<div class="card shadow-sm border-0 h-100 overflow-hidden" style="background: linear-gradient(135deg, {% if has_issues %}#ef4444 0%, #f97316{% else %}#3b82f6 0%, #8b5cf6{% endif %} 100%);">
|
|
<div class="card-body p-4 d-flex flex-column justify-content-center text-white position-relative">
|
|
<div class="position-absolute top-0 end-0 opacity-25" style="transform: translate(20%, -20%);">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" fill="currentColor" class="bi bi-{% if has_issues %}exclamation-triangle{% else %}cloud-check{% endif %}" viewBox="0 0 16 16">
|
|
{% if has_issues %}
|
|
<path d="M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.146.146 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.163.163 0 0 1-.054.06.116.116 0 0 1-.066.017H1.146a.115.115 0 0 1-.066-.017.163.163 0 0 1-.054-.06.176.176 0 0 1 .002-.183L7.884 2.073a.147.147 0 0 1 .054-.057zm1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566z"/>
|
|
<path d="M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995z"/>
|
|
{% else %}
|
|
<path fill-rule="evenodd" d="M10.354 6.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708 0z"/>
|
|
<path d="M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13H3.781C1.708 13 0 11.366 0 9.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383z"/>
|
|
{% endif %}
|
|
</svg>
|
|
</div>
|
|
<div class="mb-3">
|
|
<span class="badge bg-white {% if has_issues %}text-danger{% else %}text-primary{% endif %} fw-semibold px-3 py-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-{% if has_issues %}exclamation-circle-fill{% else %}check-circle-fill{% endif %} me-1" viewBox="0 0 16 16">
|
|
{% if has_issues %}
|
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 4a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0 0 1.1 0l.35-3.507A.905.905 0 0 0 8 4zm.002 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/>
|
|
{% else %}
|
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
|
|
{% endif %}
|
|
</svg>
|
|
v{{ app.version }}
|
|
</span>
|
|
</div>
|
|
<h4 class="card-title fw-bold mb-3">System Health</h4>
|
|
{% if has_issues %}
|
|
<ul class="list-unstyled small mb-4 opacity-90">
|
|
{% if cpu_percent > 80 %}<li class="mb-1">CPU usage is high ({{ cpu_percent }}%)</li>{% endif %}
|
|
{% if memory.percent > 85 %}<li class="mb-1">Memory usage is high ({{ memory.percent }}%)</li>{% endif %}
|
|
{% if disk.percent > 90 %}<li class="mb-1">Disk space is critically low ({{ disk.percent }}% used)</li>{% endif %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="card-text opacity-90 mb-4 small">All resources are within normal operating parameters.</p>
|
|
{% endif %}
|
|
<div class="d-flex gap-4">
|
|
<div>
|
|
<div class="h3 fw-bold mb-0">{{ app.uptime_days }}d</div>
|
|
<small class="opacity-75">Uptime</small>
|
|
</div>
|
|
<div>
|
|
<div class="h3 fw-bold mb-0">{{ app.buckets }}</div>
|
|
<small class="opacity-75">Active Buckets</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if operation_metrics_enabled %}
|
|
<div class="row g-4 mt-2">
|
|
<div class="col-12">
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-header bg-transparent border-0 pt-4 px-4 d-flex justify-content-between align-items-center">
|
|
<h5 class="card-title mb-0 fw-semibold">API Operations</h5>
|
|
<div class="d-flex align-items-center gap-3">
|
|
<span class="small text-muted" id="opStatus">Loading...</span>
|
|
<button class="btn btn-outline-secondary btn-sm" id="resetOpMetricsBtn" title="Reset current window">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
|
|
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z"/>
|
|
<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-6 col-md-4 col-lg-2">
|
|
<div class="text-center p-3 bg-light rounded h-100">
|
|
<h4 class="fw-bold mb-1" id="opTotalRequests">0</h4>
|
|
<small class="text-muted">Requests</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-4 col-lg-2">
|
|
<div class="text-center p-3 bg-light rounded h-100">
|
|
<h4 class="fw-bold mb-1 text-success" id="opSuccessRate">0%</h4>
|
|
<small class="text-muted">Success</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-4 col-lg-2">
|
|
<div class="text-center p-3 bg-light rounded h-100">
|
|
<h4 class="fw-bold mb-1 text-danger" id="opErrorCount">0</h4>
|
|
<small class="text-muted">Errors</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-4 col-lg-2">
|
|
<div class="text-center p-3 bg-light rounded h-100">
|
|
<h4 class="fw-bold mb-1 text-info" id="opAvgLatency">0ms</h4>
|
|
<small class="text-muted">Latency</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-4 col-lg-2">
|
|
<div class="text-center p-3 bg-light rounded h-100">
|
|
<h4 class="fw-bold mb-1 text-primary" id="opBytesIn">0 B</h4>
|
|
<small class="text-muted">Bytes In</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-4 col-lg-2">
|
|
<div class="text-center p-3 bg-light rounded h-100">
|
|
<h4 class="fw-bold mb-1 text-secondary" id="opBytesOut">0 B</h4>
|
|
<small class="text-muted">Bytes Out</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row g-4">
|
|
<div class="col-lg-6">
|
|
<div class="bg-light rounded p-3">
|
|
<h6 class="text-muted small fw-bold text-uppercase mb-3">Requests by Method</h6>
|
|
<div style="height: 220px; display: flex; align-items: center; justify-content: center;">
|
|
<canvas id="methodChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-6">
|
|
<div class="bg-light rounded p-3">
|
|
<h6 class="text-muted small fw-bold text-uppercase mb-3">Requests by Status</h6>
|
|
<div style="height: 220px;">
|
|
<canvas id="statusChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row g-4 mt-1">
|
|
<div class="col-lg-6">
|
|
<div class="bg-light rounded p-3">
|
|
<h6 class="text-muted small fw-bold text-uppercase mb-3">Requests by Endpoint</h6>
|
|
<div style="height: 180px;">
|
|
<canvas id="endpointChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-6">
|
|
<div class="bg-light rounded p-3 h-100 d-flex flex-column">
|
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
<h6 class="text-muted small fw-bold text-uppercase mb-0">S3 Error Codes</h6>
|
|
<span class="badge bg-secondary-subtle text-secondary" style="font-size: 0.65rem;" title="Tracks S3 API errors like NoSuchKey, AccessDenied, etc.">API Only</span>
|
|
</div>
|
|
<div class="flex-grow-1 d-flex flex-column" style="min-height: 150px;">
|
|
<div class="d-flex border-bottom pb-2 mb-2" style="font-size: 0.75rem;">
|
|
<div class="text-muted fw-semibold" style="flex: 1;">Code</div>
|
|
<div class="text-muted fw-semibold text-end" style="width: 60px;">Count</div>
|
|
<div class="text-muted fw-semibold text-end" style="width: 100px;">Distribution</div>
|
|
</div>
|
|
<div id="errorCodesContainer" class="flex-grow-1" style="overflow-y: auto;">
|
|
<div id="errorCodesBody">
|
|
<div class="text-muted small text-center py-4">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-check-circle mb-2 text-success" viewBox="0 0 16 16">
|
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
|
<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/>
|
|
</svg>
|
|
<div>No S3 API errors</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if metrics_history_enabled %}
|
|
<div class="row g-4 mt-2">
|
|
<div class="col-12">
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-header bg-transparent border-0 pt-4 px-4 d-flex justify-content-between align-items-center">
|
|
<h5 class="card-title mb-0 fw-semibold">Metrics History</h5>
|
|
<div class="d-flex gap-2 align-items-center">
|
|
<select class="form-select form-select-sm" id="historyTimeRange" style="width: auto;">
|
|
<option value="1">Last 1 hour</option>
|
|
<option value="6">Last 6 hours</option>
|
|
<option value="24" selected>Last 24 hours</option>
|
|
<option value="168">Last 7 days</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<div class="row">
|
|
<div class="col-md-4 mb-4">
|
|
<h6 class="text-muted small fw-bold text-uppercase mb-3">CPU Usage</h6>
|
|
<canvas id="cpuHistoryChart" height="200"></canvas>
|
|
</div>
|
|
<div class="col-md-4 mb-4">
|
|
<h6 class="text-muted small fw-bold text-uppercase mb-3">Memory Usage</h6>
|
|
<canvas id="memoryHistoryChart" height="200"></canvas>
|
|
</div>
|
|
<div class="col-md-4 mb-4">
|
|
<h6 class="text-muted small fw-bold text-uppercase mb-3">Disk Usage</h6>
|
|
<canvas id="diskHistoryChart" height="200"></canvas>
|
|
</div>
|
|
</div>
|
|
<p class="text-muted small mb-0 text-center" id="historyStatus">Loading history data...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
{% if metrics_history_enabled or operation_metrics_enabled %}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
|
{% endif %}
|
|
<script>
|
|
(function() {
|
|
var refreshInterval = 5000;
|
|
var countdown = 5;
|
|
var countdownEl = document.getElementById('refreshCountdown');
|
|
var refreshBtn = document.getElementById('refreshMetricsBtn');
|
|
var countdownTimer = null;
|
|
var fetchTimer = null;
|
|
|
|
function updateMetrics() {
|
|
fetch('/ui/metrics/api')
|
|
.then(function(resp) { return resp.json(); })
|
|
.then(function(data) {
|
|
var el;
|
|
el = document.querySelector('[data-metric="cpu_percent"]');
|
|
if (el) el.textContent = data.cpu_percent.toFixed(2);
|
|
el = document.querySelector('[data-metric="cpu_bar"]');
|
|
if (el) {
|
|
el.style.width = data.cpu_percent + '%';
|
|
el.className = 'progress-bar ' + (data.cpu_percent > 80 ? 'bg-danger' : data.cpu_percent > 50 ? 'bg-warning' : 'bg-primary');
|
|
}
|
|
el = document.querySelector('[data-metric="cpu_status"]');
|
|
if (el) {
|
|
el.textContent = data.cpu_percent > 80 ? 'High' : data.cpu_percent > 50 ? 'Medium' : 'Normal';
|
|
el.className = data.cpu_percent > 80 ? 'text-danger' : data.cpu_percent > 50 ? 'text-warning' : 'text-success';
|
|
}
|
|
|
|
el = document.querySelector('[data-metric="memory_percent"]');
|
|
if (el) el.textContent = data.memory.percent.toFixed(2);
|
|
el = document.querySelector('[data-metric="memory_bar"]');
|
|
if (el) el.style.width = data.memory.percent + '%';
|
|
el = document.querySelector('[data-metric="memory_used"]');
|
|
if (el) el.textContent = data.memory.used;
|
|
el = document.querySelector('[data-metric="memory_total"]');
|
|
if (el) el.textContent = data.memory.total;
|
|
|
|
el = document.querySelector('[data-metric="disk_percent"]');
|
|
if (el) el.textContent = data.disk.percent.toFixed(2);
|
|
el = document.querySelector('[data-metric="disk_bar"]');
|
|
if (el) {
|
|
el.style.width = data.disk.percent + '%';
|
|
el.className = 'progress-bar ' + (data.disk.percent > 90 ? 'bg-danger' : 'bg-warning');
|
|
}
|
|
el = document.querySelector('[data-metric="disk_free"]');
|
|
if (el) el.textContent = data.disk.free;
|
|
el = document.querySelector('[data-metric="disk_total"]');
|
|
if (el) el.textContent = data.disk.total;
|
|
|
|
el = document.querySelector('[data-metric="storage_used"]');
|
|
if (el) el.textContent = data.app.storage_used;
|
|
el = document.querySelector('[data-metric="buckets_count"]');
|
|
if (el) el.textContent = data.app.buckets;
|
|
el = document.querySelector('[data-metric="objects_count"]');
|
|
if (el) el.textContent = data.app.objects;
|
|
|
|
countdown = 5;
|
|
})
|
|
.catch(function(err) {
|
|
console.error('Metrics fetch error:', err);
|
|
});
|
|
}
|
|
|
|
function startCountdown() {
|
|
if (countdownTimer) clearInterval(countdownTimer);
|
|
countdown = 5;
|
|
if (countdownEl) countdownEl.textContent = countdown;
|
|
countdownTimer = setInterval(function() {
|
|
countdown--;
|
|
if (countdownEl) countdownEl.textContent = countdown;
|
|
if (countdown <= 0) {
|
|
countdown = 5;
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
function startPolling() {
|
|
if (fetchTimer) clearInterval(fetchTimer);
|
|
fetchTimer = setInterval(function() {
|
|
if (!document.hidden) {
|
|
updateMetrics();
|
|
}
|
|
}, refreshInterval);
|
|
startCountdown();
|
|
}
|
|
|
|
if (refreshBtn) {
|
|
refreshBtn.addEventListener('click', function() {
|
|
updateMetrics();
|
|
countdown = 5;
|
|
if (countdownEl) countdownEl.textContent = countdown;
|
|
});
|
|
}
|
|
|
|
document.addEventListener('visibilitychange', function() {
|
|
if (!document.hidden) {
|
|
updateMetrics();
|
|
startPolling();
|
|
}
|
|
});
|
|
|
|
startPolling();
|
|
})();
|
|
|
|
{% if operation_metrics_enabled %}
|
|
(function() {
|
|
var methodChart = null;
|
|
var statusChart = null;
|
|
var endpointChart = null;
|
|
var opStatus = document.getElementById('opStatus');
|
|
var opTimer = null;
|
|
var methodColors = {
|
|
'GET': '#0d6efd',
|
|
'PUT': '#198754',
|
|
'POST': '#ffc107',
|
|
'DELETE': '#dc3545',
|
|
'HEAD': '#6c757d',
|
|
'OPTIONS': '#0dcaf0'
|
|
};
|
|
var statusColors = {
|
|
'2xx': '#198754',
|
|
'3xx': '#0dcaf0',
|
|
'4xx': '#ffc107',
|
|
'5xx': '#dc3545'
|
|
};
|
|
var endpointColors = {
|
|
'object': '#0d6efd',
|
|
'bucket': '#198754',
|
|
'ui': '#6c757d',
|
|
'service': '#0dcaf0',
|
|
'kms': '#ffc107'
|
|
};
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
var k = 1024;
|
|
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function initOpCharts() {
|
|
var methodCtx = document.getElementById('methodChart');
|
|
var statusCtx = document.getElementById('statusChart');
|
|
var endpointCtx = document.getElementById('endpointChart');
|
|
|
|
if (methodCtx) {
|
|
methodChart = new Chart(methodCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
data: [],
|
|
backgroundColor: []
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
plugins: {
|
|
legend: { position: 'right', labels: { boxWidth: 12, font: { size: 11 } } }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (statusCtx) {
|
|
statusChart = new Chart(statusCtx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
data: [],
|
|
backgroundColor: []
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
y: { beginAtZero: true, ticks: { stepSize: 1 } }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (endpointCtx) {
|
|
endpointChart = new Chart(endpointCtx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
data: [],
|
|
backgroundColor: []
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
indexAxis: 'y',
|
|
animation: false,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
x: { beginAtZero: true, ticks: { stepSize: 1 } }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function updateOpMetrics() {
|
|
if (document.hidden) return;
|
|
fetch('/ui/metrics/operations')
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (!data.enabled || !data.stats) {
|
|
if (opStatus) opStatus.textContent = 'Operation metrics not available';
|
|
return;
|
|
}
|
|
var stats = data.stats;
|
|
var totals = stats.totals || {};
|
|
|
|
var totalEl = document.getElementById('opTotalRequests');
|
|
var successEl = document.getElementById('opSuccessRate');
|
|
var errorEl = document.getElementById('opErrorCount');
|
|
var latencyEl = document.getElementById('opAvgLatency');
|
|
var bytesInEl = document.getElementById('opBytesIn');
|
|
var bytesOutEl = document.getElementById('opBytesOut');
|
|
|
|
if (totalEl) totalEl.textContent = totals.count || 0;
|
|
if (successEl) {
|
|
var rate = totals.count > 0 ? ((totals.success_count / totals.count) * 100).toFixed(1) : 0;
|
|
successEl.textContent = rate + '%';
|
|
}
|
|
if (errorEl) errorEl.textContent = totals.error_count || 0;
|
|
if (latencyEl) latencyEl.textContent = (totals.latency_avg_ms || 0).toFixed(1) + 'ms';
|
|
if (bytesInEl) bytesInEl.textContent = formatBytes(totals.bytes_in || 0);
|
|
if (bytesOutEl) bytesOutEl.textContent = formatBytes(totals.bytes_out || 0);
|
|
|
|
if (methodChart && stats.by_method) {
|
|
var methods = Object.keys(stats.by_method);
|
|
var methodData = methods.map(function(m) { return stats.by_method[m].count; });
|
|
var methodBg = methods.map(function(m) { return methodColors[m] || '#6c757d'; });
|
|
methodChart.data.labels = methods;
|
|
methodChart.data.datasets[0].data = methodData;
|
|
methodChart.data.datasets[0].backgroundColor = methodBg;
|
|
methodChart.update('none');
|
|
}
|
|
|
|
if (statusChart && stats.by_status_class) {
|
|
var statuses = Object.keys(stats.by_status_class).sort();
|
|
var statusData = statuses.map(function(s) { return stats.by_status_class[s]; });
|
|
var statusBg = statuses.map(function(s) { return statusColors[s] || '#6c757d'; });
|
|
statusChart.data.labels = statuses;
|
|
statusChart.data.datasets[0].data = statusData;
|
|
statusChart.data.datasets[0].backgroundColor = statusBg;
|
|
statusChart.update('none');
|
|
}
|
|
|
|
if (endpointChart && stats.by_endpoint) {
|
|
var endpoints = Object.keys(stats.by_endpoint);
|
|
var endpointData = endpoints.map(function(e) { return stats.by_endpoint[e].count; });
|
|
var endpointBg = endpoints.map(function(e) { return endpointColors[e] || '#6c757d'; });
|
|
endpointChart.data.labels = endpoints;
|
|
endpointChart.data.datasets[0].data = endpointData;
|
|
endpointChart.data.datasets[0].backgroundColor = endpointBg;
|
|
endpointChart.update('none');
|
|
}
|
|
|
|
var errorBody = document.getElementById('errorCodesBody');
|
|
if (errorBody && stats.error_codes) {
|
|
var errorCodes = Object.entries(stats.error_codes);
|
|
errorCodes.sort(function(a, b) { return b[1] - a[1]; });
|
|
var totalErrors = errorCodes.reduce(function(sum, e) { return sum + e[1]; }, 0);
|
|
errorCodes = errorCodes.slice(0, 10);
|
|
if (errorCodes.length === 0) {
|
|
errorBody.innerHTML = '<div class="text-muted small text-center py-4">' +
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-check-circle mb-2 text-success" viewBox="0 0 16 16">' +
|
|
'<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>' +
|
|
'<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/>' +
|
|
'</svg><div>No S3 API errors</div></div>';
|
|
} else {
|
|
errorBody.innerHTML = errorCodes.map(function(e) {
|
|
var pct = totalErrors > 0 ? ((e[1] / totalErrors) * 100).toFixed(0) : 0;
|
|
return '<div class="d-flex align-items-center py-1" style="font-size: 0.8rem;">' +
|
|
'<div style="flex: 1;"><code class="text-danger">' + e[0] + '</code></div>' +
|
|
'<div class="text-end fw-semibold" style="width: 60px;">' + e[1] + '</div>' +
|
|
'<div style="width: 100px; padding-left: 10px;"><div class="progress" style="height: 6px;"><div class="progress-bar bg-danger" style="width: ' + pct + '%"></div></div></div>' +
|
|
'</div>';
|
|
}).join('');
|
|
}
|
|
}
|
|
|
|
var windowMins = Math.floor(stats.window_seconds / 60);
|
|
var windowSecs = stats.window_seconds % 60;
|
|
var windowStr = windowMins > 0 ? windowMins + 'm ' + windowSecs + 's' : windowSecs + 's';
|
|
if (opStatus) opStatus.textContent = 'Window: ' + windowStr + ' | ' + new Date().toLocaleTimeString();
|
|
})
|
|
.catch(function(err) {
|
|
console.error('Operation metrics fetch error:', err);
|
|
if (opStatus) opStatus.textContent = 'Failed to load';
|
|
});
|
|
}
|
|
|
|
function startOpPolling() {
|
|
if (opTimer) clearInterval(opTimer);
|
|
opTimer = setInterval(updateOpMetrics, 5000);
|
|
}
|
|
|
|
var resetBtn = document.getElementById('resetOpMetricsBtn');
|
|
if (resetBtn) {
|
|
resetBtn.addEventListener('click', function() {
|
|
updateOpMetrics();
|
|
});
|
|
}
|
|
|
|
document.addEventListener('visibilitychange', function() {
|
|
if (document.hidden) {
|
|
if (opTimer) clearInterval(opTimer);
|
|
opTimer = null;
|
|
} else {
|
|
updateOpMetrics();
|
|
startOpPolling();
|
|
}
|
|
});
|
|
|
|
initOpCharts();
|
|
updateOpMetrics();
|
|
startOpPolling();
|
|
})();
|
|
{% endif %}
|
|
|
|
{% if metrics_history_enabled %}
|
|
(function() {
|
|
var cpuChart = null;
|
|
var memoryChart = null;
|
|
var diskChart = null;
|
|
var historyStatus = document.getElementById('historyStatus');
|
|
var timeRangeSelect = document.getElementById('historyTimeRange');
|
|
var historyTimer = null;
|
|
var MAX_DATA_POINTS = 500;
|
|
|
|
function createChart(ctx, label, color) {
|
|
return new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
label: label,
|
|
data: [],
|
|
borderColor: color,
|
|
backgroundColor: color + '20',
|
|
fill: true,
|
|
tension: 0.3,
|
|
pointRadius: 3,
|
|
pointHoverRadius: 6,
|
|
hitRadius: 10,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
animation: false,
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(ctx) { return ctx.parsed.y.toFixed(2) + '%'; }
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
display: true,
|
|
ticks: { maxTicksAuto: true, maxRotation: 0, font: { size: 10 }, autoSkip: true, maxTicksLimit: 10 }
|
|
},
|
|
y: {
|
|
display: true,
|
|
min: 0,
|
|
max: 100,
|
|
ticks: { callback: function(v) { return v + '%'; } }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function initCharts() {
|
|
var cpuCtx = document.getElementById('cpuHistoryChart');
|
|
var memCtx = document.getElementById('memoryHistoryChart');
|
|
var diskCtx = document.getElementById('diskHistoryChart');
|
|
if (cpuCtx) cpuChart = createChart(cpuCtx, 'CPU %', '#0d6efd');
|
|
if (memCtx) memoryChart = createChart(memCtx, 'Memory %', '#0dcaf0');
|
|
if (diskCtx) diskChart = createChart(diskCtx, 'Disk %', '#ffc107');
|
|
}
|
|
|
|
function formatTime(ts) {
|
|
var d = new Date(ts);
|
|
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
function loadHistory() {
|
|
if (document.hidden) return;
|
|
var hours = timeRangeSelect ? timeRangeSelect.value : 24;
|
|
fetch('/ui/metrics/history?hours=' + hours)
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (!data.enabled || !data.history || data.history.length === 0) {
|
|
if (historyStatus) historyStatus.textContent = 'No history data available yet. Data is recorded every ' + (data.interval_minutes || 5) + ' minutes.';
|
|
return;
|
|
}
|
|
var history = data.history.slice(-MAX_DATA_POINTS);
|
|
var labels = history.map(function(h) { return formatTime(h.timestamp); });
|
|
var cpuData = history.map(function(h) { return h.cpu_percent; });
|
|
var memData = history.map(function(h) { return h.memory_percent; });
|
|
var diskData = history.map(function(h) { return h.disk_percent; });
|
|
|
|
if (cpuChart) {
|
|
cpuChart.data.labels = labels;
|
|
cpuChart.data.datasets[0].data = cpuData;
|
|
cpuChart.update('none');
|
|
}
|
|
if (memoryChart) {
|
|
memoryChart.data.labels = labels;
|
|
memoryChart.data.datasets[0].data = memData;
|
|
memoryChart.update('none');
|
|
}
|
|
if (diskChart) {
|
|
diskChart.data.labels = labels;
|
|
diskChart.data.datasets[0].data = diskData;
|
|
diskChart.update('none');
|
|
}
|
|
if (historyStatus) historyStatus.textContent = 'Showing ' + history.length + ' data points';
|
|
})
|
|
.catch(function(err) {
|
|
console.error('History fetch error:', err);
|
|
if (historyStatus) historyStatus.textContent = 'Failed to load history data';
|
|
});
|
|
}
|
|
|
|
function startHistoryPolling() {
|
|
if (historyTimer) clearInterval(historyTimer);
|
|
historyTimer = setInterval(loadHistory, 60000);
|
|
}
|
|
|
|
if (timeRangeSelect) {
|
|
timeRangeSelect.addEventListener('change', loadHistory);
|
|
}
|
|
|
|
document.addEventListener('visibilitychange', function() {
|
|
if (document.hidden) {
|
|
if (historyTimer) clearInterval(historyTimer);
|
|
historyTimer = null;
|
|
} else {
|
|
loadHistory();
|
|
startHistoryPolling();
|
|
}
|
|
});
|
|
|
|
initCharts();
|
|
loadHistory();
|
|
startHistoryPolling();
|
|
})();
|
|
{% endif %}
|
|
</script>
|
|
{% endblock %}
|