elemes/documentation.md

567 lines
26 KiB
Markdown

# Elemes LMS — Dokumentasi Teknis
**Project:** LMS-C (Learning Management System untuk Pemrograman C & Arduino)
**Terakhir diupdate:** 8 April 2026
---
## Arsitektur
```
Internet (HTTPS :443)
Tailscale Funnel (elemes-ts)
├── / → SvelteKit Frontend (elemes-frontend :3000)
├── /assets/ → Flask Backend (elemes :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 (gcc / python)
├── Token authentication (CSV)
├── Progress tracking
└── Lesson content parsing (markdown)
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 + gcc | 5000 | Flask API (compile, auth, lessons, progress) |
| `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) |
| 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.
---
## 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] 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)