update admin

This commit is contained in:
2026-06-10 10:10:44 +00:00
parent 2f903a791c
commit 21b8a782b8
4 changed files with 435 additions and 3 deletions
+150
View File
@@ -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
View File
@@ -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);
+34 -2
View File
@@ -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>ทั่วไป &amp; ฉาก</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>
+87
View File
@@ -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, {