Commit Graph

18 Commits

Author SHA1 Message Date
anggoro ffceb519be feat(content-editor): konfirmasi ganda file navigasi kritis & hapus narrow sidebar
- Tambah PROTECTED_BASENAMES (home.md, sub-home.md) dengan konfirmasi
  kedua saat rename/delete agar tidak merusak navigasi siswa (tidak
  bisa di-undo). Diteruskan ke renameEntry/deleteEntry di contentEditor.ts.
- Hapus fitur featureSidebarNarrow + CSS terkait (tidak dipakai).
- Footer & backend: penyesuaian rute/test content editor.
2026-08-23 08:18:02 +07:00
a2nr 084eb71a71 feat(content-editor): tambah editor konten guru dengan draft & publish
Backend:
- Model ContentDraft + migration 0004_content_drafts
- CRUD draft: save, reload, preview, publish, discard
- Tree ops: create file/folder, rename, delete dengan draft cleanup
- Refactor _render_markdown_string() agar bisa render dari buffer
- Refactor teacher_auth ke modul shared (teacher_auth.py)
- Blueprint routes/content_editor.py (17 endpoint)
- Permission error handling di semua write operations
- Tambah Pillow ke requirements.txt
- Mount content/ & assets/ dari :ro ke :rw

Frontend:
- Page /teacher/content dengan panel tree + editor + preview
- ContentFileTree, LessonContentView, QuizPreviewReadonly components
- contentEditor.ts service layer & contentEditor.svelte.ts state manager
- CodeEditor support @codemirror/lang-markdown + sync code prop
- Tambah @codemirror/lang-markdown dependency
2026-08-21 15:55:21 +07:00
a2nr ac10692b61 feat(prereq): dukung ambang minimum (min: N%) pada prasyarat materi
Tambahkan logika ambang minimum skor (opsional) di daftar prasyarat.
Tanpa ambang, status scored berapa pun (termasuk 0/x) memenuhi syarat
(backward compatible). Dengan ambang `min: N%`, earned/total harus
melebihi ambang untuk membuka kunci materi berikutnya.

Perubahan:
- Parser bullet prasyarat mendukung suffix `min: N%` (contoh: min: 75%)
- is_prerequisite_met: completed → selalu terpenuhi; scored → terpenuhi
  kecuali di bawah ambang; not_started → belum terpenuhi
- routes/lessons.py: 3 titik kunci sekarang pakai get_missing_prerequisites
- latex_test.md: tambahkan prereq quiz_test min: 75% (demo nyata)
- examples/content disinkronkan dengan content
- Tambah test_prerequisite_logic.py (28 unit test) + 3 integration test
2026-08-17 21:57:10 +07:00
a2nr 16031fa728 fix(docs/api): hilangkan sync berulang di api_reference + UI API Reference di /docs
- routes/docs.py: pakai current_app, bukan create_app() per request — tidak
  lagi memicu DB sync ulang di setiap hit /docs/api-reference (dead code
  route_modules/rule_map dihapus)
- frontend: entry statis API Reference di sidebar /docs, fetch lazy
  /api/docs/api-reference, render method badge + auth + docstring
- test: 4 test yatim di test_teacher_bootstrap diberi marker unit;
  Makefile test-stats kini menampilkan count per marker + orphan (harus 0)
- docs_service: hapus dead code _read_md_cached, sederhanakan cache-check
  _parse_frontmatter (satu getmtime, kondisi jelas)
- lesson_service: gabung pemanggilan bleach.clean dobel jadi satu dengan
  css_sanitizer opsional — hilangkan NoCssSanitizerWarning (7x)
- conftest: fixture autouse _isolate_database lewati isolasi untuk test unit
  supaya make test-unit tetap jalan tanpa DB walau DATABASE_URL diset

