feat(dashboard): «Лаборатория дня» синхронизирована с каталогом /api/lab/sims

Раньше карточка использовала захардкоженный список из 6 симуляций и не знала
о каталоге. Теперь ежедневный выбор берётся из /api/lab/sims: только включённые
симуляции, у которых есть превью (приоритет featured), title/категория — из БД,
поэтому переименование/выключение/рекомендация в админке отражаются автоматически.
Время/уровень/цель — из curated-карты по id (в каталоге их нет) c дефолтами.
Фолбэк на статичный список, если API недоступен. Заодно исправлен mismatch
isoprocess→molphys (href теперь = id симуляции).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-01 09:20:10 +03:00
parent ec2a207fb8
commit 927b39b0d6
+48 -11
View File
@@ -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) ═ */