29 KiB
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.sveltemenggunakan$stateuntuk semua UI state,$deriveduntuk computed values,$effectuntuk side effects) - Writable stores (
writable()) — digunakan di file.tsbiasa 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
# 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
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:
- Parse kriteria dari
expected_output(JSON) - Cek tegangan node via
simApi.getNodeVoltage()± tolerance - Cek komponen wajib via
checkKeyText()(string contains pada circuit text) - Track progress jika semua passed
Expected Output JSON Format:
{
"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
initialCircuitprop
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: Jikatrue, 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/logindibatasi 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
- 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. - Separate CSS class —
.undo-controlsterpisah dari.zoom-controlskarena zoom di-hide di mobile (display: none— pakai pinch-to-zoom), tapi undo/redo harus tetap visible. setWires()tidak push snapshot —setWires()digunakan olehEmbedBridgesaat load circuit dari LMS. Ini bukan user action, jadi tidak masuk undo history.selectedWireId: nullsaat undo/redo — Mencegah bug dimana UI mencoba menampilkan info wire yang sudah tidak ada setelah undo.
Status Implementasi
- Backend decomposition (monolith → Blueprints + services)
- SvelteKit scaffolding (adapter-node, Svelte 5, path aliases)
- Core components (CodeEditor, Navbar, auth/theme stores, API client)
- Pages (Home, Lesson, Progress) + SSR data loading
- Containerization (4-container setup, API proxy)
- CircuitJS integration (iframe, evaluasi node voltage, touch overlay)
- Floating/mobile editor panel (draggable, resizable, bottom sheet)
- Anti copy-paste system (lesson content + code editor)
- Velxio fork + EmbedBridge (PostMessage protocol)
- Velxio integration di Elemes (bridge, parsing, UI, evaluasi)
- Mobile wiring UX (pinch-zoom preserve wire, crosshair alignment)
- Wire undo/redo (snapshot-based, Ctrl+Z/Ctrl+Shift+Z, toolbar button, mobile-friendly)
- Security Review & Hardening (Cookie security, Rate limiting, Tarpitting, Blacklisting)
- 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)