diff --git a/frontend/textbooks/algebra_8.html b/frontend/textbooks/algebra_8.html
index 9e69f88..3d7c9f9 100644
--- a/frontend/textbooks/algebra_8.html
+++ b/frontend/textbooks/algebra_8.html
@@ -1069,6 +1069,52 @@ function bumpProgress(key, delta){
STATE.progress[key] = v;
saveProgress();
refreshProgressUI();
+ if(v >= 50) markParaRead(key);
+}
+
+/* Server sync of read/last_para — пишем в БД, чтобы каталог /textbooks показывал прогресс */
+const _TB_SLUG = 'algebra-8';
+const _markedRead = new Set();
+let _pendingProgressBody = null, _progressTimer = null;
+function _flushProgress(){
+ const body = _pendingProgressBody;
+ _pendingProgressBody = null;
+ if(!body) return;
+ const tok = (window.LS && LS.getToken) ? LS.getToken() : '';
+ if(!tok) return;
+ fetch('/api/textbooks/' + _TB_SLUG + '/progress', {
+ method:'POST',
+ headers:{ 'Content-Type':'application/json', 'Authorization':'Bearer ' + tok },
+ body: JSON.stringify(body),
+ keepalive: true,
+ }).catch(()=>{});
+}
+function _queueProgress(patch){
+ _pendingProgressBody = Object.assign(_pendingProgressBody || {}, patch);
+ if(_progressTimer) clearTimeout(_progressTimer);
+ _progressTimer = setTimeout(_flushProgress, 600);
+}
+function markLastPara(id){ _queueProgress({ last_para: id }); }
+function markParaRead(id){
+ if(_markedRead.has(id)) return;
+ _markedRead.add(id);
+ _queueProgress({ mark_read: id });
+}
+window.addEventListener('beforeunload', _flushProgress);
+function loadServerReadState(){
+ const tok = (window.LS && LS.getToken) ? LS.getToken() : '';
+ if(!tok) return;
+ fetch('/api/textbooks/' + _TB_SLUG, { headers:{ 'Authorization':'Bearer ' + tok } })
+ .then(r => r.ok ? r.json() : null)
+ .then(d => {
+ if(!d || !d.progress) return;
+ (d.progress.read || []).forEach(k => {
+ _markedRead.add(k);
+ if((STATE.progress[k] || 0) < 50) STATE.progress[k] = 100;
+ });
+ saveProgress(); refreshProgressUI();
+ })
+ .catch(()=>{});
}
function refreshProgressUI(){
const total = Object.values(STATE.progress).reduce((a,b)=>a+b,0) / 7;
@@ -1204,6 +1250,7 @@ function _goToFinish(id){
if(window.renderMathInElement){
setTimeout(()=>renderMathInElement(el, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}),0);
}
+ markLastPara(id);
}
/* ════════════════════════════════════════════════════════
@@ -1460,6 +1507,7 @@ function init(){
buildParaSelector();
refreshProgressUI();
initMobileSidebar();
+ loadServerReadState();
goTo('p1'); // строит только §1, остальные — лениво при переходе
setTimeout(()=>achievement('start','Начало пути по корням!'), 800);
// Sync XP с сервером: если серверный XP выше — обновляем локальный прогресс
diff --git a/frontend/textbooks/algebra_8_ch2.html b/frontend/textbooks/algebra_8_ch2.html
index 84f5320..a6e3417 100644
--- a/frontend/textbooks/algebra_8_ch2.html
+++ b/frontend/textbooks/algebra_8_ch2.html
@@ -541,6 +541,52 @@ function bumpProgress(key, delta){
STATE.progress[key] = Math.max(0, Math.min(100, (STATE.progress[key]||0) + delta));
saveProgress();
refreshProgressUI();
+ if(STATE.progress[key] >= 50) markParaRead(key);
+}
+
+/* Server sync of read/last_para — пишем в БД, чтобы каталог /textbooks показывал прогресс */
+const _TB_SLUG = 'algebra-8-ch2';
+const _markedRead = new Set();
+let _pendingProgressBody = null, _progressTimer = null;
+function _flushProgress(){
+ const body = _pendingProgressBody;
+ _pendingProgressBody = null;
+ if(!body) return;
+ const tok = (window.LS && LS.getToken) ? LS.getToken() : '';
+ if(!tok) return;
+ fetch('/api/textbooks/' + _TB_SLUG + '/progress', {
+ method:'POST',
+ headers:{ 'Content-Type':'application/json', 'Authorization':'Bearer ' + tok },
+ body: JSON.stringify(body),
+ keepalive: true,
+ }).catch(()=>{});
+}
+function _queueProgress(patch){
+ _pendingProgressBody = Object.assign(_pendingProgressBody || {}, patch);
+ if(_progressTimer) clearTimeout(_progressTimer);
+ _progressTimer = setTimeout(_flushProgress, 600);
+}
+function markLastPara(id){ _queueProgress({ last_para: id }); }
+function markParaRead(id){
+ if(_markedRead.has(id)) return;
+ _markedRead.add(id);
+ _queueProgress({ mark_read: id });
+}
+window.addEventListener('beforeunload', _flushProgress);
+function loadServerReadState(){
+ const tok = (window.LS && LS.getToken) ? LS.getToken() : '';
+ if(!tok) return;
+ fetch('/api/textbooks/' + _TB_SLUG, { headers:{ 'Authorization':'Bearer ' + tok } })
+ .then(r => r.ok ? r.json() : null)
+ .then(d => {
+ if(!d || !d.progress) return;
+ (d.progress.read || []).forEach(k => {
+ _markedRead.add(k);
+ if((STATE.progress[k] || 0) < 50) STATE.progress[k] = 100;
+ });
+ saveProgress(); refreshProgressUI();
+ })
+ .catch(()=>{});
}
function addXp(n, src){
if(!n) return;
@@ -646,6 +692,7 @@ function goTo(id){
if(window.renderMathInElement) setTimeout(()=>renderMath(el), 0);
// glossary wrap — пройти по тексту секции и обернуть термины
setTimeout(()=>{ try { wrapGlossary(el); } catch(e){} }, 60);
+ markLastPara(id);
}
/* SIDEBAR */
@@ -1206,6 +1253,7 @@ function init(){
initSearch();
buildParaSelector();
refreshProgressUI();
+ loadServerReadState();
goTo('p7');
setTimeout(()=>achievement('start','Начало главы 2!'), 600);
// Sync XP с сервером: если серверный XP выше — обновляем локальный прогресс