diff --git a/frontend/dashboard.html b/frontend/dashboard.html index d80fa3d..2e24024 100644 --- a/frontend/dashboard.html +++ b/frontend/dashboard.html @@ -3289,20 +3289,57 @@ { key:'waves', href:'/lab?sim=waves', title:'Волны и колебания', sub:'Длина волны, частота и стоячие волны.', subj:'Физика', time:'~11 мин', level:'средне', goal:'связь v = λf' }, { key:'stereo', href:'/lab?sim=stereo', title:'Стереометрия 3D', sub:'Сечения и объёмы пространственных фигур.', subj:'Геометрия', time:'~10 мин', level:'сложно', goal:'построение сечений' }, ]; - function loadLabOfDay() { + // Метаданные карточки (время/уровень/цель/подпись) — их нет в каталоге БД, + // поэтому держим curated-карту по id (источник — LAB_OF_DAY выше). + const LAB_META = {}; + LAB_OF_DAY.forEach(l => { LAB_META[l.key] = l; }); + const CAT_LABEL = { math: 'Математика', phys: 'Физика', chem: 'Химия', bio: 'Биология', game: 'Игра' }; + + // Синхронизировано с каталогом лаборатории (/api/lab/sims): крутим только + // среди ВКЛЮЧЁННЫХ симуляций, у которых есть превью; title/категория — из БД, + // поэтому переименования/выключения в админке отражаются здесь автоматически. + async function loadLabOfDay() { const card = document.getElementById('hc-lab'); if (!card) return; - const dayIdx = Math.floor(Date.now() / 86400000) % LAB_OF_DAY.length; - const lab = LAB_OF_DAY[dayIdx]; - card.href = lab.href; + + let pick = null; + try { + const r = await LS.api('/api/lab/sims'); + const sims = (r && r.sims) || []; + const hasPreview = id => window.LabPreviews && LabPreviews[id]; + let pool = sims.filter(s => s.enabled && hasPreview(s.id)); + const feat = pool.filter(s => s.featured); + if (feat.length >= 2) pool = feat; // приоритет «рекомендуемых», если их достаточно + pool.sort((a, b) => (a.sort - b.sort) || (a.id < b.id ? -1 : 1)); // стабильный порядок для детерминизма + if (pool.length) { + const s = pool[Math.floor(Date.now() / 86400000) % pool.length]; + const m = LAB_META[s.id] || {}; + pick = { + href: '/lab?sim=' + s.id, + key: s.id, + title: s.title || m.title || s.id, // title из каталога (источник истины) + sub: m.sub || 'Открой симуляцию и поэкспериментируй.', + subj: m.subj || CAT_LABEL[s.cat] || 'Лаборатория', + time: m.time || '~10 мин', + level: m.level || 'средне', + goal: m.goal || (s.title || ''), + }; + } + } catch (_) { /* каталог недоступен — упадём на статичный список ниже */ } + + if (!pick) { // фолбэк: прежний детерминированный выбор из захардкоженного списка + pick = LAB_OF_DAY[Math.floor(Date.now() / 86400000) % LAB_OF_DAY.length]; + } + + card.href = pick.href; const bg = document.getElementById('hc-lab-bg'); - if (bg && window.LabPreviews && LabPreviews[lab.key]) bg.innerHTML = LabPreviews[lab.key]; - document.getElementById('hc-lab-title').textContent = lab.title; - document.getElementById('hc-lab-sub').textContent = lab.sub; - document.getElementById('hc-lab-subj').textContent = lab.subj; - document.getElementById('hc-lab-time').textContent = lab.time; - document.getElementById('hc-lab-level').textContent = lab.level; - document.getElementById('hc-lab-meta').textContent = 'Освой: ' + lab.goal; + if (bg && window.LabPreviews && LabPreviews[pick.key]) bg.innerHTML = LabPreviews[pick.key]; + document.getElementById('hc-lab-title').textContent = pick.title; + document.getElementById('hc-lab-sub').textContent = pick.sub; + document.getElementById('hc-lab-subj').textContent = pick.subj; + document.getElementById('hc-lab-time').textContent = pick.time; + document.getElementById('hc-lab-level').textContent = pick.level; + document.getElementById('hc-lab-meta').textContent = 'Освой: ' + pick.goal; } /* ══ HERO: Pet (synced with /pet module via /api/pet + PetSprite) ═ */