update admin
This commit is contained in:
@@ -2931,3 +2931,153 @@ code {
|
||||
.ev-pool-pill.rar-common { background: #c8d2e0; }
|
||||
.ev-pool-pill.rar-rare { background: #7fb6ff; }
|
||||
.ev-pool-pill.rar-legendary { background: #ffcf6b; }
|
||||
|
||||
/* ============================================================
|
||||
Admin redesign (v38) — Sidebar layout + ระบบ CPU/RAM
|
||||
============================================================ */
|
||||
|
||||
/* App shell ภายใน: [ sidebar | content ] */
|
||||
#panel-app {
|
||||
display: grid;
|
||||
grid-template-columns: 256px minmax(0, 1fr);
|
||||
gap: 1.1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* ---- Sidebar (เดิมเป็นแถบแท็บแนวนอน) ---- */
|
||||
#panel-app .admin-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.16rem;
|
||||
margin: 0;
|
||||
padding: 0.55rem 0.5rem 0.7rem;
|
||||
overflow-x: visible;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 88px);
|
||||
position: sticky;
|
||||
top: 12px;
|
||||
background: rgba(8, 11, 20, 0.55);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
scroll-snap-type: none;
|
||||
}
|
||||
#panel-app .admin-tabs::-webkit-scrollbar { width: 7px; height: 7px; }
|
||||
|
||||
/* ช่องค้นหาเมนู (sticky บนสุดของ sidebar) */
|
||||
.admin-nav-search {
|
||||
position: sticky; top: 0; z-index: 3;
|
||||
padding: 0.1rem 0.2rem 0.5rem;
|
||||
background: linear-gradient(180deg, rgba(8,11,20,0.97) 70%, rgba(8,11,20,0));
|
||||
}
|
||||
.admin-nav-search input {
|
||||
width: 100%; box-sizing: border-box; font: inherit; font-size: 0.84rem;
|
||||
padding: 0.5rem 0.7rem; border-radius: 9px;
|
||||
border: 1px solid var(--border); background: rgba(0,0,0,0.28); color: var(--text);
|
||||
}
|
||||
.admin-nav-search input::placeholder { color: var(--muted); }
|
||||
.admin-nav-search input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-dim); }
|
||||
.admin-nav-empty { padding: 0.5rem 0.55rem; color: var(--muted); font-size: 0.8rem; }
|
||||
|
||||
/* แท็บ → แถวเต็มความกว้างแนวตั้ง */
|
||||
#panel-app .admin-tabs .tab {
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-height: 2.45rem;
|
||||
padding: 0.48rem 0.7rem;
|
||||
border-radius: 9px;
|
||||
scroll-snap-align: none;
|
||||
}
|
||||
#panel-app .admin-tabs .tab .tab-desc { display: none; }
|
||||
#panel-app .admin-tabs .tab .tab-label { font-size: 0.85rem; line-height: 1.25; }
|
||||
#panel-app .admin-tabs .tab .tab-heading-row { align-items: center; }
|
||||
#panel-app .admin-tabs .tab.is-active {
|
||||
background: var(--accent-dim);
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: inset 3px 0 0 var(--accent);
|
||||
}
|
||||
|
||||
/* หัวข้อกลุ่ม (separator) — กดยุบ/ขยายได้ */
|
||||
#panel-app .admin-tabs .admin-tab-sep {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
margin: 0.55rem 0 0.1rem;
|
||||
padding: 0.32rem 0.45rem;
|
||||
cursor: pointer;
|
||||
border-radius: 7px;
|
||||
user-select: none;
|
||||
}
|
||||
#panel-app .admin-tabs .admin-tab-sep:hover { background: rgba(255,255,255,0.04); }
|
||||
#panel-app .admin-tabs .admin-tab-sep span { font-size: 0.63rem; }
|
||||
#panel-app .admin-tabs .admin-tab-sep::after { display: none; }
|
||||
#panel-app .admin-tabs .admin-tab-sep::before {
|
||||
content: '▾'; order: 2; margin-left: auto;
|
||||
font-size: 0.58rem; color: var(--muted); transition: transform 0.15s;
|
||||
}
|
||||
#panel-app .admin-tabs .admin-tab-sep.is-collapsed::before { transform: rotate(-90deg); }
|
||||
#panel-app .admin-tabs .tab.is-group-hidden,
|
||||
#panel-app .admin-tabs .tab.is-search-hidden { display: none; }
|
||||
|
||||
/* เนื้อหา */
|
||||
.admin-content { min-width: 0; }
|
||||
.admin-content > .tab-panel { margin-top: 0; }
|
||||
|
||||
.admin-sidebar-backdrop { display: none; }
|
||||
|
||||
/* ---- แถบสถานะ CPU/RAM (บน header) ---- */
|
||||
.admin-sys-stats {
|
||||
display: flex; align-items: center; gap: 0.85rem; margin-left: auto;
|
||||
padding: 0.38rem 0.75rem; border-radius: 12px;
|
||||
border: 1px solid var(--border); background: rgba(0,0,0,0.24);
|
||||
}
|
||||
.admin-sys-stats[hidden] { display: none; }
|
||||
.sysstat { display: flex; flex-direction: column; gap: 0.2rem; min-width: 92px; }
|
||||
.sysstat--up { min-width: auto; gap: 0.12rem; align-items: flex-start; }
|
||||
.sysstat-head { display: flex; justify-content: space-between; gap: 0.5rem; align-items: baseline; }
|
||||
.sysstat-label { font-size: 0.58rem; font-weight: 700; letter-spacing: 0.09em; color: var(--muted); }
|
||||
.sysstat-val { font-size: 0.82rem; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
|
||||
.sysstat-bar { height: 5px; border-radius: 3px; background: rgba(255,255,255,0.09); overflow: hidden; }
|
||||
.sysstat-bar i { display: block; height: 100%; width: 0; border-radius: 3px; background: linear-gradient(90deg, #4ade80, #22c55e); transition: width 0.45s ease, background 0.3s; }
|
||||
.sysstat-bar i.is-warn { background: linear-gradient(90deg, #fbbf24, #f59e0b); }
|
||||
.sysstat-bar i.is-crit { background: linear-gradient(90deg, #fb7185, #ef4444); }
|
||||
|
||||
/* ปุ่มแฮมเบอร์เกอร์ (มือถือ) */
|
||||
.admin-sidebar-toggle {
|
||||
display: none; flex-direction: column; justify-content: center; gap: 4px;
|
||||
width: 40px; height: 40px; padding: 0 9px; margin-right: 0.5rem;
|
||||
border: 1px solid var(--border); border-radius: 10px; background: rgba(0,0,0,0.25); cursor: pointer;
|
||||
}
|
||||
.admin-sidebar-toggle span { display: block; height: 2px; border-radius: 2px; background: var(--text); }
|
||||
|
||||
/* ---- Responsive: มือถือ/แท็บเล็ต → sidebar เป็น off-canvas ---- */
|
||||
@media (max-width: 980px) {
|
||||
#panel-app { grid-template-columns: 1fr; }
|
||||
#panel-app .admin-tabs {
|
||||
position: fixed; top: 0; left: 0; bottom: 0; z-index: 220;
|
||||
width: 272px; max-height: 100vh; border-radius: 0;
|
||||
transform: translateX(-102%); transition: transform 0.22s ease;
|
||||
background: rgba(8, 11, 20, 0.98);
|
||||
}
|
||||
.admin-shell.sidebar-open #panel-app .admin-tabs { transform: translateX(0); }
|
||||
.admin-shell.sidebar-open .admin-sidebar-backdrop {
|
||||
display: block; position: fixed; inset: 0; z-index: 200; background: rgba(0,0,0,0.55);
|
||||
}
|
||||
.admin-sidebar-toggle:not([hidden]) { display: inline-flex; }
|
||||
.admin-sys-stats { gap: 0.5rem; padding: 0.3rem 0.55rem; }
|
||||
.admin-sys-stats .sysstat { min-width: 64px; }
|
||||
.admin-sys-stats .sysstat--up,
|
||||
.admin-sys-stats .sysstat--node { display: none; }
|
||||
}
|
||||
@media (max-width: 680px) {
|
||||
.admin-sys-stats .sysstat--disk { display: none; }
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.admin-sys-stats .sysstat--ram { display: none; }
|
||||
.admin-brand-text .admin-tagline { display: none; }
|
||||
}
|
||||
|
||||
+164
-1
@@ -4026,7 +4026,92 @@
|
||||
var p = el(id);
|
||||
if (p) p.hidden = id !== name;
|
||||
});
|
||||
el('top-actions').hidden = name !== 'panel-app';
|
||||
var inApp = name === 'panel-app';
|
||||
el('top-actions').hidden = !inApp;
|
||||
var stats = el('admin-sys-stats');
|
||||
if (stats) stats.hidden = !inApp;
|
||||
var burger = el('btn-sidebar-toggle');
|
||||
if (burger) burger.hidden = !inApp;
|
||||
if (inApp) startSystemStats();
|
||||
else stopSystemStats();
|
||||
}
|
||||
|
||||
/* ===== ระบบ: ดึง CPU/RAM ของเซิร์ฟเวอร์มาแสดง (อัปเดตทุก 5 วิ) ===== */
|
||||
var __sysStatsTimer = null;
|
||||
function fmtBytes(n) {
|
||||
if (!(n > 0)) return '0';
|
||||
var u = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = Math.floor(Math.log(n) / Math.log(1024));
|
||||
i = Math.max(0, Math.min(u.length - 1, i));
|
||||
return (n / Math.pow(1024, i)).toFixed(i >= 2 ? 1 : 0) + ' ' + u[i];
|
||||
}
|
||||
function fmtUptime(sec) {
|
||||
sec = Math.max(0, Math.floor(sec || 0));
|
||||
var d = Math.floor(sec / 86400); sec -= d * 86400;
|
||||
var h = Math.floor(sec / 3600); sec -= h * 3600;
|
||||
var m = Math.floor(sec / 60);
|
||||
if (d > 0) return d + 'd ' + h + 'h';
|
||||
if (h > 0) return h + 'h ' + m + 'm';
|
||||
return m + 'm';
|
||||
}
|
||||
function setBarTone(bar, pct) {
|
||||
if (!bar) return;
|
||||
bar.classList.remove('is-warn', 'is-crit');
|
||||
if (pct >= 85) bar.classList.add('is-crit');
|
||||
else if (pct >= 65) bar.classList.add('is-warn');
|
||||
}
|
||||
function renderSystemStats(s) {
|
||||
if (!s || !s.ok) return;
|
||||
var cpuPct = (s.cpu && typeof s.cpu.percent === 'number') ? s.cpu.percent
|
||||
: (s.cpu ? s.cpu.loadPercent : 0);
|
||||
var cpuVal = el('sysstat-cpu-val'), cpuBar = el('sysstat-cpu-bar');
|
||||
if (cpuVal) cpuVal.textContent = cpuPct + '%';
|
||||
if (cpuBar) { cpuBar.style.width = cpuPct + '%'; setBarTone(cpuBar, cpuPct); }
|
||||
var memPct = s.mem ? s.mem.usedPercent : 0;
|
||||
var ramVal = el('sysstat-ram-val'), ramBar = el('sysstat-ram-bar');
|
||||
if (ramVal && s.mem) ramVal.textContent = memPct + '% ' + fmtBytes(s.mem.used) + '/' + fmtBytes(s.mem.total);
|
||||
if (ramBar) { ramBar.style.width = memPct + '%'; setBarTone(ramBar, memPct); }
|
||||
// HDD / ดิสก์
|
||||
var diskWrap = el('sysstat-disk-val') ? el('sysstat-disk-val').closest('.sysstat') : null;
|
||||
if (s.disk) {
|
||||
var dPct = s.disk.usedPercent;
|
||||
var dVal = el('sysstat-disk-val'), dBar = el('sysstat-disk-bar');
|
||||
if (dVal) dVal.textContent = dPct + '% ' + fmtBytes(s.disk.used) + '/' + fmtBytes(s.disk.total);
|
||||
if (dBar) { dBar.style.width = dPct + '%'; setBarTone(dBar, dPct); }
|
||||
if (diskWrap) diskWrap.hidden = false;
|
||||
} else if (diskWrap) {
|
||||
diskWrap.hidden = true; // อ่านดิสก์ไม่ได้ (เช่น Node เก่า) → ซ่อน
|
||||
}
|
||||
// Node ที่ใช้อยู่ (RAM ของ process เกม)
|
||||
if (s.node) {
|
||||
var nPct = typeof s.node.rssPercent === 'number' ? s.node.rssPercent : 0;
|
||||
var nVal = el('sysstat-node-val'), nBar = el('sysstat-node-bar');
|
||||
if (nVal) nVal.textContent = fmtBytes(s.node.rss) + (s.node.version ? ' ' + s.node.version : '');
|
||||
if (nBar) { nBar.style.width = Math.min(100, nPct) + '%'; setBarTone(nBar, nPct); }
|
||||
}
|
||||
var upVal = el('sysstat-up-val');
|
||||
if (upVal && s.uptime) upVal.textContent = fmtUptime(s.uptime.os);
|
||||
var box = el('admin-sys-stats');
|
||||
if (box && s.cpu) box.title = 'CPU ' + cpuPct + '% (load ' + s.cpu.load1 + ', ' + s.cpu.cores + ' cores) · RAM '
|
||||
+ memPct + '%'
|
||||
+ (s.disk ? ' · HDD ' + s.disk.usedPercent + '% (' + fmtBytes(s.disk.free) + ' ว่าง)' : '')
|
||||
+ (s.node ? ' · Node ' + fmtBytes(s.node.rss) + ' ' + (s.node.version || '') : '')
|
||||
+ ' · uptime ' + fmtUptime(s.uptime && s.uptime.os) + ' · ' + (s.hostname || '');
|
||||
}
|
||||
function pollSystemStats() {
|
||||
fetch('/Game/api/system-stats', { cache: 'no-store' })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (s) { if (s) renderSystemStats(s); })
|
||||
.catch(function () { /* เงียบไว้ ไม่รบกวน admin */ });
|
||||
}
|
||||
function startSystemStats() {
|
||||
if (__sysStatsTimer) return;
|
||||
pollSystemStats();
|
||||
setTimeout(pollSystemStats, 1200); // ครั้งที่สองให้ได้ค่า CPU% (ครั้งแรก server คืน null)
|
||||
__sysStatsTimer = setInterval(pollSystemStats, 5000);
|
||||
}
|
||||
function stopSystemStats() {
|
||||
if (__sysStatsTimer) { clearInterval(__sysStatsTimer); __sysStatsTimer = null; }
|
||||
}
|
||||
|
||||
// ===== Evidence Cards admin (รูปอย่างเดียว · 15 คดี · rarity จากเลขไฟล์) =====
|
||||
@@ -4485,6 +4570,84 @@
|
||||
});
|
||||
});
|
||||
|
||||
/* ===== Sidebar: ค้นหาเมนู / ยุบกลุ่ม / เปิด-ปิดบนมือถือ ===== */
|
||||
(function setupAdminSidebar() {
|
||||
var shell = document.querySelector('.admin-shell');
|
||||
var nav = document.querySelector('.admin-tabs');
|
||||
if (!nav) return;
|
||||
var seps = Array.prototype.slice.call(nav.querySelectorAll('.admin-tab-sep'));
|
||||
var tabs = Array.prototype.slice.call(nav.querySelectorAll('.tab'));
|
||||
|
||||
function isMobile() { return window.matchMedia('(max-width: 980px)').matches; }
|
||||
function closeSidebar() {
|
||||
if (shell) shell.classList.remove('sidebar-open');
|
||||
var b = el('btn-sidebar-toggle'); if (b) b.setAttribute('aria-expanded', 'false');
|
||||
var bd = el('admin-sidebar-backdrop'); if (bd) bd.hidden = true;
|
||||
}
|
||||
function openSidebar() {
|
||||
if (shell) shell.classList.add('sidebar-open');
|
||||
var b = el('btn-sidebar-toggle'); if (b) b.setAttribute('aria-expanded', 'true');
|
||||
var bd = el('admin-sidebar-backdrop'); if (bd) bd.hidden = false;
|
||||
}
|
||||
var burger = el('btn-sidebar-toggle');
|
||||
if (burger) burger.addEventListener('click', function () {
|
||||
if (shell && shell.classList.contains('sidebar-open')) closeSidebar(); else openSidebar();
|
||||
});
|
||||
var backdrop = el('admin-sidebar-backdrop');
|
||||
if (backdrop) backdrop.addEventListener('click', closeSidebar);
|
||||
nav.addEventListener('click', function (e) {
|
||||
var t = e.target && e.target.closest ? e.target.closest('.tab') : null;
|
||||
if (t && isMobile()) closeSidebar();
|
||||
});
|
||||
|
||||
/* ยุบ/ขยายกลุ่ม: ซ่อนแท็บที่อยู่หลัง separator จนถึง separator ถัดไป */
|
||||
function applyGroupVisibility() {
|
||||
seps.forEach(function (sep) {
|
||||
var collapsed = sep.classList.contains('is-collapsed');
|
||||
var n = sep.nextElementSibling;
|
||||
while (n && !n.classList.contains('admin-tab-sep')) {
|
||||
if (n.classList && n.classList.contains('tab')) n.classList.toggle('is-group-hidden', collapsed);
|
||||
n = n.nextElementSibling;
|
||||
}
|
||||
});
|
||||
}
|
||||
function toggleGroup(sep) {
|
||||
sep.classList.toggle('is-collapsed');
|
||||
applyGroupVisibility();
|
||||
}
|
||||
seps.forEach(function (sep) {
|
||||
sep.setAttribute('role', 'button');
|
||||
sep.setAttribute('tabindex', '0');
|
||||
sep.addEventListener('click', function () { toggleGroup(sep); });
|
||||
sep.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleGroup(sep); }
|
||||
});
|
||||
});
|
||||
|
||||
/* ค้นหาเมนู */
|
||||
var search = el('admin-nav-search-input');
|
||||
var emptyMsg = el('admin-nav-empty');
|
||||
if (search) search.addEventListener('input', function () {
|
||||
var q = search.value.trim().toLowerCase();
|
||||
if (q) {
|
||||
var anyVisible = false;
|
||||
tabs.forEach(function (t) {
|
||||
t.classList.remove('is-group-hidden');
|
||||
var hit = (t.textContent || '').toLowerCase().indexOf(q) >= 0;
|
||||
t.classList.toggle('is-search-hidden', !hit);
|
||||
if (hit) anyVisible = true;
|
||||
});
|
||||
seps.forEach(function (s) { s.style.display = 'none'; });
|
||||
if (emptyMsg) emptyMsg.hidden = anyVisible;
|
||||
} else {
|
||||
tabs.forEach(function (t) { t.classList.remove('is-search-hidden'); });
|
||||
seps.forEach(function (s) { s.style.display = ''; });
|
||||
applyGroupVisibility();
|
||||
if (emptyMsg) emptyMsg.hidden = true;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
function bindEmbedPanelFullscreen(btnId, panelId) {
|
||||
var btn = el(btnId);
|
||||
var panel = el(panelId);
|
||||
|
||||
@@ -10,13 +10,16 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="admin.css?v=37">
|
||||
<link rel="stylesheet" href="admin.css?v=39">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#admin-main">ข้ามไปเนื้อหา</a>
|
||||
<div class="admin-bg" aria-hidden="true"></div>
|
||||
<div class="admin-shell">
|
||||
<header class="admin-header-bar">
|
||||
<button type="button" class="admin-sidebar-toggle" id="btn-sidebar-toggle" hidden aria-label="เปิด/ปิดเมนู" aria-expanded="false">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
<div class="admin-brand">
|
||||
<span class="admin-brand-mark" aria-hidden="true"></span>
|
||||
<div class="admin-brand-text">
|
||||
@@ -24,6 +27,28 @@
|
||||
<p class="admin-tagline">JD JUSTICE DIVERS</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-sys-stats" id="admin-sys-stats" hidden aria-live="polite" title="ทรัพยากรเซิร์ฟเวอร์ (อัปเดตอัตโนมัติ)">
|
||||
<div class="sysstat sysstat--cpu">
|
||||
<div class="sysstat-head"><span class="sysstat-label">CPU</span><span class="sysstat-val" id="sysstat-cpu-val">—</span></div>
|
||||
<div class="sysstat-bar"><i id="sysstat-cpu-bar" style="width:0%"></i></div>
|
||||
</div>
|
||||
<div class="sysstat sysstat--ram">
|
||||
<div class="sysstat-head"><span class="sysstat-label">RAM</span><span class="sysstat-val" id="sysstat-ram-val">—</span></div>
|
||||
<div class="sysstat-bar"><i id="sysstat-ram-bar" style="width:0%"></i></div>
|
||||
</div>
|
||||
<div class="sysstat sysstat--disk">
|
||||
<div class="sysstat-head"><span class="sysstat-label">HDD</span><span class="sysstat-val" id="sysstat-disk-val">—</span></div>
|
||||
<div class="sysstat-bar"><i id="sysstat-disk-bar" style="width:0%"></i></div>
|
||||
</div>
|
||||
<div class="sysstat sysstat--node">
|
||||
<div class="sysstat-head"><span class="sysstat-label">NODE</span><span class="sysstat-val" id="sysstat-node-val">—</span></div>
|
||||
<div class="sysstat-bar"><i id="sysstat-node-bar" style="width:0%"></i></div>
|
||||
</div>
|
||||
<div class="sysstat sysstat--up">
|
||||
<span class="sysstat-label">UPTIME</span>
|
||||
<span class="sysstat-val" id="sysstat-up-val">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-top-actions" id="top-actions" hidden>
|
||||
<a href="/Admin/justice-git.html" class="btn btn-ghost" style="text-decoration:none;margin-right:0.35rem;">อัป Git (justice)</a>
|
||||
<span class="admin-user" id="admin-user-label"></span>
|
||||
@@ -66,6 +91,10 @@
|
||||
|
||||
<div id="panel-app" class="admin-app" hidden>
|
||||
<nav class="admin-tabs" role="tablist" aria-label="เมนูหลัก Admin">
|
||||
<div class="admin-nav-search">
|
||||
<input type="search" id="admin-nav-search-input" placeholder="ค้นหาเมนู…" aria-label="ค้นหาเมนู" autocomplete="off">
|
||||
</div>
|
||||
<div class="admin-nav-empty" id="admin-nav-empty" hidden>ไม่พบเมนูที่ค้นหา</div>
|
||||
<div class="admin-tab-sep" aria-hidden="true"><span>ทั่วไป & ฉาก</span></div>
|
||||
<button type="button" class="tab is-active" data-tab="oauth" role="tab" id="tab-oauth" aria-selected="true" aria-controls="tab-panel-oauth"><span class="tab-label">OAuth</span><span class="tab-desc">Facebook / Google</span></button>
|
||||
<button type="button" class="tab" data-tab="map-editor" role="tab" id="tab-map-editor" aria-controls="tab-panel-map-editor"><span class="tab-label">Editor ฉาก</span><span class="tab-desc">แผนที่ฉาก / โซนเกม</span></button>
|
||||
@@ -92,6 +121,8 @@
|
||||
<button type="button" class="tab" data-tab="admins" role="tab" id="tab-admins" aria-controls="tab-panel-admins"><span class="tab-label">แอดมิน</span><span class="tab-desc">สิทธิ์ระบบ</span></button>
|
||||
<button type="button" class="tab" data-tab="test-mode" role="tab" id="tab-test-mode" aria-controls="tab-panel-test-mode"><span class="tab-label">Test Mode</span><span class="tab-desc">เปิด hotkeys ทดสอบเกม</span></button>
|
||||
</nav>
|
||||
<div class="admin-sidebar-backdrop" id="admin-sidebar-backdrop" hidden aria-hidden="true"></div>
|
||||
<div class="admin-content">
|
||||
|
||||
<section id="tab-panel-change-password" class="tab-panel card card--password" hidden role="tabpanel" aria-labelledby="tab-change-password">
|
||||
<h2 class="card-title-icon">เปลี่ยนรหัสผ่านของคุณ</h2>
|
||||
@@ -1056,9 +1087,10 @@
|
||||
|
||||
<p id="test-mode-msg" class="msg" role="status" style="margin-top:14px;"></p>
|
||||
</section>
|
||||
</div><!-- /.admin-content -->
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="admin.js?v=79"></script>
|
||||
<script src="admin.js?v=81"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -299,6 +299,81 @@ function qbLeaderboardGet(roomId, name) {
|
||||
}
|
||||
loadQbLeaderboard();
|
||||
|
||||
/* ===== System stats (CPU / RAM) สำหรับหน้า Admin ===== */
|
||||
const os = require('os');
|
||||
let __prevCpuSample = null;
|
||||
function __cpuSample() {
|
||||
const cpus = os.cpus() || [];
|
||||
let idle = 0, total = 0;
|
||||
for (const c of cpus) {
|
||||
for (const k in c.times) total += c.times[k];
|
||||
idle += c.times.idle;
|
||||
}
|
||||
return { idle, total };
|
||||
}
|
||||
/** CPU% เฉลี่ยช่วงเวลาระหว่างสองครั้งที่เรียก (admin poll ~ทุก 5 วิ) — ครั้งแรกคืน null */
|
||||
function __cpuPercent() {
|
||||
const cur = __cpuSample();
|
||||
const prev = __prevCpuSample;
|
||||
__prevCpuSample = cur;
|
||||
if (!prev) return null;
|
||||
const idleD = cur.idle - prev.idle;
|
||||
const totalD = cur.total - prev.total;
|
||||
if (totalD <= 0) return null;
|
||||
return Math.max(0, Math.min(100, Math.round((1 - idleD / totalD) * 100)));
|
||||
}
|
||||
__cpuSample(); // เก็บ baseline แรก
|
||||
function getDiskStats() {
|
||||
try {
|
||||
if (typeof fs.statfsSync !== 'function') return null;
|
||||
const s = fs.statfsSync(__dirname);
|
||||
const total = s.blocks * s.bsize;
|
||||
const free = s.bfree * s.bsize; // ว่างจริงทั้งหมด
|
||||
const avail = s.bavail * s.bsize; // ว่างที่ผู้ใช้ทั่วไปใช้ได้
|
||||
const used = total - free;
|
||||
if (!(total > 0)) return null;
|
||||
return { total, free: avail, used, usedPercent: Math.round((used / total) * 100) };
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
function getSystemStats() {
|
||||
const totalMem = os.totalmem();
|
||||
const freeMem = os.freemem();
|
||||
const usedMem = totalMem - freeMem;
|
||||
const load = os.loadavg();
|
||||
const cores = (os.cpus() || []).length || 1;
|
||||
const cpuPct = __cpuPercent();
|
||||
const mu = process.memoryUsage();
|
||||
return {
|
||||
ok: true,
|
||||
cpu: {
|
||||
percent: cpuPct, // 0-100 หรือ null (ครั้งแรก)
|
||||
cores,
|
||||
load1: Math.round(load[0] * 100) / 100,
|
||||
load5: Math.round(load[1] * 100) / 100,
|
||||
load15: Math.round(load[2] * 100) / 100,
|
||||
loadPercent: Math.max(0, Math.min(100, Math.round((load[0] / cores) * 100))),
|
||||
},
|
||||
mem: {
|
||||
total: totalMem,
|
||||
free: freeMem,
|
||||
used: usedMem,
|
||||
usedPercent: Math.round((usedMem / totalMem) * 100),
|
||||
},
|
||||
disk: getDiskStats(),
|
||||
node: {
|
||||
rss: mu.rss,
|
||||
heapUsed: mu.heapUsed,
|
||||
rssPercent: Math.round((mu.rss / totalMem) * 100),
|
||||
version: process.version,
|
||||
uptime: Math.round(process.uptime()),
|
||||
},
|
||||
uptime: { os: Math.round(os.uptime()), process: Math.round(process.uptime()) },
|
||||
hostname: os.hostname(),
|
||||
platform: os.platform(),
|
||||
ts: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
/** ช่องตัวเลือกบนกริด quiz_carry เก็บเลข 1..N (ไม่ใช่แค่ 0/1 เหมือน hub) */
|
||||
const QUIZ_CARRY_MAX_OPTION_SLOTS = 16;
|
||||
const maps = new Map();
|
||||
@@ -4563,6 +4638,18 @@ const server = http.createServer((req, res) => {
|
||||
return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' }));
|
||||
}
|
||||
const quizSettingsUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/';
|
||||
if (quizSettingsUrlPath === BASE_PATH + '/api/system-stats') {
|
||||
if (req.method === 'GET') {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
});
|
||||
return res.end(JSON.stringify(getSystemStats()));
|
||||
}
|
||||
res.writeHead(405);
|
||||
return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' }));
|
||||
}
|
||||
if (quizSettingsUrlPath === BASE_PATH + '/api/quiz-settings') {
|
||||
if (req.method === 'GET') {
|
||||
res.writeHead(200, {
|
||||
|
||||
Reference in New Issue
Block a user