Change : dashboard , containers
This commit is contained in:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user