Stop tracking workspace/; bundled-demos/ is the canonical demo source

`workspace/` is runtime state (per-user folders, no-auth dev's `code/`)
and shouldn't be in git. The same files were previously committed under
both `workspace/code/` and `src/static/bundled-demos/`, which forced a
Docker `diff -q` sync check and leaked user-scoped paths into version
control.

- /workspace/ added to .gitignore; all previously tracked files removed
  via `git rm --cached`.
- src/static/bundled-demos/ becomes the single source of truth: panel16
  demos, led_tutorial, led_patterns, neopixel demos, and main.py move
  here alongside the existing canonical demos.
- New BUNDLED_DEMOS_DIR config; user_workspace seeders read from it.
- main.py lifespan seeds WORKSPACE_ROOT/code/ on startup so a fresh
  clone running `pipenv run dev` still gets the full sample set
  (existing files never overwritten — user edits survive restarts).
- Dockerfile drops `COPY workspace` and the diff sanity check.
- README/LED_TUTORIAL repointed at the new canonical paths.
- test_led_patterns loads led_patterns.py from bundled-demos.
- test_api uses mkdir(exist_ok=True) for `code/` (startup pre-creates).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-10 06:55:59 +12:00
parent b8d62e01d9
commit 7ee15f8eac
24 changed files with 86 additions and 418 deletions

View File

@@ -34,7 +34,13 @@ def user_workspace_root(user_id: int, username: str, workspace_root: Path | None
def _seed_canonical_demos_into_code(code_dir: Path) -> None:
src_root = config.PROJECT_ROOT.resolve() / "workspace" / "code"
"""Copy bundled demos into a user's `code/` if missing.
Reads from `BUNDLED_DEMOS_DIR` (single source of truth, ships under
`src/static/bundled-demos/`), never from `workspace/`, so this works
even when `workspace/` is empty (gitignored runtime directory).
"""
src_root = config.BUNDLED_DEMOS_DIR.resolve()
for filename in _CANONICAL_DEMO_FILENAMES:
dst = code_dir / filename
if dst.exists():
@@ -45,7 +51,12 @@ def _seed_canonical_demos_into_code(code_dir: Path) -> None:
def ensure_default_code_main(user_root: Path) -> None:
"""Ensure code/ has main.py and self-contained NeoPixel demos (copied from repo workspace/code/)."""
"""Ensure code/ has main.py and the canonical NeoPixel demos.
Demos are sourced from `BUNDLED_DEMOS_DIR` (the single committed home
for sample scripts). Only files listed in `_CANONICAL_DEMO_FILENAMES`
get auto-seeded — the rest are available via the editor's "Reset
demos" button or a manual copy."""
code_dir = user_root / "code"
code_dir.mkdir(parents=True, exist_ok=True)
main_py = code_dir / "main.py"
@@ -54,6 +65,34 @@ def ensure_default_code_main(user_root: Path) -> None:
_seed_canonical_demos_into_code(code_dir)
def seed_all_bundled_demos(user_root: Path) -> None:
"""Copy *every* file in `BUNDLED_DEMOS_DIR` into `<user_root>/code/`.
Used at app startup to populate a fresh `workspace/code/` with the
full sample set so a no-auth dev install (`pipenv run dev` after
`git clone`) has something to play with. Existing files are not
overwritten — user edits are preserved.
"""
code_dir = user_root / "code"
code_dir.mkdir(parents=True, exist_ok=True)
main_py = code_dir / "main.py"
if not main_py.exists():
main_py.write_text(DEFAULT_MAIN_PY, encoding="utf-8")
src_root = config.BUNDLED_DEMOS_DIR.resolve()
if not src_root.is_dir():
return
for src in sorted(src_root.iterdir()):
if not src.is_file() or not src.name.endswith(".py"):
continue
dst = code_dir / src.name
if dst.exists():
continue
try:
dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
except (UnicodeDecodeError, OSError):
continue
def rename_user_workspace_leaf(
user_id: int, old_username: str, new_username: str, workspace_root: Path | None = None
) -> None: