Keine Fragenbank gefunden – tools/build_bank.py ausführen oder ein Fragenpaket importieren.
'}
${resume}
Objectives laut LPI, Quelle und Version je Prüfung in data/<prüfung>/exam.json.
Stand der Fragenbank: ${esc(window.LPIC_BANK ? window.LPIC_BANK.generated : '–')}.
Tasten: → / Leertaste weiter · ← zurück · 1–5 Antwort wählen · Enter im Textfeld = weiter.
`;
setSlides(html, 0);
updateStartForm();
}
/** Aktualisiert Themenliste und Hinweise je gewählter Prüfung/Modus. */
function updateStartForm() {
const form = $('#setup');
if (!form) return;
const exam = ui.exams.find((e) => e.id === (form.exam && form.exam.value));
const learn = form.mode.value === 'learn';
$$('.learn-only', form).forEach((el) => { el.hidden = !learn; });
if (!exam) return;
$('#exam-info').textContent = `${exam.questions} Fragen, ${exam.duration_min} Minuten, gewichtet nach Objectives, Auflösung erst nach Abgabe`;
const counts = {};
exam.bank.forEach((q) => { const t = q.objective.split('.')[0]; counts[t] = (counts[t] || 0) + 1; });
const list = $('#topic-list');
if (list.dataset.exam !== exam.id) {
list.dataset.exam = exam.id;
list.innerHTML = exam.topics.map((t) => `
`).join('');
}
}
function startFromForm(form) {
const exam = ui.exams.find((e) => e.id === form.exam.value);
if (!exam) { toast('Bitte eine Prüfung wählen.', 'warn'); return; }
const topics = $$('input[name=topic]:checked', form).map((i) => i.value);
const s = createSession(exam, {
mode: form.mode.value,
topics,
count: parseInt(form.count.value, 10) || 0,
order: form.order.value,
shuffleOptions: form.shuffle.checked,
});
if (!s.questions.length) { toast('Keine Fragen für diese Auswahl.', 'warn'); return; }
startSession(s, 0);
}
function startSession(session, index) {
ui.session = session;
save();
renderSession(index);
}
/* ----------------------------- Fragen-Folien */
function renderSession(index) {
const s = ui.session;
const slides = s.questions.map((q, i) => questionSlide(q, i)).join('');
const last = s.mode === 'exam' && !s.submitted ? submitSlide() : resultSlide();
setSlides(slides + last, Math.min(index ?? s.current ?? 0, s.questions.length));
}
/** Ist die Frage gesperrt (aufgelöst bzw. Prüfung abgegeben)? */
function isLocked(q) {
const a = ui.session.answers[q.id];
return ui.session.submitted || !!(a && a.revealed);
}
function questionSlide(q, i) {
const s = ui.session;
const a = s.answers[q.id] || {};
const locked = isLocked(q);
const info = objectiveInfo({ topics: s.topics }, q.objective);
const showSolution = s.mode === 'learn' || s.submitted;
const nOk = correctCount(q);
let hint = '';
let body;
if (q.type === 'fill') {
hint = `Antwort eintippen${q.case_sensitive === false ? '' : ' – Groß-/Kleinschreibung beachten'}.`;
body = ``;
} else {
const multi = q.type === 'multi';
if (multi) hint = `Wählen Sie ${COUNT_WORDS[nOk] || nOk}.`;
const sel = new Set(Array.isArray(a.value) ? a.value : []);
body = `${q.options.map((o, k) => `
`).join('')}
`;
}
const flag = s.mode === 'exam' && !s.submitted
? `` : '';
const solution = showSolution ? solutionBlock(q, locked) : '';
const learnBtn = s.mode === 'learn' && !locked ? '' : '';
return `
${r.correct} von ${r.total} richtig · ${r.percent} %
${verdict}
LPI bewertet auf einer Skala von 200–800 (bestanden ab 500) und veröffentlicht keine Prozentgrenze.
Der Richtwert ist eine Annahme und in exam.json (pass_percent) einstellbar.
Topic
richtig
Quote
${rows}
Falsch/offen (anklicken zum Ansehen): ${wrong}
${r.wrong.length ? '' : ''}
`;
}
function submitExam(auto) {
const s = ui.session;
s.submitted = true;
save();
renderSession(s.questions.length);
toast(auto ? 'Zeit abgelaufen – Prüfung automatisch abgegeben.' : 'Prüfung abgegeben.', auto ? 'warn' : 'info');
}
/** Neue Lernsitzung mit den falsch beantworteten Fragen der aktuellen Sitzung. */
function retryWrong() {
const s = ui.session;
const r = score(s, s.mode === 'learn');
const qs = r.wrong.map((i) => s.questions[i]);
const exam = { ...(ui.exams.find((e) => e.id === s.exam_id) || {}), id: s.exam_id, title: s.exam_title,
code: s.exam_code, topics: s.topics, pass_percent: s.pass_percent, bank: qs };
startSession(createSession(exam, { mode: 'learn', order: 'random', shuffleOptions: true }), 0);
}
/* ----------------------------- Werkzeugleiste, Timer */
/** Überträgt die tatsächliche Höhe der (ggf. umbrochenen) Werkzeugleiste nach CSS --tb-h. */
function syncToolbarHeight() {
document.documentElement.style.setProperty('--tb-h', `${$('.toolbar').offsetHeight}px`);
}
function updateToolbar() {
const s = ui.session;
$('#tb-title').textContent = s ? `${s.exam_title} · ${s.mode === 'exam' ? 'Prüfung' : 'Lernen'}` : 'LPIC-Prüfungssimulator';
$('#tb-export').disabled = !s;
if (!s) { $('#tb-progress').textContent = ''; $('#tb-timer').textContent = ''; return; }
const done = s.questions.filter((q) => isAnswered(s.answers[q.id])).length;
$('#tb-progress').textContent = `${done}/${s.questions.length} beantwortet`;
const timer = $('#tb-timer');
if (s.mode === 'exam' && !s.submitted) {
const rest = s.time_limit_s - s.elapsed_s;
timer.textContent = `⏱ ${fmtTime(rest)}`;
timer.classList.toggle('low', rest < 600);
} else {
timer.textContent = `⏱ ${fmtTime(s.elapsed_s)}`;
timer.classList.remove('low');
}
}
function tick() {
const s = ui.session;
if (!s || s.submitted) return;
s.elapsed_s++;
if (++ui.tickCount % SAVE_EVERY_S === 0) save();
if (s.mode === 'exam' && s.elapsed_s >= s.time_limit_s) { submitExam(true); return; }
updateToolbar();
}
/* ----------------------------- Export / Import */
function exportSession() {
const s = ui.session;
if (!s) return;
save();
const d = new Date();
const pad = (n) => String(n).padStart(2, '0');
const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}`;
download(`lpic_sim_${s.exam_id}_${s.mode}_${stamp}.json`, JSON.stringify(s, null, 1));
}
/** Import: erkennt Sitzung (fortsetzen) oder Fragenpaket (in Bank übernehmen). */
function importFile(file) {
const reader = new FileReader();
reader.onload = () => {
let doc;
try { doc = JSON.parse(reader.result); }
catch (e) { toast(`${file.name}: kein gültiges JSON (${e.message})`, 'error'); return; }
if (doc.format === SESSION_FORMAT) {
const err = validateSession(doc);
if (err) { toast(`${file.name}: ${err}`, 'error'); return; }
startSession(doc, doc.current || 0);
toast(`Sitzung „${doc.exam_title}“ geladen – weiter bei Frage ${Math.min((doc.current || 0) + 1, doc.questions.length)}.`);
} else if (doc.format === PACK_FORMAT) {
const res = mergePack(ui.exams, doc);
if (res.added + res.replaced > 0) {
const packs = (store.get(STORE_PACKS) || []).filter((p) => p.name !== file.name);
packs.push({ ...doc, name: file.name });
store.set(STORE_PACKS, packs);
}
res.errors.slice(0, 5).forEach((e) => toast(e, 'error'));
toast(`${file.name}: ${res.added} neu, ${res.replaced} ersetzt${res.errors.length ? `, ${res.errors.length} abgelehnt` : ''}.`,
res.errors.length ? 'warn' : 'info');
if (!ui.session) renderStart();
} else {
toast(`${file.name}: unbekanntes Format (erwartet ${SESSION_FORMAT} oder ${PACK_FORMAT})`, 'error');
}
};
reader.readAsText(file);
}
/* ----------------------------- Ereignisse */
function onChange(ev) {
const t = ev.target;
if (t.closest('#setup')) { updateStartForm(); return; }
const section = t.closest('section.question');
if (!section || !ui.session) return;
const s = ui.session;
const q = s.questions[Number(section.dataset.index)];
const a = s.answers[q.id] || (s.answers[q.id] = { value: q.type === 'fill' ? '' : [] });
if (t.hasAttribute('data-flag')) { a.flagged = t.checked; save(); return; }
if (isLocked(q)) return;
if (q.type === 'single') a.value = [t.value];
else if (q.type === 'multi') {
let sel = $$('.options input:checked', section).map((i) => i.value);
const max = correctCount(q);
if (sel.length > max) {
t.checked = false;
sel = sel.filter((v) => v !== t.value);
toast(`Höchstens ${max} Antworten wählbar.`, 'warn');
}
a.value = sel;
} else a.value = t.value;
save();
updateToolbar();
}
function onClick(ev) {
const btn = ev.target.closest('[data-act],[data-goto]');
if (!btn) return;
if (btn.dataset.goto !== undefined) { Reveal.slide(Number(btn.dataset.goto)); return; }
switch (btn.dataset.act) {
case 'next': Reveal.next(); break;
case 'prev': Reveal.prev(); break;
case 'resume': { const s = store.get(STORE_SESSION); if (s) { ui.session = s; renderSession(s.current); } break; }
case 'discard':
if (window.confirm('Gespeicherte Sitzung verwerfen? (Vorher exportieren, falls sie noch gebraucht wird.)')) {
store.del(STORE_SESSION); renderStart();
}
break;
case 'submit': {
const open = ui.session.questions.filter((q) => !isAnswered(ui.session.answers[q.id])).length;
if (!open || window.confirm(`${open} Fragen sind offen. Trotzdem abgeben?`)) submitExam(false);
break;
}
case 'retry': retryWrong(); break;
case 'export': exportSession(); break;
case 'import': $('#import-file').click(); break;
case 'menu': save(); renderStart(); break;
default: break;
}
}
function onKey(ev) {
const t = ev.target;
if (t.matches && t.matches('input.fill')) {
if (ev.key === 'Enter') { ev.preventDefault(); t.blur(); Reveal.next(); }
return;
}
if (t.matches && t.matches('input[type=text],input[type=number],select,textarea')) return;
if (ev.ctrlKey || ev.altKey || ev.metaKey) return;
const n = Number(ev.key);
if (n >= 1 && n <= 5) {
const input = $$('.present .options input')[n - 1];
if (input && !input.disabled) { input.click(); ev.preventDefault(); }
}
}
function onSlideChanged(ev) {
const s = ui.session;
if (!s) return;
s.current = ev.indexh;
const sec = ev.currentSlide;
if (sec.classList.contains('result')) {
// Lernmodus: Auswertung bei jedem Aufruf neu berechnen
const fresh = document.createElement('div');
fresh.innerHTML = resultSlide();
sec.innerHTML = fresh.firstElementChild.innerHTML;
} else if (sec.classList.contains('submit')) {
const fresh = document.createElement('div');
fresh.innerHTML = submitSlide();
sec.innerHTML = fresh.firstElementChild.innerHTML;
} else if (sec.classList.contains('question')) {
const q = s.questions[Number(sec.dataset.index)];
if (isLocked(q) && q.options) markOptions(sec, q);
sec.scrollTop = 0;
}
save();
updateToolbar();
}
function onFragmentShown(ev) {
const frag = ev.fragment;
if (frag && frag.classList.contains('solution')) revealQuestion(frag.closest('section.question'));
}
function init() {
loadBank();
// Erst eine Platzhalter-Folie, dann reveal.js starten, dann die echte Startseite setzen
$('.reveal .slides').innerHTML = '';
Reveal.initialize({
hash: false,
history: false,
controls: true,
controlsTutorial: false,
progress: true,
center: false,
disableLayout: true,
scrollActivationWidth: null,
overview: false,
touch: false,
pause: false,
transition: 'slide',
transitionSpeed: 'fast',
keyboard: { 66: null, 190: null, 79: null }, // B, Punkt, O: Pause/Übersicht aus
}).then(() => {
Reveal.on('slidechanged', onSlideChanged);
Reveal.on('fragmentshown', onFragmentShown);
const slides = $('.reveal .slides');
slides.addEventListener('change', onChange);
slides.addEventListener('input', (ev) => { if (ev.target.matches('input.fill')) onChange(ev); });
slides.addEventListener('submit', (ev) => { ev.preventDefault(); startFromForm(ev.target); });
document.addEventListener('click', onClick);
document.addEventListener('keydown', onKey);
$('#import-file').addEventListener('change', (ev) => {
const f = ev.target.files && ev.target.files[0];
if (f) importFile(f);
ev.target.value = '';
});
$('#tb-export').addEventListener('click', exportSession);
window.addEventListener('beforeunload', save);
new ResizeObserver(syncToolbarHeight).observe($('.toolbar'));
setInterval(tick, 1000);
renderStart();
});
}
window.LPICSim = { core, ui };
if (window.Reveal && !window.LPICSIM_NO_UI) document.addEventListener('DOMContentLoaded', init);
})();