diff --git a/CLAUDE.md b/CLAUDE.md index ed648b6..b7118c9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,7 +38,9 @@ serving/ Lab serving stack: dnsmasq + nginx + S (dnsmasq + fleet-ipxe + SPDK images are CI-built + pulled; see .gitea/workflows/) serving/kiosk-app/ Svelte+Tailwind spaceship-terminal SPA - (kiosk endpoint; nginx reverse-proxies /kiosk/) + — GIT SUBMODULE (own repo + CI; builds + the fleet-kiosk image the stack pulls). + OS-side kiosk runtime stays in files/kiosk/ serving/dot1x-bench/ Secure-path bench: step-ca + FreeRADIUS + hostapd authenticator + TPM enrolment .gitea/workflows/ Gitea CI: lint -> packer build -> push OCI diff --git a/serving/.env.example b/serving/.env.example index 82e8cf9..a2a5f74 100644 --- a/serving/.env.example +++ b/serving/.env.example @@ -29,6 +29,13 @@ DNSMASQ_VERSION=latest IPXE_IMAGE=git.bitasylum.org/audiobuzz/fleet-ipxe IPXE_VERSION=latest +# kiosk SPA image — the spaceship-terminal web app, now an INDEPENDENT project +# (the kiosk-app git submodule) with its own CI that builds + pushes this image. +# The 'kiosk' compose service pulls it; the main nginx reverse-proxies /kiosk/ to +# it. For local dev, uncomment `build: ./kiosk-app` on that service instead. +KIOSK_IMAGE=git.bitasylum.org/audiobuzz/fleet-kiosk +KIOSK_VERSION=latest + # --- This serving host on the boot LAN ------------------------------------- BOOT_IFACE=ens19 # interface facing the booting client / test bridge SERVER_IP=192.168.50.10 # this host's address on BOOT_IFACE (DHCP/DNS/HTTP/NVMe) diff --git a/serving/docker-compose.yml b/serving/docker-compose.yml index 7271daa..c0bf4a6 100644 --- a/serving/docker-compose.yml +++ b/serving/docker-compose.yml @@ -191,11 +191,15 @@ services: # --- Kiosk SPA (spaceship terminal shown by the diskless kiosk) ------------ # Svelte + Tailwind app built into a static bundle served by its own nginx. - # Bridge-networked (NOT host) so it doesn't fight :80; published on loopback - # only, where the host-network 'http' nginx reverse-proxies /kiosk/ to it. - # Build: docker compose build kiosk + # Now an INDEPENDENT project — the `kiosk-app` git submodule (its own repo + + # CI). It builds + pushes its own image (git.../fleet-kiosk); the serving stack + # pulls it, like dnsmasq/SPDK. Bridge-networked (NOT host) so it doesn't fight + # :80; published on loopback only, where the host-network 'http' nginx + # reverse-proxies /kiosk/ to it. kiosk: - build: ./kiosk-app + image: ${KIOSK_IMAGE}:${KIOSK_VERSION} + # Local dev from the submodule working tree instead of pulling: + # build: ./kiosk-app restart: unless-stopped ports: - "127.0.0.1:${KIOSK_APP_PORT:-8088}:80" # loopback only; main nginx proxies here diff --git a/serving/kiosk-app/.gitea/workflows/ci.yaml b/serving/kiosk-app/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..12dbf1f --- /dev/null +++ b/serving/kiosk-app/.gitea/workflows/ci.yaml @@ -0,0 +1,94 @@ +# +# .gitea/workflows/ci.yaml (fleet-kiosk-app repo) +# +# Builds the kiosk SPA into its container image and pushes it, so the fleet +# serving stack `docker compose pull`s it (consumed as a submodule there). The +# Dockerfile compiles the Svelte/Vite bundle (`npm ci` + `npm run build`), so a +# build/compile error fails this job. +# +# Tagging mirrors the fleet serving-image workflows — every branch builds a +# uniquely tagged image you can test without clobbering main: +# - tag push (refs/tags/v1.2.3) -> "1.2.3" (+ :latest) +# - main push (refs/heads/main) -> "main-" (+ :latest) +# - branch push (refs/heads/) -> "-" (dev image, no :latest) +# +# Required repo secrets: PACKAGE_USER, PACKAGE_TOKEN (registry creds). +# + +name: Build fleet-kiosk image +run-name: ${{ gitea.actor }} builds the kiosk SPA image (${{ gitea.ref_name }}) +on: + push: + branches: + - "**" + tags: + - "v*" + workflow_dispatch: + +concurrency: + group: kiosk-image-${{ gitea.ref }} + cancel-in-progress: true + +env: + KIOSK_IMAGE: "git.bitasylum.org/audiobuzz/fleet-kiosk" + +jobs: + kiosk-build: + name: Build & push kiosk image + runs-on: ubuntu-latest + container: + image: git.bitasylum.org/audiobuzz/ubuntu-act-runner-qemu:main + steps: + - + name: Checkout + uses: actions/checkout@v6 + + - + name: Install buildah + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends buildah + + - + name: Derive image tag + id: vars + run: | + short_sha="$(echo "${GITHUB_SHA}" | cut -c1-7)" + if [ "${GITHUB_REF#refs/tags/}" != "$GITHUB_REF" ]; then + version="${GITHUB_REF#refs/tags/}"; version="${version#v}" + else + branch="${GITHUB_REF#refs/heads/}" + safe_branch="$(printf '%s' "$branch" \ + | sed -e 's#[^A-Za-z0-9._-]#-#g' -e 's#^[.-]*##' | cut -c1-100)" + [ -n "$safe_branch" ] || safe_branch="branch" + version="${safe_branch}-${short_sha}" + fi + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "Building ${KIOSK_IMAGE}:${version}" + + - + name: Registry login + run: | + buildah login \ + --username "${{ secrets.PACKAGE_USER }}" \ + --password "${{ secrets.PACKAGE_TOKEN }}" \ + git.bitasylum.org + + - + name: Build & push + env: + STORAGE_DRIVER: vfs + BUILDAH_ISOLATION: chroot + run: | + VER="${{ steps.vars.outputs.version }}" + buildah bud -t "${KIOSK_IMAGE}:${VER}" . + buildah push "${KIOSK_IMAGE}:${VER}" + # Only main / release tags move :latest, so dev branches never clobber it. + case "$GITHUB_REF" in + refs/heads/main|refs/tags/*) + buildah tag "${KIOSK_IMAGE}:${VER}" "${KIOSK_IMAGE}:latest" + buildah push "${KIOSK_IMAGE}:latest" + echo "Updated ${KIOSK_IMAGE}:latest" ;; + *) + echo "Dev branch build — :latest left untouched (run with KIOSK_VERSION=${VER})" ;; + esac diff --git a/serving/kiosk-app/CLAUDE.md b/serving/kiosk-app/CLAUDE.md new file mode 100644 index 0000000..ba01fb6 --- /dev/null +++ b/serving/kiosk-app/CLAUDE.md @@ -0,0 +1,77 @@ +# CLAUDE.md — Fleet Kiosk App + +The **kiosk SPA** for the diskless fleet: a cyber sci-fi spaceship +monitoring/control terminal (Svelte 5 + Tailwind v4 + Vite) shown fullscreen by +the diskless workstation's kiosk session (weston + Chromium). Built to a static +bundle, served by a tiny nginx, and reverse-proxied at **`/kiosk/`** by the boot +serving stack. + +This is an **independent project**, consumed by the fleet boot repo +(`diskless-workstation`) as a **git submodule** at `serving/kiosk-app/`. It builds +and publishes its own container image; the serving stack pulls it. Keep this repo +focused on the app — the OS-side kiosk *runtime* (weston, Chromium lockdown +policy, the `kiosk-session` script, the systemd unit) lives in the **parent** repo +under `files/kiosk/`, not here. + +## Build & run + +```bash +npm install +npm run dev # http://localhost:5173/kiosk/ (Vite dev server) +npm run build # -> dist/ (static bundle, base path /kiosk/) +npm run preview # serve the built bundle locally + +# container (same as CI does): +docker build -t fleet-kiosk:dev . +docker run --rm -p 8088:80 fleet-kiosk:dev # http://localhost:8088/ +``` + +CI (`.gitea/workflows/ci.yaml`) builds the Dockerfile (which runs `npm ci` + +`npm run build`) and pushes `git.bitasylum.org/audiobuzz/fleet-kiosk` — +branch-aware tags, `:latest` only on `main`. Needs repo secrets `PACKAGE_USER` / +`PACKAGE_TOKEN`. + +## Hard contracts with the consuming fleet (do not break these) + +These are the integration invariants the parent serving stack + the kiosk +display depend on. Changing one means a coordinated change in the parent repo. + +1. **Base path is `/kiosk/`.** `vite.config.js` sets `base: '/kiosk/'` because the + main serving nginx reverse-proxies `/kiosk/` to this container and strips the + prefix. Emitted asset URLs must be absolute under `/kiosk/`. If this ever + changes, the parent's `http/default.conf` proxy + the `kiosk_url` Packer + default must change with it. +2. **Hash-based routing only** (`src/lib/router.svelte.js`, `#/power`, `#/nav`…). + Deep links stay client-side so the proxy needs **no SPA-fallback rewrite**. + Don't switch to history/path routing without adding that rewrite upstream. +3. **Fully self-contained — no CDNs, no web fonts, no external calls.** The boot + LAN is air-gapped and the stack's dnsmasq **sinkholes** call-home (and the + in-image Chromium policy blocks it). Bundle every asset; inline fonts. A + request that escapes to the internet will silently fail on a real host. +4. **Static / read-only by nature.** The display is a fixed kiosk (no keyboard + nav, locked-down Chromium). Telemetry is dummy/simulated (`src/lib/data.js`, + `live.svelte.js`, `clock.svelte.js`) — there is no backend. If a real data + feed is ever added it must be reachable on the boot LAN and fail gracefully + offline. +5. **Reproducible bundle.** Keep `package-lock.json` committed; the Dockerfile + uses `npm ci`. Don't introduce build-time network fetches beyond npm deps. + +## Layout + +``` +Dockerfile 2-stage: Vite build (node:alpine) -> nginx:alpine static serve +nginx.conf serves the built assets at the container root (proxied as /kiosk/) +vite.config.js base '/kiosk/', self-contained bundle settings +src/App.svelte shell: navbar + routed page +src/lib/ Navbar, Panel, Radar, Stat, Bar, Tag, StatusBar + router/clock/data helpers +src/pages/ Dashboard, Power, Navigation, LifeSupport, Comms +.gitea/workflows/ CI: build + push the fleet-kiosk image +``` + +## Conventions + +- Svelte 5 runes; Tailwind v4 via `@tailwindcss/vite`. +- Keep components small and self-contained; shared sim helpers in `src/lib/`. +- Target `es2022` (modern Chromium kiosk — no legacy transpile needed). +- Versioning: bump `package.json` `version` per release; the image tag tracks it + on `main` (plus branch-name+sha tags for dev branches). diff --git a/serving/kiosk-app/README.md b/serving/kiosk-app/README.md index 374239f..b110cd9 100644 --- a/serving/kiosk-app/README.md +++ b/serving/kiosk-app/README.md @@ -33,19 +33,28 @@ npm run dev # http://localhost:5173/kiosk/ ## Build the container -It builds as part of the serving stack: +This is an **independent project** with its own CI. The build is standalone: ```bash -cd .. # serving/ -docker compose build kiosk -docker compose up -d -# main nginx serves it at http://${SERVER_IP}/kiosk/ (kiosk_url default) +docker build -t fleet-kiosk:dev . +docker run --rm -p 8088:80 fleet-kiosk:dev # http://localhost:8088/ ``` -The container publishes on `127.0.0.1:${KIOSK_APP_PORT:-8088}`; the host-network -main nginx proxies `/kiosk/` to it. To point the diskless kiosk here, keep the -Packer `kiosk_url=http://boot.fleet.lab/kiosk/` default (resolved to the serving -host by the stack's dnsmasq). +CI ([.gitea/workflows/ci.yaml](.gitea/workflows/ci.yaml)) builds + pushes +`git.bitasylum.org/audiobuzz/fleet-kiosk` (branch-aware tags; `:latest` on +`main`). The fleet **serving stack consumes the published image** — it sets +`KIOSK_IMAGE`/`KIOSK_VERSION` in its `.env` and the `kiosk` compose service pulls +it (publishing on `127.0.0.1:${KIOSK_APP_PORT:-8088}`, proxied at `/kiosk/`). For +local serving-stack dev you can instead uncomment `build: ./kiosk-app` on that +service. To point the diskless kiosk here, keep the Packer +`kiosk_url=http://boot.fleet.lab/kiosk/` default (resolved by the stack's dnsmasq). + +## Relationship to the fleet repo + +Consumed by `diskless-workstation` as a **git submodule** at `serving/kiosk-app/`. +This repo owns the app + its image; the OS-side kiosk runtime (weston, Chromium +lockdown, `kiosk-session`, the systemd unit) lives in that parent repo under +`files/kiosk/`. See [CLAUDE.md](CLAUDE.md) for the integration contracts. ## Replacing the dummy data