Arsitektur: upload tetap menyimpan original apa adanya; optimasi terjadi
saat serving — gambar raster di-resize (maks 1600px) + encode WebP
(quality 80), di-generate on-the-fly lalu di-cache di disk.
- config.CACHE_DIR = sibling ASSETS_DIR ('assets-cache'), WAJIB di luar
ASSETS_DIR supaya export_bundle (os.walk tanpa filter) tetap lossless
- services/image_cache.py: varian WebP, cache key path+mtime+size+width,
invalidasi otomatis via mtime, atomic write, fallback original bila gagal
- routes/lessons.py::send_assets: handler (bukan static serve) —
.png/.jpg/.jpeg/.webp/.gif -> varian WebP; .svg & GIF animasi pass-through;
?orig=1 untuk original; ?w= untuk lebar alternatif; ETag + Cache-Control
- Dockerfile: buat /app/assets-cache writable
Test (services/tests/test_asset_cache.py, 20 test): lokasi cache di luar
ASSETS_DIR, cache reuse, invalidasi mtime, passthrough SVG/GIF animasi,
fallback file korup, ETag/304, dan acceptance test — upload -> request ->
export bundle berisi HANYA original tanpa .webp turunan.
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
Hasil review pasca-fix upload asset:
Keamanan:
- Mitigasi stored-XSS via SVG (route /assets/* publik, same-origin):
CSP 'default-src none; sandbox' saat serve SVG — di backend
(send_assets) DAN di proxy SvelteKit (proxy hanya meneruskan
content-type/set-cookie, jadi CSP backend tak sampai ke browser).
- Deteksi 413 proxy kini ber-test status code + pesan.
Testing:
- Regression guard: error jaringan biasa tetap 502, bukan 413.
- 413 via status code saja (tanpa regex pesan) tetap 413.
- Boundary 5 MiB: file TEPAT 5 MiB lolos; 5 MiB+1 ditolak (cabang
len(raw) — SVG dipakai karena dikecualikan validasi PIL).
- CSP: SVG dapat header, raster tidak.
- E2E script (verify_asset_upload.py) memakai file asli
mr-edu/assets/ilustrasi_beban.jpeg + test 413 terintegrasi.
Reuseability:
- Komentar silang-referensi _MAX_ASSET_BYTES (sumber) dengan pesan 413
proxy dan BODY_SIZE_LIMIT compose.
- Redesign /teacher/progress into a folder-like tree view (sub-bab -> lesson -> completed students)
- Create /teacher/classroom for student management (import/export CSV, add, bulk delete)
- Enrich /progress-report.json API response with sub_bab, sub_bab_title, and sub_bab_list
- Add Classroom navigation item to teacher navbar menu
- Only show celebration overlay and top bar completed checkmark if quiz score meets 75% threshold (>= 0.75) and exercises are done
- Return is_done, state, and composite_percent in POST /lesson-progress for exercise and quiz submissions
- Synchronize frontend LessonManager state with authoritative server is_done response
- Add has_quiz_attempt check in routes/lessons.py and token_service to recognize existing attempts even when composite state is in_progress
- Update QuizTab to display persistent attempt review, score status, and explanation instead of retake prompt on page load/refresh
- Make QuizQuestionView finish handler async, await finishQuiz(), and disable submission buttons to prevent navigation race conditions
- Update local ordered_lessons completion state dynamically upon passing
- Add unit and integration tests covering response payload attributes and has_quiz_attempt API field
- Bump APP_VERSION to 30
- Sync imported progress (completed, scored, done, reset) to composite progress model and recompute composite_percent
- Prevent caching of /progress-report.json with Cache-Control: no-store and frontend cache-busting timestamp
- Provide inline expected format guides in exported CSV lesson headers (e.g. [completed], [<earned>/<total>], [done:<1|0>:<earned>/N])
- Support robust slug extraction from formatted headers in CSV roundtrip import with full backward compatibility
- Add example composite lesson (exercise + quiz) in examples/content/dasar/composite_test.md
- Add comprehensive unit and integration tests covering progress synchronization, header formatting, and roundtrip workflows
- Auto-save dirty draft before publishing in ContentEditorManager so in-memory
Velxio circuit/code changes are never lost when clicking Publish directly
- Add resolveRestoredContent and baseline tracking to velxio-manager so lesson
view immediately reflects updated server code instead of stale localStorage
- Invalidate in-memory lesson and markdown caches on draft publish and file operations
- Update teacher content page publish button disabled condition to check saving state
- Integrate diagnostic and evaluation MCQ scoring in quiz sessions
- Extend round-trip CSV grammar with `done:<ex>:<earned>/<total>` and `RESET`
- Fix completed count aggregation in repositories to include 'done' state
- Update progress report CSV export to output composite scores per lesson
- Add comprehensive test coverage for scoring, CSV round-trip, and exports
- Perbaiki pemanggilan deleteEntry di +page.svelte baris 531: argumen
force=true tidak lagi diteruskan dengan benar setelah signature
fungsi berubah (confirmCritical ditambah di tengah)
- Koreksi komentar dengan karakter Mandarin di content_editor.py baris 488
- Tambah test unit contentEditor.test.ts dengan 5 kasus:
- deleteEntry force delete
- deleteEntry confirm_critical untuk file kritis
- deleteEntry default parameter
- renameEntry standar
- renameEntry confirm_critical
Fixes: hapus folder dengan isi tidak lagi gagal 409.
All tests: 119 passed.
- 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.
- Reset previewActiveTabs/previewSlides saat pindah ke asset & saat hapus
file aktif, agar tab dinamis (C/Python/Circuit/Arduino/Flowchart) tidak
menetap setelah berpindah konteks.
- Ganti Pillow Image.verify() dengan Image.load() di validasi upload asset.
verify() false-positive CRC pada chunk IDAT PNG valid -> backend 500.
load() cukup membuktikan file adalah gambar yang bisa di-decode.
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
- 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.
- Flask docs routes changed from /api/docs to /docs (no /api prefix)
because hooks.server.ts proxies /api/* by stripping /api, so /api/docs
reached Flask as /docs (404). Other routes (lessons, help) also lack /api.
- Frontend load functions now fetch /docs and /docs/<slug> (not /api/docs)
- Fixes 500 on /docs SSR (proxy ECONNREFUSED / 404 from stripped route)
- 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.
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.
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
- 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
- playground route: velxio/flowchart/circuit/code tabs with auto-save
- FileTree + ConsolePanel for C/Python code tab
- compileCode() forwards stdin through routes/compile.py to compiler_worker
- compiler_worker run_c_code/run_python_code accept stdin, pass via subprocess.run(input=)
- known issue: Console UI does not queue stdin properly → fix in next cycle
- Generalize _read_md_cached() for generic file caching
- Add find_sub_home_for_lesson() and get_sub_home_data() helpers
- Add /api/bab/<folder> endpoint
- Detect sub-home in api_lesson for nav context
- Skip sub-home.md in find_lesson_file() and _parse_lesson_links()
- Add title to bab frontend page data
- Update generate_tokens.py & content_parser.py to skip sub-home.md
Implements multiple layers of security to address high-risk session
and authentication vulnerabilities identified in the security review:
- Allow code compilation (C, Python, Arduino) for anonymous users.
- Enforce a 1-request-per-2-minutes rate limit for anonymous IPs.
- Implement a global anonymous compilation queue with 20 concurrent slots.
- Proxy Velxio (Arduino) compilation through Flask to prevent API hijacking.
- Exempt authenticated users (tokens/cookies) from all rate limits.
- Fix networking and DNS resolution in podman-compose.
- Fix Svelte a11y warnings and trailing slash routing issues.
- Cookie Security: Added dynamic 'secure' flag support via COOKIE_SECURE
env variable for HTTPS/Tailscale Funnel compatibility.
- Rate Limiting: Integrated Flask-Limiter on /login (50 req/min) to
prevent API abuse while accommodating shared school networks (NAT).
- Tarpitting: Added 1.5s artificial delay on failed logins to neutralize
automated brute-force tools without blocking legitimate users.
- Session Invalidation: Implemented an in-memory token blacklist on
logout to ensure session tokens cannot be reused.
- Documentation: Updated technical docs and proposal status to reflect
the current security architecture.
Ref: @elemes/proposal.md (Poin 6.1, 6.2, 6.3)
- Menambahkan service 'compiler-worker' terpisah untuk isolasi eksekusi kode C/Python.
- Mengintegrasikan gVisor (runsc) pada worker untuk mencegah RCE pada level kernel.
- Menggunakan Gunicorn (4 workers) pada compiler-worker untuk mendukung concurrency.
- Menambahkan otentikasi token wajib pada endpoint /compile dan laporan progres.
- Memperketat CORS policy menggunakan environment variable ORIGIN.
- Menerapkan secure_filename pada rute pelajaran untuk mencegah Path Traversal.
- Mengubah volume mounting backend utama menjadi Read-Only (:ro) untuk perlindungan data.
- Memperbarui proposal.md dan .env.example dengan standar keamanan terbaru.
- Create student tutorial documentation in Markdown with updated asset paths
- Add backend routes to serve rendered help content and assets via JSON API
- Implement frontend /help route using SvelteKit for professional rendering
- Add "Bantuan" link to the navigation bar with custom styling
- Generate tutorial screenshots and a demo video with virtual cursor and subtitles
- Configure Tailscale and Vite proxies to support the new help routing
- Add automated video generation scripts and assets