feat(xp): textbook XP синхронизируется с системной геймификацией

- backend: POST /api/gamification/self-award (rate-limited, validated)
- frontend/js/xp.js: load/add/flush/on клиент, ~150 LOC, дебаунс 300мс,
  keepalive fetch на unload/visibilitychange hidden
- algebra_8.html и algebra_8_ch2.html: XP_LEVELS заменён на единую
  формулу с сервером; addXp/loadProgress подключены к window.LS.xp
- При первой загрузке: merge max(local, server); далее сервер — источник
  правды
This commit is contained in:
Maxim Dolgolyov
2026-05-27 15:56:36 +03:00
parent 9199427dfd
commit 64bd44088d
5 changed files with 254 additions and 32 deletions
@@ -145,8 +145,24 @@ function claimChallenge(req, res) {
res.json({ xp: c.xp_reward });
}
/* POST /api/gamification/self-award — начисление XP из учебника */
function selfAward(req, res) {
const amount = Number(req.body.amount);
const source = String(req.body.source || '');
if (!Number.isInteger(amount) || amount < 1 || amount > 50) {
return res.status(400).json({ error: 'amount должен быть целым числом от 1 до 50' });
}
if (!/^[a-z0-9_-]{1,60}$/i.test(source)) {
return res.status(400).json({ error: 'source неверный формат (a-z, 0-9, _, -, до 60 символов)' });
}
awardXP(req.user.id, amount, 'tb:' + source);
const info = getXPInfo(req.user.id);
res.json(info);
}
module.exports = {
getMe, getFrames, setFrame, setGoalTier,
getAchievements, getLeaderboard, getXPHistory,
getChallenges, claimChallenge,
selfAward,
};