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:
Giuseppe Raffa
2026-04-13 23:23:18 +02:00
commit 87d698bc5c
48 changed files with 5558 additions and 0 deletions

137
dashboard/js/pages/logs.js Normal file
View File

@@ -0,0 +1,137 @@
import { deploys as deploysApi, createLogSocket } from '../api.js';
import { icons } from '../icons.js';
export function renderLogs(container) {
let deploysList = [];
let selectedDeploy = null;
let logWs = null;
container.innerHTML = `
<div class="page-header"><div><h2>Log Deploy</h2><p>Storico di tutti i deploy con i relativi log</p></div></div>
<div style="display:grid;grid-template-columns:350px 1fr;gap:16px;min-height:500px">
<div class="card" style="overflow:auto;max-height:70vh" id="deploy-list">
<h3 class="card-title mb-4">${icons.scrollText(16)} Deploy Recenti</h3>
<p class="text-muted">Caricamento...</p>
</div>
<div class="card" id="log-panel">
<div class="empty-state">${icons.scrollText(32)}<h3 class="mt-4">Seleziona un deploy</h3><p>Clicca su un deploy nella lista per visualizzarne i log</p></div>
</div>
</div>`;
async function load() {
try {
deploysList = await deploysApi.list();
renderList();
} catch (err) {
document.getElementById('deploy-list').innerHTML = `<p class="text-muted text-sm">Errore: ${err.message}</p>`;
}
}
function renderList() {
const el = document.getElementById('deploy-list');
el.innerHTML = `<h3 class="card-title mb-4">${icons.scrollText(16)} Deploy Recenti</h3>` +
(deploysList.length === 0 ? '<p class="text-muted text-sm">Nessun deploy trovato</p>' :
`<div class="flex flex-col gap-2">${deploysList.map(d => `
<div class="card deploy-item" data-id="${d.id}" style="cursor:pointer;padding:12px 14px;background:${selectedDeploy?.id === d.id ? 'var(--accent-muted)' : 'var(--bg-glass)'};border-color:${selectedDeploy?.id === d.id ? 'rgba(99,102,241,0.3)' : 'var(--border-primary)'}">
<div class="flex items-center justify-between mb-2">
<span class="font-bold text-sm">${d.service_name}</span>
<span class="status-badge ${d.status}">${d.status}</span>
</div>
<div class="flex items-center gap-3 text-xs text-muted">
${d.commit_sha ? `<span class="text-mono">${icons.gitCommit(10)} ${d.commit_sha}</span>` : ''}
<span>${icons.clock(10)} ${new Date(d.created_at).toLocaleString('it-IT')}</span>
</div>
${d.commit_message ? `<p class="text-xs text-muted mt-1 truncate">${esc(d.commit_message)}</p>` : ''}
</div>`).join('')}</div>`);
el.querySelectorAll('.deploy-item').forEach(item => {
item.onclick = () => {
const did = parseInt(item.dataset.id);
selectedDeploy = deploysList.find(d => d.id === did);
renderList();
renderLogPanel();
};
});
}
function renderLogPanel() {
const panel = document.getElementById('log-panel');
if (!selectedDeploy) {
panel.innerHTML = `<div class="empty-state">${icons.scrollText(32)}<h3 class="mt-4">Seleziona un deploy</h3><p>Clicca su un deploy nella lista per visualizzarne i log</p></div>`;
return;
}
if (logWs) { logWs.close(); logWs = null; }
const d = selectedDeploy;
panel.innerHTML = `
<div class="card-header">
<h3 class="card-title">Log: ${d.service_name}</h3>
<span class="status-badge ${d.status}">${d.status}</span>
</div>
<div class="flex gap-4 text-xs text-muted mb-4">
<span>${icons.user(10)} ${d.commit_author || d.trigger}</span>
<span>${icons.gitCommit(10)} ${d.commit_sha || ''}</span>
${d.duration_ms > 0 ? `<span>${icons.clock(10)} ${(d.duration_ms / 1000).toFixed(1)}s</span>` : ''}
</div>
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-muted" id="lv-count">0 righe</span>
<div class="flex gap-2">
<button class="btn btn-secondary btn-sm" id="lv-pause">${icons.pause(12)} Pause</button>
<button class="btn btn-secondary btn-sm" id="lv-download">${icons.download(12)} Download</button>
</div>
</div>
<div class="log-viewer" id="lv-container"><div class="log-line info">In attesa di log...</div></div>`;
let lines = [];
let paused = false;
const lv = panel.querySelector('#lv-container');
logWs = createLogSocket(`deploy:${d.id}`);
logWs.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === 'log' || msg.type === 'info' || msg.type === 'error') {
lines.push(msg);
if (lines.length > 2000) lines = lines.slice(-1500);
if (lv.firstChild?.classList?.contains('info') && lines.length === 1) lv.innerHTML = '';
const div = document.createElement('div');
div.className = `log-line ${msg.type}`;
div.textContent = msg.data;
lv.appendChild(div);
panel.querySelector('#lv-count').textContent = `${lines.length} righe`;
if (!paused) lv.scrollTop = lv.scrollHeight;
}
} catch {}
};
logWs.onerror = () => appendLine('error', 'WebSocket connection error');
logWs.onclose = () => appendLine('info', '— Stream ended —');
function appendLine(type, text) {
lines.push({ type, data: text });
const div = document.createElement('div');
div.className = `log-line ${type}`;
div.textContent = text;
lv.appendChild(div);
if (!paused) lv.scrollTop = lv.scrollHeight;
}
panel.querySelector('#lv-pause').onclick = () => {
paused = !paused;
panel.querySelector('#lv-pause').innerHTML = paused ? `${icons.play(12)} Resume` : `${icons.pause(12)} Pause`;
};
panel.querySelector('#lv-download').onclick = () => {
const blob = new Blob([lines.map(l => l.data).join('\n')], { type: 'text/plain' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `logs-deploy-${d.id}-${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(a.href);
};
}
load();
return () => { if (logWs) logWs.close(); };
}
function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }