- 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.
145 lines
7.2 KiB
JavaScript
145 lines
7.2 KiB
JavaScript
import { services as api } from '../api.js';
|
||
import { icons } from '../icons.js';
|
||
import { navigate } from '../router.js';
|
||
|
||
function formatTime(dateStr) {
|
||
if (!dateStr) return '–';
|
||
const d = new Date(dateStr), diff = Math.floor((Date.now() - d) / 60000);
|
||
if (diff < 1) return 'ora';
|
||
if (diff < 60) return `${diff}m fa`;
|
||
const h = Math.floor(diff / 60);
|
||
if (h < 24) return `${h}h fa`;
|
||
return `${Math.floor(h / 24)}g fa`;
|
||
}
|
||
|
||
function serviceCardHTML(s) {
|
||
const status = s.container?.status || s.status || 'stopped';
|
||
const repo = s.gitea_repo_url?.replace(/https?:\/\/[^/]+\//, '').replace('.git', '') || '';
|
||
const ld = s.last_deploy;
|
||
return `
|
||
<div class="card service-card" data-id="${s.id}">
|
||
<div class="service-card-header">
|
||
<div>
|
||
<div class="service-card-name">${s.name} <span class="status-badge ${status}">${status}</span></div>
|
||
${s.description ? `<p class="text-xs text-muted mt-2" style="max-width:260px">${s.description}</p>` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="service-card-repo">${icons.gitBranch(12)} <span class="truncate">${repo}</span> <span style="color:var(--accent)">:${s.gitea_branch}</span></div>
|
||
${s.traefik_domain ? `<div class="service-card-repo mt-2">${icons.globe(12)} <span>${s.traefik_domain}</span></div>` : ''}
|
||
<div class="service-card-meta">
|
||
<div class="service-card-meta-item">${icons.clock(12)} ${ld ? `${ld.status === 'success' ? '✅' : ld.status === 'failed' ? '❌' : '⏳'} ${formatTime(ld.created_at)}` : 'Mai deployato'}</div>
|
||
${ld?.commit_sha ? `<div class="service-card-meta-item text-mono">${ld.commit_sha}</div>` : ''}
|
||
</div>
|
||
<div class="service-card-actions">
|
||
<button class="btn btn-primary btn-sm btn-deploy" data-id="${s.id}" ${status === 'building' ? 'disabled' : ''}>${icons.play(14)} Deploy</button>
|
||
${status === 'running' || status === 'error' ? `<button class="btn btn-danger btn-sm btn-stop" data-id="${s.id}">${icons.square(14)} Stop</button>` : ''}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
export function renderDashboard(container) {
|
||
let interval;
|
||
container.innerHTML = `
|
||
<div class="page-header">
|
||
<div><h2>Dashboard</h2><p>Gestisci i tuoi servizi</p></div>
|
||
<div class="flex gap-2">
|
||
<button class="btn btn-secondary" id="refresh-btn">${icons.refreshCw(16)} Refresh</button>
|
||
<button class="btn btn-primary" id="create-btn">${icons.plus(16)} Nuovo Servizio</button>
|
||
</div>
|
||
</div>
|
||
<div id="services-grid" class="card-grid"></div>
|
||
<div id="create-modal"></div>`;
|
||
|
||
async function load() {
|
||
const grid = document.getElementById('services-grid');
|
||
try {
|
||
const list = await api.list();
|
||
if (list.length === 0) {
|
||
grid.innerHTML = `<div class="empty-state"><h3>Nessun servizio configurato</h3><p>Crea il tuo primo servizio per iniziare</p></div>`;
|
||
return;
|
||
}
|
||
grid.innerHTML = list.map(serviceCardHTML).join('');
|
||
// Events
|
||
grid.querySelectorAll('.service-card').forEach(el => {
|
||
el.onclick = (e) => { if (!e.target.closest('button')) navigate(`/services/${el.dataset.id}`); };
|
||
});
|
||
grid.querySelectorAll('.btn-deploy').forEach(b => {
|
||
b.onclick = async (e) => { e.stopPropagation(); await api.deploy(b.dataset.id); load(); };
|
||
});
|
||
grid.querySelectorAll('.btn-stop').forEach(b => {
|
||
b.onclick = async (e) => { e.stopPropagation(); await api.stop(b.dataset.id); load(); };
|
||
});
|
||
} catch (err) { grid.innerHTML = `<div class="empty-state"><p>Errore: ${err.message}</p></div>`; }
|
||
}
|
||
|
||
document.getElementById('refresh-btn').onclick = load;
|
||
document.getElementById('create-btn').onclick = () => showCreateModal(load);
|
||
load();
|
||
interval = setInterval(load, 10000);
|
||
|
||
return () => clearInterval(interval);
|
||
}
|
||
|
||
function showCreateModal(onCreated) {
|
||
const modal = document.getElementById('create-modal');
|
||
modal.innerHTML = `
|
||
<div class="modal-overlay" id="modal-overlay">
|
||
<div class="modal" style="max-width:700px" onclick="event.stopPropagation()">
|
||
<div class="modal-header"><h3 class="modal-title">Nuovo Servizio</h3><button class="btn btn-secondary btn-sm" id="modal-close">✕</button></div>
|
||
<div id="modal-error" class="login-error hidden"></div>
|
||
<form id="create-form">
|
||
<div class="form-row">
|
||
<div class="form-group"><label class="form-label">Nome Servizio</label><input class="form-input" id="cf-name" placeholder="api-service" required></div>
|
||
<div class="form-group"><label class="form-label">Container Name</label><input class="form-input mono" id="cf-container" placeholder="api-service" required></div>
|
||
</div>
|
||
<div class="form-group"><label class="form-label">Descrizione</label><input class="form-input" id="cf-desc" placeholder="Opzionale"></div>
|
||
<div class="form-row">
|
||
<div class="form-group"><label class="form-label">Gitea Repository URL</label><input class="form-input mono" id="cf-repo" value="http://gitea:3000/" required></div>
|
||
<div class="form-group"><label class="form-label">Branch</label><input class="form-input mono" id="cf-branch" value="main"></div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group"><label class="form-label">Container Port</label><input type="number" class="form-input" id="cf-port" value="3000"></div>
|
||
<div class="form-group"><label class="form-label">Dominio Traefik</label><input class="form-input mono" id="cf-domain" placeholder="api.mebboat.it"></div>
|
||
</div>
|
||
<div class="flex gap-2 mt-4" style="justify-content:flex-end">
|
||
<button type="button" class="btn btn-secondary" id="modal-cancel">Annulla</button>
|
||
<button type="submit" class="btn btn-primary" id="modal-submit">Crea Servizio</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>`;
|
||
|
||
const close = () => { modal.innerHTML = ''; };
|
||
document.getElementById('modal-overlay').onclick = close;
|
||
document.getElementById('modal-close').onclick = close;
|
||
document.getElementById('modal-cancel').onclick = close;
|
||
document.getElementById('cf-name').oninput = (e) => {
|
||
document.getElementById('cf-container').value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
||
};
|
||
|
||
document.getElementById('create-form').onsubmit = async (e) => {
|
||
e.preventDefault();
|
||
const errEl = document.getElementById('modal-error');
|
||
errEl.classList.add('hidden');
|
||
try {
|
||
const s = await api.create({
|
||
name: document.getElementById('cf-name').value,
|
||
container_name: document.getElementById('cf-container').value,
|
||
description: document.getElementById('cf-desc').value,
|
||
gitea_repo_url: document.getElementById('cf-repo').value,
|
||
gitea_branch: document.getElementById('cf-branch').value,
|
||
container_port: parseInt(document.getElementById('cf-port').value),
|
||
traefik_domain: document.getElementById('cf-domain').value,
|
||
traefik_enabled: true, traefik_tls_resolver: 'cloudflare',
|
||
traefik_network: 'meb-public', networks: ['meb-public'],
|
||
});
|
||
close();
|
||
onCreated();
|
||
navigate(`/services/${s.id}`);
|
||
} catch (err) {
|
||
errEl.textContent = err.message;
|
||
errEl.classList.remove('hidden');
|
||
}
|
||
};
|
||
}
|