From d1192ca868c7127938dfee5890edbcb13df4c4eb Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Tue, 13 Oct 2026 21:39:44 -0700 Subject: [PATCH] feat(pewpewstudio): laser job studio on nh3-docker :8185 (api + Caddy front), Homepage tile Prime's directive via pewpew-dev. Images are built on nh3-dev from a clean archive of vh/pewpewstudio (26bba3c; hamr over gitea SSH via BuildKit --ssh) and loaded on nh3-docker. Caddy serves the SPA and strips /api/ in front of the backend. It refuses foreign Host headers inside a route block, because the Host rewrite would otherwise bypass the backend's TrustedHost. The DB, assets and the app-edited shop config live on named volumes inside nh3-docker's nightly vzdump. Verified: SPA, /api/machines and /api/generate/workflows 200; foreign Host 400. --- stacks/pewpewstudio/.env.example | 4 +++ stacks/pewpewstudio/README.md | 51 ++++++++++++++++++++++++++++ stacks/pewpewstudio/build/Dockerfile | 33 ++++++++++++++++++ stacks/pewpewstudio/compose.yaml | 51 ++++++++++++++++++++++++++++ stacks/pewpewstudio/conf/Caddyfile | 21 ++++++++++++ 5 files changed, 160 insertions(+) create mode 100644 stacks/pewpewstudio/.env.example create mode 100644 stacks/pewpewstudio/README.md create mode 100644 stacks/pewpewstudio/build/Dockerfile create mode 100644 stacks/pewpewstudio/compose.yaml create mode 100644 stacks/pewpewstudio/conf/Caddyfile diff --git a/stacks/pewpewstudio/.env.example b/stacks/pewpewstudio/.env.example new file mode 100644 index 0000000..cbd47f3 --- /dev/null +++ b/stacks/pewpewstudio/.env.example @@ -0,0 +1,4 @@ +PEWPEW_REV=26bba3c +PORT=8185 +# Set both Worldtree vars or neither (WORLDTREE_BASE_URL is in compose.yaml): +WORLDTREE_API_KEY= # secret get nh3-dev/.config/worldtree/pewpewstudio-demo-key diff --git a/stacks/pewpewstudio/README.md b/stacks/pewpewstudio/README.md new file mode 100644 index 0000000..f857594 --- /dev/null +++ b/stacks/pewpewstudio/README.md @@ -0,0 +1,51 @@ +# pewpewstudio + +Laser job studio (FastAPI backend + React SPA, gitea `vh/pewpewstudio`). It composes Thunder Aurora marking jobs and +returns LightBurn `.lbrn` files as browser downloads, and never touches hardware. Deployed 2026-10-13 on Prime's directive +via pewpew-dev (thread `01M4WATRFQX3V7TSJ4XPGABWMN`). + +| | | +|---|---| +| **URL** | `http://10.100.50.40:8185/` (= `http://nh3-docker.nh3.internal:8185/`). Homepage tile in *AI - Studios*. | +| **Host** | nh3-docker: fast local disk for SQLite WAL (fsync ~2.7 ms, unlike ana-docker's HDD pool), and it reaches arbo `100.64.0.6:8201` and Worldtree DEMO `10.250.50.152:8080`. | +| **Auth** | NONE in v1. LAN/mesh only: published on the host's LAN address, with no public ingress. | +| **Images** | `pewpewstudio:` (api) and `pewpewstudio-web:` (Caddy + `web/dist`), built on nh3-dev and loaded here; `PEWPEW_REV` in `.env` picks the pair. | +| **State** | volume `pewpewstudio_state` (`/state`: `shop.db` + `assets/`) and volume `pewpewstudio_config` (`/config`: machine profiles, lenses, denylist, sizing, fonts, which **the app edits from the browser**). Both are covered by nh3-pve's nightly 21:00 whole-VM vzdump of VM 100. | +| **Secrets** | `.env` (0600, infra-ops): `WORLDTREE_API_KEY` = vault `nh3-dev/.config/worldtree/pewpewstudio-demo-key`. | + +## Topology + +`web` (Caddy, published `10.100.50.40:8185 → :80`) serves the SPA at `/`. It sends `/api/*` to `api:8000` with the +prefix **stripped** (the backend has no `/api` routes and no CORS, so both must be one origin) and Host rewritten to +`localhost`, which the backend's TrustedHost accepts. Because of that rewrite, **Caddy refuses any Host other than +`10.100.50.40` / `nh3-docker.nh3.internal` with 400**, to keep the backend's DNS-rebinding defence. Add a name to +`conf/Caddyfile` before publishing one. Verified 2026-10-13: foreign Host → 400; IP and name → 200. +⚠ That check must stay inside `route { }`. Outside it, Caddy orders `respond` after `handle_path`, and the check +silently never runs: a foreign Host got 200 until it was wrapped. +⚠ The Caddy config is mounted as the DIRECTORY `/opt/docker/conf/pewpewstudio → /etc/caddy`. deploy-stack replaces the +file, and a single-file bind mount keeps showing the old inode. + +## Build and deploy (from nh3-dev) + +```bash +REV=; C=$(mktemp -d) +git -C ~/development/pewpewstudio archive $REV | tar -x -C $C +cp stacks/pewpewstudio/build/Dockerfile $C/Dockerfile.eshpfi +# hamr is a git dependency over gitea SSH (10.250.50.70:222); BuildKit forwards lkraven's gitea key: +(cd $C && docker build -f Dockerfile.eshpfi --ssh default=$HOME/.ssh/id_rsa --target api -t pewpewstudio:$REV . \ + && docker build -f Dockerfile.eshpfi --target web -t pewpewstudio-web:$REV .) +docker save pewpewstudio:$REV pewpewstudio-web:$REV | ssh infra-ops@10.100.50.40 docker load +scripts/deploy-stack.sh nh3-docker pewpewstudio # compose + conf/Caddyfile; never .env +# on nh3-docker: set PEWPEW_REV=$REV in .env (in place, keep 0600), then +cd /opt/docker/compose/pewpewstudio && docker compose up -d +``` + +**First deploy only:** the config volume was seeded once from the repo's `config/` plus the gitignored +`config/machine.local.toml` from nh3-dev's checkout (machine parameters, no secrets). Without a `machine.*.toml` (the +registry skips `machine.example.toml`), startup fails "no machine.*.toml files". **Never reseed a populated volume**: +it holds the shop's own edits. The database started fresh; the dev `shop.db` was smoke-test data. + +## Checks + +- `GET /` 200 (the SPA); `GET /api/machines` 200 (one machine, `aurora-mopa-100w-f160`); `GET /api/generate/workflows` 200. +- The api healthcheck is `GET /machines` on loopback. diff --git a/stacks/pewpewstudio/build/Dockerfile b/stacks/pewpewstudio/build/Dockerfile new file mode 100644 index 0000000..cf62066 --- /dev/null +++ b/stacks/pewpewstudio/build/Dockerfile @@ -0,0 +1,33 @@ +# syntax=docker/dockerfile:1.7 +# pewpewstudio images, built on nh3-dev from a clean `git archive` of vh/pewpewstudio (see ../README.md). +# api: the FastAPI backend (core.api), venv built with uv; hamr is a git dependency over gitea SSH (BuildKit --ssh). +# web: Caddy serving the SPA (web/dist) and proxying /api/* to the api with the prefix stripped (Caddyfile mounted). + +FROM python:3.12-slim-bookworm AS build +RUN apt-get update && apt-get install -y --no-install-recommends git openssh-client build-essential \ + && rm -rf /var/lib/apt/lists/* && pip install --no-cache-dir uv==0.13.0 +# gitea's SSH is 10.250.50.70:222; the lockfile names ssh://git@gitea.phasefinal.com/vh/hamr.git, as nh3-dev's ssh config maps it +RUN install -d -m 700 /root/.ssh \ + && printf 'Host gitea.phasefinal.com\n HostName 10.250.50.70\n Port 222\n User git\n' > /root/.ssh/config \ + && ssh-keyscan -p 222 10.250.50.70 > /root/.ssh/known_hosts 2>/dev/null +ENV UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never UV_PROJECT_ENVIRONMENT=/app/.venv +WORKDIR /app +COPY . /app +RUN --mount=type=ssh uv sync --frozen --no-dev && uv --version > /app/.uv-version + +FROM python:3.12-slim-bookworm AS api +COPY --from=build /app /app +RUN install -d -o 1000 -g 1000 /state /state/assets /config +WORKDIR /app +ENV PATH=/app/.venv/bin:$PATH PYTHONUNBUFFERED=1 +USER 1000:1000 +EXPOSE 8000 +CMD ["python", "-c", "import uvicorn; from core.api.app import create_app; from core.api.config import ApiConfig; uvicorn.run(create_app(ApiConfig.from_env()), host='0.0.0.0', port=8000)"] + +FROM oven/bun:1.3 AS webbuild +WORKDIR /web +COPY web/ /web/ +RUN bun install --frozen-lockfile && bun run build + +FROM caddy:2-alpine AS web +COPY --from=webbuild /web/dist /srv diff --git a/stacks/pewpewstudio/compose.yaml b/stacks/pewpewstudio/compose.yaml new file mode 100644 index 0000000..adc93a7 --- /dev/null +++ b/stacks/pewpewstudio/compose.yaml @@ -0,0 +1,51 @@ +# pewpewstudio on nh3-docker: laser job studio (FastAPI + React SPA), composes Thunder Aurora marking jobs and exports +# LightBurn .lbrn. Prime 2026-10-13 via pewpew-dev. NO user auth in v1, so LAN/mesh only: published on the host's LAN +# address, never on a public ingress. Images are built on nh3-dev and loaded here (see README). +# State: the SQLite DB + assets on the named volume `state` (local disk, never NFS). The shop's own config (machine +# profiles, lenses, denylist, sizing) on the named volume `config`, because the app EDITS it from the browser (temp +# file + os.replace in the directory); a volume, not /opt/docker/conf, so deploy-stack's --delete can never touch it. +# Seeded once at first deploy (README). Both volumes ride nh3-docker's whole-VM backup. +name: pewpewstudio +services: + api: + image: pewpewstudio:${PEWPEW_REV:?set PEWPEW_REV} + container_name: pewpewstudio-api + restart: unless-stopped + env_file: .env # WORLDTREE_API_KEY (secret get nh3-dev/.config/worldtree/pewpewstudio-demo-key) + environment: + PEWPEW_DB_PATH: /state/shop.db + PEWPEW_ASSETS_ROOT: /state/assets + PEWPEW_PROFILE_DIR: /config + PEWPEW_DENYLIST: /config/materials.toml + PEWPEW_SIZING: /config/sizing.toml + PEWPEW_FONTS_DIR: /config/fonts + PEWPEW_BASELINE: /config/settings_baseline.toml + ARBO_BASE_URL: http://100.64.0.6:8201 + WORLDTREE_BASE_URL: http://10.250.50.152:8080 + volumes: + - state:/state + - config:/config + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/machines', timeout=5)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + web: + image: pewpewstudio-web:${PEWPEW_REV:?set PEWPEW_REV} + container_name: pewpewstudio + restart: unless-stopped + depends_on: [api] + ports: + - "10.100.50.40:${PORT:-8185}:80" + volumes: + - /opt/docker/conf/pewpewstudio:/etc/caddy:ro # the DIRECTORY: deploy-stack replaces the file, and a single-file bind would keep the old inode + labels: + - homepage.group=AI - Studios + - homepage.name=pewpewstudio + - homepage.icon=mdi-laser-pointer + - "homepage.description=Laser job studio: composes Thunder Aurora marking jobs, exports LightBurn .lbrn" + - homepage.href=http://10.100.50.40:${PORT:-8185}/ +volumes: + state: + config: diff --git a/stacks/pewpewstudio/conf/Caddyfile b/stacks/pewpewstudio/conf/Caddyfile new file mode 100644 index 0000000..5d05b58 --- /dev/null +++ b/stacks/pewpewstudio/conf/Caddyfile @@ -0,0 +1,21 @@ +# pewpewstudio's front: the SPA at /, /api/* to the backend with the prefix STRIPPED (the backend has no /api routes and +# no CORS, so both must be one origin). The backend's TrustedHost accepts localhost, so Host is rewritten to it. +# No request-body cap here: asset uploads are capped at 32 MB by the app itself. LAN/mesh only, no auth in v1. +:80 { + encode zstd gzip + # `route` keeps this literal order; outside it Caddy sorts `respond` AFTER handle/handle_path, and the Host check + # would never run (measured: a foreign Host got 200 before this was a route). + route { + # Keep the backend's TrustedHost meaning (DNS-rebinding defence): the proxy rewrites Host to localhost below, + # so it must refuse any Host the app would refuse. Add a name here when one is published for it. + @foreign not host 10.100.50.40 nh3-docker.nh3.internal + respond @foreign 400 + handle_path /api/* { + reverse_proxy api:8000 { + header_up Host localhost + } + } + root * /srv + file_server + } +}