diff --git a/backend/scripts/wrap_textbook_xp.py b/backend/scripts/wrap_textbook_xp.py
new file mode 100644
index 0000000..f8ad9ba
--- /dev/null
+++ b/backend/scripts/wrap_textbook_xp.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python3
+"""
+wrap_textbook_xp.py — Add `data-gamified` attribute to XP UI in textbooks.
+
+Part of Phase 1 of the gamification kill-switch (Variant 2 of plan §E.3).
+The CSS rule `body.no-gamification [data-gamified] { display:none }`
+catches every wrapped block in one selector, so future authors don't have
+to invent new class names just to be hidden by the master switch.
+
+What we wrap:
+ •
— chapter & hub hero badges
+ •
— hub overall-progress XP pill
+ •
— chapter inline progress card
+ • inline JS that generates `
` HTML strings
+ • `.xp-bar` containers when they're standalone
+
+Idempotent: if `data-gamified` is already present on an opening tag the
+script skips it. Reports each file touched.
+
+Run from repo root:
+ python backend/scripts/wrap_textbook_xp.py
+"""
+import re
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2] / "frontend" / "textbooks"
+if not ROOT.is_dir():
+ print(f"[wrap-xp] textbook dir not found: {ROOT}", file=sys.stderr)
+ sys.exit(1)
+
+# Selectors that mark an opening tag as an XP block. The first group
+# captures `
`.
+PATTERNS = [
+ # <... id="hero-xp-badge" ...>
+ re.compile(r'(<[a-zA-Z][^>]*?\bid="hero-xp-badge"[^>]*?)(>)'),
+ # <... class="po-xp" ...> or class="...po-xp..."
+ re.compile(r'(<[a-zA-Z][^>]*?\bclass="[^"]*\bpo-xp\b[^"]*"[^>]*?)(>)'),
+ # <... class="...xp-card..."> — covers HTML and inline JS `''`
+ re.compile(r'(<[a-zA-Z][^>]*?\bclass="[^"]*\bxp-card\b[^"]*"[^>]*?)(>)'),
+ # Standalone class="hero-xp-badge"
+ re.compile(r'(<[a-zA-Z][^>]*?\bclass="[^"]*\bhero-xp-badge\b[^"]*"[^>]*?)(>)'),
+]
+
+# Already-wrapped check
+RX_HAS_ATTR = re.compile(r'\bdata-gamified\b')
+
+
+def patch_file(path: Path) -> int:
+ """Return number of opening tags newly given data-gamified."""
+ text = path.read_text(encoding="utf-8")
+ new = text
+ changes = 0
+ for rx in PATTERNS:
+ def sub(m, _rx=rx):
+ nonlocal changes
+ head, close = m.group(1), m.group(2)
+ if RX_HAS_ATTR.search(head):
+ return m.group(0)
+ changes += 1
+ sep = "" if head.endswith(" ") else " "
+ return f'{head}{sep}data-gamified{close}'
+ new = rx.sub(sub, new)
+ if new != text:
+ path.write_text(new, encoding="utf-8")
+ return changes
+
+
+def main():
+ files = sorted(ROOT.glob("*.html"))
+ total_files = 0
+ total_tags = 0
+ for f in files:
+ try:
+ n = patch_file(f)
+ except Exception as e:
+ print(f"[wrap-xp] {f.name}: ERROR {e}", file=sys.stderr)
+ continue
+ if n:
+ print(f"[wrap-xp] {f.name}: +{n} tag(s)")
+ total_files += 1
+ total_tags += n
+ print(f"[wrap-xp] done — patched {total_tags} tags across {total_files} files "
+ f"(scanned {len(files)})")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/backend/src/controllers/adminController.js b/backend/src/controllers/adminController.js
index 2377865..54c9558 100644
--- a/backend/src/controllers/adminController.js
+++ b/backend/src/controllers/adminController.js
@@ -509,10 +509,12 @@ function getFeatures(_req, res) {
/* ── PATCH /api/admin/features ──────────────────────────────────────── */
function updateFeatures(req, res) {
const allowed = ['crossword', 'hangman', 'pet', 'red_book', 'collection',
- 'flashcards', 'knowledge_map', 'board', 'biochem', 'live_quiz', 'classroom'];
+ 'flashcards', 'knowledge_map', 'board', 'biochem', 'live_quiz', 'classroom',
+ 'gamification'];
const updates = req.body;
const stmt = db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)");
const getOld = db.prepare("SELECT value FROM app_settings WHERE key = ?");
+ let touchedGamification = false;
for (const [name, enabled] of Object.entries(updates)) {
if (!allowed.includes(name)) continue;
const settingKey = `feature_${name}_enabled`;
@@ -521,6 +523,15 @@ function updateFeatures(req, res) {
const newVal = enabled ? '1' : '0';
stmt.run(settingKey, newVal);
audit(req, 'feature.update', `feature:${name}`, `${oldVal} -> ${newVal}`);
+ if (name === 'gamification') touchedGamification = true;
+ }
+ // Invalidate the gamification cache so the kill-switch takes effect
+ // immediately (otherwise awardXP keeps running for up to 30s).
+ if (touchedGamification) {
+ try {
+ const { invalidateGamificationCache } = require('./gamification/_shared');
+ invalidateGamificationCache();
+ } catch { /* defensive — shouldn't fail */ }
}
res.json({ ok: true });
}
diff --git a/backend/src/controllers/gamification/_shared.js b/backend/src/controllers/gamification/_shared.js
index 0510e62..f48cd3f 100644
--- a/backend/src/controllers/gamification/_shared.js
+++ b/backend/src/controllers/gamification/_shared.js
@@ -187,8 +187,42 @@ const stmts = {
ORDER BY up.purchased_at DESC LIMIT 20`),
};
+/* ── Kill-switch ─────────────────────────────────────────────────
+ The 'gamification' feature flag is the master switch. When admin
+ turns it off, ALL XP/coin awards become no-ops, /api/gamification
+ routes return 404, and the front-end hides every XP/coin/streak
+ element via body.no-gamification.
+
+ The flag is read often (every awardXP call) so we cache the value
+ and only re-read after a short TTL. Toggling the flag also calls
+ invalidateGamificationCache() from adminController so the change
+ takes effect immediately without waiting for the TTL. */
+const _gamCache = { value: null, expiresAt: 0 };
+const _GAM_TTL_MS = 30_000;
+const _stmtFeatureGam = db.prepare("SELECT value FROM app_settings WHERE key = 'feature_gamification_enabled'");
+
+function isGamificationEnabled() {
+ const now = Date.now();
+ if (_gamCache.value !== null && now < _gamCache.expiresAt) return _gamCache.value;
+ let on = true;
+ try {
+ const row = _stmtFeatureGam.get();
+ // Default ON if row missing (migration 029 seeds '1', but be defensive).
+ on = !row || row.value === '1';
+ } catch { on = true; }
+ _gamCache.value = on;
+ _gamCache.expiresAt = now + _GAM_TTL_MS;
+ return on;
+}
+
+function invalidateGamificationCache() {
+ _gamCache.value = null;
+ _gamCache.expiresAt = 0;
+}
+
module.exports = {
xpToLevel, levelMinXp, levelMaxXp, rankName, RANKS,
GOAL_TIERS, ACHIEVEMENT_DEFS, AVATAR_FRAMES,
stmts,
+ isGamificationEnabled, invalidateGamificationCache,
};
diff --git a/backend/src/controllers/gamification/service.js b/backend/src/controllers/gamification/service.js
index de704ad..8b6ff49 100644
--- a/backend/src/controllers/gamification/service.js
+++ b/backend/src/controllers/gamification/service.js
@@ -4,7 +4,7 @@ const sse = require('../../sse');
const { pushParentNotif } = require('../../utils/notifications');
const {
stmts, xpToLevel, levelMinXp, levelMaxXp, rankName,
- GOAL_TIERS, ACHIEVEMENT_DEFS,
+ GOAL_TIERS, ACHIEVEMENT_DEFS, isGamificationEnabled,
} = require('./_shared');
/* ═══════════════════════════════════════════════════════════════════════
@@ -16,12 +16,14 @@ const {
/* ── Coins ─────────────────────────────────────────────────────────── */
function awardCoins(userId, amount /*, reason */) {
if (!amount || amount <= 0) return;
+ if (!isGamificationEnabled()) return; // master kill-switch
stmts.incrCoins.run(amount, userId);
}
/* ── XP ────────────────────────────────────────────────────────────── */
function awardXP(userId, amount, reason) {
if (!amount || amount <= 0) return;
+ if (!isGamificationEnabled()) return; // master kill-switch
stmts.insertXpLog.run(userId, amount, reason);
stmts.incrXP.run(amount, userId);
const user = stmts.getXP.get(userId);
@@ -52,6 +54,7 @@ function getXPInfo(userId) {
/* ── Streak (called by onTestFinished) ─────────────────────────────── */
function updateStreak(userId) {
+ if (!isGamificationEnabled()) return;
const user = stmts.getStreak.get(userId);
if (!user) return;
const today = new Date().toISOString().slice(0, 10);
@@ -103,6 +106,7 @@ function pushAchievementNotif(userId, ach) {
}
function unlockAchievement(userId, slug) {
+ if (!isGamificationEnabled()) return false;
const ach = stmts.getAchBySlug.get(slug);
if (!ach) return false;
const exists = stmts.hasUserAch.get(userId, ach.id);
@@ -114,6 +118,7 @@ function unlockAchievement(userId, slug) {
}
function checkAchievements(userId) {
+ if (!isGamificationEnabled()) return;
const row = stmts.getUserForAch.get(userId);
if (!row) return;
const { test_count: testCount, perfect_count: perfectCount, class_count: classCount } = row;
diff --git a/backend/src/db/migrations/029_gamification_feature_flag.sql b/backend/src/db/migrations/029_gamification_feature_flag.sql
new file mode 100644
index 0000000..79e287b
--- /dev/null
+++ b/backend/src/db/migrations/029_gamification_feature_flag.sql
@@ -0,0 +1,15 @@
+-- ═══════════════════════════════════════════════════════════════
+-- 029: Seed the gamification feature flag
+--
+-- Until now there was no row for feature_gamification_enabled in
+-- app_settings — meaning /api/features returned `gamification: undefined`
+-- and the front-end kill-switch (`body.no-gamification`) never engaged,
+-- regardless of admin intent.
+--
+-- Default = ON. Admins can disable via PATCH /api/admin/features
+-- once 'gamification' is added to the allowed list (handled in
+-- adminController.js, not this migration).
+-- ═══════════════════════════════════════════════════════════════
+
+INSERT OR IGNORE INTO app_settings (key, value)
+ VALUES ('feature_gamification_enabled', '1');
diff --git a/backend/src/routes/gamification.js b/backend/src/routes/gamification.js
index 5db65ee..98f7d84 100644
--- a/backend/src/routes/gamification.js
+++ b/backend/src/routes/gamification.js
@@ -8,6 +8,18 @@ const {
onLabExperiment, selfAward,
adminAward, adminReset, adminGamStats, adminGetUser
} = require('../controllers/gamificationController');
+const { isGamificationEnabled } = require('../controllers/gamification/_shared');
+
+/* When gamification is globally disabled, user-facing routes return 404.
+ Admin routes (under /admin/*) stay accessible so the kill-switch itself
+ can still be flipped from the panel. */
+function gamGate(req, res, next) {
+ if (req.path.startsWith('/admin/')) return next();
+ if (!isGamificationEnabled()) {
+ return res.status(404).json({ error: 'Gamification disabled' });
+ }
+ next();
+}
const labLimiter = rateLimit({ windowMs: 60_000, max: 30, message: 'Слишком частые запросы лаборатории' });
const labSchema = { body: { reactionsDiscovered: { type: 'number', min: 0, max: 100, integer: true } } };
@@ -22,6 +34,7 @@ const selfAwardSchema = {
};
router.use(authMiddleware);
+router.use(gamGate);
router.get('/me', getMe);
router.get('/achievements', getAchievements);
diff --git a/backend/src/routes/shop.js b/backend/src/routes/shop.js
index c5c8154..a00a6a2 100644
--- a/backend/src/routes/shop.js
+++ b/backend/src/routes/shop.js
@@ -6,6 +6,16 @@ const {
getItems, purchaseItem, getPurchases, getCoins, getMyActive, activateItem,
adminGetItems, adminCreateItem, adminUpdateItem, adminDeleteItem, adminAwardCoins, adminShopStats
} = require('../controllers/shopController');
+const { isGamificationEnabled } = require('../controllers/gamification/_shared');
+
+/* Same kill-switch as gamification routes — shop is part of the gam loop. */
+function shopGate(req, res, next) {
+ if (req.path.startsWith('/admin/')) return next();
+ if (!isGamificationEnabled()) {
+ return res.status(404).json({ error: 'Gamification disabled' });
+ }
+ next();
+}
const purchaseLimiter = rateLimit({ windowMs: 60_000, max: 10, message: 'Слишком много покупок, подождите минуту' });
const activateSchema = { body: { type: { type: 'string', oneOf: ['frame', 'title', 'effect'] } } };
@@ -20,6 +30,7 @@ const awardCoinsSchema = { body: {
}};
router.use(authMiddleware);
+router.use(shopGate);
router.get('/items', getItems);
router.post('/items/:id/purchase', requirePermission('shop.purchase'), purchaseLimiter, purchaseItem);
diff --git a/frontend/css/ls.css b/frontend/css/ls.css
index 4b7814c..ea7c629 100644
--- a/frontend/css/ls.css
+++ b/frontend/css/ls.css
@@ -1017,13 +1017,33 @@ body {
/* Student without a class: hide leaderboard */
body.no-class #lb-section { display: none !important; }
+/* Gamification kill-switch.
+ When admin turns off the feature, body.no-gamification is set by
+ api.js/hideDisabledFeatures and EVERY XP / coin / streak / shop /
+ achievement / frame element must vanish — across the whole app,
+ not just the dashboard. The rules below cover:
+ • dashboard widgets (.gam-bar, .lb-widget)
+ • profile tabs (achievements, shop, frames)
+ • textbook XP badges and progress cards (.hero-xp-badge, .po-xp,
+ .xp-bar, .xp-card)
+ • a catch-all [data-gamified] hook that wraps any future block —
+ authors of new pages should wrap XP UI in a
+ instead of inventing new classes. */
body.no-gamification .gam-bar,
body.no-gamification .lb-widget,
body.no-gamification .achievements-section,
body.no-gamification #tab-btn-achievements,
body.no-gamification #tab-btn-shop,
body.no-gamification #tab-achievements,
-body.no-gamification #tab-shop { display: none !important; }
+body.no-gamification #tab-shop,
+body.no-gamification #frames-section,
+body.no-gamification .hero-xp-badge,
+body.no-gamification .po-xp,
+body.no-gamification .xp-card,
+body.no-gamification .xp-bar,
+body.no-gamification .xp-pill,
+body.no-gamification .xp-badge,
+body.no-gamification [data-gamified] { display: none !important; }
/* ══════════════════════════════════════════
RESPONSIVE — SMALL PHONES (≤ 480px)
diff --git a/frontend/textbooks/algebra_10_ch1.html b/frontend/textbooks/algebra_10_ch1.html
index c43a018..c2962ba 100644
--- a/frontend/textbooks/algebra_10_ch1.html
+++ b/frontend/textbooks/algebra_10_ch1.html
@@ -233,7 +233,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -565,7 +565,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/algebra_10_ch2.html b/frontend/textbooks/algebra_10_ch2.html
index a92e311..49c193c 100644
--- a/frontend/textbooks/algebra_10_ch2.html
+++ b/frontend/textbooks/algebra_10_ch2.html
@@ -204,7 +204,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -423,7 +423,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+='
'+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/algebra_10_ch3.html b/frontend/textbooks/algebra_10_ch3.html
index 5cf27ad..9674e0d 100644
--- a/frontend/textbooks/algebra_10_ch3.html
+++ b/frontend/textbooks/algebra_10_ch3.html
@@ -208,7 +208,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -429,7 +429,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/algebra_10_hub.html b/frontend/textbooks/algebra_10_hub.html
index f4a7e46..eb75518 100644
--- a/frontend/textbooks/algebra_10_hub.html
+++ b/frontend/textbooks/algebra_10_hub.html
@@ -137,7 +137,7 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
Загрузка...
- 0 XP
+ 0 XP
diff --git a/frontend/textbooks/algebra_11_ch1.html b/frontend/textbooks/algebra_11_ch1.html
index 0de30ad..fc8a740 100644
--- a/frontend/textbooks/algebra_11_ch1.html
+++ b/frontend/textbooks/algebra_11_ch1.html
@@ -253,7 +253,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -426,7 +426,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/algebra_11_ch2.html b/frontend/textbooks/algebra_11_ch2.html
index b06e806..9d84125 100644
--- a/frontend/textbooks/algebra_11_ch2.html
+++ b/frontend/textbooks/algebra_11_ch2.html
@@ -233,7 +233,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -406,7 +406,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/algebra_11_ch3.html b/frontend/textbooks/algebra_11_ch3.html
index 819395b..968e526 100644
--- a/frontend/textbooks/algebra_11_ch3.html
+++ b/frontend/textbooks/algebra_11_ch3.html
@@ -254,7 +254,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -432,7 +432,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/algebra_11_hub.html b/frontend/textbooks/algebra_11_hub.html
index 555cf75..6025c6a 100644
--- a/frontend/textbooks/algebra_11_hub.html
+++ b/frontend/textbooks/algebra_11_hub.html
@@ -228,7 +228,7 @@ html.dark .final-cta-sub{color:#fcd34d}
Загрузка...
- 0 XP
+ 0 XP
diff --git a/frontend/textbooks/algebra_7_ch1.html b/frontend/textbooks/algebra_7_ch1.html
index 40bfd76..97a987d 100644
--- a/frontend/textbooks/algebra_7_ch1.html
+++ b/frontend/textbooks/algebra_7_ch1.html
@@ -286,7 +286,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -541,7 +541,7 @@ function buildSidebar(id){
const xpForLv = _xpForLevel(STATE.level), xpNext = _xpForLevel(STATE.level + 1);
const xpInLv = STATE.xp - xpForLv, xpRange = xpNext - xpForLv;
const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100;
- html += 'XP-прогрессУр. ' + STATE.level + '
' + STATE.xp + ' XP' + xpNext + ' XP
';
+ html += 'XP-прогрессУр. ' + STATE.level + '
' + STATE.xp + ' XP' + xpNext + ' XP
';
html += '' + sb.title + '
';
sb.rows.forEach(([k,v])=>{ html += '
' + k + '' + (v ? ' — ' + v : '') + '
'; });
html += '
';
diff --git a/frontend/textbooks/algebra_7_ch2.html b/frontend/textbooks/algebra_7_ch2.html
index d7caa7c..efada5f 100644
--- a/frontend/textbooks/algebra_7_ch2.html
+++ b/frontend/textbooks/algebra_7_ch2.html
@@ -253,7 +253,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -528,7 +528,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/algebra_7_ch3.html b/frontend/textbooks/algebra_7_ch3.html
index e43a62e..5a88fbc 100644
--- a/frontend/textbooks/algebra_7_ch3.html
+++ b/frontend/textbooks/algebra_7_ch3.html
@@ -246,7 +246,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -484,7 +484,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/algebra_7_ch4.html b/frontend/textbooks/algebra_7_ch4.html
index f2f5a68..db80adb 100644
--- a/frontend/textbooks/algebra_7_ch4.html
+++ b/frontend/textbooks/algebra_7_ch4.html
@@ -241,7 +241,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -462,7 +462,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/algebra_7_hub.html b/frontend/textbooks/algebra_7_hub.html
index 36eca89..01fe758 100644
--- a/frontend/textbooks/algebra_7_hub.html
+++ b/frontend/textbooks/algebra_7_hub.html
@@ -142,7 +142,7 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
Загрузка...
- 0 XP
+ 0 XP
diff --git a/frontend/textbooks/algebra_8.html b/frontend/textbooks/algebra_8.html
index 990ed7b..99a9a75 100644
--- a/frontend/textbooks/algebra_8.html
+++ b/frontend/textbooks/algebra_8.html
@@ -788,7 +788,7 @@ input,select,textarea{font-family:inherit}
0%
-
+
@@ -1341,8 +1341,8 @@ function buildSidebar(id){
const xpInLevel = STATE.xp - xpForLevel;
const xpRange = xpNext - xpForLevel;
const xpPct = xpRange > 0 ? Math.round(xpInLevel / xpRange * 100) : 100;
- let html = `
-
+ let html = `
+
XP-прогресс
Ур. ${STATE.level}
diff --git a/frontend/textbooks/algebra_8_ch2.html b/frontend/textbooks/algebra_8_ch2.html
index e2b9127..df2226c 100644
--- a/frontend/textbooks/algebra_8_ch2.html
+++ b/frontend/textbooks/algebra_8_ch2.html
@@ -373,7 +373,7 @@ input,select,textarea{font-family:inherit}
0%
-
+
@@ -764,8 +764,8 @@ function buildSidebar(id){
const xpInLv = STATE.xp - xpForLv;
const xpRange = xpNext - xpForLv;
const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100;
- html += `
-
+ html += `
+
XP-прогресс
Ур. ${STATE.level}
diff --git a/frontend/textbooks/algebra_8_ch3.html b/frontend/textbooks/algebra_8_ch3.html
index 5b42489..aae9a2a 100644
--- a/frontend/textbooks/algebra_8_ch3.html
+++ b/frontend/textbooks/algebra_8_ch3.html
@@ -299,7 +299,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -679,8 +679,8 @@ function buildSidebar(id){
const xpInLv = STATE.xp - xpForLv;
const xpRange = xpNext - xpForLv;
const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100;
- html += `
-
+ html += `
+
XP-прогресс
Ур. ${STATE.level}
diff --git a/frontend/textbooks/algebra_8_hub.html b/frontend/textbooks/algebra_8_hub.html
index 2f134ae..3a61b8c 100644
--- a/frontend/textbooks/algebra_8_hub.html
+++ b/frontend/textbooks/algebra_8_hub.html
@@ -147,7 +147,7 @@ html.dark .ch-tag.indigo{color:#818cf8}
Загрузка...
-
0 XP
+
0 XP
diff --git a/frontend/textbooks/algebra_9_ch1.html b/frontend/textbooks/algebra_9_ch1.html
index e9e4048..7a7111a 100644
--- a/frontend/textbooks/algebra_9_ch1.html
+++ b/frontend/textbooks/algebra_9_ch1.html
@@ -229,7 +229,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -411,7 +411,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+='
'+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/algebra_9_ch2.html b/frontend/textbooks/algebra_9_ch2.html
index fbb048a..7e1fe8e 100644
--- a/frontend/textbooks/algebra_9_ch2.html
+++ b/frontend/textbooks/algebra_9_ch2.html
@@ -228,7 +228,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -406,7 +406,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='
XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+='
'+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/algebra_9_ch3.html b/frontend/textbooks/algebra_9_ch3.html
index 31d6b60..56ebc1e 100644
--- a/frontend/textbooks/algebra_9_ch3.html
+++ b/frontend/textbooks/algebra_9_ch3.html
@@ -229,7 +229,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -407,7 +407,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/algebra_9_ch4.html b/frontend/textbooks/algebra_9_ch4.html
index b231f46..194290b 100644
--- a/frontend/textbooks/algebra_9_ch4.html
+++ b/frontend/textbooks/algebra_9_ch4.html
@@ -224,7 +224,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -410,7 +410,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/algebra_9_hub.html b/frontend/textbooks/algebra_9_hub.html
index a27dcec..e8dff14 100644
--- a/frontend/textbooks/algebra_9_hub.html
+++ b/frontend/textbooks/algebra_9_hub.html
@@ -229,7 +229,7 @@ html.dark .final-cta-sub{color:#fcd34d}
Загрузка...
- 0 XP
+ 0 XP
diff --git a/frontend/textbooks/geometry_10_hub.html b/frontend/textbooks/geometry_10_hub.html
index 8ee0928..93ab9b0 100644
--- a/frontend/textbooks/geometry_10_hub.html
+++ b/frontend/textbooks/geometry_10_hub.html
@@ -233,7 +233,7 @@ html.dark .final-cta-sub{color:#fcd34d}
Загрузка...
- 0 XP
+ 0 XP
diff --git a/frontend/textbooks/geometry_10_r1.html b/frontend/textbooks/geometry_10_r1.html
index 0b4df36..a4fef95 100644
--- a/frontend/textbooks/geometry_10_r1.html
+++ b/frontend/textbooks/geometry_10_r1.html
@@ -212,7 +212,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -370,7 +370,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_10_r2.html b/frontend/textbooks/geometry_10_r2.html
index 0e348e2..1c1824e 100644
--- a/frontend/textbooks/geometry_10_r2.html
+++ b/frontend/textbooks/geometry_10_r2.html
@@ -206,7 +206,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -365,7 +365,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_10_r3.html b/frontend/textbooks/geometry_10_r3.html
index d2ed8d4..0bf42aa 100644
--- a/frontend/textbooks/geometry_10_r3.html
+++ b/frontend/textbooks/geometry_10_r3.html
@@ -206,7 +206,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -369,7 +369,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_10_r4.html b/frontend/textbooks/geometry_10_r4.html
index 59359c8..e0f08b2 100644
--- a/frontend/textbooks/geometry_10_r4.html
+++ b/frontend/textbooks/geometry_10_r4.html
@@ -209,7 +209,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -374,7 +374,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_11_ch1.html b/frontend/textbooks/geometry_11_ch1.html
index 4ad49eb..4b61063 100644
--- a/frontend/textbooks/geometry_11_ch1.html
+++ b/frontend/textbooks/geometry_11_ch1.html
@@ -256,7 +256,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -426,7 +426,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_11_ch2.html b/frontend/textbooks/geometry_11_ch2.html
index 20a6427..170321f 100644
--- a/frontend/textbooks/geometry_11_ch2.html
+++ b/frontend/textbooks/geometry_11_ch2.html
@@ -256,7 +256,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -446,7 +446,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_11_ch3.html b/frontend/textbooks/geometry_11_ch3.html
index 0292315..f82714a 100644
--- a/frontend/textbooks/geometry_11_ch3.html
+++ b/frontend/textbooks/geometry_11_ch3.html
@@ -257,7 +257,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -464,7 +464,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_11_ch4.html b/frontend/textbooks/geometry_11_ch4.html
index 03045b1..b8bfc61 100644
--- a/frontend/textbooks/geometry_11_ch4.html
+++ b/frontend/textbooks/geometry_11_ch4.html
@@ -258,7 +258,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -438,7 +438,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_11_hub.html b/frontend/textbooks/geometry_11_hub.html
index ac1e694..87eca50 100644
--- a/frontend/textbooks/geometry_11_hub.html
+++ b/frontend/textbooks/geometry_11_hub.html
@@ -236,7 +236,7 @@ html.dark .final-cta-sub{color:#fcd34d}
Загрузка...
- 0 XP
+ 0 XP
diff --git a/frontend/textbooks/geometry_7_ch1.html b/frontend/textbooks/geometry_7_ch1.html
index e024c03..2f30174 100644
--- a/frontend/textbooks/geometry_7_ch1.html
+++ b/frontend/textbooks/geometry_7_ch1.html
@@ -212,7 +212,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -437,7 +437,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_7_ch2.html b/frontend/textbooks/geometry_7_ch2.html
index ebccbc4..7bb5d60 100644
--- a/frontend/textbooks/geometry_7_ch2.html
+++ b/frontend/textbooks/geometry_7_ch2.html
@@ -207,7 +207,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -432,7 +432,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_7_ch3.html b/frontend/textbooks/geometry_7_ch3.html
index bd796e6..fcaad80 100644
--- a/frontend/textbooks/geometry_7_ch3.html
+++ b/frontend/textbooks/geometry_7_ch3.html
@@ -208,7 +208,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -407,7 +407,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_7_ch4.html b/frontend/textbooks/geometry_7_ch4.html
index 43f377e..c4541a5 100644
--- a/frontend/textbooks/geometry_7_ch4.html
+++ b/frontend/textbooks/geometry_7_ch4.html
@@ -196,7 +196,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -425,7 +425,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_7_ch5.html b/frontend/textbooks/geometry_7_ch5.html
index 1478ee9..53e5688 100644
--- a/frontend/textbooks/geometry_7_ch5.html
+++ b/frontend/textbooks/geometry_7_ch5.html
@@ -198,7 +198,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -405,7 +405,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_7_hub.html b/frontend/textbooks/geometry_7_hub.html
index ccf8e00..701debb 100644
--- a/frontend/textbooks/geometry_7_hub.html
+++ b/frontend/textbooks/geometry_7_hub.html
@@ -159,7 +159,7 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
Загрузка...
- 0 XP
+ 0 XP
diff --git a/frontend/textbooks/geometry_8_ch1.html b/frontend/textbooks/geometry_8_ch1.html
index 69cc99f..2bd79b7 100644
--- a/frontend/textbooks/geometry_8_ch1.html
+++ b/frontend/textbooks/geometry_8_ch1.html
@@ -286,7 +286,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -558,7 +558,7 @@ function buildSidebar(id){
const xpForLv = _xpForLevel(STATE.level), xpNext = _xpForLevel(STATE.level + 1);
const xpInLv = STATE.xp - xpForLv, xpRange = xpNext - xpForLv;
const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100;
- html += `XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`;
+ html += `XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`;
html += `${sb.title}
`;
sb.rows.forEach(([k,v])=>{ html += `
${k}${v ? ' — ' + v : ''}
`; });
html += '
';
diff --git a/frontend/textbooks/geometry_8_ch2.html b/frontend/textbooks/geometry_8_ch2.html
index 04f0501..27ca2fa 100644
--- a/frontend/textbooks/geometry_8_ch2.html
+++ b/frontend/textbooks/geometry_8_ch2.html
@@ -262,7 +262,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -432,7 +432,7 @@ function buildSidebar(id){
const box=document.getElementById('sidebar-content'); const sb=SIDEBARS[id]||SIDEBARS.p1; let html='';
const xpForLv=_xpForLevel(STATE.level),xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv,xpRange=xpNext-xpForLv,xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+=`XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`;
+ html+=`XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`;
html+=`${sb.title}
`; sb.rows.forEach(([k,v])=>{ html+=`
${k}${v?' — '+v:''}
`; }); html+='
';
const tip=TIPS.find(t=>t.sec===id)||TIPS[0];
html+=``;
diff --git a/frontend/textbooks/geometry_8_ch3.html b/frontend/textbooks/geometry_8_ch3.html
index 9d9f8d8..71623a9 100644
--- a/frontend/textbooks/geometry_8_ch3.html
+++ b/frontend/textbooks/geometry_8_ch3.html
@@ -254,7 +254,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -368,7 +368,7 @@ const TIPS=[
{sec:'final3',html:'Три признака подобия — главный инструмент геометрических доказательств.'},
];
-function buildSidebar(id){const box=document.getElementById('sidebar-content');const sb=SIDEBARS[id]||SIDEBARS.p1;let html='';const xpForLv=_xpForLevel(STATE.level),xpNext=_xpForLevel(STATE.level+1);const xpInLv=STATE.xp-xpForLv,xpRange=xpNext-xpForLv,xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;html+=`XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`;html+=`${sb.title}
`;sb.rows.forEach(([k,v])=>{html+=`
${k}${v?' — '+v:''}
`;});html+='
';const tip=TIPS.find(t=>t.sec===id)||TIPS[0];html+=``;if(STATE.achievements.size>0){html+=`Достижения ${STATE.achievements.size}
`;[...STATE.achievements.values()].slice(-4).forEach(text=>{html+=`
✓ ${text}
`;});html+='
';}box.innerHTML=html;if(window.renderMathInElement)try{renderMath(box);}catch(e){}}
+function buildSidebar(id){const box=document.getElementById('sidebar-content');const sb=SIDEBARS[id]||SIDEBARS.p1;let html='';const xpForLv=_xpForLevel(STATE.level),xpNext=_xpForLevel(STATE.level+1);const xpInLv=STATE.xp-xpForLv,xpRange=xpNext-xpForLv,xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;html+=`XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`;html+=`${sb.title}
`;sb.rows.forEach(([k,v])=>{html+=`
${k}${v?' — '+v:''}
`;});html+='
';const tip=TIPS.find(t=>t.sec===id)||TIPS[0];html+=``;if(STATE.achievements.size>0){html+=`Достижения ${STATE.achievements.size}
`;[...STATE.achievements.values()].slice(-4).forEach(text=>{html+=`
✓ ${text}
`;});html+='
';}box.innerHTML=html;if(window.renderMathInElement)try{renderMath(box);}catch(e){}}
function initTheme(){const t=localStorage.getItem('geometry8_ch3_theme')||'light';if(t==='dark')document.documentElement.classList.add('dark');document.getElementById('theme-lab').textContent=t==='dark'?'Светлая':'Тёмная';document.getElementById('theme-btn').addEventListener('click',()=>{document.documentElement.classList.toggle('dark');const dark=document.documentElement.classList.contains('dark');localStorage.setItem('geometry8_ch3_theme',dark?'dark':'light');document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';});}
function renderMath(root){if(window.renderMathInElement){try{renderMathInElement(root,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false});}catch(e){}}}
diff --git a/frontend/textbooks/geometry_8_ch4.html b/frontend/textbooks/geometry_8_ch4.html
index 3e4e655..89cf1a1 100644
--- a/frontend/textbooks/geometry_8_ch4.html
+++ b/frontend/textbooks/geometry_8_ch4.html
@@ -260,7 +260,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -402,7 +402,7 @@ const TIPS=[
{sec:'final4',html:'Ключевая теорема: вписанный угол = ½ дуги. Из неё следуют большинство остальных.'},
];
-function buildSidebar(id){const box=document.getElementById('sidebar-content');const sb=SIDEBARS[id]||SIDEBARS.p1;let html='';const xpForLv=_xpForLevel(STATE.level),xpNext=_xpForLevel(STATE.level+1);const xpInLv=STATE.xp-xpForLv,xpRange=xpNext-xpForLv,xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;html+=`XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`;html+=`${sb.title}
`;sb.rows.forEach(([k,v])=>{html+=`
${k}${v?' — '+v:''}
`;});html+='
';const tip=TIPS.find(t=>t.sec===id)||TIPS[0];html+=``;if(STATE.achievements.size>0){html+=`Достижения ${STATE.achievements.size}
`;[...STATE.achievements.values()].slice(-4).forEach(text=>{html+=`
✓ ${text}
`;});html+='
';}box.innerHTML=html;if(window.renderMathInElement)try{renderMath(box);}catch(e){}}
+function buildSidebar(id){const box=document.getElementById('sidebar-content');const sb=SIDEBARS[id]||SIDEBARS.p1;let html='';const xpForLv=_xpForLevel(STATE.level),xpNext=_xpForLevel(STATE.level+1);const xpInLv=STATE.xp-xpForLv,xpRange=xpNext-xpForLv,xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;html+=`XP-прогрессУр. ${STATE.level}
${STATE.xp} XP${xpNext} XP
`;html+=`${sb.title}
`;sb.rows.forEach(([k,v])=>{html+=`
${k}${v?' — '+v:''}
`;});html+='
';const tip=TIPS.find(t=>t.sec===id)||TIPS[0];html+=``;if(STATE.achievements.size>0){html+=`Достижения ${STATE.achievements.size}
`;[...STATE.achievements.values()].slice(-4).forEach(text=>{html+=`
✓ ${text}
`;});html+='
';}box.innerHTML=html;if(window.renderMathInElement)try{renderMath(box);}catch(e){}}
function initTheme(){const t=localStorage.getItem('geometry8_ch4_theme')||'light';if(t==='dark')document.documentElement.classList.add('dark');document.getElementById('theme-lab').textContent=t==='dark'?'Светлая':'Тёмная';document.getElementById('theme-btn').addEventListener('click',()=>{document.documentElement.classList.toggle('dark');const dark=document.documentElement.classList.contains('dark');localStorage.setItem('geometry8_ch4_theme',dark?'dark':'light');document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';});}
function renderMath(root){if(window.renderMathInElement){try{renderMathInElement(root,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false});}catch(e){}}}
diff --git a/frontend/textbooks/geometry_8_hub.html b/frontend/textbooks/geometry_8_hub.html
index 1729fcd..07685e5 100644
--- a/frontend/textbooks/geometry_8_hub.html
+++ b/frontend/textbooks/geometry_8_hub.html
@@ -162,7 +162,7 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
Загрузка...
- 0 XP
+ 0 XP
diff --git a/frontend/textbooks/geometry_9_ch1.html b/frontend/textbooks/geometry_9_ch1.html
index 9c4ffc9..5035659 100644
--- a/frontend/textbooks/geometry_9_ch1.html
+++ b/frontend/textbooks/geometry_9_ch1.html
@@ -248,7 +248,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -433,7 +433,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_9_ch2.html b/frontend/textbooks/geometry_9_ch2.html
index 7788d5e..2bff135 100644
--- a/frontend/textbooks/geometry_9_ch2.html
+++ b/frontend/textbooks/geometry_9_ch2.html
@@ -229,7 +229,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -402,7 +402,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_9_ch3.html b/frontend/textbooks/geometry_9_ch3.html
index 9087060..129c613 100644
--- a/frontend/textbooks/geometry_9_ch3.html
+++ b/frontend/textbooks/geometry_9_ch3.html
@@ -229,7 +229,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -402,7 +402,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_9_ch4.html b/frontend/textbooks/geometry_9_ch4.html
index 616a4bc..93b6982 100644
--- a/frontend/textbooks/geometry_9_ch4.html
+++ b/frontend/textbooks/geometry_9_ch4.html
@@ -244,7 +244,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -421,7 +421,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/geometry_9_hub.html b/frontend/textbooks/geometry_9_hub.html
index a8a4c5b..cf53c08 100644
--- a/frontend/textbooks/geometry_9_hub.html
+++ b/frontend/textbooks/geometry_9_hub.html
@@ -261,7 +261,7 @@ html.dark .final-cta-sub{color:#fcd34d}
Загрузка...
- 0 XP
+ 0 XP
diff --git a/frontend/textbooks/physics_10_ch1.html b/frontend/textbooks/physics_10_ch1.html
index 3f8e8ae..769642a 100644
--- a/frontend/textbooks/physics_10_ch1.html
+++ b/frontend/textbooks/physics_10_ch1.html
@@ -267,7 +267,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -476,7 +476,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/physics_10_ch2.html b/frontend/textbooks/physics_10_ch2.html
index a387ef8..6f126d3 100644
--- a/frontend/textbooks/physics_10_ch2.html
+++ b/frontend/textbooks/physics_10_ch2.html
@@ -257,7 +257,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -441,7 +441,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/physics_10_ch3.html b/frontend/textbooks/physics_10_ch3.html
index 1b33912..b662426 100644
--- a/frontend/textbooks/physics_10_ch3.html
+++ b/frontend/textbooks/physics_10_ch3.html
@@ -261,7 +261,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -465,7 +465,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/physics_10_ch4.html b/frontend/textbooks/physics_10_ch4.html
index be929a3..4aac39e 100644
--- a/frontend/textbooks/physics_10_ch4.html
+++ b/frontend/textbooks/physics_10_ch4.html
@@ -254,7 +254,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -423,7 +423,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/physics_10_ch5.html b/frontend/textbooks/physics_10_ch5.html
index 9c507b7..cf3d34b 100644
--- a/frontend/textbooks/physics_10_ch5.html
+++ b/frontend/textbooks/physics_10_ch5.html
@@ -259,7 +259,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -453,7 +453,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/physics_10_ch6.html b/frontend/textbooks/physics_10_ch6.html
index 61f577f..7e1b147 100644
--- a/frontend/textbooks/physics_10_ch6.html
+++ b/frontend/textbooks/physics_10_ch6.html
@@ -256,7 +256,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
0%
-
+
@@ -435,7 +435,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' \u2014 '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/physics_10_hub.html b/frontend/textbooks/physics_10_hub.html
index 3af7649..b33ef72 100644
--- a/frontend/textbooks/physics_10_hub.html
+++ b/frontend/textbooks/physics_10_hub.html
@@ -246,7 +246,7 @@ html.dark .final-cta-sub{color:#fcd34d}
Загрузка...
- 0 XP
+ 0 XP
diff --git a/frontend/textbooks/physics_11_ch1.html b/frontend/textbooks/physics_11_ch1.html
index e403e07..8d02bc4 100644
--- a/frontend/textbooks/physics_11_ch1.html
+++ b/frontend/textbooks/physics_11_ch1.html
@@ -194,7 +194,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -367,7 +367,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/physics_11_ch2.html b/frontend/textbooks/physics_11_ch2.html
index 54a8c63..43e0527 100644
--- a/frontend/textbooks/physics_11_ch2.html
+++ b/frontend/textbooks/physics_11_ch2.html
@@ -189,7 +189,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -371,7 +371,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/physics_11_ch3.html b/frontend/textbooks/physics_11_ch3.html
index 66da196..54cd5a7 100644
--- a/frontend/textbooks/physics_11_ch3.html
+++ b/frontend/textbooks/physics_11_ch3.html
@@ -189,7 +189,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -388,7 +388,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/physics_11_ch4.html b/frontend/textbooks/physics_11_ch4.html
index 2323523..a4ec5d2 100644
--- a/frontend/textbooks/physics_11_ch4.html
+++ b/frontend/textbooks/physics_11_ch4.html
@@ -185,7 +185,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -346,7 +346,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/physics_11_ch5.html b/frontend/textbooks/physics_11_ch5.html
index 42625e6..16c709a 100644
--- a/frontend/textbooks/physics_11_ch5.html
+++ b/frontend/textbooks/physics_11_ch5.html
@@ -185,7 +185,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -346,7 +346,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/physics_11_ch6.html b/frontend/textbooks/physics_11_ch6.html
index 22b40fe..63cc774 100644
--- a/frontend/textbooks/physics_11_ch6.html
+++ b/frontend/textbooks/physics_11_ch6.html
@@ -185,7 +185,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -356,7 +356,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/physics_11_ch7.html b/frontend/textbooks/physics_11_ch7.html
index bcb6a18..8ab5585 100644
--- a/frontend/textbooks/physics_11_ch7.html
+++ b/frontend/textbooks/physics_11_ch7.html
@@ -185,7 +185,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -367,7 +367,7 @@ function buildSidebar(id){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+=''+sb.title+'
';
sb.rows.forEach(([k,v])=>{ html+='
'+k+''+(v?' — '+v:'')+'
'; });
html+='
';
diff --git a/frontend/textbooks/physics_11_ch8.html b/frontend/textbooks/physics_11_ch8.html
index 6fe7844..faf763a 100644
--- a/frontend/textbooks/physics_11_ch8.html
+++ b/frontend/textbooks/physics_11_ch8.html
@@ -155,7 +155,7 @@ a{color:inherit;text-decoration:none}
0%
-
+
@@ -240,7 +240,7 @@ function buildSidebar(){
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
- html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
+ html+='XP-прогрессУр. '+STATE.level+'
'+STATE.xp+' XP'+xpNext+' XP
';
html+='Курс физики 11
'
+ '
Глав — 8
'
+ '
Параграфов — 45
'
diff --git a/js/api.js b/js/api.js
index ad6711a..827be51 100644
--- a/js/api.js
+++ b/js/api.js
@@ -806,14 +806,24 @@ function initPage({ requireLogin = true } = {}) {
/* ── Feature flags (cached per page load, bust on demand) ───────────── */
let _featuresCache = null;
+/* Synchronous mirror of features.gamification, populated on first
+ loadFeatures() resolve. Used by xp.js to bail out without network. */
+let _gamificationEnabled = null;
+function isGamificationEnabled() {
+ // Default to true when the cache hasn't resolved yet — page-load order
+ // means some code runs before /api/features. The CSS kill-switch
+ // (body.no-gamification) catches the visual side regardless.
+ return _gamificationEnabled !== false;
+}
async function loadFeatures() {
if (_featuresCache) return _featuresCache;
try {
_featuresCache = await apiFetch('/api/features');
} catch { _featuresCache = {}; }
+ _gamificationEnabled = _featuresCache.gamification !== false;
return _featuresCache;
}
-function clearFeaturesCache() { _featuresCache = null; }
+function clearFeaturesCache() { _featuresCache = null; _gamificationEnabled = null; }
/**
* Show board sidebar link only for teachers/admins and students in a class.
@@ -1045,6 +1055,7 @@ window.LS = {
patch: (path, body) => apiFetch(path, { method: 'PATCH', body: JSON.stringify(body) }),
applyCosmetics: applyCosmetics,
refreshNavAvatar,
+ isGamificationEnabled,
loadFeatures,
clearFeaturesCache,
hideDisabledFeatures,
@@ -1060,6 +1071,9 @@ window.LS = {
═══════════════════════════════════════════════════════════════════════ */
async function applyCosmetics() {
if (!isLoggedIn()) return;
+ // Skip the round-trip entirely when gamification is off. Server will
+ // 404 anyway, but a synchronous bail-out keeps the network quiet.
+ if (!isGamificationEnabled()) return;
try {
const c = await getMyActiveCosmetics();
if (!c) return;
diff --git a/js/xp.js b/js/xp.js
index 1421ce5..13edf41 100644
--- a/js/xp.js
+++ b/js/xp.js
@@ -73,8 +73,19 @@
try { localStorage.setItem(LS_KEY, String(xp)); } catch (e) {}
}
+ /* ── Master kill-switch ──
+ If the gamification feature is globally disabled, every public
+ method becomes a no-op so textbooks don't display XP bars and
+ don't burn CPU/network on now-pointless syncs. The body class
+ `.no-gamification` (set by api.js) hides the DOM in parallel. */
+ function _gamOff() {
+ return window.LS && typeof window.LS.isGamificationEnabled === 'function'
+ && window.LS.isGamificationEnabled() === false;
+ }
+
/* ── Загрузка с сервера + merge ── */
async function load() {
+ if (_gamOff()) return null;
try {
const data = await _apiFetch(ENDPOINT_ME, { method: 'GET' });
const serverXp = data.xp || 0;
@@ -87,6 +98,7 @@
return _state;
} catch (e) {
if (e && e.status === 401) return null; // не авторизован — тихо пропускаем
+ if (e && e.status === 404) return null; // геймификация выключена — тихо
return null; // сетевая ошибка — не ломаем учебник
}
}
@@ -124,6 +136,7 @@
function flush() {
if (_debounceTimer) { clearTimeout(_debounceTimer); _debounceTimer = null; }
if (_pending <= 0) return;
+ if (_gamOff()) { _pending = 0; return; } // drop pending — feature off
const amount = _pending;
_pending = 0;
// source для flush — generic, реальный source записывается в каждом add()
@@ -152,6 +165,7 @@
/* ── add: sync обновление localStorage + постановка в очередь ── */
function add(amount, source) {
if (!amount || amount <= 0) return;
+ if (_gamOff()) return; // no-op when gamification globally off
// Немедленно обновляем локальный XP
const newXp = _localXp() + amount;
_saveLocal(newXp);