From fa31704a56d6d941b5cafc042012902a37131b5f Mon Sep 17 00:00:00 2001 From: CAtlas0784 Date: Sun, 18 Oct 2026 20:31:15 +0700 Subject: [PATCH] fix: sync avatar details and Srtool session --- README.md | 4 ++++ js/app.js | 43 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d2cd05b..5645509 100644 --- a/README.md +++ b/README.md @@ -111,3 +111,7 @@ GaC-Srtool สื่อสารกับเซิร์ฟเวอร์ GaC-h | `/api/avatar/rank` | POST | เปลี่ยนระดับ Eidolon ของตัวละคร | | `/api/avatar/max_all` | POST | ปรับตัวละครทุกตัวเป็น Max E6 และสกิลเต็ม | | `/api/command` | POST | ส่งคำสั่ง GM ไปยังเซิร์ฟเวอร์ | + +### บันทึกตัวละครและ Traces + +การบันทึกจากหน้ารายละเอียดส่งข้อมูลตัวละครทั้งชุดผ่าน `/api/freesr` ในคำขอเดียว รวมถึง `data.skills` และ `data.skills_by_anchor_type` สำหรับ Traces เพื่อไม่ให้ server sync ข้อมูลระดับ/Eidolon ระหว่างทางก่อนข้อมูล Trace จะถูกบันทึก `saveAvatarDetails(closeModal = true)` ยังรองรับการเรียกจากปุ่ม build ที่ไม่ต้องปิดหน้าต่าง ถ้า server ตอบผิดพลาดจะแสดงข้อความบันทึกไม่สำเร็จแทนการแจ้งว่าสำเร็จ diff --git a/js/app.js b/js/app.js index a1bc33a..356f11a 100644 --- a/js/app.js +++ b/js/app.js @@ -381,7 +381,8 @@ async function apiCall(endpoint, method = 'GET', data = null) { const options = { method, headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'X-GaC-Srtool': '1' } }; @@ -409,8 +410,34 @@ async function apiCall(endpoint, method = 'GET', data = null) { } } +// Explicit, expiring custom-battle connection. The backend returns its TTL. +let toolSessionTimer = null; +let toolSessionConnected = false; +let toolSessionGeneration = 0; +async function syncToolSession(connected) { + const generation = ++toolSessionGeneration; + if (toolSessionTimer) { clearTimeout(toolSessionTimer); toolSessionTimer = null; } + toolSessionConnected = connected; + try { + const result = await apiCall('/api/tool-session', 'POST', { connected, client: 'GaC-Srtool' }); + if (generation === toolSessionGeneration && connected && toolSessionConnected && Number(result.lease_seconds) > 0) { + toolSessionTimer = setTimeout(() => syncToolSession(true), Number(result.lease_seconds) * 1000 / 3); + } + } catch (error) { + if (generation === toolSessionGeneration) toolSessionConnected = false; + // Older backends remain usable, but cannot enable the custom encounter lease. + console.warn('Tool session unavailable:', error.message); + } +} +window.addEventListener('pagehide', () => { + if (!toolSessionConnected) return; + const base = state.apiUrl.replace(/\/+$/, ''); + navigator.sendBeacon(`${base}/api/tool-session`, JSON.stringify({ connected: false, client: 'GaC-Srtool' })); +}); + // Update connection status UI function setConnectionStatus(connected, extraText = '') { + if (connected !== toolSessionConnected) syncToolSession(connected); state.isConnected = connected; if (connected) { el.statusDot.classList.add('connected'); @@ -4600,7 +4627,7 @@ async function maxAllBuildForCurrentAvatar() { } // Save avatar details from modal -async function saveAvatarDetails() { +async function saveAvatarDetails(closeModal = true) { if (!state.selectedAvatarDetail) return; const aid = state.selectedAvatarDetail.avatar_id; const level = parseInt(el.modalCharLevel.value, 10) || 80; @@ -4735,15 +4762,13 @@ async function saveAvatarDetails() { if (state.isConnected) { try { - if (state.serverMode === 'gac') { - await apiCall('/api/freesr/avatars', 'POST', { avatar_id: aid, level, promotion, rank }); - await saveFreesrChangesToServer(); - } else { - await saveFreesrChangesToServer(); - } + // Save the complete avatar record once. The full payload includes skills + // and Trace values; a preceding rank-only request can trigger live sync + // with an incomplete intermediate state. + await saveFreesrChangesToServer(); showToast(t('toastCharSaved', { name: getAvatarInfo(aid).name }), 'success'); } catch (err) { - showToast(t('toastCharSaved', { name: getAvatarInfo(aid).name }), 'info'); + showToast(`บันทึกไม่สำเร็จ: ${err.message}`, 'error'); } } else { showToast(t('toastCharSaved', { name: getAvatarInfo(aid).name }), 'success');