diff --git a/containers/libreportal/frontend/components/apps/services/js/services-manager.js b/containers/libreportal/frontend/components/apps/services/js/services-manager.js index 672154c..7194a97 100644 --- a/containers/libreportal/frontend/components/apps/services/js/services-manager.js +++ b/containers/libreportal/frontend/components/apps/services/js/services-manager.js @@ -282,8 +282,7 @@ class ServicesManager { _renderRichDetail(info) { if (!info || !info.detail) return ''; const d = info.detail; - const fmt = window.SystemFmt; - if (!fmt) return ''; + const fmt = window.SystemFmt || SVC_FMT; const lim = d.limits || {}; const memLimit = lim.memory || 0; const cpuLimit = lim.nano_cpus @@ -350,7 +349,7 @@ class ServicesManager { const stats = info?.stats; const detail = info?.detail; const memLimit = detail?.limits?.memory || 0; - const fmt = window.SystemFmt; + const fmt = window.SystemFmt || SVC_FMT; const cpuEl = item.querySelector('[data-svc-live="cpu"]'); const memEl = item.querySelector('[data-svc-live="mem"]'); if (cpuEl) cpuEl.textContent = stats ? `${(stats.cpu_percent ?? 0).toFixed(1)}% CPU` : '—'; @@ -411,7 +410,7 @@ class ServicesManager { const stats = info.stats; const detail = info.detail; const memLimit = detail?.limits?.memory || 0; - const fmt = window.SystemFmt; + const fmt = window.SystemFmt || SVC_FMT; const cpuTxt = stats ? `${(stats.cpu_percent ?? 0).toFixed(1)}% CPU` : '—'; const memUsed = stats?.memory?.used ?? 0; const memPct = memLimit > 0 ? (memUsed / memLimit) * 100 : (stats?.memory?.percent ?? 0); @@ -804,6 +803,34 @@ class ServicesManager { // Tiny helpers ---------------------------------------------------------- +// Number/time formatters for the rich detail panel + live stat chips. Mirrors +// the admin System page's window.SystemFmt, but owned here so the Services tab +// renders its advanced detail even when that page module hasn't loaded — it's +// lazy, so visiting /admin/system must NOT be a prerequisite for this tab. +// Prefer window.SystemFmt when present so both surfaces share one implementation. +const SVC_FMT = { + bytes(n) { + n = Number(n) || 0; + const u = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0; + while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; } + return `${n.toFixed(i ? 1 : 0)} ${u[i]}`; + }, + rate(n) { return `${this.bytes(n)}/s`; }, + timeAgo(unixSec) { + if (!unixSec) return ''; + const diff = Math.floor(Date.now() / 1000) - unixSec; + if (diff < 60) return `${diff}s ago`; + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; + return `${Math.floor(diff / 86400)}d ago`; + }, + timeAgoIso(iso) { + if (!iso) return ''; + const t = Math.floor(new Date(iso).getTime() / 1000); + return Number.isFinite(t) && t > 0 ? this.timeAgo(t) : ''; + }, +}; + function escapeHtml(s) { return String(s ?? '') .replace(/&/g, '&')