Commit Graph

5 Commits

Author SHA1 Message Date
a2nr 81cb9c69ca feat(quiz): taksonomi key quiz per jenis + MCQ berbobot (partial credit)
Fitur B — setiap jenis soal kini punya key sendiri sehingga model
penilaiannya eksplisit, tidak ditebak dari isi konten:

- ---QUIZ_MCQ---      pilihan ganda biner (tepat satu [x]; larang [N])
- ---QUIZ_WEIGHTED--- pilihan ganda berbobot ([N] >= 1, minimal satu [ ],
                      bobot tertinggi unik = kunci; poin parsial)
- ---FLASHCARD---     kartu hafalan tanpa penilaian (larang opsi)
- legacy ---QUIZ_FLASHCARD--- / ---INITIAL_QUIZ--- tetap dibaca tanpa
  perubahan perilaku (regex opsi legacy sengaja tidak berubah — baris
  '- [10]' di konten lama BUKAN opsi); dihapus dari docs/template
- ::diagnostic tetap kategori di dalam QUIZ_MCQ & tetap masuk skor resmi

Model skor: rata-rata persentase per jenis yang ada, Math.round,
lulus >= 75. statusString: "X/Y" bila hanya MCQ/legacy, "P/100" bila ada
weighted, "completed" bila hanya flashcard (auto-lulus). Satu sesi/
attempt untuk semua jenis; soal flat urut dokumen.

Parser (backend & frontend mirror, ekstraksi multi-blok per key urut
dokumen via loop marker-start paling awal; id q-N deterministik):
- lesson_service.py: regex [ xX]?[0-9]* utk key baru, _iter_option_lines
  kind-aware, _validate_quiz_options per kind, _parse_flashcards(kind,
  id_offset), _extract_quiz_sections + guard loop maju (marker end
  hilang tidak lagi infinite loop), get_lesson_quiz_total -> 100 bila
  ada weighted (round-trip CSV "P/100")
- parseFlashcards.ts / index.ts / detectActiveTabs.ts / extractSections.ts:
  mirror perilaku; heading soal <h3> kini fence-aware (fix fixture stale)

Payload attempt baru: quiz_type, earned_points, question_max_points —
opsional di validasi backend supaya klien lama tetap diterima; poin
di-recompute dari konten saat resume/review.

UI: QuizTab skor via statusString + alert berbasis percent, badge
weighted (✓ 10 / 🟡 7/10 / ✗ 0) di review, preview guru badge bobot +
"Berbobot (total N)", laporan guru badge MCQ/Bobot + tooltip, CSV
progress 2 kolom baru per lesson (_quiz_mcq, _quiz_weighted), template
editor konten QUIZ_MCQ/Quiz Bobot/Flashcard.

Tooling & docs: scripts/convert_quiz_keys.py (konversi legacy -> key
baru per run jenis, idempoten, verifikasi parse identik; examples/
content dikonversi), scanner mendukung semua key, docs/07 ditulis ulang,
docs/15 direvisi ke desain final, README/documentation/docs 02/04/09
diperbarui.

Tes: backend 444 lulus (+33: validasi ketat per key, urutan dokumen,
multi-blok, zero regression, field selalu di-emit), frontend 249 lulus
(+13 parser key baru, +11 model skor), parity fixture diregenerasi,
svelte-check 0 error, vite build OK.
2026-10-25 20:16:56 +07:00
a2nr 087d338477 feat(quiz): izinkan guru melanjutkan attempt terminated dari jawaban tersimpan
Kebijakan termination tetap strict, tapi guru pegang override untuk kendala
tak terduga (false-positive blur dari dialog OS, browser crash, refresh
tak sengaja). Selain reset (siswa mengulang dari nol), guru kini bisa
memberi izin resume — siswa melanjutkan dari jawaban yang sudah tersimpan.

- Tambah kolom quiz_attempts.resumed_at (migrasi 0006) sebagai penanda izin
  guru melanjutkan attempt terminated
- Tambah endpoint teacher-only POST /resume-quiz-attempt — kontrak keamanan
  sama dengan /reset-progress (guru cukup student_id anonim, tanpa token siswa)
- Syarat: hanya attempt ber-status terminated yang bisa di-resume; submitted
  ditolak karena kuis sudah selesai normal
- has_quiz_attempt meloloskan attempt ber-izin sehingga gate one-attempt
  terbuka selama sesi lanjutan
- finalize_quiz_attempt menimpa row attempt sekali (status/skor/answers baru)
  lalu clear resumed_at; retry berikutnya kembali ke jalur idempotent normal
- Sinyal ke frontend siswa lewat quiz_resumable pada data lesson; laporan guru
  menampilkan badge "Dilanjutkan" dan tombol "Lanjutkan" untuk attempt
  terminated
- Frontend: restoreQuizSession() merekonstruksi sesi dari jawaban tersimpan per
  question_id, is_correct dihitung ulang dari source sebagai guard bila
  konten soal berubah; option_id tak dikenal dianggap belum dijawab
