diff --git a/nginx/srv1361159.hstgr.cloud b/nginx/srv1361159.hstgr.cloud index e7a76b2..f60986b 100644 --- a/nginx/srv1361159.hstgr.cloud +++ b/nginx/srv1361159.hstgr.cloud @@ -63,6 +63,16 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } + # ชื่อไฟล์ .php อยู่ใต้ /Game/ ไม่ใช่ /Game/api/ — fastcgi จะหา $document_root/Game/... ผิดที่ (ไฟล์จริงใน Game/public/) → ส่งไป Node แทน + location = /Game/api-quiz-carry-from-disk.php { + client_max_body_size 20M; + proxy_pass http://127.0.0.1:3001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } location ~ ^/Game/(?!socket\.io/)(.+\.(html|js))$ { alias /var/www/html/Game/public/$1; add_header Cache-Control "no-store, no-cache, must-revalidate"; diff --git a/www/html/Admin/admin.css b/www/html/Admin/admin.css index d9bc8e3..c8b55b1 100644 --- a/www/html/Admin/admin.css +++ b/www/html/Admin/admin.css @@ -1466,6 +1466,73 @@ code { } } +/* —— Quiz carry: map panel theme (color + alpha) —— */ +.quiz-carry-theme-grid { + display: flex; + flex-direction: column; + gap: 0.85rem; + margin-top: 0.25rem; +} +.quiz-carry-theme-row { + display: grid; + grid-template-columns: minmax(7.5rem, 10rem) 1fr; + gap: 0.65rem 1rem; + align-items: center; +} +.quiz-carry-theme-row--narrow { + grid-template-columns: 1fr; + max-width: 12rem; +} +.quiz-carry-theme-label { + font-size: 0.88rem; + font-weight: 600; + color: var(--text); +} +.quiz-carry-color-controls { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.65rem 1rem; +} +.quiz-carry-color-controls input[type='color'] { + width: 44px; + height: 36px; + padding: 2px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--card); + cursor: pointer; +} +.quiz-carry-alpha-label { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.35rem 0.5rem; + font-size: 0.8rem; + color: var(--muted); + flex: 1; + min-width: min(100%, 220px); +} +.quiz-carry-alpha-label input[type='range'] { + flex: 1; + min-width: 120px; + max-width: 280px; + accent-color: var(--accent); +} +.quiz-carry-alpha-out { + font-variant-numeric: tabular-nums; + min-width: 2.75rem; + font-size: 0.8rem; + color: var(--text); +} +.quiz-carry-theme-code { + font-size: 0.72rem; + color: var(--muted); + word-break: break-all; + max-width: 100%; + flex-basis: 100%; +} + /* —— Quiz Battle admin (โดม + ป๊อปอัป cyber) —— */ .tab-panel-quiz-battle { --qb-cyan: #00f2ff; diff --git a/www/html/Admin/admin.js b/www/html/Admin/admin.js index 72aa2b2..29c5038 100644 --- a/www/html/Admin/admin.js +++ b/www/html/Admin/admin.js @@ -352,8 +352,194 @@ }); } - function loadQuizCarryPanel() { - gameQuizFetch('GET').then(function (data) { + /** ค่าเริ่มต้น RGBA สำหรับแผงคำถามบนแผนที่ (quiz_carry) */ + var QUIZ_CARRY_THEME_DEFAULTS = { + bg: { r: 12, g: 14, b: 28, a: 0.88 }, + border: { r: 255, g: 214, b: 102, a: 0.7 }, + text: { r: 241, g: 245, b: 249, a: 1 }, + }; + + function quizCarryClampByte(n) { + return Math.max(0, Math.min(255, Math.round(Number(n)) || 0)); + } + + function quizCarryHexFromRgb(r, g, b) { + return '#' + [r, g, b].map(function (x) { + var h = quizCarryClampByte(x).toString(16); + return h.length === 1 ? '0' + h : h; + }).join(''); + } + + function quizCarryParseHexToRgb(hex) { + var h = String(hex || '').trim().replace(/^#/, ''); + if (!h) return null; + if (h.length === 3) { + return { + r: parseInt(h[0] + h[0], 16), + g: parseInt(h[1] + h[1], 16), + b: parseInt(h[2] + h[2], 16), + alpha01: null, + }; + } + if (h.length === 6) { + return { + r: parseInt(h.slice(0, 2), 16), + g: parseInt(h.slice(2, 4), 16), + b: parseInt(h.slice(4, 6), 16), + alpha01: null, + }; + } + if (h.length === 8) { + return { + r: parseInt(h.slice(0, 2), 16), + g: parseInt(h.slice(2, 4), 16), + b: parseInt(h.slice(4, 6), 16), + alpha01: parseInt(h.slice(6, 8), 16) / 255, + }; + } + return null; + } + + function quizCarryParseCssColor(str, fallback) { + var s = String(str == null ? '' : str).trim(); + if (!s) return fallback; + var m = /^rgba?\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*(?:,\s*([0-9.]+)\s*)?\)$/i.exec(s); + if (m) { + var a = m[4] != null && m[4] !== '' ? parseFloat(m[4]) : 1; + if (!Number.isFinite(a)) a = 1; + return { + r: quizCarryClampByte(parseInt(m[1], 10)), + g: quizCarryClampByte(parseInt(m[2], 10)), + b: quizCarryClampByte(parseInt(m[3], 10)), + a: Math.max(0, Math.min(1, a)), + }; + } + if (/^#/.test(s)) { + var pr = quizCarryParseHexToRgb(s); + if (!pr) return fallback; + var a = pr.alpha01 != null ? pr.alpha01 : 1; + return { r: pr.r, g: pr.g, b: pr.b, a: Math.max(0, Math.min(1, a)) }; + } + return fallback; + } + + function quizCarryRgbToRgbaString(o) { + var a = Math.max(0, Math.min(1, Number(o.a))); + var t = Math.round(a * 1000) / 1000; + return 'rgba(' + quizCarryClampByte(o.r) + ', ' + quizCarryClampByte(o.g) + ', ' + quizCarryClampByte(o.b) + ', ' + t + ')'; + } + + function quizCarryRowPrefix(kind) { + return 'quiz-carry-theme-' + kind; + } + + function quizCarryThemeHiddenId(kind) { + if (kind === 'bg') return 'quiz-carry-theme-bg'; + if (kind === 'border') return 'quiz-carry-theme-border'; + return 'quiz-carry-theme-text'; + } + + function quizCarrySetPickersFromColorString(kind, cssStr) { + var fb = QUIZ_CARRY_THEME_DEFAULTS[kind]; + var o = quizCarryParseCssColor(cssStr, fb); + var pref = quizCarryRowPrefix(kind); + var sw = el(pref + '-swatch'); + var ra = el(pref + '-alpha'); + var out = el(pref + '-alpha-out'); + var hi = el(quizCarryThemeHiddenId(kind)); + var pr = el(pref + '-preview'); + if (!sw || !ra || !hi) return; + sw.value = quizCarryHexFromRgb(o.r, o.g, o.b); + var pct = Math.max(0, Math.min(100, Math.round(Number(o.a) * 100))); + ra.value = String(pct); + if (out) out.textContent = pct + '%'; + hi.value = quizCarryRgbToRgbaString(o); + if (pr) pr.textContent = hi.value; + } + + function quizCarryComposeFromPickers(kind) { + var pref = quizCarryRowPrefix(kind); + var sw = el(pref + '-swatch'); + var ra = el(pref + '-alpha'); + if (!sw || !ra) return ''; + var pr = quizCarryParseHexToRgb(sw.value); + if (!pr) return ''; + var pct = parseInt(String(ra.value), 10); + if (!Number.isFinite(pct)) pct = 100; + pct = Math.max(0, Math.min(100, pct)); + return quizCarryRgbToRgbaString({ r: pr.r, g: pr.g, b: pr.b, a: pct / 100 }); + } + + function quizCarrySyncRowOutput(kind) { + var s = quizCarryComposeFromPickers(kind); + var hi = el(quizCarryThemeHiddenId(kind)); + var pref = quizCarryRowPrefix(kind); + var ra = el(pref + '-alpha'); + var out = el(pref + '-alpha-out'); + var pr = el(pref + '-preview'); + if (hi) hi.value = s; + if (out && ra) out.textContent = String(Math.max(0, Math.min(100, parseInt(ra.value, 10) || 0))) + '%'; + if (pr && hi) pr.textContent = hi.value; + } + + /** อ่านค่าจาก color + range โดยตรง (ไม่พึ่ง hidden ที่อาจว่าง) ก่อน PUT */ + function quizCarryBuildThemeForSave() { + quizCarryBindThemePickersOnce(); + ['bg', 'border', 'text'].forEach(function (k) { + quizCarrySyncRowOutput(k); + }); + var fbBg = QUIZ_CARRY_THEME_DEFAULTS.bg; + var fbBr = QUIZ_CARRY_THEME_DEFAULTS.border; + var fbTx = QUIZ_CARRY_THEME_DEFAULTS.text; + var sBg = quizCarryComposeFromPickers('bg'); + var sBr = quizCarryComposeFromPickers('border'); + var sTx = quizCarryComposeFromPickers('text'); + var bwEl = el('quiz-carry-theme-border-w'); + var bw = bwEl ? parseInt(String(bwEl.value || '2'), 10) : 2; + if (!Number.isFinite(bw)) bw = 2; + bw = Math.max(0, Math.min(12, Math.round(bw))); + return { + panelBg: (sBg && sBg.trim()) ? sBg.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbBg), + panelBorder: (sBr && sBr.trim()) ? sBr.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbBr), + textColor: (sTx && sTx.trim()) ? sTx.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbTx), + borderWidthPx: bw, + }; + } + + function quizCarryBindThemePickersOnce() { + if (quizCarryBindThemePickersOnce._done) return; + ['bg', 'border', 'text'].forEach(function (kind) { + var pref = quizCarryRowPrefix(kind); + var sw = el(pref + '-swatch'); + var ra = el(pref + '-alpha'); + if (!sw || !ra) return; + if (ra.getAttribute('data-bound') === '1') return; + ra.setAttribute('data-bound', '1'); + function sync() { + quizCarrySyncRowOutput(kind); + } + sw.addEventListener('input', sync); + sw.addEventListener('change', sync); + ra.addEventListener('input', sync); + ra.addEventListener('change', sync); + }); + quizCarryBindThemePickersOnce._done = true; + } + + function loadQuizCarryPanel(opts) { + opts = opts || {}; + var delay = Number(opts.fetchDelayMs) || 0; + var runLoad = function () { + gameQuizFetch('GET').then(function (data) { + if (opts.themeOverride && typeof opts.themeOverride === 'object') { + data.carryMapPanelTheme = opts.themeOverride; + } + if (opts.timingOverride && typeof opts.timingOverride === 'object') { + var t = opts.timingOverride; + if (t.carryReadMs != null) data.carryReadMs = t.carryReadMs; + if (t.carryAnswerMs != null) data.carryAnswerMs = t.carryAnswerMs; + if (t.carrySessionLength != null) data.carrySessionLength = t.carrySessionLength; + } renderQuizCarryAdminList(data.carryQuestions || []); var readSec = el('quiz-carry-read-sec'); var ansSec = el('quiz-carry-answer-sec'); @@ -370,10 +556,23 @@ var sl = parseInt(String(data.carrySessionLength), 10); sessLen.value = String(Number.isFinite(sl) && sl >= 0 ? sl : 0); } - setMsg('quiz-carry-settings-msg', '', ''); + quizCarryBindThemePickersOnce(); + var th = data.carryMapPanelTheme && typeof data.carryMapPanelTheme === 'object' ? data.carryMapPanelTheme : {}; + quizCarrySetPickersFromColorString('bg', th.panelBg); + quizCarrySetPickersFromColorString('border', th.panelBorder); + quizCarrySetPickersFromColorString('text', th.textColor); + var bwInp = el('quiz-carry-theme-border-w'); + if (bwInp) { + var bw = parseInt(String(th.borderWidthPx), 10); + bwInp.value = String(Number.isFinite(bw) && bw >= 0 ? Math.min(12, bw) : 2); + } + if (opts.clearMsg !== false) setMsg('quiz-carry-settings-msg', '', ''); }).catch(function (e) { setMsg('quiz-carry-settings-msg', e.message || 'โหลดไม่ได้', 'error'); }); + }; + if (delay > 0) setTimeout(runLoad, delay); + else runLoad(); } function saveQuizCarryPanel() { @@ -426,13 +625,52 @@ if (!Number.isFinite(carrySessionLength) || carrySessionLength < 0) carrySessionLength = 0; if (carrySessionLength > 500) carrySessionLength = 500; - gameQuizFetch('PUT', { - carryQuestions: carryQuestions, + var carryMapPanelTheme = quizCarryBuildThemeForSave(); + + var blockEls = list.querySelectorAll('.quiz-carry-admin-block'); + var hasAnyQuestionText = false; + for (var bi = 0; bi < blockEls.length; bi++) { + var qn = blockEls[bi].querySelector('.quiz-carry-q-text'); + if (qn && String(qn.value || '').trim()) { + hasAnyQuestionText = true; + break; + } + } + if (carryQuestions.length === 0 && hasAnyQuestionText) { + setMsg( + 'quiz-carry-settings-msg', + 'ยังบันทึกชุดคำถามไม่ได้: แต่ละข้อต้องมีคำถาม ตัวเลือกอย่างน้อย 2 ช่องที่กรอกแล้ว และเลือก «ข้อที่ถูกต้อง» — ตรวจแถวที่มีข้อความค้าง', + 'error', + ); + return; + } + + var putBody = { carryReadMs: carryReadMs, carryAnswerMs: carryAnswerMs, carrySessionLength: carrySessionLength, - }).then(function () { - setMsg('quiz-carry-settings-msg', 'บันทึกแล้ว', 'ok'); + carryMapPanelTheme: carryMapPanelTheme, + }; + if (carryQuestions.length > 0) { + putBody.carryQuestions = carryQuestions; + } + + gameQuizFetch('PUT', putBody).then(function (res) { + var tail = carryQuestions.length ? ' · คำถาม ' + carryQuestions.length + ' ข้อ' : ' · คงชุดคำถามเดิม (อัปเดตเวลา/ธีมอย่างเดียว)'; + var themeFromRes = res && res.carryMapPanelTheme && typeof res.carryMapPanelTheme === 'object' ? res.carryMapPanelTheme : null; + var themeOk = !!themeFromRes; + var themeTail = themeOk ? ' · ธีมแผงบันทึกแล้ว' : ''; + setMsg('quiz-carry-settings-msg', 'บันทึกแล้ว' + tail + themeTail, 'ok'); + loadQuizCarryPanel({ + clearMsg: false, + themeOverride: themeFromRes || carryMapPanelTheme, + timingOverride: { + carryReadMs: carryReadMs, + carryAnswerMs: carryAnswerMs, + carrySessionLength: carrySessionLength, + }, + fetchDelayMs: 120, + }); }).catch(function (e) { setMsg('quiz-carry-settings-msg', e.message || 'บันทึกไม่ได้', 'error'); }); @@ -1578,6 +1816,7 @@ }); } if (btnQuizCarrySave) btnQuizCarrySave.addEventListener('click', saveQuizCarryPanel); + quizCarryBindThemePickersOnce(); var btnQbBattleAdd = el('btn-qb-battle-add'); var btnQbBattleSave = el('btn-qb-battle-save'); diff --git a/www/html/Admin/api/game-quiz-settings.php b/www/html/Admin/api/game-quiz-settings.php index 7fb7a8e..0e2ff72 100644 --- a/www/html/Admin/api/game-quiz-settings.php +++ b/www/html/Admin/api/game-quiz-settings.php @@ -6,7 +6,7 @@ declare(strict_types=1); * แก้กรณี nginx ส่ง /Game/ เป็น static แล้ว PUT ไม่ถึง Node — เบราว์เซอร์เรียก PHP แทน * * รองรับ Node เวอร์ชันเก่าที่ GET ไม่ส่ง battleQuizMcq / PUT เขียนทับไฟล์แล้วตัดฟิลด์: - * - GET: ดึง battleQuizMcq จากไฟล์บนดิสก์มาแปะในคำตอบ + * - GET: ดึงจากดิสก์มาแปะทับบางฟิลด์ (battleQuizMcq + carry*) เพราะหลัง PUT เรา merge ลงไฟล์ใต้ www เสมอ — Node อาจอ่านคนละ path กับ PHP * - PUT: merge body ลง quiz-settings.json บนดิสก์เสมอ (หลัง proxy Node) เพื่อให้ข้อมูลคงอยู่หลังรีเฟรช */ require __DIR__ . '/_common.php'; @@ -37,16 +37,22 @@ function merge_quiz_settings_disk_from_put(string $rawBody): bool } $path = quiz_settings_disk_path(); $base = []; + $hadFileButInvalidJson = false; if (is_file($path)) { $prev = @file_get_contents($path); if ($prev !== false && $prev !== '') { $d = json_decode($prev, true); if (is_array($d)) { $base = $d; + } else { + $hadFileButInvalidJson = true; } } } - $mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'questions', 'carryQuestions', 'battleQuizMcq']; + if ($hadFileButInvalidJson) { + return false; + } + $mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'questions', 'carryQuestions', 'battleQuizMcq']; foreach ($mergeKeys as $k) { if (array_key_exists($k, $patch)) { $base[$k] = $patch[$k]; @@ -66,9 +72,10 @@ function merge_quiz_settings_disk_from_put(string $rawBody): bool } /** - * Node เก่าอาจไม่ใส่ battleQuizMcq ใน JSON — อ่านจากดิสก์มาแปะให้แอดมินโหลดได้หลังรีเฟรช + * แปะค่าจาก quiz-settings.json ใต้ docroot ทับ JSON จาก Node — ให้แอดมินรีเฟรชแล้วเห็นค่าที่ merge หลัง PUT (กันคนละไฟล์กับ Node) + * Overlay keys from on-disk quiz-settings.json over Node JSON — Admin refresh matches post-PUT merge (fixes Node vs disk path mismatch). */ -function overlay_battle_quiz_mcq_from_disk(string $nodeBody): string +function overlay_quiz_settings_from_disk(string $nodeBody): string { $j = json_decode($nodeBody, true); if (!is_array($j)) { @@ -83,10 +90,23 @@ function overlay_battle_quiz_mcq_from_disk(string $nodeBody): string return $nodeBody; } $disk = json_decode($raw, true); - if (!is_array($disk) || !array_key_exists('battleQuizMcq', $disk)) { + if (!is_array($disk)) { return $nodeBody; } - $j['battleQuizMcq'] = $disk['battleQuizMcq']; + if (array_key_exists('battleQuizMcq', $disk)) { + $j['battleQuizMcq'] = $disk['battleQuizMcq']; + } + if (array_key_exists('carryMapPanelTheme', $disk) && is_array($disk['carryMapPanelTheme'])) { + $j['carryMapPanelTheme'] = $disk['carryMapPanelTheme']; + } + foreach (['carryReadMs', 'carryAnswerMs', 'carrySessionLength'] as $ck) { + if (array_key_exists($ck, $disk)) { + $j[$ck] = $disk[$ck]; + } + } + if (array_key_exists('carryQuestions', $disk) && is_array($disk['carryQuestions'])) { + $j['carryQuestions'] = $disk['carryQuestions']; + } $enc = json_encode($j, JSON_UNESCAPED_UNICODE); return $enc !== false ? $enc : $nodeBody; } @@ -141,7 +161,26 @@ function proxy_quiz_curl(string $method, ?string $body = null): void header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); header('Cache-Control: no-store'); - echo json_encode(['ok' => true, 'diskFallback' => true], JSON_UNESCAPED_UNICODE); + $payload = ['ok' => true, 'diskFallback' => true]; + $rawDisk = @file_get_contents(quiz_settings_disk_path()); + if ($rawDisk !== false && $rawDisk !== '') { + $dj = json_decode($rawDisk, true); + if (is_array($dj)) { + if (isset($dj['carryMapPanelTheme']) && is_array($dj['carryMapPanelTheme'])) { + $payload['carryMapPanelTheme'] = $dj['carryMapPanelTheme']; + } + if (array_key_exists('carryReadMs', $dj)) { + $payload['carryReadMs'] = $dj['carryReadMs']; + } + if (array_key_exists('carryAnswerMs', $dj)) { + $payload['carryAnswerMs'] = $dj['carryAnswerMs']; + } + if (array_key_exists('carrySessionLength', $dj)) { + $payload['carrySessionLength'] = $dj['carrySessionLength']; + } + } + } + echo json_encode($payload, JSON_UNESCAPED_UNICODE); exit; } json_response([ @@ -154,7 +193,7 @@ function proxy_quiz_curl(string $method, ?string $body = null): void header('X-Content-Type-Options: nosniff'); header('Cache-Control: no-store'); if ($method === 'GET' && $code >= 200 && $code < 300) { - echo overlay_battle_quiz_mcq_from_disk($out); + echo overlay_quiz_settings_from_disk($out); } else { echo $out; } diff --git a/www/html/Admin/game-quiz-settings.php b/www/html/Admin/game-quiz-settings.php new file mode 100644 index 0000000..0d9f6f1 --- /dev/null +++ b/www/html/Admin/game-quiz-settings.php @@ -0,0 +1,195 @@ + false, 'error' => 'ต้องเปิด PHP extension curl (ติดตั้งแพ็กเกจ php-curl แล้วรีสตาร์ท PHP-FPM)'], 500); +} + +$port = preg_replace('/[^0-9]/', '', (string)(getenv('GAME_NODE_PORT') ?: '3001')) ?: '3001'; +$host = getenv('GAME_NODE_INTERNAL_HOST') ?: '127.0.0.1'; +$target = 'http://' . $host . ':' . $port . '/Game/api/quiz-settings'; + +function quiz_settings_disk_path(): string +{ + return dirname(__DIR__, 2) . '/Game/data/quiz-settings.json'; +} + +/** + * รวมฟิลด์ที่ส่งมาใน PUT เข้ากับไฟล์บนดิสก์ (ไม่ทับฟิลด์ที่ไม่ได้ส่งมา) + */ +function merge_quiz_settings_disk_from_put(string $rawBody): bool +{ + $patch = json_decode($rawBody, true); + if (!is_array($patch)) { + return false; + } + $path = quiz_settings_disk_path(); + $base = []; + if (is_file($path)) { + $prev = @file_get_contents($path); + if ($prev !== false && $prev !== '') { + $d = json_decode($prev, true); + if (is_array($d)) { + $base = $d; + } + } + } + $mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'questions', 'carryQuestions', 'battleQuizMcq']; + foreach ($mergeKeys as $k) { + if (array_key_exists($k, $patch)) { + $base[$k] = $patch[$k]; + } + } + $dir = dirname($path); + if (!is_dir($dir)) { + if (!@mkdir($dir, 0755, true)) { + return false; + } + } + $json = json_encode($base, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + if ($json === false) { + return false; + } + return @file_put_contents($path, $json) !== false; +} + +/** + * Node เก่าอาจไม่ใส่ battleQuizMcq ใน JSON — อ่านจากดิสก์มาแปะให้แอดมินโหลดได้หลังรีเฟรช + */ +function overlay_battle_quiz_mcq_from_disk(string $nodeBody): string +{ + $j = json_decode($nodeBody, true); + if (!is_array($j)) { + return $nodeBody; + } + $path = quiz_settings_disk_path(); + if (!is_file($path)) { + return $nodeBody; + } + $raw = @file_get_contents($path); + if ($raw === false || $raw === '') { + return $nodeBody; + } + $disk = json_decode($raw, true); + if (!is_array($disk) || !array_key_exists('battleQuizMcq', $disk)) { + return $nodeBody; + } + $j['battleQuizMcq'] = $disk['battleQuizMcq']; + $enc = json_encode($j, JSON_UNESCAPED_UNICODE); + return $enc !== false ? $enc : $nodeBody; +} + +function proxy_quiz_curl(string $method, ?string $body = null): void +{ + global $target; + $ch = curl_init($target); + if ($ch === false) { + json_response(['ok' => false, 'error' => 'curl init failed'], 500); + } + $headers = ['Accept: application/json']; + if ($body !== null && $method !== 'GET') { + $headers[] = 'Content-Type: application/json'; + } + $opts = [ + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 15, + ]; + if ($body !== null && $method !== 'GET') { + $opts[CURLOPT_POSTFIELDS] = $body; + } + curl_setopt_array($ch, $opts); + $out = curl_exec($ch); + $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + $err = curl_error($ch); + curl_close($ch); + + if ($method === 'PUT' && $body !== null && $body !== '') { + merge_quiz_settings_disk_from_put($body); + } + + if ($out === false || $out === '') { + if ($method === 'GET') { + $disk = quiz_settings_disk_path(); + if (is_file($disk)) { + $rawFile = @file_get_contents($disk); + if ($rawFile !== false && $rawFile !== '') { + http_response_code(200); + header('Content-Type: application/json; charset=utf-8'); + header('X-Content-Type-Options: nosniff'); + header('Cache-Control: no-store'); + echo $rawFile; + exit; + } + } + } + if ($method === 'PUT' && $body !== null && $body !== '' && is_file(quiz_settings_disk_path())) { + http_response_code(200); + header('Content-Type: application/json; charset=utf-8'); + header('X-Content-Type-Options: nosniff'); + header('Cache-Control: no-store'); + $payload = ['ok' => true, 'diskFallback' => true]; + $rawDisk = @file_get_contents(quiz_settings_disk_path()); + if ($rawDisk !== false && $rawDisk !== '') { + $dj = json_decode($rawDisk, true); + if (is_array($dj)) { + if (isset($dj['carryMapPanelTheme']) && is_array($dj['carryMapPanelTheme'])) { + $payload['carryMapPanelTheme'] = $dj['carryMapPanelTheme']; + } + if (array_key_exists('carryReadMs', $dj)) { + $payload['carryReadMs'] = $dj['carryReadMs']; + } + if (array_key_exists('carryAnswerMs', $dj)) { + $payload['carryAnswerMs'] = $dj['carryAnswerMs']; + } + if (array_key_exists('carrySessionLength', $dj)) { + $payload['carrySessionLength'] = $dj['carrySessionLength']; + } + } + } + echo json_encode($payload, JSON_UNESCAPED_UNICODE); + exit; + } + json_response([ + 'ok' => false, + 'error' => 'ไม่ต่อถึงเซิร์ฟเวอร์เกม (Node) — ตรวจว่า pm2/systemd รัน Game/server.js อยู่ที่พอร์ต ' . (getenv('GAME_NODE_PORT') ?: '3001') . ' · ' . $err, + ], 502); + } + http_response_code($code > 0 ? $code : 500); + header('Content-Type: application/json; charset=utf-8'); + header('X-Content-Type-Options: nosniff'); + header('Cache-Control: no-store'); + if ($method === 'GET' && $code >= 200 && $code < 300) { + echo overlay_battle_quiz_mcq_from_disk($out); + } else { + echo $out; + } + exit; +} + +$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; +if ($method === 'GET') { + proxy_quiz_curl('GET', null); +} +if ($method === 'PUT') { + $raw = file_get_contents('php://input'); + if ($raw === false) { + $raw = ''; + } + proxy_quiz_curl('PUT', $raw); +} + +json_response(['ok' => false, 'error' => 'Method not allowed'], 405); diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html index 570e03d..2ae322c 100644 --- a/www/html/Admin/index.html +++ b/www/html/Admin/index.html @@ -9,7 +9,7 @@ - +
ข้ามไปเนื้อหา @@ -180,6 +180,53 @@ สุ่มคำถามจากชุดจนกว่าจะครบจำนวนนี้ (นับทุกข้อที่จบด้วยถูกหรือหมดเวลา) · 0 = เล่นต่อไม่จบอัตโนมัติ · English: Preview / editor embed only. +