diff --git a/frontend/sitemap.html b/frontend/sitemap.html
index 4a9798e..cc1084e 100644
--- a/frontend/sitemap.html
+++ b/frontend/sitemap.html
@@ -335,29 +335,88 @@ const CAT_INFO = {
personal: { label:'Личное', rgb:[255,179,71] },
};
-/* ══ Build HTML nodes ══ */
+/* ══ Feature flag → module ID mapping ══ */
+const FEAT_MAP = {
+ hangman: ['hangman'],
+ crossword: ['crossword'],
+ pet: ['pet'],
+ red_book: ['redbook'],
+ collection: ['collection'],
+ lab: ['lab'],
+ knowledge_map: ['knowledge'],
+ board: ['board'],
+ biochem: ['biochem'],
+};
+const NO_CLASS_IDS = new Set(['board','lab','hangman','crossword','pet','collection','knowledge','redbook']);
+
+/* activeModules — updated after features load; canvas uses this for connections */
+let activeModules = MODULES;
+
+/* ══ Build HTML nodes (called after features are known) ══ */
const nodesWrap = document.getElementById('gx-nodes');
const nodeMap = {};
-MODULES.forEach((m, i) => {
- const [r,g,b] = CAT_INFO[m.cat].rgb;
- const el = document.createElement('div');
- el.className = 'gx-node' + (m.id === 'dashboard' ? ' gx-main' : '');
- el.dataset.id = m.id;
- el.dataset.cat = m.cat;
- el.style.cssText = `left:${m.x*100}%;top:${m.y*100}%;--nc:${r},${g},${b};animation-delay:${i*0.045}s`;
- el.innerHTML = `
-
- ${m.label}`;
- el.addEventListener('mouseenter', () => showTip(m, el));
- el.addEventListener('mouseleave', queueHideTip);
- el.addEventListener('click', () => location.href = m.href);
- nodesWrap.appendChild(el);
- nodeMap[m.id] = el;
-});
+function buildNodes(mods) {
+ nodesWrap.innerHTML = '';
+ Object.keys(nodeMap).forEach(k => delete nodeMap[k]);
+ mods.forEach((m, i) => {
+ const [r,g,b] = CAT_INFO[m.cat].rgb;
+ const el = document.createElement('div');
+ el.className = 'gx-node' + (m.id === 'dashboard' ? ' gx-main' : '');
+ el.dataset.id = m.id;
+ el.dataset.cat = m.cat;
+ el.style.cssText = `left:${m.x*100}%;top:${m.y*100}%;--nc:${r},${g},${b};animation-delay:${i*0.045}s`;
+ el.innerHTML = `
+
+ ${m.label}`;
+ el.addEventListener('mouseenter', () => showTip(m, el));
+ el.addEventListener('mouseleave', queueHideTip);
+ el.addEventListener('click', () => location.href = m.href);
+ nodesWrap.appendChild(el);
+ nodeMap[m.id] = el;
+ });
+ if (window.lucide) lucide.createIcons();
+ document.getElementById('gx-count').textContent = mods.length + ' модулей';
+}
-if (window.lucide) lucide.createIcons();
-document.getElementById('gx-count').textContent = MODULES.length + ' модулей';
+/* ══ Mobile fallback (called after features are known) ══ */
+function buildMobile(mods) {
+ const mobEl = document.getElementById('gx-mobile');
+ mobEl.innerHTML = '';
+ const byCat = {};
+ mods.forEach(m => { (byCat[m.cat] = byCat[m.cat]||[]).push(m); });
+ ['study','practice','games','personal'].forEach(cat => {
+ if (!byCat[cat]?.length) return;
+ const info = CAT_INFO[cat];
+ const [r,g,b] = info.rgb;
+ let html = `${info.label}
`;
+ byCat[cat].forEach(m => {
+ html += `
+
+
+ `;
+ });
+ html += '
';
+ mobEl.insertAdjacentHTML('beforeend', html);
+ });
+ if (window.lucide) lucide.createIcons();
+}
+
+/* ══ Async init: load features, filter modules, render ══ */
+(async () => {
+ let disabled = new Set();
+ try {
+ const feats = await LS.loadFeatures();
+ for (const [key, ids] of Object.entries(FEAT_MAP)) {
+ if (feats[key] === false) ids.forEach(id => disabled.add(id));
+ }
+ if (feats._no_class) NO_CLASS_IDS.forEach(id => disabled.add(id));
+ } catch { /* если не залогинен — показываем всё */ }
+
+ activeModules = disabled.size ? MODULES.filter(m => !disabled.has(m.id)) : MODULES;
+ buildNodes(activeModules);
+ buildMobile(activeModules);
+})();
/* ══ Tooltip ══ */
const tt = document.getElementById('gx-tt');
@@ -479,12 +538,12 @@ function drawFrame() {
});
ctx.globalAlpha = 1;
- // Connection lines
+ // Connection lines (only between visible modules)
ctx.save();
ctx.setLineDash([3, 10]);
CONNECTIONS.forEach(([a, b]) => {
- const ma = MODULES.find(m => m.id === a);
- const mb = MODULES.find(m => m.id === b);
+ const ma = activeModules.find(m => m.id === a);
+ const mb = activeModules.find(m => m.id === b);
if (!ma || !mb) return;
const isHovered = hoveredId && (a === hoveredId || b === hoveredId);
ctx.strokeStyle = isHovered ? 'rgba(255,255,255,0.38)' : 'rgba(255,255,255,0.065)';
@@ -513,24 +572,6 @@ document.querySelectorAll('.gx-fil').forEach(btn => {
});
});
-/* ══ Mobile fallback: card grid ══ */
-const mobEl = document.getElementById('gx-mobile');
-const byCat = {};
-MODULES.forEach(m => { (byCat[m.cat] = byCat[m.cat]||[]).push(m); });
-['study','practice','games','personal'].forEach(cat => {
- const info = CAT_INFO[cat];
- const [r,g,b] = info.rgb;
- let html = `${info.label}
`;
- (byCat[cat]||[]).forEach(m => {
- html += `
-
-
- `;
- });
- html += '
';
- mobEl.insertAdjacentHTML('beforeend', html);
-});
-if (window.lucide) lucide.createIcons();