diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..58fe5fa --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +* text=auto eol=lf +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.woff binary +*.woff2 binary +*.svg text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dfcefbe..11c1bae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,3 +64,22 @@ jobs: name: playwright-report path: playwright-report/ retention-days: 14 + + lighthouse: + name: Lighthouse + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run build + - run: npm run start & + env: + PORT: "3000" + - run: npx wait-on http://localhost:3000 + - run: npx @lhci/cli@0.14 autorun --collect.url=http://localhost:3000 --upload.target=temporary-public-storage + continue-on-error: true diff --git a/.gitignore b/.gitignore index 2a55a45..c611a4d 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,8 @@ next-env.d.ts # IDE .vscode/ .idea/ -<<<<<<< HEAD -======= + +# hive agent working copies +.hive/ +repos/ firebase-debug.log ->>>>>>> origin/feature/REQ-I1QAV-1Z diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..41094ec --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "repos/mini-marty"] + path = repos/mini-marty + url = https://github.com/tzone85/mini-marty.git + branch = main diff --git a/README.md b/README.md index b58e121..6e02d44 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,67 @@ # Mini Marty -Visual programming environment for the Marty robot. +[![CI](https://github.com/tzone85/mini-marty/actions/workflows/ci.yml/badge.svg)](https://github.com/tzone85/mini-marty/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](#license) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Ftzone85%2Fmini-marty) -## Getting Started +A virtual programming environment for learning to code with Marty the Robot. No physical robot needed. + +Write Python or drag Blockly blocks; a `VirtualMarty` instance enqueues commands; a React Three Fiber scene animates them at 60 fps. + +## Quick start ```bash npm install npm run dev ``` -Open [http://localhost:3000](http://localhost:3000). +Open http://localhost:3000. ## Scripts | Command | Description | -|---------|-------------| -| `npm run dev` | Start development server | +|---|---| +| `npm run dev` | Start the dev server | | `npm run build` | Production build | -| `npm run lint` | Run ESLint | -| `npm run format` | Format code with Prettier | -| `npm run typecheck` | TypeScript type checking | -| `npm test` | Run unit tests | -| `npm run test:e2e` | Run E2E tests | - -## Tech Stack - -- **Framework:** Next.js 16 (App Router) -- **Language:** TypeScript -- **Styling:** Tailwind CSS v4 -- **Unit Testing:** Vitest + React Testing Library -- **E2E Testing:** Playwright -- **Linting:** ESLint + Prettier +| `npm run lint` | ESLint | +| `npm run format:check` | Prettier check | +| `npm run typecheck` | TypeScript | +| `npm test` | Vitest unit + integration | +| `npm run test:coverage` | Coverage with 80% gate | +| `npm run test:e2e` | Playwright | + +## Tech stack + +Next.js 16 (App Router) + TypeScript + Tailwind. Three.js / React Three Fiber for 3D. Pyodide for Python in the browser. Blockly for blocks. Monaco for code editing. Vitest + Playwright for tests. Vercel for hosting. + +## Marty commands + +| Command | Effect | +|---|---| +| `walk(steps)` | Walk forward | +| `dance()` | Dance routine | +| `kick("left"\|"right")` | Kick with leg | +| `slide("left"\|"right")` | Slide sideways | +| `lean("left"\|"right")` | Lean | +| `wiggle()` | Wiggle body | +| `circle_dance()` | Circular dance | +| `celebrate()` | Celebrate | +| `get_ready()` / `stand_straight()` | Pose | +| `eyes(expression)` | Set eye expression | +| `arms(left, right)` | Set arm angles | +| `foot_on_ground(side)` | Read foot sensor | +| `get_distance_sensor()` | Distance in cm | +| `get_accelerometer()` | `{x, y, z}` tilt | +| `stop()` | Stop all motion | + +## Documentation + +- [`docs/`](./docs) — architecture, runbook, contributing +- [`docs/architecture/overview.md`](./docs/architecture/overview.md) — layered diagram +- [`docs/runbook.md`](./docs/runbook.md) — incident response +- [`docs/contributing.md`](./docs/contributing.md) — branch model, code review +- [`TRAINING.md`](./TRAINING.md) — parent-child learning guide + +## License + +MIT diff --git a/TRAINING.md b/TRAINING.md new file mode 100644 index 0000000..b2af7a5 --- /dev/null +++ b/TRAINING.md @@ -0,0 +1,238 @@ +# Mini Marty — Training Guide + +A guide for parents and kids to learn programming together using Mini Marty. + +## Getting Started + +### Setup (5 minutes) + +1. Open your terminal and navigate to the project folder +2. Run `npm install` to install dependencies +3. Run `npm run dev` to start the development server +4. Open http://localhost:3000 in your browser + +### First Look (5 minutes) + +When you open Mini Marty, you will see: + +- A **header** with navigation links at the top +- A **sidebar** on the left with context-specific options +- The **main area** showing the current page +- A **3D Marty robot** on the home page that you can rotate by clicking and dragging + +Explore the navigation: Home, Block Editor, Python Editor, Tutorials, and Challenges. + +--- + +## Learning Path + +We recommend following these sessions in order. Each session is designed for 30-45 minutes. + +### Session 1: Block Coding (Beginner) + +**Goal:** Understand the concept of giving instructions to a robot. + +1. Go to the **Block Editor** +2. Open the **Motion** category in the toolbox +3. Drag a "when program starts" block (from Events) to the workspace +4. Connect "get ready" and "walk 2 steps" blocks below it +5. Click **Save** to save your work + +**Discussion points:** +- What is a program? (A list of instructions for a computer/robot) +- Why does order matter? (Marty needs to get ready before walking) +- What happens if you change the number of steps? + +### Session 2: First Python Program (Beginner) + +**Goal:** Write your first Python code and see the virtual robot respond. + +1. Go to the **Python Editor** +2. Wait for "Python ready" (green dot) in the top right +3. The starter code is already there — click **Run** +4. Watch Marty move in the 3D viewport on the right! +5. Try the "Hello Marty!" tutorial (Tutorials page) + +**Key concepts to explain:** +- `from martypy import Marty` — importing a tool/library +- `Marty("virtual")` — creating an instance of the robot +- `await` — waiting for an action to complete before the next one +- `print()` — showing text output in the console + +### Session 3: Movement Commands (Beginner) + +**Goal:** Learn all of Marty's movement commands. + +1. Go to the **Tutorials** page +2. Work through "Hello Marty!" and "Dance Moves" tutorials +3. Try the "First Walk" and "Dance Party" challenges + +**Experiment together:** +- Change `walk(2)` to `walk(5)` — what happens? +- Change `kick("left")` to `kick("right")` +- Chain multiple commands: walk → kick → dance → celebrate + +### Session 4: Sensors & Decisions (Intermediate) + +**Goal:** Learn about sensors and if-else statements. + +1. Work through the "Sensing the World" tutorial +2. Try the "Sensor Check" challenge + +**Key concepts:** +- Sensors give robots information about the world +- `if`/`elif`/`else` lets the program make decisions +- The distance sensor measures how far away things are +- The accelerometer detects tilt and gravity + +### Session 5: Loops & Functions (Intermediate) + +**Goal:** Learn to repeat actions and organise code into functions. + +1. Work through "Loops & Control Flow" tutorial +2. Try "Loop Walker" and "Smart Marty" challenges + +**Key concepts:** +- `for i in range(5):` — repeat something 5 times +- Functions let you name a group of commands +- `async def my_function():` — defining a reusable function +- Why functions are useful (avoid copy-pasting code) + +### Session 6: Advanced Python (Advanced) + +**Goal:** Use lists, variables, and advanced patterns. + +1. Work through the "Python Power" tutorial +2. Try the advanced challenges: "Robot Choreographer", "Function Factory", "The Marty Show" + +**Key concepts:** +- Lists store multiple items: `moves = ["walk", "dance", "kick"]` +- `getattr()` calls methods by name — powerful for dynamic code +- Building a full program with structure (intro, main, finale) + +--- + +## Tips for Parents + +### Making it fun +- Let your child experiment freely — there is no way to break anything +- Celebrate small wins: "Look, you made Marty walk!" +- Take turns: you code one command, they code the next +- Challenge each other: "Can you make Marty do 3 different things?" + +### When they get stuck +- Use the **hints** in challenges — they reveal one at a time +- Check the **API Reference** on the Tutorials page +- Read error messages together — they often say exactly what went wrong +- Look at tutorial code for similar examples + +### Building confidence +- Start with Block Editor if Python feels intimidating +- The "Hello Marty" tutorial is designed to be impossible to fail +- Emphasise that professional programmers Google things and make mistakes constantly +- Save working programs with the Save button so they can show others + +### Extending the learning +- Ask "what if" questions: "What if Marty kicked 10 times?" +- Suggest modifications: "Can you add more moves to your dance?" +- Introduce debugging: intentionally break code and fix it together +- Connect to real-world concepts: "Traffic lights use if-else too!" + +--- + +## Troubleshooting + +### Python says "loading" for a long time +The Python runtime (Pyodide) is about 10MB and loads from a CDN. On a slow connection, this can take 30-60 seconds. After the first load, it is cached by your browser. + +### 3D scene is blank +Make sure your browser supports WebGL. Most modern browsers do. Try Chrome or Firefox if you're having issues. + +### Code runs but Marty does not move +Make sure you're using `await` before movement commands: +- Correct: `await my_marty.walk(2)` +- Incorrect: `my_marty.walk(2)` (this sends the command but does not wait for it) + +### Block Editor is empty +Blockly requires JavaScript to be enabled. If blocks disappear after saving, try clearing your browser's localStorage for localhost:3000. + +--- + +## Progression Checklist + +Use this to track your child's progress: + +**Beginner** +- [ ] Explored the 3D Marty on the home page +- [ ] Created a block program in the Block Editor +- [ ] Ran their first Python program +- [ ] Completed "Hello Marty!" tutorial +- [ ] Completed "Dance Moves" tutorial +- [ ] Solved all 3 beginner challenges + +**Intermediate** +- [ ] Completed "Sensing the World" tutorial +- [ ] Used an if-else statement +- [ ] Completed "Loops & Control Flow" tutorial +- [ ] Used a for loop +- [ ] Wrote their first function +- [ ] Solved all 3 intermediate challenges + +**Advanced** +- [ ] Completed "Python Power" tutorial +- [ ] Used lists and variables +- [ ] Created a multi-function program +- [ ] Solved all 3 advanced challenges +- [ ] Created their own original program from scratch! + +--- + +## Troubleshooting on Windows + +Mini Marty runs on Windows 10 / 11 (PowerShell or Windows Terminal). Below are the most common rough edges. + +### Line endings + +Git on Windows usually converts `\n` to `\r\n`. The repo has not pinned a `.gitattributes` yet, so if your editor or a script trips on the difference, run: + +```powershell +git config --global core.autocrlf input +``` + +This keeps the working tree as `\n` for source files while still letting Windows tools open them. + +### Installing on PowerShell + +Use PowerShell (not Command Prompt). If `npm install` fails with a script execution policy error: + +```powershell +Set-ExecutionPolicy -Scope CurrentUser RemoteSigned +``` + +Then close and reopen the terminal and retry. + +### WebGL check + +The 3D scene needs WebGL. Open `chrome://gpu` in Chrome or `about:support` in Firefox; look for "WebGL: Hardware accelerated". If it says "Software only" or "Disabled": + +1. Update your graphics driver. +2. In Chrome, enable `chrome://flags/#ignore-gpu-blocklist` if your GPU is on the blocklist but otherwise healthy. +3. As a last resort, the Block Editor and Python Editor still work without WebGL; only the home-page scene degrades. + +### Long paths + +If `npm install` errors with `ENAMETOOLONG`, enable long paths: + +```powershell +git config --global core.longpaths true +``` + +And in an admin PowerShell: + +```powershell +New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force +``` + +### Antivirus slowing `node_modules` + +Windows Defender scans every file in `node_modules`, which can multiply install time. Add the project folder to Defender's exclusion list under Settings -> Virus & threat protection -> Exclusions if you control the machine. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..81fda71 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,26 @@ +# Mini Marty — Documentation Index + +Mini Marty is a browser-only virtual robot programming environment. Learners write Python or drag Blockly blocks; a `VirtualMarty` instance enqueues commands; an `AnimationPlayer` drives a React Three Fiber scene. + +## Architecture pages + +- [Overview](./architecture/overview.md) — layered diagram and module responsibilities +- [Virtual Marty](./architecture/virtual-marty.md) — `VirtualMarty`, `CommandQueue`, `Clock` DI +- [Python runtime](./architecture/python-runtime.md) — Pyodide loader, `martypy` shim, executor +- [Scene](./architecture/scene.md) — animation sequences, player, R3F integration +- [Security](./architecture/security.md) — CSP, sandboxing, secret handling +- [Testing](./architecture/testing.md) — pyramid, coverage gate, E2E +- [Deployment](./architecture/deployment.md) — Vercel pipeline, env flags + +## Operations + +- [Runbook](./runbook.md) — incident response, debugging, common failures +- [Contributing](./contributing.md) — local setup, branch model, code review + +## Diagrams + +Hand-authored SVGs in [`diagrams/`](./diagrams). Light/dark via `prefers-color-scheme`. No Mermaid. + +## Obsidian vault + +The `obsidian/` directory is symlinked into the user's vault as `Mini Marty (docs)`. Front-matter, wikilinks, and embedded SVGs let knowledge cross-flow between repo and vault. diff --git a/docs/architecture/deployment.md b/docs/architecture/deployment.md new file mode 100644 index 0000000..1ad74c1 --- /dev/null +++ b/docs/architecture/deployment.md @@ -0,0 +1,41 @@ +# Deployment + +Mini Marty is a static + middleware Next.js app deployed to Vercel. + +![Deployment flow](../diagrams/deployment.svg) + +## Pipeline + +1. Push to a branch -> Vercel preview deploy. +2. Merge to `main` -> production deploy. +3. GitHub Actions (`.github/workflows/ci.yml`) runs lint, typecheck, unit tests with coverage gate, build, Playwright, and a non-blocking Lighthouse audit on every PR. + +## Vercel config + +`vercel.json` sets the framework, build command, and install command. CSP and other security headers are emitted by `proxy.ts` at request time; Vercel's edge does not need to know. + +### Why `--legacy-peer-deps` + +`installCommand` is `npm ci --legacy-peer-deps`. The app uses React 19, which is ahead of the peer range some transitive packages (notably `@vercel/analytics`) declare. `--legacy-peer-deps` keeps `npm ci` from failing on a peer-range mismatch we know is safe; remove the flag once the upstream peer ranges catch up. + +## Feature flags + +`src/lib/flags.ts` reads from `process.env` at module evaluation. Public flags: + +| Flag | Purpose | +|---|---| +| `NEXT_PUBLIC_SENTRY_DSN` | Sentry DSN — when set, `SentryErrorReporter` replaces `MemoryErrorReporter` | +| `NEXT_PUBLIC_ANALYTICS` | Set to `vercel` to enable `VercelAnalytics`; any other value (including unset) leaves `NoopAnalytics` in place | + +Off by default, opt-in via Vercel env UI. No flag toggles destructive behaviour. + +## Rollback + +Vercel's UI exposes one-click rollback to any prior production deploy. There is no database, so rollback is safe. + +## Smoke check post-deploy + +- Home page renders, 3D scene visible +- `/python-editor` loads Pyodide and prints `hello from marty` +- `/block-editor` shows toolbox and workspace +- DevTools console shows no CSP violations diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 0000000..148488d --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,27 @@ +# Architecture overview + +Mini Marty is a layered Next.js app. Each layer depends only on those below it. + +![Layered architecture](../diagrams/architecture.svg) + +## Layers + +| Layer | Responsibility | Key symbols | +|---|---|---| +| App | Routing, page composition | `app/page.tsx`, `app/python-editor/page.tsx`, `app/block-editor/page.tsx` | +| UI | Reusable React components | `components/Header`, `components/Sidebar`, `components/system/ErrorBoundary` | +| Features | Domain logic and screens | `features/marty`, `features/python-runtime`, `features/scene`, `features/blocks`, `features/editor`, `features/tutorials`, `features/challenges` | +| Engine | Robot abstraction + queue | `VirtualMarty`, `CommandQueue`, `MartyEventEmitter` | +| Runtime | Python execution | `executePythonCode`, `registerMartyModule`, `pyodide-loader` | +| Vendors | Third-party libs | Pyodide, Blockly, Monaco, three.js, R3F | + +## Cross-cutting + +- Observability: `ObservabilityProvider` injects `Logger` + `ErrorReporter` +- Analytics: `AnalyticsProvider` injects an `Analytics` port; `VercelAnalytics` or `NoopAnalytics` +- Theming: `ThemeProvider` toggles Tailwind dark class +- Security: `proxy.ts` sets a static CSP plus companion hardening headers + +## Module dependency graph + +![Module dependencies](../diagrams/module-dependencies.svg) diff --git a/docs/architecture/python-runtime.md b/docs/architecture/python-runtime.md new file mode 100644 index 0000000..96c162b --- /dev/null +++ b/docs/architecture/python-runtime.md @@ -0,0 +1,24 @@ +# Python runtime + +Python runs in the browser via [Pyodide](https://pyodide.org). The runtime layer hides Pyodide behind a small surface so the rest of the app can speak Python without touching WebAssembly. + +## Pipeline + +![Python execution sequence](../diagrams/sequence-python.svg) + +1. `usePyodide` loads the Pyodide runtime once per session through `PyodideLoader`. The loader caches the resolved instance and an in-flight promise, so duplicate loads under React Strict Mode double-mount collapse to a single CDN fetch. +2. `registerMartyModule` injects a `martypy`-shaped Python module backed by a JS bridge to `VirtualMarty`. +3. `executePythonCode` wraps the user's code with `wrapUserCode` (adds an async entrypoint) and runs it; stdout, stderr, and errors are streamed via `ExecutorCallbacks`. +4. Each `marty.()` call enqueues a command on the `CommandQueue`. + +## Error formatting + +`formatPythonError` trims tracebacks down to the user's frame so a 13-year-old sees one useful line, not the wrapper machinery. The line-number offset is derived from `EXECUTION_WRAPPER_PRELUDE_LINES` in `martypy-module.ts`, so any edit to the wrapper template stays in sync automatically. + +## Files + +- `features/python-runtime/pyodide-loader.ts` — CDN load, version pin, and the single-instance cache that survives React Strict Mode double-mounts +- `features/python-runtime/martypy-module.ts` — `MARTYPY_MODULE_CODE`, `wrapUserCode`, `EXECUTION_WRAPPER_PRELUDE_LINES`, `createMartyBridge` +- `features/python-runtime/python-executor.ts` — `executePythonCode`, `formatPythonError` +- `features/python-runtime/hooks/usePyodide.ts` — React lifecycle +- `features/python-runtime/hooks/usePythonExecution.ts` — run/stop/console state diff --git a/docs/architecture/scene.md b/docs/architecture/scene.md new file mode 100644 index 0000000..3b0ce12 --- /dev/null +++ b/docs/architecture/scene.md @@ -0,0 +1,28 @@ +# Scene + +The 3D view is React Three Fiber. The robot model is procedural (no GLTF); joints are direct Three.js groups updated each frame. + +## Sequence from blocks + +![Block execution sequence](../diagrams/sequence-blocks.svg) + +## Animation pipeline + +`AnimationPlayer` consumes `AnimationSequence` definitions (`WALK_SEQUENCE`, `DANCE_SEQUENCE`, etc.), interpolates keyframes via `lerpJoints`, `lerpEyes`, `lerpPose`, and writes the resulting `MartyPose` into the model handle each frame. `useMartyAnimation` is the React adapter that subscribes to `MartyEventEmitter` and drives the player. + +## Pose model + +`MartyPose` is a flat record of joint angles, eye position, and lift. `MartyModelHandle.applyPose` writes to refs; no React re-render per frame. + +## Performance + +The scene pauses on `document.hidden` and respects `prefers-reduced-motion`. The canvas itself loads via `next/dynamic` with `ssr: false`. + +## Files + +- `features/scene/components/MartyScene.tsx` — top-level R3F canvas +- `features/scene/components/AnimatedMarty.tsx` — model + animation wiring +- `features/scene/components/MartyModel.tsx` — procedural geometry +- `features/scene/animation/player.ts` — `AnimationPlayer` +- `features/scene/animation/definitions.ts` — keyframe sequences +- `features/scene/animation/useMartyAnimation.ts` — React hook diff --git a/docs/architecture/security.md b/docs/architecture/security.md new file mode 100644 index 0000000..cb325df --- /dev/null +++ b/docs/architecture/security.md @@ -0,0 +1,35 @@ +# Security + +Mini Marty has no backend, no auth, and no PII. Risk surface is the browser sandbox plus a single third-party script (Pyodide from CDN). + +## Content Security Policy + +`proxy.ts` (Next.js middleware exported as `proxy`) issues a fresh nonce per request and sets: + +| Directive | Value | +|---|---| +| `default-src` | `'self'` | +| `script-src` | `'self' 'nonce-' 'wasm-unsafe-eval' cdn.jsdelivr.net va.vercel-scripts.com` | +| `style-src` | `'self' 'unsafe-inline'` (Tailwind utility classes) | +| `connect-src` | `'self' cdn.jsdelivr.net *.ingest.sentry.io vitals.vercel-insights.com` | +| `worker-src` | `'self' blob:` (Pyodide workers) | +| `frame-ancestors` | `'none'` | + +The nonce is propagated to App Router by mirroring it onto the request as the `x-nonce` header. Next.js automatically attaches that nonce to every inline `