fix(tracker): mark_read больше не дропается из-за syncPending
Раньше: клик по .para-pill вызывал setLastPara() → POST с last_para
→ syncPending=true. Тут же вызывался markRead() → второй POST с
mark_read → guard 'if (syncPending) return' молча отбрасывал его.
Результат: каталог показывал 'Продолжить' (last_para пришёл),
но '0 из N прочитано' (paragraphs_read остался пуст).
Два уровня фикса:
1) wirePillTracking объединяет last_para + mark_read в ОДИН POST
через коалесцирующий syncToServer(firstTime ? {mark_read:key} : {})
2) syncToServer теперь не дропает патчи: если предыдущий POST в
полёте, новые поля сохраняются в pendingExtra и отправляются
после .finally() — гарантия 'ни один mark_read не теряется'.
Затрагивает chemistry-9, physics-9, physics8_thermal/electro/optics —
у них теперь '0/N прочитано' начнёт расти при кликах по пилюлям.
This commit is contained in:
@@ -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 } : {});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user