- Change runtime from debian:12-slim to gcr.io/distroless/static-debian12:nonroot
- Remove wget health check (distroless has no shell)
- Embed dashboard via go:embed (dashboard files now part of binary)
- Add build tag support for conditional embedding (production vs development)
- Dashboard serving code supports both embedded and filesystem-based serving
The dashboard is now embedded in the Go binary using go:embed with the
'embed' build tag. Production Docker builds use -tags=embed to enable
dashboard embedding, while development builds fall back to filesystem
serving. This aligns with the plan's security requirements for non-root
distroless runtime while maintaining developer ergonomics.
Closes: bf-1chgr
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add ARG TARGETPLATFORM/TARGETARCH for cross-platform builds
- Cross-compile Go binary using GOOS/GOARCH from TARGETPLATFORM
- ESP32 firmware build is amd64-only (ESP-IDF is x86_64)
- Creates placeholder on arm64 builds
- Removes placeholder in final stage, adds README
- Supports docker buildx --platform linux/amd64,linux/arm64
Closes: bf-2bxpx
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Dockerfile: use --flash_size 4MB and drop OTA data from merge_bin (OTA
data at 0xc10000 inflated binary to 12.6MB, exceeding 4MB chip flash)
- main.go: seedFirmwareDir now overwrites when source size differs, fixing
PVC staleness where old 1.6MB app-only binary was never replaced
- onboard.js: renderFlashFirmware() rewritten so all elements (button,
progress bar, status text, retry help, log panel) are inline in one
container — no separate floating modal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Guru Meditation/IllegalInstruction after flashing was caused by the
merged binary using default flash parameters instead of the project's
settings. esptool merge_bin flags use underscores and go after the
subcommand: --flash_mode dio --flash_freq 80m --flash_size 16MB.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
esptool merge_bin combines bootloader (0x0), partition table (0x8000),
application (0x10000), and OTA data (0xc10000) into a single binary
flashable at offset 0x0 — matching the manifest address and enabling
correct initial flashing via the onboarding wizard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ESP-IDF 5.x requires explicit set-target even when CONFIG_IDF_TARGET
is present in sdkconfig.defaults.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
espressif/idf entrypoint is not invoked in multi-stage builds, so
idf.py is not in PATH. Sourcing export.sh activates the toolchain.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add espressif/idf:v5.2 as a multi-stage build step so the firmware
binary is baked into the image at /firmware/spaxel-firmware.bin.
On startup the mothership copies it into /data/firmware/ (PVC) if not
already present, making it immediately available for the onboarding
wizard without a manual upload.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add complete health check implementation for Docker HEALTHCHECK and
Traefik health routing with:
Response fields:
- status: "ok" or "degraded"
- uptime_s: seconds since mothership boot
- version: mothership version string
- nodes_online: count of connected nodes
- db: "ok" or "failing" (SELECT 1 with 100ms timeout)
- load_level: 0-3 from load shedding state
- reason: human-readable explanation (only when degraded)
HTTP status codes:
- 200 for healthy (status="ok")
- 503 for degraded (status="degraded")
Degraded conditions:
- Database unreachable
- Load level 3 sustained for >60 seconds
- No nodes connected after 5 minutes uptime
Docker HEALTHCHECK updated to verify status="ok" response.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>