610 lines
29 KiB
Markdown
610 lines
29 KiB
Markdown
# 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/<slug>.json, /get-key-text/<slug>
|
|
│ └── 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/<slug>.json` | `/lesson/<slug>.json` | Data lesson lengkap |
|
|
| GET | `/api/get-key-text/<slug>` | `/get-key-text/<slug>` | 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/<path>` | `/assets/<path>` | 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/<slug>.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/<slug>.json)
|
|
│ { initial_circuit, expected_output, key_text, active_tabs: ["circuit"] }
|
|
│
|
|
▼ +page.ts SSR loader
|
|
lesson/[slug]/+page.svelte
|
|
│ <CircuitEditor initialCircuit={data.initial_circuit} />
|
|
│
|
|
▼ 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)
|