- Implemented Docker operations including image building, container management, and resource stats. - Added Gitea API client for repository management and webhook handling. - Introduced monitoring service to collect and store container metrics in InfluxDB. - Created a queue system using BullMQ for managing deployment jobs with real-time log streaming. - Developed Telegram notification service for deployment status updates. - Added Traefik label generation for dynamic reverse proxy configuration. - Implemented WebSocket endpoints for log streaming and terminal access to containers. - Created an updater sidecar for self-updating the AutoDeployer container.
185 lines
7.4 KiB
JavaScript
185 lines
7.4 KiB
JavaScript
import { monitoring as monitoringApi, services as servicesApi } from '../api.js';
|
|
import { icons } from '../icons.js';
|
|
|
|
const RANGES = [
|
|
{ value: '-1h', label: '1 ora' },
|
|
{ value: '-6h', label: '6 ore' },
|
|
{ value: '-24h', label: '24 ore' },
|
|
{ value: '-7d', label: '7 giorni' },
|
|
];
|
|
|
|
const METRICS = [
|
|
{ value: 'cpu_percent', label: 'CPU %', color: '#6366f1' },
|
|
{ value: 'memory_percent', label: 'RAM %', color: '#8b5cf6' },
|
|
{ value: 'network_rx', label: 'Network RX', color: '#10b981' },
|
|
{ value: 'network_tx', label: 'Network TX', color: '#f59e0b' },
|
|
];
|
|
|
|
export function renderMonitoring(container) {
|
|
let stats = {};
|
|
let selectedService = null;
|
|
let chartRange = '-1h';
|
|
let chartMetric = 'cpu_percent';
|
|
let chart = null;
|
|
let interval;
|
|
|
|
container.innerHTML = `
|
|
<div class="page-header"><div><h2>Monitoring</h2><p>Risorse in tempo reale di tutti i servizi attivi</p></div></div>
|
|
<div class="monitoring-grid mb-4" id="stats-grid"></div>
|
|
<div id="empty-msg" class="hidden"></div>
|
|
<div id="chart-section" class="hidden"></div>`;
|
|
|
|
async function loadInitial() {
|
|
try {
|
|
const st = await monitoringApi.realtime();
|
|
stats = st;
|
|
renderStats();
|
|
} catch {}
|
|
}
|
|
|
|
async function pollStats() {
|
|
try { stats = await monitoringApi.realtime(); renderStats(); } catch {}
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
if (!bytes) return '0 B';
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
|
}
|
|
|
|
function renderStats() {
|
|
const grid = document.getElementById('stats-grid');
|
|
const entries = Object.entries(stats);
|
|
|
|
if (entries.length === 0) {
|
|
grid.innerHTML = '';
|
|
document.getElementById('empty-msg').innerHTML = `<div class="empty-state">${icons.activity(48)}<h3 class="mt-4">Nessun servizio attivo</h3><p>I dati di monitoring saranno visibili quando almeno un servizio sarà in esecuzione</p></div>`;
|
|
document.getElementById('empty-msg').classList.remove('hidden');
|
|
return;
|
|
}
|
|
document.getElementById('empty-msg').classList.add('hidden');
|
|
|
|
grid.innerHTML = entries.map(([name, s]) => `
|
|
<div class="card metric-card" data-svc="${name}" style="cursor:pointer;${selectedService === name ? 'border-color:var(--accent)' : ''}">
|
|
<div class="card-header" style="margin-bottom:8px">
|
|
<span class="card-title">${name}</span>
|
|
<span class="status-badge running">running</span>
|
|
</div>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
|
<div>
|
|
<div class="flex items-center gap-2 text-muted text-xs mb-1">${icons.cpu(12)} CPU</div>
|
|
<div class="metric-value" style="font-size:1.4rem">${s?.cpu_percent?.toFixed(1) || '0'}%</div>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center gap-2 text-muted text-xs mb-1">${icons.hardDrive(12)} RAM</div>
|
|
<div class="metric-value" style="font-size:1.4rem">${formatBytes(s?.memory_usage)}</div>
|
|
<div class="text-xs text-muted">/ ${formatBytes(s?.memory_limit)}</div>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center gap-2 text-muted text-xs mb-1">${icons.wifi(12)} Net RX</div>
|
|
<div class="text-sm font-bold">${formatBytes(s?.network_rx)}</div>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center gap-2 text-muted text-xs mb-1">${icons.wifi(12)} Net TX</div>
|
|
<div class="text-sm font-bold">${formatBytes(s?.network_tx)}</div>
|
|
</div>
|
|
</div>
|
|
</div>`).join('');
|
|
|
|
grid.querySelectorAll('.metric-card').forEach(card => {
|
|
card.onclick = () => {
|
|
selectedService = card.dataset.svc;
|
|
renderStats();
|
|
renderChart();
|
|
};
|
|
});
|
|
}
|
|
|
|
async function renderChart() {
|
|
if (!selectedService) { document.getElementById('chart-section').classList.add('hidden'); return; }
|
|
const section = document.getElementById('chart-section');
|
|
section.classList.remove('hidden');
|
|
|
|
section.innerHTML = `
|
|
<div class="card animate-slide-in">
|
|
<div class="card-header">
|
|
<h3 class="card-title">📊 ${selectedService} — Storico</h3>
|
|
<div class="flex gap-2" id="range-btns">${RANGES.map(r => `<button class="btn btn-sm ${chartRange === r.value ? 'btn-primary' : 'btn-secondary'}" data-range="${r.value}">${r.label}</button>`).join('')}</div>
|
|
</div>
|
|
<div class="flex gap-2 mb-4" id="metric-btns">${METRICS.map(m => `<button class="btn btn-sm ${chartMetric === m.value ? 'btn-primary' : 'btn-secondary'}" data-metric="${m.value}" ${chartMetric === m.value ? `style="background:${m.color};border-color:${m.color}"` : ''}>${m.label}</button>`).join('')}</div>
|
|
<div id="chart-area" style="width:100%;height:300px">
|
|
<canvas id="monitoring-canvas"></canvas>
|
|
</div>
|
|
</div>`;
|
|
|
|
section.querySelectorAll('[data-range]').forEach(b => {
|
|
b.onclick = () => { chartRange = b.dataset.range; renderChart(); };
|
|
});
|
|
section.querySelectorAll('[data-metric]').forEach(b => {
|
|
b.onclick = () => { chartMetric = b.dataset.metric; renderChart(); };
|
|
});
|
|
|
|
await loadChartData();
|
|
}
|
|
|
|
async function loadChartData() {
|
|
try {
|
|
const result = await monitoringApi.history(selectedService, chartRange, chartMetric);
|
|
const labels = result.map(d => new Date(d.time).toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }));
|
|
const values = result.map(d => Math.round(d.value * 100) / 100);
|
|
const currentMetric = METRICS.find(m => m.value === chartMetric);
|
|
|
|
if (chart) chart.destroy();
|
|
const canvas = document.getElementById('monitoring-canvas');
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Use Chart.js if available
|
|
if (typeof Chart !== 'undefined') {
|
|
chart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels,
|
|
datasets: [{
|
|
data: values,
|
|
borderColor: currentMetric.color,
|
|
backgroundColor: currentMetric.color + '30',
|
|
fill: true,
|
|
tension: 0.3,
|
|
pointRadius: 0,
|
|
pointHitRadius: 10,
|
|
borderWidth: 2,
|
|
}],
|
|
},
|
|
options: {
|
|
responsive: true, maintainAspectRatio: false,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
x: { ticks: { color: '#6b7280', font: { size: 11 } }, grid: { color: 'rgba(255,255,255,0.05)' } },
|
|
y: {
|
|
min: chartMetric.includes('percent') ? 0 : undefined,
|
|
max: chartMetric.includes('percent') ? 100 : undefined,
|
|
ticks: { color: '#6b7280', font: { size: 11 } },
|
|
grid: { color: 'rgba(255,255,255,0.05)' },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
} else {
|
|
document.getElementById('chart-area').innerHTML = `<div class="empty-state" style="padding:40px"><p class="text-muted">Chart.js non caricato. Aggiungi chart.min.js nella cartella lib/</p></div>`;
|
|
}
|
|
} catch (err) {
|
|
document.getElementById('chart-area').innerHTML = `<div class="empty-state" style="padding:40px"><p class="text-muted">Nessun dato disponibile</p></div>`;
|
|
}
|
|
}
|
|
|
|
loadInitial();
|
|
interval = setInterval(pollStats, 5000);
|
|
|
|
return () => {
|
|
clearInterval(interval);
|
|
if (chart) chart.destroy();
|
|
};
|
|
}
|