Change : dashboard , containers

This commit is contained in:
2026-06-12 21:49:47 +01:00
parent df99603d9e
commit c3e3a3b28c
5 changed files with 480 additions and 190 deletions

View File

@@ -4,6 +4,8 @@ let manualBackupJobId = null;
let manualBackupPoll = null;
let currentJobId = null;
let pollInterval = null;
let allContainersCache = [];
let allContainersStats = {};
function el(id) { return document.getElementById(id); }
@@ -204,12 +206,8 @@ function updateContainerStatusBadge(name, status) {
document.querySelectorAll(`.ctr-status-cell[data-ctr="${name}"]`).forEach((cell) => {
cell.innerHTML = statusBadgeHTML(status);
});
document.querySelectorAll('#all-containers-body tr').forEach((row) => {
const nameTd = row.querySelector('.ct-name');
if (nameTd && nameTd.textContent.trim() === name) {
const statusTd = row.cells[2];
if (statusTd) statusTd.innerHTML = statusBadgeHTML(status);
}
document.querySelectorAll(`#all-containers-body tr[data-ctr="${name}"] .ctr-status-cell`).forEach((cell) => {
cell.innerHTML = statusBadgeHTML(status);
});
}
@@ -248,44 +246,129 @@ function buildActionBtns(name) {
</div>`;
}
function parseCpuPct(cpuStr) {
if (!cpuStr || cpuStr === '—') return 0;
return parseFloat(String(cpuStr).replace('%', '')) || 0;
}
function parseCreatedTs(createdAt) {
if (!createdAt) return 0;
const t = Date.parse(createdAt);
return Number.isNaN(t) ? 0 : t;
}
function parseUptimeSeconds(uptimeText) {
if (!uptimeText || !uptimeText.startsWith('Up')) return 0;
const text = uptimeText.replace(/^Up\s+/i, '').toLowerCase();
let sec = 0;
const dayM = text.match(/(\d+)\s*day/);
const hrM = text.match(/(\d+)\s*hour/);
const minM = text.match(/(\d+)\s*minute/);
const secM = text.match(/(\d+)\s*second/);
if (dayM) sec += parseInt(dayM[1], 10) * 86400;
if (hrM) sec += parseInt(hrM[1], 10) * 3600;
if (minM) sec += parseInt(minM[1], 10) * 60;
if (secM) sec += parseInt(secM[1], 10);
return sec;
}
function filterAndSortContainers(containers, stats, query, sortBy) {
let list = [...containers];
const q = (query || '').trim().toLowerCase();
if (q) {
list = list.filter((c) =>
(c.name || '').toLowerCase().includes(q) ||
(c.image || '').toLowerCase().includes(q) ||
(c.owner || '').toLowerCase().includes(q) ||
(c.ports || '').toLowerCase().includes(q) ||
(c.uptime || '').toLowerCase().includes(q)
);
}
list.sort((a, b) => {
switch (sortBy) {
case 'cpu_desc':
return parseCpuPct((stats[b.name] || {}).cpu) - parseCpuPct((stats[a.name] || {}).cpu);
case 'cpu_asc':
return parseCpuPct((stats[a.name] || {}).cpu) - parseCpuPct((stats[b.name] || {}).cpu);
case 'created_desc':
return parseCreatedTs(b.created_at) - parseCreatedTs(a.created_at);
case 'created_asc':
return parseCreatedTs(a.created_at) - parseCreatedTs(b.created_at);
case 'uptime_desc':
return parseUptimeSeconds(b.uptime) - parseUptimeSeconds(a.uptime);
case 'name':
default:
return (a.name || '').localeCompare(b.name || '');
}
});
return list;
}
function renderAllContainersRows(containers, stats) {
const body = el('all-containers-body');
if (!body) return;
const filterMeta = el('ctr-filter-meta');
const total = allContainersCache.length;
if (filterMeta) {
filterMeta.textContent = containers.length === total
? `${total} shown`
: `${containers.length} of ${total} shown`;
}
if (!containers.length) {
body.innerHTML = `<tr><td colspan="11"><div class="empty-state"><i class="fas fa-inbox"></i>${total ? 'No matches' : 'No containers'}</div></td></tr>`;
return;
}
const showExtra = extraState.all;
const ed = showExtra ? '' : 'display:none;';
body.innerHTML = containers.map((c) => {
const up = c.running || (c.status && c.status.includes('Up'));
const s = stats[c.name] || {};
const pct = parseFloat(s.mem_pct) || 0;
const cls = pct > 85 ? 'crit' : pct > 65 ? 'warn' : '';
const uptimeCls = up ? 'ct-uptime' : 'ct-uptime stopped';
const uptimeText = c.uptime || (up ? c.status : c.status || '—');
return `<tr data-ctr="${escapeHtml(c.name)}">
<td class="ct-name">${escapeHtml(c.name)}</td>
<td class="ct-owner">${escapeHtml(c.owner || 'root')}</td>
<td class="ctr-status-cell" data-ctr="${escapeHtml(c.name)}">${up ? '<span class="badge badge-run">Running</span>' : '<span class="badge badge-stop">Stopped</span>'}</td>
<td><span class="${uptimeCls}">${escapeHtml(uptimeText)}</span></td>
<td><span class="stat-pct">${escapeHtml(s.cpu || '—')}</span></td>
<td><div class="stat-bar-wrap"><div class="stat-bar-bg"><div class="stat-bar-fill ${cls}" style="width:${Math.min(pct, 100)}%"></div></div><span class="stat-pct">${escapeHtml(s.mem_pct || '—')}</span></div></td>
<td><span class="stat-pct" style="color:var(--cyan)">${escapeHtml(s.net || '—')}</span></td>
<td class="col-extra all-extra" style="${ed}"><span class="stat-pct" style="color:var(--yellow)">${escapeHtml(s.block || '—')}</span></td>
<td class="col-extra all-extra ct-image" style="${ed}">${escapeHtml(c.image)}</td>
<td class="col-extra all-extra ct-ports" style="${ed}">${escapeHtml(c.ports || '—')}</td>
<td>${buildActionBtns(c.name)}</td></tr>`;
}).join('');
}
function applyContainersFilter() {
const query = el('ctr-search')?.value || '';
const sortBy = el('ctr-sort')?.value || 'name';
const filtered = filterAndSortContainers(allContainersCache, allContainersStats, query, sortBy);
renderAllContainersRows(filtered, allContainersStats);
}
async function loadAllContainers() {
const body = el('all-containers-body');
if (!body) return;
const meta = el('all-ctr-meta');
body.innerHTML = '<tr><td colspan="10"><div class="empty-state"><i class="fas fa-spinner fa-spin"></i> Loading…</div></td></tr>';
body.innerHTML = '<tr><td colspan="11"><div class="empty-state"><i class="fas fa-spinner fa-spin"></i> Loading…</div></td></tr>';
try {
const [ctrRes, statRes] = await Promise.all([fetch('/api/containers/all'), fetch('/api/stats')]);
const { containers, running } = await ctrRes.json();
const stats = await statRes.json();
if (meta) meta.textContent = `${containers.length} total · ${running} running`;
if (el('nav-badge-containers')) el('nav-badge-containers').textContent = containers.length;
if (!containers.length) {
body.innerHTML = '<tr><td colspan="10"><div class="empty-state"><i class="fas fa-inbox"></i>No containers</div></td></tr>';
return;
}
const showExtra = extraState.all;
body.innerHTML = containers.map((c) => {
const up = c.status.includes('Up');
const s = stats[c.name] || {};
const pct = parseFloat(s.mem_pct) || 0;
const cls = pct > 85 ? 'crit' : pct > 65 ? 'warn' : '';
const ed = showExtra ? '' : 'display:none;';
return `<tr>
<td class="ct-name">${c.name}</td><td class="ct-owner">${c.owner}</td>
<td>${up ? '<span class="badge badge-run">Running</span>' : '<span class="badge badge-stop">Stopped</span>'}</td>
<td><span class="stat-pct">${s.cpu || '—'}</span></td>
<td><div class="stat-bar-wrap"><div class="stat-bar-bg"><div class="stat-bar-fill ${cls}" style="width:${Math.min(pct,100)}%"></div></div><span class="stat-pct">${s.mem_pct || '—'}</span></div></td>
<td><span class="stat-pct" style="color:var(--cyan)">${s.net || '—'}</span></td>
<td class="col-extra all-extra" style="${ed}"><span class="stat-pct" style="color:var(--yellow)">${s.block || '—'}</span></td>
<td class="col-extra all-extra ct-image" style="${ed}">${c.image}</td>
<td class="col-extra all-extra ct-ports" style="${ed}">${c.ports || '—'}</td>
<td>${buildActionBtns(c.name)}</td></tr>`;
}).join('');
allContainersCache = containers || [];
allContainersStats = await statRes.json();
if (meta) meta.textContent = `${allContainersCache.length} total · ${running} running`;
if (el('nav-badge-containers')) el('nav-badge-containers').textContent = allContainersCache.length;
applyContainersFilter();
} catch (e) {
body.innerHTML = `<tr><td colspan="10"><div class="empty-state">${e}</div></td></tr>`;
body.innerHTML = `<tr><td colspan="11"><div class="empty-state">${escapeHtml(String(e))}</div></td></tr>`;
}
}
window.applyContainersFilter = applyContainersFilter;
async function refreshBackupsList() {
try {
const r = await fetch('/api/backups');