{% extends "base.html" %} {% block title %}Monitoring{% endblock %} {% block breadcrumb %}Monitoring{% endblock %} {% block extra_head %} {% endblock %} {% block content %} {# ── Alert Strip — dynamic threshold alerts ── #} {# ── Stats Row ── #}
{{ monitoring.cpu.percent|default(0)|round(1) }}%
CPU Usage
{{ monitoring.cpu.cores_logical|default(0) }} cores
{{ monitoring.memory.percent|default(0)|round(1) }}%
Memory
{{ monitoring.memory.used_human|default('—') }} / {{ monitoring.memory.total_human|default('—') }}
{{ monitoring.disk.percent|default(0)|round(1) }}%
Disk
{{ monitoring.disk.used_human|default('—') }} / {{ monitoring.disk.total_human|default('—') }}
{{ monitoring.process.threads|default(0) }}
Threads
PID {{ monitoring.process.pid|default('—') }}
{{ monitoring.process.rss_human|default('—') }}
Process RSS
{{ monitoring.process.mem_percent|default(0)|round(2) }}% of system
{{ monitoring.python.gc_objects|default(0) }}
GC Objects
{{ monitoring.python.gc_gen0|default(0) }} gen0 collections
{{ monitoring.process.uptime_human|default('—') }}
Uptime
{{ monitoring.process.status|default('—') }}
{# ── Tabs ── #}
{# ═══════════════════════════════════════════════════════════════════ #} {# ── TAB 1: Overview — live line charts + gauges + system info ── #} {# ═══════════════════════════════════════════════════════════════════ #}
Live Metrics 60-second rolling window
{# ── 2-column live charts ── #}
CPU History {{ monitoring.cpu.percent|default(0)|round(1) }}%
Memory History {{ monitoring.memory.percent|default(0)|round(1) }}%
{# ── 4-column gauge row ── #}
Resource Gauges
CPU
Memory
Disk
Swap
{# ── System Information ── #}
System Information
Platform
OS{{ monitoring.system.os|default('—') }}
Platform{{ monitoring.system.platform|default('—') }}
Architecture{{ monitoring.system.arch|default('—') }}
Hostname{{ monitoring.system.hostname|default('—') }}
CPU Cores{{ monitoring.cpu.cores_physical|default('—') }}P / {{ monitoring.cpu.cores_logical|default('—') }}L
Python Runtime
Version{{ monitoring.python.version|default('—') }}
Implementation{{ monitoring.python.implementation|default('—') }}
PID{{ monitoring.process.pid|default('—') }}
GC Objects{{ monitoring.python.gc_objects|default(0)|int }}
Executable{{ monitoring.python.executable|default('—') }}
Load Average
1 min{{ monitoring.cpu.load_avg_1|default('—') }}
5 min{{ monitoring.cpu.load_avg_5|default('—') }}
15 min{{ monitoring.cpu.load_avg_15|default('—') }}
CPU Freq{{ monitoring.cpu.freq_current|default('—') }} MHz
Ctx Switches{{ monitoring.process.ctx_switches|default('—') }}
{# ═══════════════════════════════════════════════════════════════════ #} {# ── TAB 2: CPU — per-core chart & times breakdown ── #} {# ═══════════════════════════════════════════════════════════════════ #}
Per-Core Utilisation
CPU Times Breakdown
Time Distribution
CPU Details
Physical Cores{{ monitoring.cpu.cores_physical|default('—') }}
Logical Cores{{ monitoring.cpu.cores_logical|default('—') }}
Current Freq{{ monitoring.cpu.freq_current|default('—') }} MHz
Max Freq{{ monitoring.cpu.freq_max|default('—') }} MHz
User Time{{ monitoring.cpu.times_user|default('—') }}s
System Time{{ monitoring.cpu.times_system|default('—') }}s
Idle Time{{ monitoring.cpu.times_idle|default('—') }}s
Load Avg (1m){{ monitoring.cpu.load_avg_1|default('—') }}
{# ═══════════════════════════════════════════════════════════════════ #} {# ── TAB 3: Memory — breakdown & swap ── #} {# ═══════════════════════════════════════════════════════════════════ #}
Memory Allocation
Physical Memory
Process Memory
Memory Details
System RAM
Total{{ monitoring.memory.total_human|default('—') }}
Available{{ monitoring.memory.available_human|default('—') }}
Used{{ monitoring.memory.used_human|default('—') }}
Utilisation{{ monitoring.memory.percent|default(0)|round(1) }}%
Swap
Total{{ monitoring.memory.swap_total_human|default('—') }}
Used{{ monitoring.memory.swap_used_human|default('—') }}
Free{{ monitoring.memory.swap_free_human|default('—') }}
Utilisation{{ monitoring.memory.swap_percent|default(0)|round(1) }}%
Process (RSS)
RSS{{ monitoring.process.rss_human|default('—') }}
VMS{{ monitoring.process.vms_human|default('—') }}
Shared{{ monitoring.process.shared|default(0) }}
% of System{{ monitoring.process.mem_percent|default(0)|round(2) }}%
{# ═══════════════════════════════════════════════════════════════════ #} {# ── TAB 4: Network — I/O, throughput, connections ── #} {# ═══════════════════════════════════════════════════════════════════ #}
Network I/O
Throughput (KB/s)
Cumulative I/O
Network Counters
Bytes Sent{{ monitoring.network.bytes_sent_human|default('—') }}
Bytes Received{{ monitoring.network.bytes_recv_human|default('—') }}
Packets Sent{{ monitoring.network.packets_sent|default('—') }}
Packets Received{{ monitoring.network.packets_recv|default('—') }}
Errors In{{ monitoring.network.errin|default(0) }}
Errors Out{{ monitoring.network.errout|default(0) }}
Drops In{{ monitoring.network.dropin|default(0) }}
Drops Out{{ monitoring.network.dropout|default(0) }}
Active Connections
{% for status, count in (monitoring.network.connections_by_status|default({})).items() %}
{{ count }}
{{ status }}
{% endfor %} {% if not monitoring.network.connections_by_status|default({}) %}
No connection data available
{% endif %}
{# ═══════════════════════════════════════════════════════════════════ #} {# ── TAB 5: Process — detailed info & environment ── #} {# ═══════════════════════════════════════════════════════════════════ #}
Process Details
Current Process
PID{{ monitoring.process.pid|default('—') }}
Name{{ monitoring.process.name|default('—') }}
Status{{ monitoring.process.status|default('—') }}
Started{{ monitoring.process.create_time|default('—') }}
Uptime{{ monitoring.process.uptime_human|default('—') }}
Threads{{ monitoring.process.threads|default('—') }}
Open Files{{ monitoring.process.open_files|default('—') }}
Ctx Switches (vol){{ monitoring.process.ctx_switches_voluntary|default('—') }}
Ctx Switches (invol){{ monitoring.process.ctx_switches_involuntary|default('—') }}
Process Memory Map
Environment Snapshot
{% for key, val in (monitoring.process.env_snapshot|default({})).items() %} {% endfor %} {% if not monitoring.process.env_snapshot|default({}) %} {% endif %}
{{ key }} {{ val }}
No environment variables captured
{# ═══════════════════════════════════════════════════════════════════ #} {# ── TAB 6: GC & Runtime — garbage collector, IO counters ── #} {# ═══════════════════════════════════════════════════════════════════ #}
Garbage Collector
GC Generations
{% for gen in monitoring.python.gc_generations|default([]) %}
Gen {{ loop.index0 }}
{{ gen.collections }}
{% endfor %} {% if not monitoring.python.gc_generations|default([]) %}
GC generation data not available
{% endif %}
Tracked Objects{{ monitoring.python.gc_objects|default(0) }}
GC Enabled{{ 'Yes' if monitoring.python.gc_enabled|default(true) else 'No' }}
Freeze Count{{ monitoring.python.gc_frozen|default(0) }}
GC Objects Over Time
I/O Counters
Process I/O
Read Count{{ monitoring.process.io_read_count|default('—') }}
Write Count{{ monitoring.process.io_write_count|default('—') }}
Read Bytes{{ monitoring.process.io_read_bytes_human|default('—') }}
Write Bytes{{ monitoring.process.io_write_bytes_human|default('—') }}
Python Internals
Loaded Modules{{ monitoring.python.loaded_modules|default(0) }}
Thread Count{{ monitoring.python.active_threads|default(0) }}
Recursion Limit{{ monitoring.python.recursion_limit|default(0) }}
Allocated Blocks{{ monitoring.python.allocator_blocks|default('—') }}
{# ═══════════════════════════════════════════════════════════════════ #} {# ── TAB 7: Health — subsystem checks & disk partitions ── #} {# ═══════════════════════════════════════════════════════════════════ #}
Subsystem Health
{% if monitoring.health_checks %}
{% for check in monitoring.health_checks %}
{{ check.name }} {{ check.latency_ms|default(0)|round(1) }}ms
{{ check.message|default('No details') }}
{{ check.checked_at|default('—') }}
{% endfor %}
{% else %}
No health checks registered yet
Subsystems will appear here once they register with the HealthRegistry.
{% endif %}
Disk Partitions
{% if monitoring.disk.partitions|default([]) %}
{% for p in monitoring.disk.partitions|default([]) %} {% endfor %}
Device Mount FS Total Used Free Use%
{{ p.device }} {{ p.mountpoint }} {{ p.fstype }} {{ p.total_human }} {{ p.used_human }} {{ p.free_human }}
{{ p.percent }}%
{% else %}
No partition data available.
{% endif %}
Prometheus Text Format
Metrics Export OpenMetrics
Click Refresh to load current Prometheus metrics…
{% endblock %} {% block extra_js %} // ═══════════════════════════════════════════════════════════════════ // ── Chart.js Monitoring Dashboard — Live Polling ── // ═══════════════════════════════════════════════════════════════════ var _monData = {{ monitoring|tojson }}; var _urlPrefix = '{{ url_prefix|default("/admin") }}'; var _pollInterval = null; var _polling = true; var MAX_POINTS = 20; // ── Color palette ── var C = { green: '#22c55e', greenFade: 'rgba(34,197,94,0.15)', blue: '#3b82f6', blueFade: 'rgba(59,130,246,0.15)', amber: '#f59e0b', amberFade: 'rgba(245,158,11,0.15)', purple: '#a855f7', purpleFade: 'rgba(168,85,247,0.15)', cyan: '#06b6d4', cyanFade: 'rgba(6,182,212,0.15)', rose: '#f43f5e', roseFade: 'rgba(244,63,94,0.15)', gray: '#64748b', grayFade: 'rgba(100,116,139,0.15)', teal: '#14b8a6' }; // ── Theme-aware defaults ── function isDark() { return document.documentElement.getAttribute('data-theme') === 'dark'; } function gridColor() { return isDark() ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)'; } function textColor() { return isDark() ? '#94a3b8' : '#64748b'; } // ── Chart.js global defaults ── Chart.defaults.font.family = "'Outfit', sans-serif"; Chart.defaults.font.size = 11; Chart.defaults.color = textColor(); Chart.defaults.animation.duration = 600; Chart.defaults.animation.easing = 'easeOutQuart'; Chart.defaults.responsive = true; Chart.defaults.maintainAspectRatio = false; Chart.defaults.plugins.legend.display = false; Chart.defaults.plugins.tooltip.backgroundColor = isDark() ? 'rgba(0,0,0,0.85)' : 'rgba(255,255,255,0.95)'; Chart.defaults.plugins.tooltip.titleColor = isDark() ? '#e4e4e7' : '#18181b'; Chart.defaults.plugins.tooltip.bodyColor = isDark() ? '#a1a1aa' : '#52525b'; Chart.defaults.plugins.tooltip.borderColor = isDark() ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'; Chart.defaults.plugins.tooltip.borderWidth = 1; Chart.defaults.plugins.tooltip.cornerRadius = 8; Chart.defaults.plugins.tooltip.padding = 10; // ── History arrays ── var _cpuHistory = [_monData.cpu.percent || 0]; var _memHistory = [_monData.memory.percent || 0]; var _netSentKBs = [0]; var _netRecvKBs = [0]; var _gcHistory = [_monData.python.gc_objects || 0]; var _prevNetSent = _monData.network.bytes_sent || 0; var _prevNetRecv = _monData.network.bytes_recv || 0; var _timeLabels = ['0']; // ── Chart instances ── var charts = {}; function scaleOpts(maxVal, unit) { return { x: { display: true, grid: { color: gridColor(), drawBorder: false }, ticks: { color: textColor(), maxTicksLimit: 6, font: { size: 9 } } }, y: { display: true, min: 0, max: maxVal || undefined, grid: { color: gridColor(), drawBorder: false }, ticks: { color: textColor(), callback: function(v) { return v + (unit||''); }, font: { size: 9 } } } }; } function lineDataset(label, data, color, fade) { return { label: label, data: data, borderColor: color, backgroundColor: fade, borderWidth: 2, pointRadius: 0, pointHoverRadius: 4, fill: true, tension: 0.35 }; } function initCharts() { // ── CPU History (line) ── charts.cpuHistory = new Chart(document.getElementById('chartCpuHistory'), { type: 'line', data: { labels: _timeLabels.slice(), datasets: [lineDataset('CPU %', _cpuHistory.slice(), C.green, C.greenFade)] }, options: { scales: scaleOpts(100, '%'), plugins: { tooltip: { callbacks: { label: function(c) { return 'CPU: ' + c.raw.toFixed(1) + '%'; } } } } } }); // ── Memory History (line) ── charts.memHistory = new Chart(document.getElementById('chartMemHistory'), { type: 'line', data: { labels: _timeLabels.slice(), datasets: [lineDataset('Memory %', _memHistory.slice(), C.blue, C.blueFade)] }, options: { scales: scaleOpts(100, '%'), plugins: { tooltip: { callbacks: { label: function(c) { return 'Mem: ' + c.raw.toFixed(1) + '%'; } } } } } }); // ── Gauges (doughnut) ── function gaugeData(pct, color) { return { type: 'doughnut', data: { labels: ['Used', 'Free'], datasets: [{ data: [pct, 100 - pct], backgroundColor: [color, gridColor()], borderWidth: 0, cutout: '72%' }] }, options: { circumference: 180, rotation: 270, plugins: { tooltip: { enabled: false }, legend: { display: false } }, animation: { animateRotate: true, duration: 800 } }, plugins: [{ id: 'gaugeCenter', afterDraw: function(chart) { var ctx = chart.ctx, w = chart.width, h = chart.height; ctx.save(); ctx.font = '700 1.3rem Outfit, sans-serif'; ctx.fillStyle = textColor(); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(chart.data.datasets[0].data[0].toFixed(1) + '%', w/2, h * 0.65); ctx.restore(); } }] }; } charts.gaugeCpu = new Chart(document.getElementById('gaugeCpu'), gaugeData(_monData.cpu.percent || 0, C.green)); charts.gaugeMem = new Chart(document.getElementById('gaugeMem'), gaugeData(_monData.memory.percent || 0, C.blue)); charts.gaugeDisk = new Chart(document.getElementById('gaugeDisk'), gaugeData(_monData.disk.percent || 0, C.amber)); charts.gaugeSwap = new Chart(document.getElementById('gaugeSwap'), gaugeData(_monData.memory.swap_percent || 0, C.purple)); // ── CPU Per-Core (bar) ── var coreLabels = (_monData.cpu.per_core || []).map(function(_, i) { return 'Core ' + i; }); var coreColors = (_monData.cpu.per_core || []).map(function(_, i) { return i % 2 === 0 ? C.green : C.cyan; }); charts.cpuCores = new Chart(document.getElementById('chartCpuCores'), { type: 'bar', data: { labels: coreLabels, datasets: [{ label: 'Usage %', data: _monData.cpu.per_core || [], backgroundColor: coreColors, borderRadius: 4, maxBarThickness: 40 }] }, options: { scales: scaleOpts(100, '%'), plugins: { tooltip: { callbacks: { label: function(c) { return c.label + ': ' + c.raw.toFixed(1) + '%'; } } } } } }); // ── CPU Times (doughnut) ── charts.cpuTimes = new Chart(document.getElementById('chartCpuTimes'), { type: 'doughnut', data: { labels: ['User', 'System', 'Idle'], datasets: [{ data: [_monData.cpu.times_user||0, _monData.cpu.times_system||0, _monData.cpu.times_idle||0], backgroundColor: [C.green, C.amber, C.gray], borderWidth: 0, cutout: '55%' }] }, options: { plugins: { legend: { display: true, position: 'bottom', labels: { padding: 14, usePointStyle: true, pointStyle: 'circle', font: { size: 10 } } } } } }); // ── Memory Breakdown (doughnut) ── charts.memBreakdown = new Chart(document.getElementById('chartMemBreakdown'), { type: 'doughnut', data: { labels: ['Used', 'Available'], datasets: [{ data: [_monData.memory.used||0, _monData.memory.available||0], backgroundColor: [C.blue, C.gray], borderWidth: 0, cutout: '55%' }] }, options: { plugins: { legend: { display: true, position: 'bottom', labels: { padding: 14, usePointStyle: true, pointStyle: 'circle', font: { size: 10 } } } } } }); // ── Process Memory (doughnut) ── var procRss = _monData.process.rss || 0; var procVms = _monData.process.vms || 0; charts.procMem = new Chart(document.getElementById('chartProcMem'), { type: 'doughnut', data: { labels: ['RSS', 'VMS (other)'], datasets: [{ data: [procRss, Math.max(0, procVms - procRss)], backgroundColor: [C.green, C.gray], borderWidth: 0, cutout: '55%' }] }, options: { plugins: { legend: { display: true, position: 'bottom', labels: { padding: 14, usePointStyle: true, pointStyle: 'circle', font: { size: 10 } } } } } }); // ── Network Throughput (line) ── charts.netThroughput = new Chart(document.getElementById('chartNetThroughput'), { type: 'line', data: { labels: _timeLabels.slice(), datasets: [ lineDataset('Sent KB/s', _netSentKBs.slice(), C.green, C.greenFade), lineDataset('Recv KB/s', _netRecvKBs.slice(), C.cyan, C.cyanFade) ] }, options: { scales: scaleOpts(null, ''), plugins: { legend: { display: true, position: 'top', labels: { padding: 10, usePointStyle: true, pointStyle: 'circle', font: { size: 10 } } } } } }); // ── Network I/O cumulative (bar) ── charts.netIO = new Chart(document.getElementById('chartNetIO'), { type: 'bar', data: { labels: ['Sent', 'Received'], datasets: [{ label: 'MB', data: [(_monData.network.bytes_sent||0)/1048576, (_monData.network.bytes_recv||0)/1048576], backgroundColor: [C.green, C.cyan], borderRadius: 6, maxBarThickness: 80 }] }, options: { indexAxis: 'y', scales: { x: { grid: { color: gridColor() }, ticks: { color: textColor(), callback: function(v) { return v.toFixed(0) + ' MB'; } } }, y: { grid: { display: false }, ticks: { color: textColor() } } } } }); // ── Process Memory Map (doughnut) ── charts.procMemMap = new Chart(document.getElementById('chartProcMemMap'), { type: 'doughnut', data: { labels: ['RSS', 'Shared', 'Private'], datasets: [{ data: [_monData.process.rss||0, _monData.process.shared||0, _monData.process.private||0], backgroundColor: [C.green, C.blue, C.purple], borderWidth: 0, cutout: '55%' }] }, options: { plugins: { legend: { display: true, position: 'bottom', labels: { padding: 14, usePointStyle: true, pointStyle: 'circle', font: { size: 10 } } } } } }); // ── GC History (line) ── charts.gcHistory = new Chart(document.getElementById('chartGCHistory'), { type: 'line', data: { labels: _timeLabels.slice(), datasets: [lineDataset('GC Objects', _gcHistory.slice(), C.purple, C.purpleFade)] }, options: { scales: scaleOpts(null, '') } }); } // ── Gauge updater ── function updateGauge(chart, pct, color) { chart.data.datasets[0].data = [pct, Math.max(0, 100 - pct)]; var effectiveColor = pct < 60 ? color : (pct < 85 ? C.amber : C.rose); chart.data.datasets[0].backgroundColor = [effectiveColor, gridColor()]; chart.update('none'); } // ── Alerts ── function updateAlerts(d) { var strip = document.getElementById('monAlertStrip'); var alerts = []; var cpuP = d.cpu.percent || 0; var memP = d.memory.percent || 0; var diskP = d.disk.percent || 0; if (cpuP > 90) alerts.push({ cls: 'crit', msg: '⚠ CPU usage critical: ' + cpuP.toFixed(1) + '%' }); else if (cpuP > 75) alerts.push({ cls: 'warn', msg: '⚡ CPU usage elevated: ' + cpuP.toFixed(1) + '%' }); if (memP > 90) alerts.push({ cls: 'crit', msg: '⚠ Memory usage critical: ' + memP.toFixed(1) + '%' }); else if (memP > 80) alerts.push({ cls: 'warn', msg: '⚡ Memory usage elevated: ' + memP.toFixed(1) + '%' }); if (diskP > 90) alerts.push({ cls: 'crit', msg: '⚠ Disk usage critical: ' + diskP.toFixed(1) + '%' }); else if (diskP > 80) alerts.push({ cls: 'warn', msg: '⚡ Disk usage elevated: ' + diskP.toFixed(1) + '%' }); if (alerts.length === 0) { alerts.push({ cls: 'ok', msg: '✓ All systems operating within normal parameters' }); } strip.style.display = 'block'; strip.innerHTML = alerts.map(function(a) { return '
' + a.msg + '
'; }).join(''); } // ── Update stat cards ── function updateStats(d) { var el; el = document.getElementById('statCpu'); if (el) el.textContent = (d.cpu.percent||0).toFixed(1) + '%'; el = document.getElementById('statMem'); if (el) el.textContent = (d.memory.percent||0).toFixed(1) + '%'; el = document.getElementById('statDisk'); if (el) el.textContent = (d.disk.percent||0).toFixed(1) + '%'; el = document.getElementById('statThreads'); if (el) el.textContent = d.process.threads || 0; el = document.getElementById('statRSS'); if (el) el.textContent = d.process.rss_human || '—'; el = document.getElementById('statGC'); if (el) el.textContent = d.python.gc_objects || 0; el = document.getElementById('statUptime'); if (el) el.textContent = d.process.uptime_human || '—'; el = document.getElementById('statMemSub'); if (el) el.textContent = (d.memory.used_human||'—') + ' / ' + (d.memory.total_human||'—'); el = document.getElementById('statDiskSub'); if (el) el.textContent = (d.disk.used_human||'—') + ' / ' + (d.disk.total_human||'—'); el = document.getElementById('statMemPct'); if (el) el.textContent = (d.process.mem_percent||0).toFixed(2) + '% of system'; el = document.getElementById('statGCSub'); if (el) el.textContent = ((d.python.gc_generations||[])[0]||{}).collections + ' gen0 collections'; el = document.getElementById('cpuBadge'); if (el) el.textContent = (d.cpu.percent||0).toFixed(1) + '%'; el = document.getElementById('memBadge'); if (el) el.textContent = (d.memory.percent||0).toFixed(1) + '%'; el = document.getElementById('netBytesSent'); if (el) el.textContent = d.network.bytes_sent_human || '—'; el = document.getElementById('netBytesRecv'); if (el) el.textContent = d.network.bytes_recv_human || '—'; el = document.getElementById('netPktsSent'); if (el) el.textContent = d.network.packets_sent || '—'; el = document.getElementById('netPktsRecv'); if (el) el.textContent = d.network.packets_recv || '—'; el = document.getElementById('overviewCtxSw'); if (el) el.textContent = d.process.ctx_switches || '—'; el = document.getElementById('gcTracked'); if (el) el.textContent = d.python.gc_objects || 0; el = document.getElementById('procUptime'); if (el) el.textContent = d.process.uptime_human || '—'; el = document.getElementById('procThreads'); if (el) el.textContent = d.process.threads || '—'; el = document.getElementById('procFDs'); if (el) el.textContent = d.process.open_files || '—'; el = document.getElementById('procRSS'); if (el) el.textContent = d.process.rss_human || '—'; el = document.getElementById('procVMS'); if (el) el.textContent = d.process.vms_human || '—'; el = document.getElementById('procMemPct'); if (el) el.textContent = (d.process.mem_percent||0).toFixed(2) + '%'; el = document.getElementById('memTotal'); if (el) el.textContent = d.memory.total_human || '—'; el = document.getElementById('memAvail'); if (el) el.textContent = d.memory.available_human || '—'; el = document.getElementById('memUsed'); if (el) el.textContent = d.memory.used_human || '—'; el = document.getElementById('memPct'); if (el) el.textContent = (d.memory.percent||0).toFixed(1) + '%'; } // ── Live polling ── function pollOnce() { fetch(_urlPrefix + '/monitoring/api/') .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }) .then(function(data) { _monData = data; // ── History push ── _cpuHistory.push(data.cpu.percent || 0); _memHistory.push(data.memory.percent || 0); _gcHistory.push(data.python.gc_objects || 0); if (_cpuHistory.length > MAX_POINTS) _cpuHistory.shift(); if (_memHistory.length > MAX_POINTS) _memHistory.shift(); if (_gcHistory.length > MAX_POINTS) _gcHistory.shift(); // Network delta var deltaSent = Math.max(0, ((data.network.bytes_sent||0) - _prevNetSent) / 1024); var deltaRecv = Math.max(0, ((data.network.bytes_recv||0) - _prevNetRecv) / 1024); _netSentKBs.push(deltaSent); _netRecvKBs.push(deltaRecv); if (_netSentKBs.length > MAX_POINTS) _netSentKBs.shift(); if (_netRecvKBs.length > MAX_POINTS) _netRecvKBs.shift(); _prevNetSent = data.network.bytes_sent || 0; _prevNetRecv = data.network.bytes_recv || 0; // ── Update line charts ── if (charts.cpuHistory) { charts.cpuHistory.data.labels = _cpuHistory.map(function() { return ''; }); charts.cpuHistory.data.datasets[0].data = _cpuHistory.slice(); charts.cpuHistory.update('none'); } if (charts.memHistory) { charts.memHistory.data.labels = _memHistory.map(function() { return ''; }); charts.memHistory.data.datasets[0].data = _memHistory.slice(); charts.memHistory.update('none'); } if (charts.gcHistory) { charts.gcHistory.data.labels = _gcHistory.map(function() { return ''; }); charts.gcHistory.data.datasets[0].data = _gcHistory.slice(); charts.gcHistory.update('none'); } if (charts.netThroughput) { charts.netThroughput.data.labels = _netSentKBs.map(function() { return ''; }); charts.netThroughput.data.datasets[0].data = _netSentKBs.slice(); charts.netThroughput.data.datasets[1].data = _netRecvKBs.slice(); charts.netThroughput.update('none'); } // ── Update gauges ── if (charts.gaugeCpu) updateGauge(charts.gaugeCpu, data.cpu.percent||0, C.green); if (charts.gaugeMem) updateGauge(charts.gaugeMem, data.memory.percent||0, C.blue); if (charts.gaugeDisk) updateGauge(charts.gaugeDisk, data.disk.percent||0, C.amber); if (charts.gaugeSwap) updateGauge(charts.gaugeSwap, data.memory.swap_percent||0, C.purple); // ── Update bar charts ── if (charts.cpuCores && data.cpu.per_core) { charts.cpuCores.data.datasets[0].data = data.cpu.per_core; charts.cpuCores.update('none'); } if (charts.netIO) { charts.netIO.data.datasets[0].data = [(data.network.bytes_sent||0)/1048576, (data.network.bytes_recv||0)/1048576]; charts.netIO.update('none'); } // ── Update doughnuts ── if (charts.cpuTimes) { charts.cpuTimes.data.datasets[0].data = [data.cpu.times_user||0, data.cpu.times_system||0, data.cpu.times_idle||0]; charts.cpuTimes.update('none'); } if (charts.memBreakdown) { charts.memBreakdown.data.datasets[0].data = [data.memory.used||0, data.memory.available||0]; charts.memBreakdown.update('none'); } if (charts.procMem) { var rss = data.process.rss||0, vms = data.process.vms||0; charts.procMem.data.datasets[0].data = [rss, Math.max(0, vms - rss)]; charts.procMem.update('none'); } if (charts.procMemMap) { charts.procMemMap.data.datasets[0].data = [data.process.rss||0, data.process.shared||0, data.process.private||0]; charts.procMemMap.update('none'); } // ── Update stat cards + alerts ── updateStats(data); updateAlerts(data); // ── Pulse indicator ── var ps = document.getElementById('pollStatus'); if (ps) { ps.style.color = C.green; setTimeout(function() { ps.style.color = ''; }, 350); } }) .catch(function(err) { var ps = document.getElementById('pollStatus'); if (ps) { ps.innerHTML = '● Connection lost'; } }); } function startPolling() { if (_pollInterval) clearInterval(_pollInterval); _pollInterval = setInterval(pollOnce, 3000); } function togglePolling() { _polling = !_polling; var btn = document.getElementById('pollToggle'); var ps = document.getElementById('pollStatus'); if (_polling) { startPolling(); if (btn) btn.innerHTML = 'Pause'; if (ps) ps.innerHTML = 'Polling every 3s'; } else { if (_pollInterval) clearInterval(_pollInterval); _pollInterval = null; if (btn) btn.innerHTML = 'Resume'; if (ps) ps.innerHTML = '● Paused'; } } // ── Initialize ── initCharts(); updateAlerts(_monData); startPolling(); // ── Theme observer — update Chart.js on theme toggle ── new MutationObserver(function() { Chart.defaults.color = textColor(); Object.keys(charts).forEach(function(k) { var ch = charts[k]; if (ch && ch.options && ch.options.scales) { if (ch.options.scales.x) ch.options.scales.x.grid.color = gridColor(); if (ch.options.scales.y) ch.options.scales.y.grid.color = gridColor(); if (ch.options.scales.x) ch.options.scales.x.ticks.color = textColor(); if (ch.options.scales.y) ch.options.scales.y.ticks.color = textColor(); } ch.update('none'); }); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); // ── Cleanup ── window.addEventListener('beforeunload', function() { if (_pollInterval) clearInterval(_pollInterval); }); // ── Prometheus Text Format Highlighter ────────────────────────── (function() { 'use strict'; function promEsc(s) { return s.replace(/&/g,'&').replace(//g,'>'); } function highlightPromLine(line) { var trimmed = line.trim(); // Comment / HELP / TYPE lines if (trimmed.startsWith('#')) { var m = trimmed.match(/^(#\s*)(HELP|TYPE)(\s+)(\S+)(\s+.*)?$/); if (m) { return '' + promEsc(m[1]) + '' + '' + promEsc(m[2]) + '' + promEsc(m[3]) + '' + promEsc(m[4]) + '' + '' + promEsc(m[5] || '') + ''; } return '' + promEsc(line) + ''; } if (!trimmed) return ''; // Metric line: metric_name{labels} value [timestamp] var metricRe = /^([a-zA-Z_:][a-zA-Z0-9_:]*)(\{[^}]*\})?(\s+)([+\-]?[0-9eE.+\-]+(?:Inf|NaN)?)(\s+\d+)?(.*)$/; var mm = metricRe.exec(trimmed); if (mm) { var name = '' + promEsc(mm[1]) + ''; var lbls = mm[2] ? highlightPromLabels(mm[2]) : ''; var sp = promEsc(mm[3]); var val = '' + promEsc(mm[4]) + ''; var ts = mm[5] ? '' + promEsc(mm[5]) + '' : ''; var rest = mm[6] ? '' + promEsc(mm[6]) + '' : ''; return name + lbls + sp + val + ts + rest; } return promEsc(line); } function highlightPromLabels(lblStr) { // lblStr = "{key=\"val\",key2=\"val2\"}" var inner = lblStr.slice(1, -1); var highlighted = inner.replace(/([a-zA-Z_][a-zA-Z0-9_]*)(\s*=\s*)("(?:[^"\\]|\\.)*")/g, function(_, k, eq, v) { return '' + promEsc(k) + '' + '' + promEsc(eq) + '' + '' + promEsc(v) + ''; }); return '{' + highlighted + '}'; } window._renderPromMetrics = function(text) { var lines = text.split('\n'); return lines.map(highlightPromLine).join('\n'); }; })(); function fetchPrometheusMetrics(btn) { var out = document.getElementById('prom-output'); if (!out) return; if (btn) { btn.disabled = true; btn.innerHTML = 'Loading…'; } fetch(_urlPrefix + '/monitoring/api/?sections=prometheus') .then(function(r) { return r.ok ? r.text() : Promise.reject('HTTP ' + r.status); }) .then(function(text) { try { var j = JSON.parse(text); text = j.prometheus || j.metrics_text || JSON.stringify(j, null, 2); } catch(e) {} out.innerHTML = window._renderPromMetrics ? window._renderPromMetrics(text) : '' + text + ''; if (btn) { btn.disabled = false; btn.innerHTML = 'Refresh'; } }) .catch(function(err) { // Fallback: build metrics from _monData var d = _monData; var lines = [ '# HELP process_cpu_percent CPU usage percentage', '# TYPE process_cpu_percent gauge', 'process_cpu_percent ' + (d.cpu.percent || 0), '# HELP process_memory_rss_bytes Resident set size in bytes', '# TYPE process_memory_rss_bytes gauge', 'process_memory_rss_bytes ' + (d.process.rss || 0), '# HELP system_memory_percent System memory usage percentage', '# TYPE system_memory_percent gauge', 'system_memory_percent ' + (d.memory.percent || 0), '# HELP system_disk_percent Disk usage percentage', '# TYPE system_disk_percent gauge', 'system_disk_percent ' + (d.disk.percent || 0), '# HELP process_threads_total Number of threads', '# TYPE process_threads_total gauge', 'process_threads_total{pid="' + (d.process.pid || 0) + '"} ' + (d.process.threads || 0), '# HELP python_gc_objects_total GC tracked objects', '# TYPE python_gc_objects_total gauge', 'python_gc_objects_total ' + (d.python.gc_objects || 0), ]; out.innerHTML = window._renderPromMetrics ? window._renderPromMetrics(lines.join('\n')) : lines.join('\n'); if (btn) { btn.disabled = false; btn.innerHTML = 'Refresh'; } }); } {% endblock %}