# Elemes LMS — Dokumentasi Teknis **Project:** LMS-C (Learning Management System untuk Pemrograman C & Arduino) **Terakhir diupdate:** 22 April 2026 --- ## Arsitektur ``` Internet (HTTPS :443) │ ▼ Tailscale Funnel (elemes-ts) │ ├── / → SvelteKit Frontend (elemes-frontend :3000) ├── /assets/ → Flask Backend (elemes :5000) ├── /velxio/api/compile → Flask Backend (Rate-limited Proxy :5000) ├── /velxio/ → Velxio Arduino Simulator (velxio :80) │ ▼ SvelteKit Frontend (elemes-frontend :3000) ├── SSR pages (lesson content embedded in HTML) ├── CodeMirror 6 editor (lazy-loaded) ├── CircuitJS simulator (iframe, GWT-compiled) — mode "circuit" ├── Velxio Arduino simulator (iframe, React) — mode "velxio" ├── API proxy: /api/* → Flask └── PWA manifest │ ▼ /api/* Flask API Backend (elemes :5000) ├── Code compilation (Proxied to Compiler Worker) ├── Arduino Proxy (/velxio-compile → Velxio :80) ├── Token authentication (CSV) ├── Progress tracking └── Lesson content parsing (markdown) │ ▼ HTTP Compiler Worker (compiler-worker :8080) ├── gVisor Sandbox (runsc runtime) ├── Gunicorn (4 workers) └── Isolation: gcc / python3 execution │ Velxio Arduino Simulator (velxio :80) ├── React + Vite frontend (editor + simulator canvas) ├── FastAPI backend (arduino-cli compile) ├── AVR8 / RP2040 CPU emulation (browser) └── PostMessage bridge ↔ Elemes (EmbedBridge.ts) ``` ### Container Setup | Container | Image | Port | Fungsi | |-----------|-------|------|--------| | `elemes` | Python 3.11 | 5000 | Flask API (auth, lessons, progress, compile-proxy) | | `compiler-worker` | Python 3.11 + gcc | 8080 | **Sandboxed** execution engine (gVisor) | | `elemes-frontend` | Node 20 | 3000 | SvelteKit SSR | | `velxio` | Node + Python + arduino-cli | 80 | Simulator Arduino (React + FastAPI) | | `elemes-ts` | Tailscale | 443 | HTTPS Funnel + reverse proxy | Container berkomunikasi via hostname Podman (compose service name). Proxy rules di `config/sinau-c-tail.json`. --- ## Struktur Direktori ``` lms-c/ ├── content/ # Lesson markdown files │ ├── home.md # Daftar lesson + landing page │ ├── hello_world.md # Lesson C │ ├── variabel.md # Lesson C │ ├── rangkaian_dasar.md # Lesson Circuit │ └── led_blink_arduino.md # Lesson Arduino/Velxio ├── assets/ # Gambar untuk lesson ├── tokens_siswa.csv # Data siswa & progress ├── state/ # Tailscale runtime state ├── .env # Environment variables │ └── elemes/ # Semua kode aplikasi ├── app.py # Flask create_app() factory ├── config.py # CONTENT_DIR, TOKENS_FILE ├── Dockerfile # Flask container ├── gunicorn.conf.py # Production WSGI ├── requirements.txt # Python deps ├── podman-compose.yml # 4 services (elemes, frontend, velxio, ts) ├── elemes.sh # CLI: init, run, runbuild, stop, generatetoken ├── generate_tokens.py # Utility: generate CSV tokens ├── proposal.md # Proposal integrasi Velxio (referensi arsitektur) │ ├── config/ │ └── sinau-c-tail.json # Tailscale Serve proxy rules │ ├── compiler/ # Code compilation │ ├── __init__.py # CompilerFactory │ ├── base_compiler.py # Abstract base │ ├── c_compiler.py # gcc wrapper │ └── python_compiler.py # python wrapper │ ├── routes/ # Flask Blueprints │ ├── auth.py # /login, /logout, /validate-token │ ├── compile.py # /compile │ ├── lessons.py # /lessons, /lesson/.json, /get-key-text/ │ └── progress.py # /track-progress, /progress-report.json, export-csv │ ├── services/ # Business logic │ ├── token_service.py # CSV token CRUD │ └── lesson_service.py # Markdown parsing + rendering │ ├── frontend/ # SvelteKit (Svelte 5) │ ├── package.json │ ├── svelte.config.js # adapter-node + path aliases │ ├── vite.config.ts │ ├── Dockerfile # Multi-stage build │ └── src/ │ ├── hooks.server.ts # API proxy: /api/* → Flask, /assets/* → Flask │ ├── app.html │ ├── app.css │ ├── lib/ │ │ ├── components/ │ │ │ ├── CodeEditor.svelte # CodeMirror 6 (lazy-loaded, anti-paste) │ │ │ ├── CircuitEditor.svelte # CircuitJS iframe wrapper │ │ │ ├── CrosshairOverlay.svelte # Touch precision overlay (CircuitJS) │ │ │ ├── OutputPanel.svelte # Multi-section output (C, Python, Circuit, Arduino) │ │ │ ├── CelebrationOverlay.svelte # Lesson completion animation │ │ │ ├── WorkspaceHeader.svelte # Tab switcher + floating/mobile controls │ │ │ ├── LessonFooterNav.svelte # Prev/next lesson navigation │ │ │ ├── Navbar.svelte │ │ │ ├── LessonCard.svelte │ │ │ ├── LessonList.svelte │ │ │ ├── ProgressBadge.svelte │ │ │ └── Footer.svelte │ │ ├── stores/ │ │ │ ├── auth.ts # Svelte writable stores (login, token) │ │ │ ├── lessonContext.ts # Current lesson nav context │ │ │ └── theme.ts # Dark/light toggle │ │ ├── services/ │ │ │ ├── api.ts # Flask API client │ │ │ ├── exercise.ts # checkKeyText(), validateNodes() │ │ │ └── velxio-bridge.ts # PostMessage bridge ke Velxio iframe │ │ ├── actions/ │ │ │ ├── floatingPanel.svelte.ts # Draggable/resizable editor panel │ │ │ ├── highlightCode.ts # Syntax highlighting post-render │ │ │ ├── noSelect.ts # Anti-select directive │ │ │ └── renderCircuitEmbeds.ts # Inline circuit embed renderer │ │ └── types/ │ │ ├── lesson.ts # LessonContent interface │ │ ├── auth.ts │ │ ├── compiler.ts │ │ └── circuitjs.ts # CircuitJSApi interface │ ├── routes/ │ │ ├── +layout.svelte # App shell (Navbar, theme) │ │ ├── +page.svelte # Home (lesson grid) │ │ ├── +page.ts # SSR data loader │ │ ├── lesson/[slug]/ │ │ │ ├── +page.svelte # Lesson viewer (semua mode) │ │ │ └── +page.ts # SSR data loader │ │ └── progress/ │ │ └── +page.svelte # Teacher dashboard │ └── static/ │ ├── manifest.json # PWA manifest │ └── circuitjs1/ # CircuitJS simulator (GWT-compiled) │ └── velxio/ # Velxio fork (Git submodule) ├── frontend/ # React + Vite + TypeScript │ └── src/ │ ├── services/EmbedBridge.ts # PostMessage listener (Velxio side) │ ├── pages/EditorPage.tsx # Editor + embed mode │ ├── components/editor/EditorToolbar.tsx │ ├── components/simulator/SimulatorCanvas.tsx # Canvas + touch/pinch + undo/redo toolbar │ ├── components/simulator/SimulatorCanvas.css # Canvas styling (undo-controls class) │ ├── components/simulator/WireInProgressRenderer.tsx # Wire preview + crosshair │ ├── components/simulator/WireLayer.tsx # Wire SVG rendering + segment handles │ ├── components/simulator/PinOverlay.tsx │ ├── store/useEditorStore.ts # Multi-file workspace │ └── store/useSimulatorStore.ts # Simulation state, wires, undo/redo stacks ├── backend/ # FastAPI + arduino-cli ├── Dockerfile.standalone └── CLAUDE.md # Dokumentasi teknis lengkap Velxio ``` --- ## Keputusan Teknis ### Kenapa SvelteKit, bukan Flutter? - CodeMirror 6 (code editor production-grade) — tidak ada equivalent di Flutter - Bundle ~250KB vs Flutter Web 2-4MB - SSR untuk konten markdown (instant first paint) - PWA installable tanpa app store ### Svelte 5 Runes vs Writable Stores Proyek ini menggunakan **Svelte 5** dengan dua pola state management: - **Runes (`$state`, `$derived`, `$effect`)** — digunakan di file `.svelte` (contoh: `lesson/[slug]/+page.svelte` menggunakan `$state` untuk semua UI state, `$derived` untuk computed values, `$effect` untuk side effects) - **Writable stores (`writable()`)** — digunakan di file `.ts` biasa karena runes tidak diproses oleh Svelte compiler di luar file `.svelte`. Contoh: `auth.ts`, `lessonContext.ts`, `theme.ts` ### Kenapa hooks.server.ts untuk API proxy? Vite `server.proxy` hanya bekerja di dev mode (`vite dev`). Di production (adapter-node), SvelteKit tidak punya proxy. `hooks.server.ts` mem-forward `/api/*` dan `/assets/*` ke Flask backend saat runtime. Prefix `/api/` di-strip sebelum forward (Flask routes tidak pakai `/api/` prefix). ### Kenapa transparent overlay untuk touch CircuitJS? CircuitJS adalah GWT-compiled app — tidak bisa modify source code-nya. Transparent overlay mengintercept PointerEvent di parent, konversi ke MouseEvent, dispatch ke iframe via `contentDocument.elementFromPoint()`. Overlay dinonaktifkan (`pointer-events: none`) di desktop sehingga mouse events langsung tembus ke iframe. ### Kenapa lazy-load CodeMirror? CodeMirror 6 bundle ~475KB. Dengan dynamic `import()`, lesson content (text) muncul langsung via SSR, editor menyusul setelah JS bundle selesai download. ### Kenapa Velxio di-embed via iframe, bukan komponen? Velxio adalah aplikasi React lengkap (editor + simulator + serial monitor + wire system). Mengekstrak komponen-komponennya ke SvelteKit tidak praktis. Iframe + PostMessage bridge memungkinkan kedua sistem dikembangkan secara independen. Same-origin via Tailscale Serve proxy (`/velxio/`), jadi tidak perlu CORS/CSP khusus. --- ## Cara Menjalankan ```bash # Setup awal (sekali) cd elemes/ ./elemes.sh init # Edit ../.env sesuai kebutuhan (ELEMES_HOST, TS_AUTHKEY, branding) # Build & run semua container ./elemes.sh runbuild # Atau: run tanpa rebuild ./elemes.sh run # Stop ./elemes.sh stop # Rebuild tanpa cache ./elemes.sh runclearbuild # Generate token siswa dari content ./elemes.sh generatetoken ``` - **Frontend:** http://localhost:3000 (dev) - **Tailscale:** https://{ELEMES_HOST}.{tailnet}.ts.net ### Logs ```bash podman logs elemes # Flask API podman logs elemes-frontend # SvelteKit podman logs velxio # Velxio simulator podman logs elemes-ts # Tailscale ``` --- ## API Endpoints Semua endpoint Flask diakses via SvelteKit proxy (`/api/*` → Flask `:5000`, prefix `/api/` di-strip): | Method | Frontend Path | Flask Path | Fungsi | |--------|--------------|------------|--------| | POST | `/api/login` | `/login` | Login dengan token | | POST | `/api/logout` | `/logout` | Logout | | POST | `/api/validate-token` | `/validate-token` | Validasi token | | GET | `/api/lessons` | `/lessons` | Daftar lesson + home content | | GET | `/api/lesson/.json` | `/lesson/.json` | Data lesson lengkap | | GET | `/api/get-key-text/` | `/get-key-text/` | Key text untuk lesson | | POST | `/api/compile` | `/compile` | Compile & run kode (C/Python) via worker | | POST | `/velxio/api/compile` | `/velxio-compile` | **Rate-limited** Arduino compile proxy | | POST | `/api/track-progress` | `/track-progress` | Track progress siswa | | GET | `/api/progress-report.json` | `/progress-report.json` | Data progress semua siswa | | GET | `/api/progress-report/export-csv` | `/progress-report/export-csv` | Export CSV | | GET | `/assets/` | `/assets/` | Static assets (proxy langsung, tanpa strip) | --- ## Mode Lesson Elemes mendukung beberapa mode lesson melalui **marker** di file markdown. Mode ditentukan otomatis dari marker yang ada: | Mode | Marker | Tab yang muncul | Evaluasi | |------|--------|-----------------|----------| | **C** | `---INITIAL_CODE---` | Editor (C) + Output | stdout matching + key_text | | **Python** | `---INITIAL_PYTHON---` | Editor (Python) + Output | stdout matching + key_text | | **Circuit** | `---INITIAL_CIRCUIT---` | Circuit + Output | node voltage + key_text | | **Arduino/Velxio** | `---INITIAL_CODE_ARDUINO---` | Velxio (iframe) + Output | serial + wiring + key_text | | **Velxio circuit-only** | `---VELXIO_CIRCUIT---` (tanpa code) | Velxio (no editor) + Output | wiring + key_text | | **Hybrid** | C/Python + Circuit | Editor + Circuit + Output | AND-logic: kedua harus pass | ### Markdown Sections yang Dikenali | Section | Fungsi | |---------|--------| | `---LESSON_INFO---` / `---END_LESSON_INFO---` | Info tab (learning objectives) | | `---EXERCISE---` | Instruksi latihan (separator) | | `---INITIAL_CODE---` / `---END_INITIAL_CODE---` | Kode awal C | | `---INITIAL_PYTHON---` / `---END_INITIAL_PYTHON---` | Kode awal Python | | `---INITIAL_CIRCUIT---` / `---END_INITIAL_CIRCUIT---` | Circuit text awal (CircuitJS format) | | `---INITIAL_QUIZ---` / `---END_INITIAL_QUIZ---` | Quiz data | | `---INITIAL_CODE_ARDUINO---` / `---END_INITIAL_CODE_ARDUINO---` | Kode awal Arduino | | `---VELXIO_CIRCUIT---` / `---END_VELXIO_CIRCUIT---` | Circuit JSON untuk Velxio (komponen + wires) | | `---EXPECTED_OUTPUT---` / `---END_EXPECTED_OUTPUT---` | Expected stdout (C/Python) atau node voltage JSON (Circuit) | | `---EXPECTED_CIRCUIT_OUTPUT---` / `---END_EXPECTED_CIRCUIT_OUTPUT---` | Expected circuit output (hybrid mode) | | `---EXPECTED_SERIAL_OUTPUT---` / `---END_EXPECTED_SERIAL_OUTPUT---` | Expected serial output (Arduino) | | `---EXPECTED_WIRING---` / `---END_EXPECTED_WIRING---` | Expected wiring JSON (Arduino) | | `---KEY_TEXT---` / `---END_KEY_TEXT---` | Keyword wajib di source code / circuit | | `---KEY_TEXT_CIRCUIT---` / `---END_KEY_TEXT_CIRCUIT---` | Keyword wajib di circuit (hybrid) | | `---SOLUTION_CODE---` / `---END_SOLUTION_CODE---` | Solusi kode (ditampilkan setelah selesai) | | `---SOLUTION_CIRCUIT---` / `---END_SOLUTION_CIRCUIT---` | Solusi circuit | --- ## Velxio Integration (Arduino Simulator) ### PostMessage Bridge Protocol Komunikasi antara Elemes dan Velxio iframe via `window.postMessage`: **Elemes → Velxio (Commands):** | Message Type | Payload | Fungsi | |---|---|---| | `elemes:load_code` | `{ files: [{name, content}] }` | Load source code ke editor | | `elemes:load_circuit` | `{ board, components, wires }` | Load rangkaian ke simulator | | `elemes:set_embed_mode` | `{ hideAuth, hideComponentPicker }` | Configure embed UI | | `elemes:get_source_code` | — | Request source code | | `elemes:get_serial_log` | — | Request serial output | | `elemes:get_wires` | — | Request wire topology | | `elemes:stop` | — | Stop simulation | | `elemes:ping` | — | Re-trigger ready signal | **Velxio → Elemes (Events):** | Message Type | Payload | Fungsi | |---|---|---| | `velxio:ready` | `{ version }` | Iframe siap (broadcast tiap 300ms sampai parent acknowledge) | | `velxio:compile_result` | `{ success }` | Kompilasi selesai | | `velxio:source_code` | `{ files }` | Response ke get_source_code | | `velxio:serial_log` | `{ log }` | Response ke get_serial_log | | `velxio:wires` | `{ wires }` | Response ke get_wires | ### Flow Lesson Arduino ``` 1. User buka lesson → frontend fetch /api/lesson/.json 2. Backend parse markdown → extract INITIAL_CODE_ARDUINO, VELXIO_CIRCUIT, dll 3. Frontend detect active_tabs=['velxio'] → render iframe src="/velxio/editor?embed=true" 4. Iframe load → Velxio EmbedBridge broadcast velxio:ready (tiap 300ms) 5. Frontend initVelxioBridge() → create VelxioBridge → acknowledge → stop broadcast 6. Bridge send: set_embed_mode, load_circuit, load_code 7. Siswa edit kode + wiring di Velxio → klik Compile & Run 8. Velxio notify: velxio:compile_result { success: true } 9. Auto-evaluate setelah 3 detik (tunggu serial output) 10. Evaluate: get_source_code → key_text, get_serial_log → serial match, get_wires → wiring match 11. Jika semua pass → completeLesson() → celebration + track progress ``` ### Evaluasi Arduino (3 jenis, semua harus pass) | Evaluasi | Mekanisme | Sumber Data | |----------|-----------|-------------| | **Key Text** | Keyword wajib ada di source code | `---KEY_TEXT---` di markdown | | **Serial Output** | Subsequence matching (expected lines muncul dalam urutan di actual) | `---EXPECTED_SERIAL_OUTPUT---` | | **Wiring** | Lenient graph comparison (expected edges harus ada, extra OK) | `---EXPECTED_WIRING---` (JSON array of pairs) | ### File-file Kunci | Sisi | File | Fungsi | |------|------|--------| | Elemes | `frontend/src/lib/services/velxio-bridge.ts` | VelxioBridge class (send commands, evaluate) | | Elemes | `frontend/src/routes/lesson/[slug]/+page.svelte` | initVelxioBridge(), handleVelxioSubmit() | | Velxio | `frontend/src/services/EmbedBridge.ts` | PostMessage listener (handle commands) | | Velxio | `frontend/src/pages/EditorPage.tsx` | Embed mode UI control | | Velxio | `frontend/src/components/editor/EditorToolbar.tsx` | Embed-aware toolbar | --- ## CircuitJS Integration Integrasi Falstad CircuitJS1 sebagai simulator rangkaian interaktif di tab "Circuit". CircuitJS adalah aplikasi GWT (Java → JavaScript) yang di-embed via iframe same-origin. ### Arsitektur ``` content/rangkaian_dasar.md │ ---INITIAL_CIRCUIT--- ... ---END_INITIAL_CIRCUIT--- │ ▼ lesson_service.py: _extract_section() Flask API (/lesson/.json) │ { initial_circuit, expected_output, key_text, active_tabs: ["circuit"] } │ ▼ +page.ts SSR loader lesson/[slug]/+page.svelte │ │ ▼ iframe onload → oncircuitjsloaded callback CircuitEditor.svelte │ simApi.importCircuit(text, false) │ ▼ circuitjs1/circuitjs.html (GWT app in iframe) │ Canvas rendering, simulasi real-time │ ▼ CrosshairOverlay.svelte (touch devices only) Touch event forwarding → synthetic MouseEvent dispatch ``` ### CircuitJS API (via `iframe.contentWindow.CircuitJS1`) API object didapatkan melalui callback `window.oncircuitjsloaded`, dengan fallback 3 detik via `window.CircuitJS1`. | Method | Fungsi | |--------|--------| | `importCircuit(text, showMessage)` | Load circuit dari text | | `exportCircuit()` | Ekspor circuit saat ini sebagai text | | `getNodeVoltage(nodeName)` | Query tegangan di named node | | `setSimRunning(bool)` | Jalankan/hentikan simulasi | | `updateCircuit()` | Redraw setelah perubahan | ### Evaluasi Rangkaian Saat siswa klik "Cek Rangkaian", fungsi `evaluateCircuit()` menjalankan: 1. Parse kriteria dari `expected_output` (JSON) 2. Cek tegangan node via `simApi.getNodeVoltage()` ± tolerance 3. Cek komponen wajib via `checkKeyText()` (string contains pada circuit text) 4. Track progress jika semua passed **Expected Output JSON Format:** ```json { "nodes": { "TestPoint_A": { "voltage": 2.5, "tolerance": 0.2 } } } ``` ### Auto-save `CircuitEditor` mendukung auto-save ke `sessionStorage` (polling setiap 5 detik): - **Key:** `elemes_circuit_{slug}` (hanya saat user login & bukan mode solusi) - **Restore:** Cek sessionStorage dulu, fallback ke `initialCircuit` prop --- ## Anti Copy-Paste System Sistem berlapis untuk mencegah siswa meng-copy konten pelajaran dan mem-paste kode dari sumber eksternal. ### Selection & Copy Prevention (Konten Lesson) **File:** `lesson/[slug]/+page.svelte` | Layer | Mekanisme | Target | |-------|-----------|--------| | CSS | `user-select: none`, `-webkit-touch-callout: none` | `.lesson-content`, info/exercise tabs | | Events | `onselectstart`, `oncopy`, `oncut`, `oncontextmenu` → `preventDefault()` | Konten lesson | | Directive | `use:noSelect` | Reusable action | ### Paste Prevention (CodeEditor) **File:** `CodeEditor.svelte` Diaktifkan via prop `noPaste={true}`. | Layer | Mekanisme | Menangani | |-------|-----------|-----------| | DOM handlers | `paste`, `drop`, `beforeinput` → `preventDefault()` | Desktop paste, iOS paste | | Transaction filter | Block `input.paste` + heuristik ukuran (>2 baris atau >20 chars) | Standard paste + GBoard clipboard panel | | Clipboard filter | Replace clipboard text → `''` | Standard paste (jika API tersedia) | | Input handler | Block multi-line insertion >20 chars | GBoard clipboard via DOM mutations | **Limitasi:** GBoard clipboard panel menggunakan IME composition, tidak bisa dibedakan 100% dari ketikan biasa. Paste 1 baris pendek (<20 chars) masih bisa lolos. --- ## Keamanan & Autentikasi (Security) Sistem pengamanan untuk melindungi data siswa dan mencegah penyalahgunaan API. ### 1. Eksekusi Terisolasi (gVisor Sandbox) Seluruh kode yang dikirim oleh siswa (C dan Python) tidak dieksekusi di dalam container utama, melainkan diteruskan ke **Compiler Worker** yang berjalan menggunakan **runtime gVisor (`runsc`)**. Ini mencegah serangan *Remote Code Execution* (RCE) yang menargetkan kernel sistem operasi host. ### 2. Anonymous Access & Rate Limiting Sistem mengizinkan pengguna tanpa token (anonim) untuk mencoba pembelajaran dengan batasan ketat: - **Rate Limit**: Pengguna anonim dibatasi **1 kali kompilasi setiap 2 menit** per IP. - **Global Queue**: Maksimal **20 slot** antrean kompilasi anonim secara global untuk menjaga stabilitas server. - **Bypass**: Pengguna yang telah login (memiliki token/cookie valid) **bebas** dari batasan rate limit ini. - **Velxio Protection**: Kompilasi Arduino diproteksi melalui proxy backend Flask untuk mencegah *hijacking* API dari sisi klien. ### 3. Cookie Security (Dynamic Secure Flag) LMS menggunakan cookie `student_token` untuk mengelola sesi. Keamanannya diatur secara dinamis melalui environment variable. - **`httponly: true`**: Mencegah akses cookie via JavaScript (proteksi XSS). - **`secure: COOKIE_SECURE`**: Jika `true`, cookie hanya dikirim via HTTPS. Sangat penting saat dideploy via Tailscale Funnel. - **`samesite: 'Lax'`**: Proteksi dasar terhadap CSRF. ### 2. Proteksi Brute-Force & Anti-Spam Untuk mencegah penebakan token secara massal, terutama pada jaringan WiFi sekolah yang menggunakan satu IP publik (NAT): - **Rate Limiting**: Endpoint `/api/login` dibatasi maksimal **50 request per menit per IP**. Angka ini diatur untuk mengakomodasi satu kelas (50 siswa) yang login bersamaan tanpa saling memblokir. - **Tarpitting (Login Delay)**: Setiap percobaan login yang **gagal** akan ditahan selama **1.5 detik** sebelum server memberikan respons. Ini melumpuhkan efektivitas alat brute-force otomatis tanpa mengganggu pengalaman siswa asli yang hanya sesekali salah ketik. ### 3. Session Invalidation (Token Blacklist) Mekanisme logout server-side untuk memastikan token tidak bisa digunakan kembali setelah sesi berakhir. - **Mekanisme**: Menggunakan `LOGOUT_BLACKLIST` (in-memory set) di Flask backend. - **Flow**: Saat user klik logout, token ditambahkan ke blacklist. Semua request berikutnya dengan token tersebut akan ditolak oleh `validate_token()`. --- ## Touch Crosshair System (CircuitJS) Overlay untuk presisi interaksi di iframe CircuitJS pada perangkat sentuh. Aktif hanya pada touch device (CSS media query `hover: none` + `pointer: coarse`). **File:** `CrosshairOverlay.svelte` (dimount di `CircuitEditor.svelte`) ### Gesture Mapping | Gesture | Action | |---------|--------| | Single tap | `click` (delay 300ms) | | Double tap | `dblclick` (edit komponen) | | Triple tap | Right-click (`contextmenu`) | | Two-finger tap | Right-click | | Long tap (400ms) | Crosshair aiming mode (4-phase state machine) | ### Cara Kerja Overlay transparan mengintercept touch events → konversi koordinat ke iframe-local → `elementFromPoint()` untuk target → dispatch synthetic `MouseEvent` ke iframe. ### Konfigurasi | Variable | Default | Fungsi | |----------|---------|--------| | `PUBLIC_CURSOR_OFFSET_Y` | `50` | Offset Y crosshair dari jari (pixel) | --- ## Velxio Mobile Wiring Crosshair Berbeda dari CircuitJS crosshair (yang menggunakan overlay), Velxio crosshair ada di dalam simulator canvas sendiri (SVG). **File:** `velxio/frontend/src/components/simulator/WireInProgressRenderer.tsx` Saat user sedang menarik wire, garis bantu horizontal + vertikal muncul di posisi cursor: - Style: dashed `rgba(255,255,255,0.25)`, `strokeWidth=0.5`, `strokeDasharray="8,6"` - Panjang: 8000px ke setiap arah (cukup untuk semua zoom level) - Terinspirasi CircuitJS — membantu alignment karena jari menutupi pin target ### Pinch-Zoom Preserve Wire **File:** `velxio/frontend/src/components/simulator/SimulatorCanvas.tsx` Sebelumnya, pinch-to-zoom (2 jari) otomatis cancel wire yang sedang ditarik. Sekarang wire-in-progress tetap aktif selama zoom. Preview freeze (hanya update saat 1 jari), lalu resume setelah zoom selesai. --- ## Wire Undo/Redo Snapshot-based undo/redo untuk operasi wire di Velxio simulator. Memungkinkan user membatalkan kesalahan wiring tanpa harus memilih dan menghapus wire secara manual. ### Arsitektur ``` User action (add/remove/update/finish wire) │ ├── Push current state.wires → wireUndoStack (max 50) ├── Clear wireRedoStack └── Mutate state.wires Undo (Ctrl+Z / toolbar button) │ ├── Pop wireUndoStack → restore as state.wires ├── Push current state.wires → wireRedoStack └── Reset selectedWireId = null Redo (Ctrl+Shift+Z / toolbar button) │ ├── Pop wireRedoStack → restore as state.wires ├── Push current state.wires → wireUndoStack └── Reset selectedWireId = null ``` ### File yang Terlibat | File | Perubahan | |------|-----------| | `velxio/frontend/src/store/useSimulatorStore.ts` | State: `wireUndoStack`, `wireRedoStack`. Actions: `undoWire()`, `redoWire()`. Snapshot push di `addWire`, `removeWire`, `updateWire`, `finishWireCreation`. | | `velxio/frontend/src/components/simulator/SimulatorCanvas.tsx` | Keyboard handler (`Ctrl+Z`/`Ctrl+Shift+Z`), toolbar buttons (undo/redo icons), store selectors (`canUndoWire`, `canRedoWire`). | | `velxio/frontend/src/components/simulator/SimulatorCanvas.css` | Class `.undo-controls` — styling identik dengan `.zoom-controls` tapi **tidak di-hide** di mobile media query. | ### Keputusan Desain 1. **Snapshot-based vs action-based** — Dipilih snapshot (simpan seluruh `wires[]` per step) karena lebih sederhana dan reliable. Trade-off: memory lebih besar, tapi dengan limit 50 entries dan array wire yang kecil, tidak masalah. 2. **Separate CSS class** — `.undo-controls` terpisah dari `.zoom-controls` karena zoom di-hide di mobile (`display: none` — pakai pinch-to-zoom), tapi undo/redo harus tetap visible. 3. **`setWires()` tidak push snapshot** — `setWires()` digunakan oleh `EmbedBridge` saat load circuit dari LMS. Ini bukan user action, jadi tidak masuk undo history. 4. **`selectedWireId: null` saat undo/redo** — Mencegah bug dimana UI mencoba menampilkan info wire yang sudah tidak ada setelah undo. --- ## Status Implementasi - [x] Backend decomposition (monolith → Blueprints + services) - [x] SvelteKit scaffolding (adapter-node, Svelte 5, path aliases) - [x] Core components (CodeEditor, Navbar, auth/theme stores, API client) - [x] Pages (Home, Lesson, Progress) + SSR data loading - [x] Containerization (4-container setup, API proxy) - [x] CircuitJS integration (iframe, evaluasi node voltage, touch overlay) - [x] Floating/mobile editor panel (draggable, resizable, bottom sheet) - [x] Anti copy-paste system (lesson content + code editor) - [x] Velxio fork + EmbedBridge (PostMessage protocol) - [x] Velxio integration di Elemes (bridge, parsing, UI, evaluasi) - [x] Mobile wiring UX (pinch-zoom preserve wire, crosshair alignment) - [x] Wire undo/redo (snapshot-based, Ctrl+Z/Ctrl+Shift+Z, toolbar button, mobile-friendly) - [x] Security Review & Hardening (Cookie security, Rate limiting, Tarpitting, Blacklisting) - [x] Contoh lesson Arduino (LED Blink) - [ ] PWA (service worker, offline caching) - [ ] Contoh lesson Arduino tambahan (2-3 lagi) - [ ] Velxio enhancements (lock komponen, solution overlay, multi-board)