From c3e3a3b28c6ce014265e21d25480984046efd233 Mon Sep 17 00:00:00 2001 From: ameniboukattaya Date: Fri, 12 Jun 2026 21:49:47 +0100 Subject: [PATCH] Change : dashboard , containers --- platform/app.py | 29 +- platform/modules/backups.py | 37 ++- platform/static/js/platform.js | 149 +++++++-- platform/templates/pages/containers.html | 88 +++++- platform/templates/pages/dashboard.html | 367 ++++++++++++++--------- 5 files changed, 480 insertions(+), 190 deletions(-) diff --git a/platform/app.py b/platform/app.py index 6bf6d08..7034b7a 100644 --- a/platform/app.py +++ b/platform/app.py @@ -91,19 +91,23 @@ def dashboard(): vm_backups = get_vm_backups() if RUNNING_ON_MAIN_SERVER: - containers = get_containers() - running_count = sum(1 for c in containers if 'Up' in c.get('status', '')) + root_ctrs = get_all_root_containers() + user_ctrs = get_rootless_user_containers_remote() + all_ctrs = root_ctrs + user_ctrs + running_count = sum(1 for c in all_ctrs if c.get('running')) system = get_system_info() users = get_all_users() + container_count = len(all_ctrs) else: - containers = [] running_count = 0 system = {} users = [] + container_count = 0 return render_template('pages/dashboard.html', - containers=containers, + container_count=container_count, running_count=running_count, + site_count=len(SITES), backups=backups, vm_backups=vm_backups, main_server=MAIN_SERVER_IP, @@ -295,15 +299,18 @@ def api_dashboard(): user_ctrs = get_rootless_user_containers_remote() all_ctrs = root_ctrs + user_ctrs users = get_all_users() - running = sum(1 for c in all_ctrs if 'Up' in c.get('status', '')) + running = sum(1 for c in all_ctrs if c.get('running')) + stopped = len(all_ctrs) - running return jsonify({ - 'system': system, - 'containers': all_ctrs, - 'running_count': running, - 'user_count': len(users), - 'local_backups': len(get_local_backups()), - 'vm_backups': len(get_vm_backups()), + 'system': system, + 'container_count': len(all_ctrs), + 'running_count': running, + 'stopped_count': stopped, + 'sites_count': len(SITES), + 'user_count': len(users), + 'local_backups': len(get_local_backups()), + 'vm_backups': len(get_vm_backups()), }) diff --git a/platform/modules/backups.py b/platform/modules/backups.py index 3e2d3bb..4293b46 100644 --- a/platform/modules/backups.py +++ b/platform/modules/backups.py @@ -446,6 +446,18 @@ def get_backup_script_path(): # CONTAINERS # ──────────────────────────────────────────────────────────────── +def _parse_uptime(status): + """Human uptime snippet from docker ps Status field.""" + if not status: + return '—' + if status.startswith('Up'): + m = re.match(r'Up\s+([^(]+)', status) + return ('Up ' + m.group(1).strip()) if m else status.split('(')[0].strip() + if status.startswith('Exited'): + return status.split('(')[0].strip() or status + return status + + def _parse_containers(raw, owner='root'): containers = [] if raw: @@ -453,19 +465,28 @@ def _parse_containers(raw, owner='root'): if '|' not in line: continue parts = line.split('|') + status = parts[1].strip() if len(parts) > 1 else '' containers.append({ - 'name': parts[0].strip(), - 'status': parts[1].strip() if len(parts) > 1 else '', - 'image': parts[2].strip() if len(parts) > 2 else '', - 'ports': parts[3].strip() if len(parts) > 3 else '', - 'owner': owner, + 'name': parts[0].strip(), + 'status': status, + 'image': parts[2].strip() if len(parts) > 2 else '', + 'ports': parts[3].strip() if len(parts) > 3 else '', + 'created_at': parts[4].strip() if len(parts) > 4 else '', + 'uptime': _parse_uptime(status), + 'running': status.startswith('Up'), + 'owner': owner, }) return containers +_DOCKER_PS_FMT = ( + "{{.Names}}|{{.Status}}|{{.Image}}|{{.Ports}}|{{.CreatedAt}}" +) + + def get_containers(): stdout, _ = _ssh_main( - "docker ps -a --format '{{.Names}}|{{.Status}}|{{.Image}}|{{.Ports}}' 2>/dev/null | " + f"docker ps -a --format '{_DOCKER_PS_FMT}' 2>/dev/null | " "grep -E 'frappe|nextcloud|mautic|n8n|odoo'" ) return _parse_containers(stdout) @@ -473,7 +494,7 @@ def get_containers(): def get_all_root_containers(): stdout, _ = _ssh_main( - "docker ps -a --format '{{.Names}}|{{.Status}}|{{.Image}}|{{.Ports}}' 2>/dev/null" + f"docker ps -a --format '{_DOCKER_PS_FMT}' 2>/dev/null" ) return _parse_containers(stdout) @@ -496,7 +517,7 @@ def get_rootless_user_containers_remote(): username = name_out.strip() or f"uid{uid}" ctr_out, _ = _ssh_main( f"DOCKER_HOST=unix://{sock_path} " - f"docker ps -a --format '{{{{.Names}}}}|{{{{.Status}}}}|{{{{.Image}}}}|{{{{.Ports}}}}' 2>/dev/null" + f"docker ps -a --format '{_DOCKER_PS_FMT}' 2>/dev/null" ) containers.extend(_parse_containers(ctr_out, owner=username)) return containers diff --git a/platform/static/js/platform.js b/platform/static/js/platform.js index 4a18cf7..7c11589 100644 --- a/platform/static/js/platform.js +++ b/platform/static/js/platform.js @@ -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) { `; } +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 = `
${total ? 'No matches' : 'No containers'}
`; + 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 ` + ${escapeHtml(c.name)} + ${escapeHtml(c.owner || 'root')} + ${up ? 'Running' : 'Stopped'} + ${escapeHtml(uptimeText)} + ${escapeHtml(s.cpu || '—')} +
${escapeHtml(s.mem_pct || '—')}
+ ${escapeHtml(s.net || '—')} + ${escapeHtml(s.block || '—')} + ${escapeHtml(c.image)} + ${escapeHtml(c.ports || '—')} + ${buildActionBtns(c.name)}`; + }).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 = '
Loading…
'; + body.innerHTML = '
Loading…
'; 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 = '
No containers
'; - 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 ` - ${c.name}${c.owner} - ${up ? 'Running' : 'Stopped'} - ${s.cpu || '—'} -
${s.mem_pct || '—'}
- ${s.net || '—'} - ${s.block || '—'} - ${c.image} - ${c.ports || '—'} - ${buildActionBtns(c.name)}`; - }).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 = `
${e}
`; + body.innerHTML = `
${escapeHtml(String(e))}
`; } } +window.applyContainersFilter = applyContainersFilter; + async function refreshBackupsList() { try { const r = await fetch('/api/backups'); diff --git a/platform/templates/pages/containers.html b/platform/templates/pages/containers.html index 7838735..3b10b50 100644 --- a/platform/templates/pages/containers.html +++ b/platform/templates/pages/containers.html @@ -1,5 +1,71 @@ {% extends "base.html" %} {% block content %} + + +
All Containers
@@ -11,6 +77,25 @@
+ +
+
+ + +
+ + +
+
@@ -18,6 +103,7 @@ + @@ -28,7 +114,7 @@ - +
NAME OWNER STATUSUP TIME CPU MEMORY NET I/O
Loading…
Loading…
diff --git a/platform/templates/pages/dashboard.html b/platform/templates/pages/dashboard.html index f5cfe81..7712cac 100644 --- a/platform/templates/pages/dashboard.html +++ b/platform/templates/pages/dashboard.html @@ -1,16 +1,68 @@ {% extends "base.html" %} {% block content %} -{% macro ctr_actions(name) %} - - - -{% endmacro %} + +
@@ -41,8 +93,10 @@ Docker {{ system.docker_v or '…' }} · {{ main_server }}
-
{{ containers|length }}
App Containers
-
{{ running_count }}
Running
+
{{ container_count }}
All Containers
+
{{ running_count }}
Running
+
{{ container_count - running_count }}
Stopped
+
{{ site_count }}
App Sites
{{ users|length }}
Linux Users
{{ backups|length }}
Local Backups
{{ vm_backups|length }}
VM Backups
@@ -53,157 +107,196 @@
-
-
-
App Containers
-
- Auto-refresh 15s - +
+
+

Containers

+
+
+
-
- - - - - - - - - - - - - - - - {% for c in containers %} - - - - - - - - - - - - {% else %} - - {% endfor %} - -
NAMESTATUSCPUMEMORYNET I/OACTIONS
{{ c.name }} - {% if 'Up' in c.status %} - Running - {% else %} - Stopped - {% endif %} - -
-
- -
-
{{ ctr_actions(c.name) }}
No containers
+
+

Application Sites

+
+
+ +
+
+

Backups & Users

+
+
+
+ -{% endblock %} \ No newline at end of file +{% endblock %}