Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
0e6de89
feat: scratch3_spark extension
WarutC Apr 22, 2026
9a16801
style: auto-format spark extension (linter whitespace)
WarutC Apr 23, 2026
d050954
feat(spark/3.2): add 8 IMU reporter blocks with Thai labels
WarutC May 8, 2026
ffbbbac
feat(spark/2.3): handle event-push for whenButtonPressed HAT block
WarutC May 9, 2026
aa51dae
fix(spark): whenButtonPressed HAT — pure event, no polling fallback
WarutC May 10, 2026
dac189b
refactor(spark): r2 led enum sweep — drop blue/white, add yellow
WarutC May 11, 2026
438ba30
feat(spark/3-3): whenShake HAT + setShakeSensitivity command
WarutC May 11, 2026
7376e80
fix(spark): repair extension card — broken PNG → playful 600x372 banner
WarutC May 11, 2026
f24039c
fix(spark): connect modal illustration — placeholder → dev board art
WarutC May 11, 2026
585d758
fix(spark): connect modal illustration — compact microbit-style sizing
WarutC May 11, 2026
92f2bde
feat(spark/3-4): mic/light/tof stub blocks with thai sensor-pending t…
WarutC May 11, 2026
c856783
fix(3-4): _readStubField — fire the stub toast on send() rejection to…
WarutC May 12, 2026
f294dc2
feat(spark): live mic/light/tof reporters + whenLoud/Bright/Near + th…
WarutC May 12, 2026
dd204dd
fix(spark): use english default labels for the new sensor blocks (pro…
WarutC May 12, 2026
b801270
fix(spark): re-apply block translations in getInfo() so they survive …
WarutC May 12, 2026
d65be4e
feat(spark): imuYaw reporter + setImuFusion block (3.11)
WarutC May 12, 2026
61747b2
feat(spark): inject env-config for K8s deployment and add AGPL footer
WarutC May 14, 2026
eb720b5
feat(docker): multi-stage non-root nginx image for k8s deploy (10.1)
WarutC May 14, 2026
db4d1a8
fix(docker): code-review patches — input validation, /tmp render, dro…
WarutC May 14, 2026
36ac4e0
fix(spark): update extension ID and category name to 'Sparky'
WarutC May 17, 2026
6224f62
fix(docker): scope build to scratch-gui dep closure + add artifact gate
WarutC May 17, 2026
b540bb9
feat(spark): rename extension display name to Sparky / สปาร์กี้
WarutC May 17, 2026
d3437d9
ci(uat): add deploy-uat workflow (build -> GHCR -> forced-command SSH)
WarutC May 17, 2026
4c0be96
ci(uat): harden deploy workflow — key via env, SHA-pin actions, timeout
WarutC May 17, 2026
746d532
ci(uat): pin actions to reviewed majors (checkout v4 / login v3 / bui…
WarutC May 17, 2026
b8a6167
feat(spark): rename LED color yellow -> amber
WarutC May 17, 2026
7b26b98
fix(spark): correct amber LED RGB (lime -> true amber)
WarutC May 17, 2026
ba50609
refactor(spark): derive LED color menu/default from LED_COLOR_MAP
WarutC May 17, 2026
ff5d67a
test(spark): led color single-source parity + derived-menu regression
WarutC May 17, 2026
179e412
fix(spark): clean up formatting in LED_COLOR_MAP object
WarutC May 17, 2026
7790d54
test(spark): pin extensionId invariant; correct stale spark->Sparky a…
WarutC May 17, 2026
5f1154b
fix(spark): builtin-loader map key spark->Sparky to match extension id
WarutC May 17, 2026
45f7273
fix(spark): align GUI extension-library id spark->Sparky (fixes conne…
WarutC May 17, 2026
fce84ef
refactor(spark): regroup palette — outputs, HAT-first sensors, camera…
WarutC May 17, 2026
fbe434d
ci(7-6): bake GitLab-published firmware into nginx and serve /firmware/
WarutC Jun 1, 2026
166cc73
ci(7-5): bump deploy-uat actions to Node24-compatible SHAs
WarutC Jun 3, 2026
75f9894
feat(4-5): on-device AI (ai.classify) Scratch blocks
WarutC Jun 3, 2026
383e808
feat(4-5): complete AI blocks — motion threshold, gesture menu, bbox …
WarutC Jul 24, 2026
d2d0f5d
feat(2-8): per-LED addressing in setLedColor/Brightness + fix amber t…
WarutC Jul 24, 2026
934a997
feat(12): add QR card sensing blocks + capability gating (Epic 12)
WarutC Aug 2, 2026
8bf9eab
docs(spark): micLevel reporter description — 0..100 sound level, not …
WarutC Aug 3, 2026
daaf23a
fix(12-7-review): qr blocks — per-step hat latch, sightings queue, ra…
WarutC Aug 3, 2026
810b342
ci(7-4): restore stranded GitLab CI pipeline (test + build)
WarutC Aug 5, 2026
0195847
feat(4-9): "จำใบหน้าได้" recognition block (FR58)
WarutC Aug 6, 2026
f89b3e0
docs(examples): restructure example projects + add face_id teaching set
WarutC Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.git
.gitignore
.github
.vscode
.idea
node_modules
**/node_modules
build
dist
**/build
**/dist
coverage
**/coverage
*.log
.env
.env.*
.DS_Store
Thumbs.db
docs
documents
test
**/test
*.md
!README.md
.dockerignore
70 changes: 70 additions & 0 deletions .github/workflows/deploy-uat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: deploy-uat

on:
push:
branches: [uat]
workflow_dispatch: {}
# Story 7.6: cross-forge trigger — spark-firmware GitLab CI fires this on a vX.Y.Z release
# so the image rebuilds with the freshly published firmware baked into /firmware/.
repository_dispatch:
types: [firmware-release]

concurrency:
group: uat-deploy
cancel-in-progress: false

permissions:
contents: read
packages: write

jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5

- name: Short SHA
id: vars
run: echo "sha=sha-$(git rev-parse --short=12 HEAD)" >> "$GITHUB_OUTPUT"

# Story 7.6: pull the latest published firmware from the GitLab pkg registry, verify
# sha256 (fail-closed), and generate firmware/manifest.json (incl. signature) into the
# build context so the Dockerfile bakes it into /usr/share/nginx/html/firmware/.
- name: Fetch published firmware + generate manifest
env:
GITLAB_API_BASE: ${{ vars.GITLAB_API_BASE }}
GITLAB_FW_PROJECT_ID: ${{ vars.GITLAB_FW_PROJECT_ID }}
GITLAB_FW_READ_TOKEN: ${{ secrets.GITLAB_FW_READ_TOKEN }}
run: ./scripts/fetch-firmware.sh

- name: Log in to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: ./Dockerfile
push: true
tags: |
ghcr.io/warutc/scratch-editor:uat
ghcr.io/warutc/scratch-editor:${{ steps.vars.outputs.sha }}

- name: Deploy over SSH (forced command)
env:
UAT_HOST: ${{ vars.UAT_HOST }}
UAT_USER: ${{ vars.UAT_USER }}
UAT_SSH_KEY: ${{ secrets.UAT_SSH_KEY }}
run: |
install -d -m700 ~/.ssh
printf '%s\n' "$UAT_SSH_KEY" > ~/.ssh/uat_key
chmod 600 ~/.ssh/uat_key
ssh-keyscan -H "$UAT_HOST" >> ~/.ssh/known_hosts 2>/dev/null \
|| { echo "ssh-keyscan failed for $UAT_HOST" >&2; exit 1; }
ssh -i ~/.ssh/uat_key -o IdentitiesOnly=yes -o ConnectTimeout=15 \
"$UAT_USER@$UAT_HOST" "deploy ${{ steps.vars.outputs.sha }}"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,7 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.tap/

# local git worktrees (superpowers)
.worktrees/
79 changes: 79 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Sparky Scratch Editor — GitLab CI (Story 7.4)
#
# Scope (7.4): test + build only. The current UAT deploy still runs on
# GitHub Actions (`.github/workflows/deploy-uat.yml`); that workflow stays
# alive until **Story 10.3** migrates the image build/push to the GitLab
# Container Registry and wires ArgoCD. Story 7.5 keeps the GH-Actions
# workflow working during the transition window (Node24 action-SHA bump,
# deadline 2026-06-02). DO NOT remove or modify `.github/workflows/*` here.
#
# Cross-references:
# - Story 7.4 (this file) — test/build CI on GitLab
# - Story 7.5 — keep GH-Actions deploy alive (Node24)
# - Story 10.3 — image build/push to GitLab Container
# Registry + ArgoCD + retire deploy-uat.yml
#
# Per-workspace `lint` scripts exist on a subset of workspaces
# (task-herder, scratch-render, scratch-vm, scratch-media-lib-scripts);
# `--if-present` makes the run a no-op on workspaces without one. There is
# NO top-level lint script today; adding one is out of scope for 7.4.

stages:
- test
- build

variables:
# Node 24 to match the editor's .nvmrc (24.15.0) and the Node24 bump
# tracked by Story 7.5 for the parallel GH-Actions deploy workflow.
NODE_IMAGE: 'node:24'
# Cache npm's download cache (not node_modules). `npm ci` always wipes and
# re-creates node_modules, so caching node_modules is wasted I/O and can
# leak state across MRs on the same branch. Caching `~/.npm` is the supported
# idiom and gives most of the install speedup.
npm_config_cache: '$CI_PROJECT_DIR/.npm'

.node_cache: &node_cache
key:
files:
- package-lock.json
prefix: '$CI_COMMIT_REF_SLUG'
paths:
- .npm/

test:
stage: test
image: $NODE_IMAGE
cache:
<<: *node_cache
before_script:
- npm ci --prefer-offline
script:
# Per-workspace lint (no-op on workspaces without a `lint` script).
- npm run lint --workspaces --if-present
# `--if-present` so workspaces without a `test` script (asset / tooling
# packages) don't break the pipeline. Lint already uses --if-present above;
# parity here was missing on the initial commit (Story 7.4 code review).
- npm test --workspaces --if-present
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH

build:
stage: build
image: $NODE_IMAGE
needs:
- test
cache:
<<: *node_cache
policy: pull
before_script:
- npm ci --prefer-offline
script:
- npm run build
artifacts:
paths:
- packages/scratch-gui/build/
expire_in: 7 days
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH
78 changes: 78 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# syntax=docker/dockerfile:1.7
# Story 10.1: Multi-stage image for serving scratch-gui playground from a
# hardened non-root nginx. Build-once-deploy-many via runtime envsubst of
# MIDDLEWARE_WS_URL into /env-config.js.

# ─── Stage 1: builder ──────────────────────────────────────────────────────
FROM node:24.15.0-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS builder
WORKDIR /src

# Copy full source first. The scratch-gui workspace has a `prepare` lifecycle
# script (scripts/prepare.mjs — extracts media-library zips) that requires the
# source tree to be present, so manifest-only priming won't work. The root
# package's `prepare: husky install` is git-only — we run it skipped via
# --ignore-scripts and then trigger only scratch-gui's prepare explicitly.
COPY . .
RUN npm ci --workspaces --include-workspace-root --ignore-scripts && \
npm rebuild && \
npm run --workspace=packages/scratch-gui prepare
ENV NODE_ENV=production
# Build ONLY scratch-gui's dependency closure, in dependency order
# (scratch-svg-renderer → scratch-render → scratch-vm → scratch-gui).
# We deliberately do NOT run the monorepo-wide `npm run build`: the sibling
# `@scratch/task-herder` package builds with rolldown-vite, and rolldown
# 1.0.0-beta.53 fails to resolve its entry module ("[UNRESOLVED_ENTRY]"),
# which aborts the whole workspace build. task-herder is not a dependency of
# scratch-gui and is not shipped in this image, so excluding it is correct
# scoping, not error suppression. (Tracked as a separate task-herder defect.)
# scratch-gui's webpack consumes the upstream packages' `dist/` via their
# `main` fields, so the upstream three must be built first, in this order.
RUN npm run --workspace=packages/scratch-svg-renderer \
--workspace=packages/scratch-render \
--workspace=packages/scratch-vm \
--workspace=packages/scratch-gui build
# Verification gate: fail loudly if the deployable artifact is incomplete,
# so a silently-broken scratch-gui build can never reach the runtime stage.
RUN set -e; \
d=packages/scratch-gui/build; \
for f in "$d/index.html" "$d/gui.js"; do \
test -s "$f" || { echo "FATAL: missing/empty $f" >&2; exit 1; }; \
done; \
test -d "$d/chunks" && [ -n "$(ls -A "$d/chunks")" ] || { echo "FATAL: $d/chunks missing/empty" >&2; exit 1; }; \
test -d "$d/static" && [ -n "$(ls -A "$d/static")" ] || { echo "FATAL: $d/static missing/empty" >&2; exit 1; }; \
echo "artifact OK: $(du -sh "$d" | cut -f1) in $d"
RUN node -p "require('./package.json').version" > /src/VERSION

# ─── Stage 2: runtime ──────────────────────────────────────────────────────
FROM nginxinc/nginx-unprivileged:1.27-alpine@sha256:65e3e85dbaed8ba248841d9d58a899b6197106c23cb0ff1a132b7bfe0547e4c0 AS runtime
# Base image USER is already 101 (nginx). No USER root needed — env-config.js
# renders to /tmp (world-writable) and the template reads from /usr/share/...
# (world-readable). /usr/share/nginx/html stays read-only, so a future Helm
# chart can set `readOnlyRootFilesystem: true` without shadowing the assets.

# Deployable subset of build/ only — drop standalone/player/blocks-only/compatibility
# HTML entries and their ~16 MB sibling bundles (not part of the public web service).
COPY --from=builder /src/packages/scratch-gui/build/index.html /usr/share/nginx/html/
COPY --from=builder /src/packages/scratch-gui/build/gui.js /usr/share/nginx/html/
COPY --from=builder /src/packages/scratch-gui/build/gui.js.LICENSE.txt /usr/share/nginx/html/
COPY --from=builder /src/packages/scratch-gui/build/extension-worker.js /usr/share/nginx/html/
COPY --from=builder /src/packages/scratch-gui/build/extension-worker.js.LICENSE.txt /usr/share/nginx/html/
COPY --from=builder /src/packages/scratch-gui/build/30d09ba32a17082ef820b57d52d60b7b.hex /usr/share/nginx/html/
COPY --from=builder /src/packages/scratch-gui/build/chunks/ /usr/share/nginx/html/chunks/
COPY --from=builder /src/packages/scratch-gui/build/static/ /usr/share/nginx/html/static/
COPY --from=builder /src/VERSION /usr/share/nginx/html/VERSION

# Story 7.6: firmware update artifacts (firmware/manifest.json + .bin) — generated in the
# BUILD CONTEXT by scripts/fetch-firmware.sh (the deploy-uat fetch step), so copy from context,
# NOT --from=builder. Baked so the same nginx serves /firmware/ alongside Scratch (one host).
COPY firmware/ /usr/share/nginx/html/firmware/

COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
# Flat path under /usr/share/ — avoids /etc/nginx/templates/ (the base image's
# 20-envsubst-on-templates.sh would render anything in there to the wrong
# place). Our own 30-spark-env.sh reads from here and writes to /tmp.
COPY docker/env-config.js.template /usr/share/spark.env-config.js.template
COPY --chmod=0755 docker/30-spark-env.sh /docker-entrypoint.d/30-spark-env.sh

EXPOSE 8080
# Base image's ENTRYPOINT/CMD already invokes /docker-entrypoint.d/*.sh then nginx -g 'daemon off;'.
25 changes: 25 additions & 0 deletions docker/30-spark-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/sh
set -e
: "${MIDDLEWARE_WS_URL:=}"

# Allow only ws:// or wss:// URLs built from a safe character set. By
# restricting via allow-list we also block the chars that would break the
# single-quoted JS string literal in env-config.js (', `, $, newline).
# Empty is allowed — the Spark extension's || fallback handles it.
if [ -n "$MIDDLEWARE_WS_URL" ]; then
if ! printf '%s' "$MIDDLEWARE_WS_URL" | grep -Eq '^wss?://[A-Za-z0-9._:/?#=&%@~+-]+$'; then
echo "[spark] ERROR: MIDDLEWARE_WS_URL must match ws://|wss:// + [A-Za-z0-9._:/?#=&%@~+-]+ (got: ${MIDDLEWARE_WS_URL})" >&2
exit 1
fi
fi

if [ -f /usr/share/nginx/html/VERSION ]; then
SPARK_VERSION=$(cat /usr/share/nginx/html/VERSION)
else
SPARK_VERSION="dev"
fi
export MIDDLEWARE_WS_URL SPARK_VERSION
envsubst '${MIDDLEWARE_WS_URL} ${SPARK_VERSION}' \
< /usr/share/spark.env-config.js.template \
> /tmp/env-config.js
echo "[spark] env-config.js rendered to /tmp: MIDDLEWARE_WS_URL='${MIDDLEWARE_WS_URL}' VERSION='${SPARK_VERSION}'"
Loading
Loading