style/security: эмодзи→SVG, safeUrl в ассистенте, prefs в localStorage (Спринт3)
- Убраны эмодзи (правило: только inline SVG .ic): classes.html 🃏→layers, collection-rb.html ⭐→star, pet.html 😋/😢→текст (textContent не держит SVG). - assistant.js: safeUrl() на динамических href (FAQ/поиск/RAG/правила) — блокирует javascript:/data:, пропускает /… и https://…. - LS.prefs: персистентность через localStorage (раньше sync был отключён, настройки терялись при перезагрузке). Грузим синхронно + flush на pagehide. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -717,7 +717,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;font-size:0.85rem;font-weight:600;color:var(--text-1)">
|
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;font-size:0.85rem;font-weight:600;color:var(--text-1)">
|
||||||
<input type="checkbox" id="feat-collection" style="width:16px;height:16px;accent-color:var(--violet);cursor:pointer">
|
<input type="checkbox" id="feat-collection" style="width:16px;height:16px;accent-color:var(--violet);cursor:pointer">
|
||||||
🃏 Коллекция
|
<svg class="ic" viewBox="0 0 24 24"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></svg> Коллекция
|
||||||
</label>
|
</label>
|
||||||
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;font-size:0.85rem;font-weight:600;color:var(--text-1)">
|
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;font-size:0.85rem;font-weight:600;color:var(--text-1)">
|
||||||
<input type="checkbox" id="feat-hangman" style="width:16px;height:16px;accent-color:var(--violet);cursor:pointer">
|
<input type="checkbox" id="feat-hangman" style="width:16px;height:16px;accent-color:var(--violet);cursor:pointer">
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ const METHOD_LABELS = {
|
|||||||
explore: '<svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> Исследование',
|
explore: '<svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> Исследование',
|
||||||
quest: '<svg class="ic" viewBox="0 0 24 24"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg> Квест',
|
quest: '<svg class="ic" viewBox="0 0 24 24"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg> Квест',
|
||||||
sighting:'<svg class="ic" viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg> Наблюдение',
|
sighting:'<svg class="ic" viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg> Наблюдение',
|
||||||
daily: '⭐ Вид дня',
|
daily: '<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg> Вид дня',
|
||||||
};
|
};
|
||||||
|
|
||||||
const ACHIEVEMENTS = [
|
const ACHIEVEMENTS = [
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
/* ── helpers ─────────────────────────────────────────────────────────── */
|
/* ── helpers ─────────────────────────────────────────────────────────── */
|
||||||
function esc(s) { return (window.LS && LS.escapeHtml) ? LS.escapeHtml(String(s == null ? '' : s)) : String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]; }); }
|
function esc(s) { return (window.LS && LS.escapeHtml) ? LS.escapeHtml(String(s == null ? '' : s)) : String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]; }); }
|
||||||
|
// Безопасный href: только внутренний путь /… или https://… — блокирует javascript:/data:.
|
||||||
|
function safeUrl(u) { u = String(u == null ? '' : u).trim(); return (/^\//.test(u) || /^https?:\/\//i.test(u)) ? u : '#'; }
|
||||||
function lsGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
|
function lsGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
|
||||||
function lsSet(k, v) { try { localStorage.setItem(k, v); } catch (e) {} }
|
function lsSet(k, v) { try { localStorage.setItem(k, v); } catch (e) {} }
|
||||||
function todayKey() { return new Date().toISOString().slice(0, 10); }
|
function todayKey() { return new Date().toISOString().slice(0, 10); }
|
||||||
@@ -372,7 +374,7 @@
|
|||||||
|
|
||||||
function hintHtml(rule) {
|
function hintHtml(rule) {
|
||||||
var act = null; try { act = rule.action(); } catch (e) {}
|
var act = null; try { act = rule.action(); } catch (e) {}
|
||||||
var actHtml = act && act.url ? '<a class="asst-btn" href="' + esc(act.url) + '">' + esc(act.label || 'Открыть') + '</a>' : '';
|
var actHtml = act && act.url ? '<a class="asst-btn" href="' + esc(safeUrl(act.url)) + '">' + esc(act.label || 'Открыть') + '</a>' : '';
|
||||||
var dismiss = (rule.scope !== 'celebration') ? '<button class="asst-link" data-a="dismiss">Не показывать</button>' : '';
|
var dismiss = (rule.scope !== 'celebration') ? '<button class="asst-link" data-a="dismiss">Не показывать</button>' : '';
|
||||||
return '<div class="asst-name">' + esc(PET && PET.petName ? PET.petName : 'Квантик') + '</div>' +
|
return '<div class="asst-name">' + esc(PET && PET.petName ? PET.petName : 'Квантик') + '</div>' +
|
||||||
'<div class="asst-text">' + esc(rule.text ? rule.text() : rule.text) + '</div>' +
|
'<div class="asst-text">' + esc(rule.text ? rule.text() : rule.text) + '</div>' +
|
||||||
@@ -633,13 +635,13 @@
|
|||||||
// источники (RAG)
|
// источники (RAG)
|
||||||
if (model && sources.length) {
|
if (model && sources.length) {
|
||||||
var sc = document.createElement('div'); sc.className = 'asst-src';
|
var sc = document.createElement('div'); sc.className = 'asst-src';
|
||||||
sc.innerHTML = 'Источник: ' + sources.map(function (s) { return '<a href="' + esc(srcUrl(s)) + '">' + esc(s.title) + (s.section ? ', ' + esc(s.section) : '') + '</a>'; }).join('; ');
|
sc.innerHTML = 'Источник: ' + sources.map(function (s) { return '<a href="' + esc(safeUrl(srcUrl(s))) + '">' + esc(s.title) + (s.section ? ', ' + esc(s.section) : '') + '</a>'; }).join('; ');
|
||||||
chatEl.appendChild(sc);
|
chatEl.appendChild(sc);
|
||||||
}
|
}
|
||||||
// ссылки FAQ/платформа
|
// ссылки FAQ/платформа
|
||||||
var links = '';
|
var links = '';
|
||||||
if (!model && ans.length) links += ans.slice(0, 2).filter(function (a) { return a.url; }).map(function (a) { return '<a class="asst-ans-link" href="' + esc(a.url) + '">' + esc(a.q) + '</a>'; }).join(' · ');
|
if (!model && ans.length) links += ans.slice(0, 2).filter(function (a) { return a.url; }).map(function (a) { return '<a class="asst-ans-link" href="' + esc(safeUrl(a.url)) + '">' + esc(a.q) + '</a>'; }).join(' · ');
|
||||||
if (found.length) links += (links ? '<br>' : '') + '<span style="color:#8a94a6">На платформе: </span>' + found.slice(0, 3).map(function (f) { return '<a class="asst-ans-link" href="' + esc(f.url || '#') + '">' + esc(f.title || '…') + '</a>'; }).join(' · ');
|
if (found.length) links += (links ? '<br>' : '') + '<span style="color:#8a94a6">На платформе: </span>' + found.slice(0, 3).map(function (f) { return '<a class="asst-ans-link" href="' + esc(safeUrl(f.url)) + '">' + esc(f.title || '…') + '</a>'; }).join(' · ');
|
||||||
if (links) { var l = document.createElement('div'); l.className = 'asst-msg-links'; l.innerHTML = links; chatEl.appendChild(l); }
|
if (links) { var l = document.createElement('div'); l.className = 'asst-msg-links'; l.innerHTML = links; chatEl.appendChild(l); }
|
||||||
// оценка ответа
|
// оценка ответа
|
||||||
if (model) {
|
if (model) {
|
||||||
|
|||||||
+2
-2
@@ -1776,11 +1776,11 @@ function showFeedResult(correct, xp, customMsg) {
|
|||||||
el.style.cssText = 'display:block;padding:12px 14px;border-radius:12px;background:rgba(56,217,90,.1);border:1.5px solid rgba(56,217,90,.25);font-size:.87rem;font-weight:700;text-align:center;color:#38D95A';
|
el.style.cssText = 'display:block;padding:12px 14px;border-radius:12px;background:rgba(56,217,90,.1);border:1.5px solid rgba(56,217,90,.25);font-size:.87rem;font-weight:700;text-align:center;color:#38D95A';
|
||||||
el.textContent = `Правильно! +${xp} XP — питомец доволен!`;
|
el.textContent = `Правильно! +${xp} XP — питомец доволен!`;
|
||||||
floatLabel(`+${xp} XP`, '#38D95A');
|
floatLabel(`+${xp} XP`, '#38D95A');
|
||||||
document.getElementById('pet-bubble').textContent = '😋 Вкуснятина!';
|
document.getElementById('pet-bubble').textContent = 'Вкуснятина!';
|
||||||
setTimeout(() => { closeFeedGame(); startFeedCooldown(1800); }, 1800);
|
setTimeout(() => { closeFeedGame(); startFeedCooldown(1800); }, 1800);
|
||||||
} else {
|
} else {
|
||||||
el.style.cssText = 'display:block;padding:12px 14px;border-radius:12px;background:rgba(249,65,68,.08);border:1.5px solid rgba(249,65,68,.2);font-size:.87rem;font-weight:700;text-align:center;color:#F94144';
|
el.style.cssText = 'display:block;padding:12px 14px;border-radius:12px;background:rgba(249,65,68,.08);border:1.5px solid rgba(249,65,68,.2);font-size:.87rem;font-weight:700;text-align:center;color:#F94144';
|
||||||
el.textContent = 'Неверно — питомец остался голодным 😢';
|
el.textContent = 'Неверно — питомец остался голодным';
|
||||||
setTimeout(closeFeedGame, 1800);
|
setTimeout(closeFeedGame, 1800);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -960,28 +960,17 @@ async function biochemSavePathwayProgress(pathway,step,completed){ return req('P
|
|||||||
const _prefsCache = {};
|
const _prefsCache = {};
|
||||||
let _prefsDirty = false;
|
let _prefsDirty = false;
|
||||||
let _prefsTimer = null;
|
let _prefsTimer = null;
|
||||||
|
const _PREFS_LS_KEY = 'ls_prefs';
|
||||||
|
|
||||||
// SYNC DISABLED (debug mode) — раскомментировать для включения синхронизации
|
// Персистентность настроек — в localStorage (per-device). Раньше sync был отключён,
|
||||||
async function _prefsLoad() { /* disabled */ }
|
// и настройки молча терялись при перезагрузке. Грузим синхронно сразу при загрузке api.js.
|
||||||
function _prefsFlush() { /* disabled */ }
|
try { const raw = localStorage.getItem(_PREFS_LS_KEY); if (raw) Object.assign(_prefsCache, JSON.parse(raw)); } catch (e) {}
|
||||||
|
async function _prefsLoad() { try { const raw = localStorage.getItem(_PREFS_LS_KEY); if (raw) Object.assign(_prefsCache, JSON.parse(raw)); } catch (e) {} }
|
||||||
// async function _prefsLoad() {
|
function _prefsFlush() {
|
||||||
// if (!isLoggedIn()) return;
|
if (!_prefsDirty) return;
|
||||||
// try {
|
_prefsDirty = false;
|
||||||
// const data = await apiFetch('/api/preferences', { method: 'GET' });
|
try { localStorage.setItem(_PREFS_LS_KEY, JSON.stringify(_prefsCache)); } catch (e) {}
|
||||||
// Object.assign(_prefsCache, data);
|
}
|
||||||
// } catch (e) {}
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// function _prefsFlush() {
|
|
||||||
// if (!_prefsDirty) return;
|
|
||||||
// _prefsDirty = false;
|
|
||||||
// if (!isLoggedIn()) return;
|
|
||||||
// apiFetch('/api/preferences', {
|
|
||||||
// method: 'PATCH',
|
|
||||||
// body: JSON.stringify(_prefsCache),
|
|
||||||
// }).catch(() => {});
|
|
||||||
// }
|
|
||||||
|
|
||||||
const lsPrefs = {
|
const lsPrefs = {
|
||||||
get(key, def) {
|
get(key, def) {
|
||||||
@@ -1442,6 +1431,7 @@ function connectSSE(onEvent) {
|
|||||||
window.addEventListener('pagehide', () => {
|
window.addEventListener('pagehide', () => {
|
||||||
if (_sseShared) { _sseShared.close(); _sseShared = null; }
|
if (_sseShared) { _sseShared.close(); _sseShared = null; }
|
||||||
_sseListeners.clear();
|
_sseListeners.clear();
|
||||||
|
try { lsPrefs.flush(); } catch (e) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ── assignment templates ─────────────────────────────────────────────────── */
|
/* ── assignment templates ─────────────────────────────────────────────────── */
|
||||||
|
|||||||
Reference in New Issue
Block a user