diff --git a/frontend/js/textbook-tracker.js b/frontend/js/textbook-tracker.js index e5e6ad9..7422b0b 100644 --- a/frontend/js/textbook-tracker.js +++ b/frontend/js/textbook-tracker.js @@ -24,11 +24,18 @@ })(); if (!Array.isArray(localState.read)) localState.read = []; - /* ── 1. Server sync (best-effort) ──────────────────────────────── */ + /* ── 1. Server sync (best-effort, с очередью) ───────────────────── + Если POST уже в полёте — следующий патч копится в pendingExtra + и отправляется после завершения. Так ни один mark_read не + теряется при быстрых кликах. */ let syncPending = false; + let pendingExtra = null; function syncToServer(extra) { if (typeof LS === 'undefined' || !LS.getToken || !LS.getToken()) return; - if (syncPending) return; + if (syncPending) { + pendingExtra = Object.assign(pendingExtra || {}, extra || {}); + return; + } syncPending = true; fetch('/api/textbooks/' + slug + '/progress', { method: 'POST', @@ -36,8 +43,14 @@ 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + LS.getToken(), }, - body: JSON.stringify({ last_para: localState.last, ...extra }), - }).finally(() => { syncPending = false; }).catch(() => {}); + body: JSON.stringify({ last_para: localState.last, ...(extra || {}) }), + }).finally(() => { + syncPending = false; + if (pendingExtra) { + const next = pendingExtra; pendingExtra = null; + syncToServer(next); + } + }).catch(() => {}); } /* ── 2. Initial load: merge server data into local state ──────── */ @@ -191,18 +204,23 @@ localState.read.forEach(k => { refreshPillUI(k); refreshCheckUI(k); }); } - /* ── 7. Pill click → mark as last visited ─────────────────────── */ + /* ── 7. Pill click → last_para + (первый раз) mark_read ──────── + Объединяем оба обновления в один POST, чтобы syncPending-guard + в syncToServer не дропнул второй вызов в том же тике. */ function wirePillTracking() { document.body.addEventListener('click', e => { const pill = e.target.closest('.para-pill[data-para]'); if (!pill) return; const key = pill.dataset.para; - setLastPara(key); - // Auto-mark-as-read: первый клик по пилюле = открыл параграф = считается прочитанным - // (мягкая семантика — соответствует реальному поведению учеников) - if (!localState.read.includes(key)) { - markRead(key); + localState.last = key; + const firstTime = !localState.read.includes(key); + if (firstTime) { + localState.read.push(key); + refreshPillUI(key); + refreshCheckUI(key); } + persist(); + syncToServer(firstTime ? { mark_read: key } : {}); }); }