- Frontend: firstUnansweredIndex() menentukan soal awal sesi lanjutan;
  resolveQuizTabView() memusatkan keputusan view agar transisi resume dapat
  diuji Vitest
- UI siswa: panel "Lanjutkan Kuis" + snackbar mode lanjutan (sekali muncul).
  Attempt di-refetch segar sebelum sesi dimulai agar izin yang sudah dicabut
  guru tidak memulai sesi yang jawabannya akan terbuang
- Test: 16 integrasi backend (services/tests/test_resume_quiz.py), 9 unit
  quiz-session, 10 unit quiz-view
- Docs: alur resume + keterbatasan di 12-quiz-integrity.md, ringkasan di
  07-quiz-authoring.md, kontrak endpoint di 02-backend-flask.md
2026-10-20 19:24:40 +07:00
a2nr 9c2563a2bf feat(docs): docs viewer, API reference generator, markdown rendering
- Add YAML frontmatter (title/order/category) to 13 docs/*.md files
- Add services/docs_service.py: mtime-cache, frontmatter parsing w/ fallback,
  bleach sanitize, path traversal protection
- Add routes/docs.py: GET /api/docs, GET /api/docs/<slug>, GET /api/docs/api-reference
- Register docs_bp in app.py
- Add frontend /docs + /docs/[slug] routes (sidebar nav + markdown render)
- Add frontend/src/types/docs.ts interfaces
- Add link from /help page to /docs viewer
- Add scripts/validate_docs.py: frontmatter + broken link validation
- Add elemes.sh docs-validate command

Endpoints documented in API reference: /quiz-attempts/<lesson>,
/sessions, /bab/<folder>, /compile/sessions, /lesson/<slug>.json + all 30 routes
2026-08-17 11:45:05 +07:00
a2nr 24502380aa feat(quiz): strict focus-loss anti-cheat + audit attempt
Kebijakan strict: kehilangan fokus saat kuis aktif langsung mengakhiri kuis
dengan penalti, satu attempt per kuis tercatat audit, pembahasan disembunyikan
saat focus_lost, badge pelanggaran muncul di laporan guru.

Backend:
- tabel quiz_attempts (migration 0002) + endpoint POST /quiz-attempts/submit
  idempotent dengan unique constraint (satu attempt per token+lesson, 409
  untuk duplikat), bounded termination_reason
- models/repositories/storage: QuizAttempt + record/query pelanggaran untuk
  laporan guru (progress)

Frontend:
- quiz-integrity.ts: policy hub (isFocusLossEvent, terminateQuiz,
  shouldShowQuizReview) + attempt_id via crypto.randomUUID()
- lesson.svelte.ts: finalisasi idempotent; sendBeacon untuk exit path yang
  bisa suspend (focus_lost/page_unload), fetch keepalive untuk reason lain
- QuizTab.svelte: summary menyembunyikan pembahasan saat focus_lost
- progress/+page.svelte: badge pelanggaran focus_lost di laporan guru
- APP_VERSION 26 -> 27

Tests & docs:
- 204 backend test pass (unit host + integrasi container, DB elemes_test)
- 107 frontend test pass, svelte-check 0 error
- docs/12-quiz-integrity.md (baru), docs/07-quiz-authoring.md + README FAQ
  anti-cheat diperbarui
2026-08-11 20:55:39 +07:00
a2nr 02d5969619 refactor(quiz): restructure quiz session, question view, and tab safety
Restructure quiz flow into QuizSession primitive + atomic quiz state in
LessonManager, and extract QuizQuestionView for split-view display.

Tab-safety fix: add lesson-tabs.ts with pickDefaultTab(), getAvailableTabs()
mirroring the exact {#if} conditions in +page.svelte, and ensureActiveTab()
safety-net that falls back to pickDefaultTab() when the desired tab is not
rendered. A reactive $effect in lesson/[slug]/+page.svelte validates
activeTab against lesson data on every change, preventing the "tab quiz
kosong" (empty workspace) regression for quiz-only lessons and phantom-tab
state after data changes.

Files:
- lib/services/lesson-tabs.ts (+test): pure helpers, 18 tests pass
- lesson/[slug]/+page.svelte: ensureActiveTab reactive guard + QuizQuestionView
- lesson/[slug]/lesson.svelte.ts: atomic startQuiz, getExitStatus, idempotent
  submit on navigate/exit
- lib/services/quiz-session.ts (+test), lib/types/quiz.ts, QuizTab.svelte
  (mgr-driven), QuizQuestionView.svelte, lesson.css
- services/lesson_service.py, services/tests/test_lesson_service_quiz.py
  (test_lesson_service_quiz.py), requirements.txt (+pytest)
- docs/07-quiz-authoring.md
- version.ts APP_VERSION 21 -> 22 (forces SW update for the fix)
- examples/content/dasar/quiz_test.md synced to live content root
2026-08-01 00:14:29 +07:00