Verifikasi: unit 114 passed, full suite 239 passed (0 warnings) dengan
Postgres test, svelte-check 0 error, vitest 114 passed, docs-validate ok.
2026-08-17 16:37:30 +07:00
a2nr a5aedf66d4 refactor(test): unified test runner, markers, elemes.sh cleanup
- Add pytest.ini with unit/integration/e2e markers + --strict-markers
- Add Makefile as centralized test runner entrypoint
- Add @pytest.mark.integration to 7 DB-dependent test files
- Add @pytest.mark.unit to 6 pure-logic test files
- Refactor elemes.sh: test-* commands delegate to 'python -m pytest -m <marker>'
- Move test_elemes_sh.sh -> scripts/ (path-adjusted), add test-shell Makefile target
- Add test-worker target for compiler_worker/tests (separate PYTHONPATH)
- Update runclearbuild smoke gate to use -m unit + sub-home subset
- Add .github/workflows/ci.yml: test-unit + test-integration on PR to dev
- 239 tests collected (110 unit, 125 integration) — no count reduction
2026-08-17 11:29:29 +07:00
a2nr 25269fca08 fix(quiz/progress): perbaiki temuan audit — btn:disabled, css mati, indentasi 2026-08-17 09:23:05 +07:00
a2nr 9aef612f01 feat(quiz/progress): pindah navigator ke quiz view, tampilkan guru di /progress, tambah 1 siswa, fix breakdown eval/diag
- Task 1: pindahkan .quiz-navigator & .quiz-controls dari QuizTab ke
  QuizQuestionView; refactor props jadi mgr: LessonManager; CSS ke lesson.css.
  Navigator+kontrol kuis selalu terlihat di mobile walau workspace hidden.
- Task 2: get_all_students_progress tidak filter role guru; field role per row;
  badge 'Guru', bulk-action & empty-state & ringkasan hanya hitung siswa.
- Task 3: endpoint POST /students/add (reuse StudentRoundTripRow +
  find_student_import_conflicts + run_student_import); validate_single_student_input;
  AddStudentDialog + addStudent(); import/export/bulk-delete CSV lama tetap ada.
- Fix (Opsi A): breakdown eval/diag hanya hitung MCQ (flashcard netral) —
  selaraskan dengan skor resmi FE; perbaiki tampilan eval:4/6 yg membingungkan.
  Tambah field type ke QuizAnswerPayload.

Test: backend 239 passed (termasuk test baru teacher-in-report, add-student,
breakdown-excludes-flashcards); svelte-check 0 error; build sukses.
version.ts 27->28.
2026-08-17 08:32:42 +07:00
a2nr 58cb35a01a feat(quiz): tag kategori diagnostik + breakdown evaluasi/diagnostik di kuis & report guru
- Backend parser (_parse_flashcards): deteksi marker ::diagnostic di baris sendiri -> category='diagnostik' (default evaluasi), strip dari tampilan.
- Persistensi: kolom answers_json (migration 0003) simpan ringkasan per-soal {question_id, selected_option_id, is_correct, category}.
- quiz_attempts: validasi/serialize answers_json + endpoint GET /quiz-attempts/<lesson> untuk review-after-refresh.
- quiz-session: hitung evalCorrect/evalTotal/diagCorrect/diagTotal/diagUnmastered; skor resmi = evaluasi saja.
- Frontend: QuizTab badge per-soal + breakdown 'Belum dikuasai'; progress guru tampil badge Eval/Diag per lesson.
- Content: pindahkan ::diagnostic ke baris sendiri (fix kerusakan fenced code/blockquote dari edit sebelumnya).
- Tests: unit parser kategori + payload answers.
2026-08-15 09:33:23 +07:00
a2nr 79edcd1596 fix: sync lesson registry termasuk sub-bab (sub-home.md) agar kolom materi muncul di CSV guru
lesson_specs() hanya memindai home.md root sehingga lesson di
bab/<folder>/sub-home.md tidak pernah masuk tabel lessons. Akibatnya kolom
materi sub-bab hilang saat guru import/export siswa CSV.

- services/lesson_service.py: tambah find_all_sub_home_folders() (enumerate
  level-1 folder di CONTENT_DIR berisi sub-home.md)
- services/lesson_registry.py: lesson_specs() gabungkan union root + sub-bab,
  dedupe slug (root priority) — satu lesson entitas
- services/tests/test_lesson_registry.py: test unit + DB integration

