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:29:43 +07:00
---
title: Backend (Flask API)
order: 2
category: backend
---
2026-05-14 16:33:45 +07:00
# 02. Backend (Flask API)
The backend is built with Flask, providing API endpoints for the SvelteKit frontend to fetch lessons, track progress, manage authentication, and proxy compilation requests.
## Application Factory (`elemes/app.py`)
- `def create_app():`
Initializes the Flask application, loads configuration from `elemes/config.py` (which reads from `.env` ), and registers the following blueprints:
- `auth_bp` (`routes/auth.py`)
- `lessons_bp` (`routes/lessons.py`)
- `compile_bp` (`routes/compile.py`)
- `progress_bp` (`routes/progress.py`)
## Core API Routes
### Authentication (`routes/auth.py`)
- `def login():` (POST `/login` )
Receives `token` in JSON payload. Validates via `token_service.validate_token()` . On success, sets the `student_token` cookie. Rate-limited and includes a 1.5s tarpit for failures.
- `def logout():` (POST `/logout` )
Clears the `student_token` cookie.
- `def validate_token_route():` (POST `/validate-token` )
Checks if the current `student_token` cookie is valid.
### Lessons (`routes/lessons.py`)
- `def api_lessons():` (GET `/lessons` )
Returns a list of all lessons and the rendered `home.md` content via `lesson_service.get_ordered_lessons_with_learning_objectives()` .
2026-08-13 20:08:50 +07:00
- `def api_bab(folder):` (GET `/bab/<folder>` )
Returns parsed `sub-home.md` data for a folder (title, intro HTML, lesson list) via `lesson_service.get_sub_home_data()` . Returns 404 when the folder has no `sub-home.md` .
2026-05-14 16:33:45 +07:00
- `def api_lesson(filename):` (GET `/lesson/<slug>.json` )
2026-08-13 20:08:50 +07:00
Returns the fully parsed lesson data (content, initial code, circuits, key texts, active tabs) via `lesson_service.render_markdown_content(filepath)` . When the lesson lives in a folder that has a `sub-home.md` , `ordered_lessons` (sidebar) and prev/next navigation are scoped to that folder's lesson list; otherwise it falls back to the global `home.md` list.
2026-05-14 16:33:45 +07:00
- `def get_key_text(filename):` (GET `/get-key-text/<slug>` )
Returns only the required keywords for a specific lesson without exposing the full content logic.
### Compilation (`routes/compile.py`)
- `def compile_code():` (POST `/compile` )
Accepts `code` and `language` . Routes execution to the sandboxed worker using the `CompilerFactory` . Incorporates rate-limiting for anonymous users.
2026-09-05 10:07:16 +07:00
- `def velxio_compile():` (POST `/velxio-compile` mapped from `/api/velxio-compile` )
2026-05-14 16:33:45 +07:00
A proxy endpoint that forwards Arduino compilation requests to the Velxio container, enforcing rate limits for anonymous users.
### Progress Tracking (`routes/progress.py`)
- `def track_progress():` (POST `/track-progress` )
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
Accepts `lesson_name` and `status` . Persists progress to PostgreSQL via `token_service.update_student_progress()` .
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
- `def reset_progress():` (POST `/reset-progress` )
Resets a student's progress and deletes the quiz attempt (one-attempt) by anonymous `student_id` . Requires a teacher token.
- `def resume_quiz_attempt_route():` (POST `/resume-quiz-attempt` )
Grants a student permission to resume a `terminated` quiz attempt from saved answers, by anonymous `student_id` . The attempt row is kept (audit) and marked `resumed_at` ; the one-attempt gate opens until the student finishes again. Requires a teacher token. Returns `success: false` when there is no terminated attempt.
2026-05-14 16:33:45 +07:00
- `def api_progress_report():` (GET `/progress-report.json` )
Returns a matrix of all student progress. Requires a teacher token.
- `def export_progress_csv():` (GET `/progress-report/export-csv` )
Exports progress data as a CSV download. Requires a teacher token.
## Services
### Token Service (`services/token_service.py`)
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
Facade over the single active storage backend — PostgreSQL, via
`services/storage/postgres_backend.py` . The legacy CSV backend and its helpers
(`initialize_tokens_file`, `_load_tokens_safely` ) have been **removed** .
- `def validate_token(token):` Returns `{'student_name', 'is_teacher'}` or `None` for a valid token.
- `def is_teacher_token(token):` Returns `True` if the token belongs to the teacher account.
2026-05-14 16:33:45 +07:00
- `def get_student_progress(token):` Returns a dictionary of lesson progress for a specific token.
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
- `def update_student_progress(token, lesson_name, status="completed"):` Persists progress to PostgreSQL.
- `def get_teacher_token():` Returns the active teacher token (implemented in `postgres_backend` ).
- `def reset_student_progress(student_id, lesson_name):` Resets progress by anonymous `student_id` (never the raw token).
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
- `def resume_quiz_attempt(student_id, lesson_name):` Grants a student permission to resume a `terminated` quiz attempt from saved answers (teacher-only; never the raw token). Opens the one-attempt gate (`resumed_at` set) without deleting the audit row.
- `def is_quiz_attempt_resumable(token, lesson_name):` Returns `True` when the student's attempt has been resumed by a teacher — surfaced to the lesson payload as `quiz_resumable` .
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
Student tokens are stored **only as an HMAC-SHA256 digest** in the
`access_tokens` table (peppered with `TOKEN_PEPPER` from `.env` ) — raw tokens
are never persisted and cannot be recovered or exported.
2026-05-14 16:33:45 +07:00
### Lesson Service (`services/lesson_service.py`)
Parses Markdown files to extract content and configuration.
2026-08-13 20:08:50 +07:00
- `def get_lessons(source_path=None):` Returns lessons listed in the `Available_Lessons` section of `home.md` (or of `source_path` when given, e.g. a `sub-home.md` ).
- `def get_ordered_lessons_with_learning_objectives(progress=None, source_path=None):` Returns lessons ordered as they appear in `home.md` (or `source_path` ), optionally injected with user progress status.
- `def find_sub_home_for_lesson(file_path):` Returns `(sub_home_path, folder_name)` when the lesson's folder (one level inside `content/` ) has a `sub-home.md` , else `(None, None)` .
2026-08-14 05:10:59 +07:00
- `def get_sub_home_path(folder_name):` Returns the absolute path to a folder's `sub-home.md` (or `None` ), using the same `CONTENT_DIR` as `get_sub_home_data` .
2026-08-13 20:08:50 +07:00
- `def get_sub_home_data(folder_name):` Parses a folder's `sub-home.md` (title, intro HTML, lesson list) with an mtime-based cache so edits to the file are picked up without restart.
2026-05-14 16:33:45 +07:00
- `def render_markdown_content(file_path):` The core parsing function. Uses regex to extract markers like `---INITIAL_CODE---` , `---VELXIO_CIRCUIT---` , etc. It identifies the `active_tabs` needed for the frontend.
- `def _parse_flashcards(text):` Specifically parses `---QUIZ_FLASHCARD---` blocks into a structured JSON array for the frontend MCQ/Flashcard component.
2026-08-13 20:08:50 +07:00
See `docs/13-content-sub-home.md` for the author-facing guide on writing `sub-home.md` .
2026-05-14 16:33:45 +07:00
## Compiler Framework (`compiler/`)
The compilation logic is abstracted via a factory pattern.
- `class CompilerFactory:` (`compiler/__init__.py`)
- `def get_compiler(self, language):` Returns the appropriate `BaseCompiler` instance (e.g., `CCompiler` or `PythonCompiler` ).
- `class BaseCompiler(ABC):` (`compiler/base_compiler.py`)
- `def compile(self, code, timeout=10):` Abstract method.
- `def run(self, file_path, timeout=5):` Abstract method.
- `class CCompiler(BaseCompiler):` and `class PythonCompiler(BaseCompiler):`
Implementation wrappers that construct payloads and send HTTP requests to the `compiler-worker` container (`http://compiler-worker:8080/execute`).