feat: add Docker and Gitea services, monitoring, queue, and Telegram notification functionalities
- 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.
This commit is contained in:
184
dashboard/js/pages/monitoring.js
Normal file
184
dashboard/js/pages/monitoring.js
Normal file
@@ -0,0 +1,184 @@
|
||||
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();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user