Verifikasi: 3 passed, 2 skipped (unit); test_sync_includes_sub_bab_lessons
passed (DB); 58 passed total termasuk test_repositories + student_management
tanpa regresi.
2026-08-14 07:07:25 +07:00
a2nr 432a8c2eca fix: show lesson progress on sub-home /bab pages
The /bab/<folder> endpoint returned raw get_sub_home_data() output, so
lessons had no completed/locked fields and green checkmarks never
appeared. api_bab now reads the student token, injects progress via
get_ordered_lessons_with_learning_objectives (scoped to the folder's
sub-home.md), and computes lock status from prerequisites — matching
/lessons. The bab page load also forwards the student token, and tests
cover progress injection and prerequisite locking.
2026-08-14 05:23:50 +07:00
a2nr 67ed58a327 feat: scope sub-home lesson lists & make sub-home cache mtime-aware
Lengkapi rencana sub-home (fitur bab sudah ada sejak 2f689af) dengan
perilaku sidebar/navigasi yang akurat per sub-folder:

- get_lessons()/get_ordered_lessons_with_learning_objectives() menerima
  source_path opsional sehingga daftar materi dapat diambil dari
  sub-home.md alih-alih home.md root
- api_lesson: ordered_lessons + prev/next di-scope ke sub-home.md bila
  folder memilikinya, fallback ke daftar global home.md
- get_sub_home_data kini mtime-cached (bukan lru_cache) sehingga edit
  sub-home.md langsung terlihat tanpa restart; edit juga meng-invalidate
  find_lesson_file agar materi baru terdeteksi

Testing & docs:
- test_sub_home.py (unit): _read_md_cached mtime, find_sub_home_for_lesson,
  get_sub_home_data refresh, ordered_lessons via source_path
- test_sub_home_api.py (integration): GET /bab/<folder> + scoped
  ordered_lessons di /lesson/<slug>.json
- docs/13-content-sub-home.md (baru), README + docs/02-backend-flask.md
  diperbarui dengan catatan fallback
- elemes.sh: perintah `test` + verifikasi tes sub-home di runclearbuild
2026-08-13 20:08:50 +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 9ee92cfaaf feat(teacher): manajemen akun guru canonical + first-run otomatis; cabut backend CSV (PG only)
- feat: ./elemes.sh teacher — upsert satu akun guru canonical (prompt nama default TEACHER_NAME, token tersembunyi via stdin, rotasi token saat guru sudah ada, idempotent); TEACHER_NAME/TEACHER_TOKEN di .env
- feat: first-run otomatis — db_init jalankan alembic upgrade head + bootstrap guru bila TEACHER_TOKEN terisi (run/runbuild/runclearbuild)
- refactor: backend CSV dicabut penuh — hapus mount tokens_siswa.csv, TOKENS_FILE, STORAGE_BACKEND=csv, csv_backend.py, csv_importer.py, generate_tokens.py, script migrate/verify/export CSV, command generatetoken/dbimport/dbverify/dbexport/synclessons; storage fail-loud postgresql-only
- fix: dbrestore gagal diam-diam saat restore ke DB berisi data (dump tanpa --clean) — dbbackup kini --clean --if-exists + dbrestore reset schema public; roundtrip terverifikasi
- test: suite kontrak PG-native + route auth/progress integrasi PG; 183 passed container, 96 passed host, frontend 91 passed
- docs: README, documentation.md, docs/01/02/11, load-test/README disinkronkan ke realita PostgreSQL-only; proposal.md dipertahankan (historis)
- chore: load-test token sintetis di-seed ke PG (content_parser), config.py bersih TOKENS_FILE, .dockerignore/.gitignore dibersihkan
2026-08-09 11:21:08 +07:00
a2nr d602b0f642 feat(student-management): kelola siswa round-trip CSV + bulk delete (PG)
- Export siswa terpilih/seluruh + progress ke CSV round-trip (token tidak
  pernah diekspor; teacher tidak ikut; urutan deterministik)
- Import preview all-or-nothing: siswa existing di-restore (nama+progress,
  token lama dipertahankan), siswa baru dibuat dari student_id kosong +
  token; conflict tidak menyebut raw token/digest
- Bulk delete multi-seleksi dengan validasi ketat (teacher terlindungi,
  zero-delete bila ada ID bermasalah)
