diff --git a/.cursor/hooks.json b/.cursor/hooks.json new file mode 100644 index 0000000..8490b03 --- /dev/null +++ b/.cursor/hooks.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "hooks": { + "afterFileEdit": [ + { + "command": ".cursor/hooks/dev-reload-touch.sh", + "timeout": 5, + "matcher": "Write|TabWrite" + } + ] + } +} diff --git a/.cursor/hooks/dev-reload-touch.sh b/.cursor/hooks/dev-reload-touch.sh new file mode 100755 index 0000000..39921ea --- /dev/null +++ b/.cursor/hooks/dev-reload-touch.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Consume hook JSON input (not needed for this hook). +cat >/dev/null || true + +mkdir -p "src/static" +date +%s%3N > "src/static/.reload-token" diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..186d081 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.gitignore +.venv +__pycache__/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.cursor/ +tests/ +agent-transcripts/ +data/ +*.pyc +*.pyo +*.pyd diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4034e25 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +RUN pip install --no-cache-dir pipenv + +COPY Pipfile Pipfile.lock ./ +RUN pipenv install --system --deploy + +COPY src ./src +COPY workspace ./workspace + +EXPOSE 8080 + +CMD ["uvicorn", "app:app", "--app-dir", "src", "--host", "0.0.0.0", "--port", "8080"] diff --git a/LED_TUTORIAL.md b/LED_TUTORIAL.md new file mode 100644 index 0000000..ceaa526 --- /dev/null +++ b/LED_TUTORIAL.md @@ -0,0 +1,75 @@ +# Python LED Tutorial (NeoPixel Focus) + +This tutorial is for the browser editor's ESP32-style mocks: + +- `machine.Pin` +- `neopixel.NeoPixel` + +Use `workspace/code/led_tutorial.py` while reading this guide. + +## 1) Basic setup + +```python +from machine import Pin +import neopixel + +np = neopixel.NeoPixel(Pin(4), 12) +``` + +- `Pin(4)` means data pin 4 (matching common ESP32 examples). +- `12` is the number of LEDs in the strip/ring. +- `np` is your LED strip object. + +## 2) Set one LED color + +```python +np[0] = (255, 0, 0) # red +np.write() +``` + +- Colors are `(red, green, blue)` from `0` to `255`. +- Nothing updates visually until `np.write()`. + +## 3) Fill all LEDs + +```python +np.fill((0, 0, 255)) # all blue +np.write() +``` + +## 4) Clear LEDs (turn off) + +```python +np.fill((0, 0, 0)) +np.write() +``` + +## 5) Animate over time + +```python +import time + +for step in range(20): + np.fill((step * 10, 0, 255 - step * 10)) + np.write() + time.sleep(0.08) +``` + +`time.sleep(...)` controls animation speed. + +## 6) Moving pixel example + +```python +for i in range(len(np)): + np.fill((0, 0, 0)) + np[i] = (255, 120, 0) + np.write() + time.sleep(0.06) +``` + +## 7) Tips + +- Keep color values in `0..255`. +- Use helper functions for repeated color logic. +- Start with short loops, then increase frames once behavior looks good. +- If the simulator is closed, run your script again to show updates. diff --git a/Pipfile b/Pipfile index 48f870d..1ceb9d0 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,7 @@ pytest = "*" pytest-cov = "*" httpx = "*" selenium = "*" +playwright = "*" [packages] fastapi = "*" diff --git a/Pipfile.lock b/Pipfile.lock index f7c0140..0ca5574 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f9de958b9982b3a30c107c2c02746aece031522dc429be84e4f9589c73404282" + "sha256": "ce177325185c5a9e04401b9368766f419921dfc5f9de934820507dae79998082" }, "pipfile-spec": 6, "requires": { @@ -586,6 +586,71 @@ "markers": "python_version >= '3.10'", "version": "==7.13.5" }, + "greenlet": { + "hashes": [ + "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", + "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", + "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", + "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce", + "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2", + "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", + "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", + "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", + "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a", + "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", + "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", + "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", + "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", + "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136", + "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", + "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", + "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", + "sha256:4b28037cb07768933c54d81bfe47a85f9f402f57d7d69743b991a713b63954eb", + "sha256:4d0eadc7e4d9ffb2af4247b606cae307be8e448911e5a0d0b16d72fc3d224cfd", + "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", + "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", + "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", + "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d", + "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", + "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", + "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", + "sha256:64d6ac45f7271f48e45f67c95b54ef73534c52ec041fcda8edf520c6d811f4bc", + "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", + "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", + "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", + "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", + "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", + "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2", + "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", + "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", + "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", + "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f", + "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", + "sha256:884f649de075b84739713d41dd4dfd41e2b910bfb769c4a3ea02ec1da52cd9bb", + "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", + "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", + "sha256:9c615f869163e14bb1ced20322d8038fb680b08236521ac3f30cd4c1288785a0", + "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", + "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", + "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", + "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", + "sha256:ba8f0bdc2fae6ce915dfd0c16d2d00bca7e4247c1eae4416e06430e522137858", + "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37", + "sha256:cda05425526240807408156b6960a17a79a0c760b813573b67027823be760977", + "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", + "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", + "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", + "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", + "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", + "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", + "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", + "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", + "sha256:f8c30c2225f40dd76c50790f0eb3b5c7c18431efb299e2782083e1981feed243", + "sha256:fa94cb2288681e3a11645958f1871d48ee9211bd2f66628fdace505927d6e564" + ], + "markers": "platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", + "version": "==3.5.0" + }, "h11": { "hashes": [ "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", @@ -643,6 +708,21 @@ "markers": "python_version >= '3.8'", "version": "==26.2" }, + "playwright": { + "hashes": [ + "sha256:4a4a2d4842b0e4120de3fa48636e4b69085a05b81d8a35ad4353f530ade72ed6", + "sha256:6989c476be2b9cd3e24a18cc9dcf202e266fb3d91e3e5395cd668c54ea54b119", + "sha256:8c881a19377d2b900af855fb525b5f22a27bf3cfbecba6d1edb36766d56cb100", + "sha256:93581ad515728cadc8af39b288a5633ba6d36e7d72048e79d890ce01ea2156f9", + "sha256:af068143a0c045ec11608b67d6c42e58db7e9cf65a742dd21fddedc1a9802c47", + "sha256:bfc6940100b57423175c819ce2422ec5880d55fa2769987f62ab7a1f5fe6783e", + "sha256:c5792aad9e22b91a09264b9edbc18553cf05ea5a39404d65dc19a012c6b2e51d", + "sha256:d5a5cc064b82ca92996080025710844e417f44df8fda9001102c28f44174171c" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==1.59.0" + }, "pluggy": { "hashes": [ "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", @@ -651,6 +731,14 @@ "markers": "python_version >= '3.9'", "version": "==1.6.0" }, + "pyee": { + "hashes": [ + "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", + "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228" + ], + "markers": "python_version >= '3.8'", + "version": "==13.0.1" + }, "pygments": { "hashes": [ "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", diff --git a/README.md b/README.md index ed483b7..d7ce749 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,24 @@ If nothing is listening, the smoke test **skips** with a short message instead o Open [http://localhost:8080](http://localhost:8080). +## Deploy with Docker + +Build and run with Docker Compose: + +```bash +cp .env.example .env +mkdir -p data +docker compose up --build +``` + +Then open [http://localhost:8080](http://localhost:8080). + +Notes: + +- `workspace/` is mounted to `/app/workspace` so your code persists locally. +- `data/` is mounted to `/app/data` for the SQLite auth DB. +- In container mode, `WORKSPACE_ROOT` and `AUTH_DATABASE_PATH` are set by `docker-compose.yml`. + **User accounts** — Set `AUTH_ENABLED=true` in `.env` to require sign-in for workspace APIs. Users live in a SQLite file (`AUTH_DATABASE_PATH`, default `./data/editor.db`). Use `/register` (if `AUTH_REGISTER_OPEN=true`) or `BOOTSTRAP_ADMIN_USERNAME` / `BOOTSTRAP_ADMIN_PASSWORD` for the first superuser. Superusers can **GET/POST/DELETE `/api/users`** to list, create, or remove accounts. **API key** — If `EDITOR_API_KEY` is set, requests may use `Authorization: Bearer …` instead of a session (useful for automation). When `AUTH_ENABLED=true`, a valid session *or* API key is accepted. @@ -58,3 +76,46 @@ The home page can store the API key in `sessionStorage` when you are not using c - `src/` — FastAPI app and static UI (`src/static/`) - `workspace/` — default tree: `code/` (editable), `lib/` (read-only via API) + +## ESP32 / NeoPixel mock + +The browser runtime now includes MicroPython-style mocks in `workspace/lib`: + +- `machine.Pin` +- `neopixel.NeoPixel` + +Use them from scripts in `workspace/code` exactly like ESP32 examples: + +```python +from machine import Pin +import neopixel + +np = neopixel.NeoPixel(Pin(4), 8) +np[0] = (255, 0, 0) +np.write() +``` + +`write()` updates the NeoPixel simulator window so you can verify behavior visually. + +Tutorial files: + +- `LED_TUTORIAL.md` - step-by-step NeoPixel tutorial +- `workspace/code/led_tutorial.py` - runnable guided LED example +- `workspace/code/led_patterns.py` - reusable pattern helpers (`rainbow_frame`, `chase_frame`, `twinkle_frame`) +- `workspace/code/pattern_rainbow_demo.py` - rainbow animation demo +- `workspace/code/pattern_chase_demo.py` - chase animation demo +- `workspace/code/pattern_twinkle_demo.py` - twinkle animation demo +- `workspace/code/panel16_utils.py` - helpers for 16x16 serpentine mapping +- `workspace/code/panel16_rainbow_wave.py` - 16x16 rainbow wave +- `workspace/code/panel16_bounce.py` - 16x16 bouncing pixel with trail +- `workspace/code/panel16_matrix_rain.py` - 16x16 matrix rain effect + +## Dev auto-reload hook + +Project hook files are included in `.cursor/`: + +- `.cursor/hooks.json` +- `.cursor/hooks/dev-reload-touch.sh` + +When files are edited through Cursor tools, the hook updates `src/static/.reload-token`. +The editor (on localhost) polls that token and auto-reloads the browser when it changes. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ba85a63 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + python-editor: + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:8080" + env_file: + - .env + environment: + - WORKSPACE_ROOT=/app/workspace + - AUTH_DATABASE_PATH=/app/data/editor.db + volumes: + - ./workspace:/app/workspace + - ./data:/app/data + restart: unless-stopped diff --git a/src/static/.reload-token b/src/static/.reload-token new file mode 100644 index 0000000..75786fb --- /dev/null +++ b/src/static/.reload-token @@ -0,0 +1 @@ +1777623664358 diff --git a/src/static/index.html b/src/static/index.html index 564fbcc..387b880 100644 --- a/src/static/index.html +++ b/src/static/index.html @@ -5,7 +5,7 @@