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 = [];
|
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 syncPending = false;
|
||||||
|
let pendingExtra = null;
|
||||||
function syncToServer(extra) {
|
function syncToServer(extra) {
|
||||||
if (typeof LS === 'undefined' || !LS.getToken || !LS.getToken()) return;
|
if (typeof LS === 'undefined' || !LS.getToken || !LS.getToken()) return;
|
||||||
if (syncPending) return;
|
if (syncPending) {
|
||||||
|
pendingExtra = Object.assign(pendingExtra || {}, extra || {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
syncPending = true;
|
syncPending = true;
|
||||||
fetch('/api/textbooks/' + slug + '/progress', {
|
fetch('/api/textbooks/' + slug + '/progress', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -36,8 +43,14 @@
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': 'Bearer ' + LS.getToken(),
|
'Authorization': 'Bearer ' + LS.getToken(),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ last_para: localState.last, ...extra }),
|
body: JSON.stringify({ last_para: localState.last, ...(extra || {}) }),
|
||||||
}).finally(() => { syncPending = false; }).catch(() => {});
|
}).finally(() => {
|
||||||
|
syncPending = false;
|
||||||
|
if (pendingExtra) {
|
||||||
|
const next = pendingExtra; pendingExtra = null;
|
||||||
|
syncToServer(next);
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 2. Initial load: merge server data into local state ──────── */
|
/* ── 2. Initial load: merge server data into local state ──────── */
|
||||||
@@ -191,18 +204,23 @@
|
|||||||
localState.read.forEach(k => { refreshPillUI(k); refreshCheckUI(k); });
|
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() {
|
function wirePillTracking() {
|
||||||
document.body.addEventListener('click', e => {
|
document.body.addEventListener('click', e => {
|
||||||
const pill = e.target.closest('.para-pill[data-para]');
|
const pill = e.target.closest('.para-pill[data-para]');
|
||||||
if (!pill) return;
|
if (!pill) return;
|
||||||
const key = pill.dataset.para;
|
const key = pill.dataset.para;
|
||||||
setLastPara(key);
|
localState.last = key;
|
||||||
// Auto-mark-as-read: первый клик по пилюле = открыл параграф = считается прочитанным
|
const firstTime = !localState.read.includes(key);
|
||||||
// (мягкая семантика — соответствует реальному поведению учеников)
|
if (firstTime) {
|
||||||
if (!localState.read.includes(key)) {
|
localState.read.push(key);
|
||||||
markRead(key);
|
refreshPillUI(key);
|
||||||
|
refreshCheckUI(key);
|
||||||
}
|
}
|
||||||
|
persist();
|
||||||
|
syncToServer(firstTime ? { mark_read: key } : {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user