- fix(import): deteksi delimiter berbasis schema — menerima titik koma
  (;) maupun koma (,), ekspor tetap ';' + BOM; header tab/campuran ditolak
  dengan pesan jelas (regresi: file Excel/Sheets kini bisa diimpor)
- refactor: progress_status.py — kontrak status legacy tunggal dipakai
  storage PG, export, dan import; report siswa hanya role student
- UI /progress: dialog import/export + bulk delete, selection store,
  respon import tanpa raw token; APP_VERSION 22 -> 26
- ops: ORIGIN env var di podman-compose.yml
- docs: README section 9 (workflow round-trip + delimiter) dan
  docs/11-database-migration (kelola siswa via round-trip CSV)
- test: parser round-trip (45), repositories, route management (24,
  PostgreSQL), frontend 91, fixtures CSV valid/invalid
2026-08-09 07:29:19 +07:00
a2nr ac37db5c91 feat(storage): migrasi penyimpanan CSV ke PostgreSQL (SQLAlchemy + Alembic)
- facade storage: csv_backend + postgres_backend, dipilih via STORAGE_BACKEND
- token disimpan sebagai HMAC-SHA256 digest (TOKEN_PEPPER), tanpa plaintext
- schema alembic 0001: users, access_tokens, lessons, student_progress
- importer CSV idempotent + dry-run + sparse model (not_started tanpa row)
- lesson registry: sync dari home.md (lesson hilang di-deactivate, bukan dihapus)
- routes progress/auth: report & reset via DB, validasi via token hash
- elemes.sh: dbupgrade/dbstatus/dbimport/synclessons/dbverify/dbbackup/dbrestore/dbexport
- podman-compose: service postgres (PG18), tokens_siswa.csv mount :ro
- frontend: api client + progress page; tests kontrak dual-backend + isolasi DB
- load-test: skenario database (locustfile_db.py)
- docs: 01-11 termasuk 11-database-migration.md (arsitektur & operasional)
2026-08-08 04:57:08 +00:00
a2nr c103b55dc4 feat(compiler-worker): sesi interaktif PTY untuk playground — Python input() dan C scanf() tanpa EOFError
- session_manager.py: InteractiveSession + SessionManager (PTY Python/C, bounded
  output buffer, delta cursor, sweeper, semaphore compile max 2, limit 50 sesi)
- app.py: hybrid — endpoint batch /execute lama dipertahankan + endpoint sesi baru
  (POST /sessions, GET delta output, POST input, DELETE stop) + health
- Dockerfile: gunicorn gthread 1 worker/32 thread (registry in-memory aman)
- routes/compile.py: proxy Flask /compile/sessions + anon slot lifecycle + rate limit
- podman-compose.yml: env COMPILER_WORKER_BASE_URL
- tests: test_session_manager.py (baru) + test_app.py + services/tests/test_compile_sessions.py
2026-08-02 14:14:45 +00: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
a2nr 2a09ee77ab feat(embed): add raw HTML embed fence with bleach sanitization
- Add embed markdown fence: user pastes raw embed HTML code
  (from Canva/YouTube/Google Docs Share→Embed) into lesson markdown
  and slide content. Backend sanitizes via bleach (whitelist
  tags/attrs/styles) + checks iframe src against domain blacklist
  (SSRF prevention). Frontend renders iframe directly — no lazy
  action needed.
- Backend: _process_embed_embeds + _sanitize_embed_html in
  lesson_service.py, applied to lesson_content, exercise,
  lesson_info, and slide loop. Graceful fallback if tinycss2
  missing (CSS unsanitized but tags/attrs still stripped).
- Tests: 9 pytest cases (Canva/YouTube HTML, script stripping,
  onclick stripping, blocked domain, non-https iframe, empty,
  unchanged, dangerous style).
- Frontend: remove renderEmbedEmbeds.ts + wire-up + .generic-embed
  CSS (URL-only approach from earlier iteration, superseded).
  Keep .embed-error CSS for error messages.
- Example: update test_slides.md with raw HTML Canva embed (slide)
  + YouTube embed (body).
- Deps: bleach>=6.0.0, tinycss2>=1.2.0 in requirements.txt.
- Docs: consolidate 4 plan files into docs/06-embed-content.md.
2026-07-19 15:39:38 +07:00