feat(uno-q): fleet provisioner + printable QR sticker sheet
Two host-side helpers that close the operational gap between minting kit codes and boards-in-hand. - provision-fleet.sh: provision a whole fleet from fleet.csv (kit,serial). Per board it pushes the per-kit apess-node.env + apess-selfregister.sh over adb and enables the boot/refresh systemd timer (MODE=systemd, root) or an equivalent cron (@reboot + every 5 min; the no-root default). fleet.csv.example is the assignment template. - gen-qr-sheet.sh: render the sticker CSV into a self-contained, printable QR sheet (A4, ~9/page). QRs are baked in as inline SVG via qrencode, so the HTML has zero external refs — prints offline. Each sticker: kit id, QR of the claim URL (/workshop?kit=..&code=..), and the 6-digit code. - .gitignore: never commit kit-codes/ (env files hold FLEET_SECRET + claim codes) or fleet.csv. - README: documents both, plus the one-command fleet path. Verified: provision-fleet loop logic (header/comment/missing skips) and a 4-kit end-to-end sheet build (4 inline SVGs, 0 external refs). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a26185d47b
commit
19593a637c
@@ -12,6 +12,10 @@ dist
|
|||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# Fleet onboarding secrets/output (claim codes, FLEET_SECRET, board assignments)
|
||||||
|
deploy/uno-q/kit-codes/
|
||||||
|
deploy/uno-q/fleet.csv
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|||||||
+30
-5
@@ -76,7 +76,17 @@ The same `FLEET_SECRET` must be set on the APESS API (`FLEET_SECRET` env) — it
|
|||||||
gates `/nodes/self-register` so only your boards can seed the pool. Each sticker
|
gates `/nodes/self-register` so only your boards can seed the pool. Each sticker
|
||||||
QR encodes `…/workshop?kit=KIT-NN&code=NNNNNN`, so scanning it pre-fills both.
|
QR encodes `…/workshop?kit=KIT-NN&code=NNNNNN`, so scanning it pre-fills both.
|
||||||
|
|
||||||
**Per board.** Drop its env file + the self-register unit:
|
**All boards at once (recommended).** Map each kit to its board's adb serial in
|
||||||
|
`fleet.csv` (see `fleet.csv.example`), then provision the fleet in one command —
|
||||||
|
it pushes each board's env + the self-register script and enables the boot +
|
||||||
|
refresh timer (root) or an equivalent cron (`MODE=cron`, the default, no root):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp fleet.csv.example fleet.csv # fill in kit,serial for each board
|
||||||
|
./provision-fleet.sh fleet.csv # (MODE=systemd for root-capable boards)
|
||||||
|
```
|
||||||
|
|
||||||
|
**One board (manual equivalent):**
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
adb -s <serial> push kit-codes/env/KIT-07.env /home/arduino/.zeroclaw/apess-node.env
|
adb -s <serial> push kit-codes/env/KIT-07.env /home/arduino/.zeroclaw/apess-node.env
|
||||||
@@ -85,12 +95,21 @@ adb -s <serial> shell 'chmod +x /home/arduino/apess-selfregister.sh'
|
|||||||
# with root: enable the boot + refresh timer
|
# with root: enable the boot + refresh timer
|
||||||
sudo cp systemd/apess-selfregister.service systemd/apess-selfregister.timer /etc/systemd/system/
|
sudo cp systemd/apess-selfregister.service systemd/apess-selfregister.timer /etc/systemd/system/
|
||||||
sudo systemctl enable --now apess-selfregister.timer
|
sudo systemctl enable --now apess-selfregister.timer
|
||||||
# no root: run it once now (and let zeroclaw-supervisor / cron re-run it)
|
# no root: run it once now (and let cron re-run it)
|
||||||
/home/arduino/apess-selfregister.sh
|
/home/arduino/apess-selfregister.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Print the claim stickers.** Turn the sticker CSV into a self-contained,
|
||||||
|
printable QR sheet (QRs baked in as inline SVG — no network needed to print):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
brew install qrencode # one-time (build-time only)
|
||||||
|
./gen-qr-sheet.sh # kit-codes/kit-codes.csv → kit-codes/qr-sheet.html
|
||||||
|
open kit-codes/qr-sheet.html # print A4 at 100%; dashed borders are cut lines
|
||||||
|
```
|
||||||
|
|
||||||
`provision-uno-q.sh` (admin-driven, binds a board straight to a known team) is
|
`provision-uno-q.sh` (admin-driven, binds a board straight to a known team) is
|
||||||
still there for pre-provisioning / demo boards — the two paths coexist.
|
still there for pre-provisioning / demo boards — the paths coexist.
|
||||||
|
|
||||||
## Provision (production — systemd, boots on power-up)
|
## Provision (production — systemd, boots on power-up)
|
||||||
|
|
||||||
@@ -167,7 +186,13 @@ cloud-with-fallback. In APESS, the team's provider/fallback toggle picks the ali
|
|||||||
|
|
||||||
- `config.template.toml` — the node config (secrets stripped; `__CLOUD_URI__` /
|
- `config.template.toml` — the node config (secrets stripped; `__CLOUD_URI__` /
|
||||||
`__CLOUD_MODEL__` substituted at provision time).
|
`__CLOUD_MODEL__` substituted at provision time).
|
||||||
- `provision-uno-q.sh` — one-shot provisioner (adb-driven).
|
- `provision-uno-q.sh` — one-shot provisioner (adb-driven, binds to a known team).
|
||||||
- `systemd/*.service` — production units (need root).
|
- `gen-kit-codes.sh` — host: mint per-kit claim codes + per-board env files + sticker CSV.
|
||||||
|
- `provision-fleet.sh` — host: provision a whole fleet from `fleet.csv` (env + self-register + timer/cron).
|
||||||
|
- `fleet.csv.example` — kit→adb-serial assignment template for `provision-fleet.sh`.
|
||||||
|
- `gen-qr-sheet.sh` — host: render the sticker CSV into a self-contained printable QR sheet (needs `qrencode`).
|
||||||
|
- `apess-selfregister.sh` — on-board: announce into APESS's unclaimed pool on boot.
|
||||||
|
- `apess-node.env.example` — per-board onboarding identity (KIT_ID / CLAIM_CODE / FLEET_SECRET / APESS_URL).
|
||||||
|
- `systemd/*.service` + `apess-selfregister.timer` — production units (need root).
|
||||||
- `zeroclaw-supervisor.sh` — on-board no-root watchdog (endpoint health + restart).
|
- `zeroclaw-supervisor.sh` — on-board no-root watchdog (endpoint health + restart).
|
||||||
- `recover-uno-q.sh` — host-side post-disconnect recovery (re-tunnel + health-check).
|
- `recover-uno-q.sh` — host-side post-disconnect recovery (re-tunnel + health-check).
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Fleet assignments: map each kit to its board's adb serial (`adb devices`).
|
||||||
|
# Copy to fleet.csv and fill in the serials, then: ./provision-fleet.sh fleet.csv
|
||||||
|
kit,serial
|
||||||
|
KIT-01,65301572
|
||||||
|
KIT-02,REPLACE-with-adb-serial
|
||||||
|
KIT-03,REPLACE-with-adb-serial
|
||||||
Executable
+73
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build a printable QR sticker sheet from gen-kit-codes.sh output. Each sticker
|
||||||
|
# shows the kit id, a QR of the claim URL, and the 6-digit code — scanning it
|
||||||
|
# opens /workshop?kit=..&code=.. with both pre-filled. QRs are baked in as inline
|
||||||
|
# SVG, so the output HTML is fully self-contained (no fonts/scripts/network).
|
||||||
|
#
|
||||||
|
# ./gen-qr-sheet.sh [kit-codes.csv] [out.html]
|
||||||
|
#
|
||||||
|
# Requires qrencode at build time (brew install qrencode). Open the HTML and
|
||||||
|
# print it (A4, ~9 stickers/page); the dashed borders are cut lines.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CSV="${1:-kit-codes/kit-codes.csv}"
|
||||||
|
OUT="${2:-kit-codes/qr-sheet.html}"
|
||||||
|
|
||||||
|
command -v qrencode >/dev/null || { echo "need qrencode — brew install qrencode" >&2; exit 1; }
|
||||||
|
[ -r "$CSV" ] || { echo "no CSV at $CSV (run gen-kit-codes.sh first)" >&2; exit 1; }
|
||||||
|
|
||||||
|
esc() { sed -e 's/&/\&/g; s/</\</g; s/>/\>/g'; }
|
||||||
|
|
||||||
|
{
|
||||||
|
cat <<'HEAD'
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>APESS 2026 — kit claim stickers</title>
|
||||||
|
<style>
|
||||||
|
@page { size: A4; margin: 10mm; }
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { font-family: system-ui, -apple-system, sans-serif; color: #111; margin: 0; }
|
||||||
|
h1 { font-size: 13pt; margin: 0 0 2mm; }
|
||||||
|
.lead { font-size: 9pt; color: #555; margin: 0 0 6mm; }
|
||||||
|
.sheet { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5mm; }
|
||||||
|
.sticker {
|
||||||
|
border: 1px dashed #999; border-radius: 8px; padding: 5mm 4mm;
|
||||||
|
text-align: center; break-inside: avoid; display: flex; flex-direction: column;
|
||||||
|
align-items: center; gap: 2mm;
|
||||||
|
}
|
||||||
|
.kit { font: 700 13pt ui-monospace, "SF Mono", Menlo, monospace; letter-spacing: 1px; }
|
||||||
|
.qr { width: 34mm; height: 34mm; }
|
||||||
|
.qr svg { width: 34mm; height: 34mm; display: block; }
|
||||||
|
.code { font: 700 17pt ui-monospace, Menlo, monospace; letter-spacing: 3px; }
|
||||||
|
.code small { display: block; font: 400 7pt sans-serif; letter-spacing: 1px; color: #777; text-transform: uppercase; }
|
||||||
|
.hint { font-size: 7.5pt; color: #666; }
|
||||||
|
@media print { .noprint { display: none; } }
|
||||||
|
</style>
|
||||||
|
<h1>APESS 2026 · Uno Q kit claim stickers</h1>
|
||||||
|
<p class="lead">One sticker per kit. Scan the QR (or open the code manually) to claim your board to your team.</p>
|
||||||
|
<p class="lead noprint">Tip: print at 100% scale. The dashed borders are cut lines.</p>
|
||||||
|
<div class="sheet">
|
||||||
|
HEAD
|
||||||
|
|
||||||
|
n=0
|
||||||
|
while IFS=',' read -r kit code url || [ -n "$kit" ]; do
|
||||||
|
kit="$(printf '%s' "$kit" | tr -d ' \r')"
|
||||||
|
code="$(printf '%s' "$code" | tr -d ' \r')"
|
||||||
|
url="$(printf '%s' "$url" | tr -d ' \r')"
|
||||||
|
[ -z "$kit" ] && continue
|
||||||
|
[ "$kit" = "kit" ] && continue
|
||||||
|
case "$kit" in \#*) continue ;; esac
|
||||||
|
svg="$(qrencode -t SVG -m 1 -s 4 -o - "$url" | sed -n '/<svg/,$p')"
|
||||||
|
printf ' <div class="sticker">\n'
|
||||||
|
printf ' <div class="kit">%s</div>\n' "$(printf '%s' "$kit" | esc)"
|
||||||
|
printf ' <div class="qr">%s</div>\n' "$svg"
|
||||||
|
printf ' <div class="code">%s<small>claim code</small></div>\n' "$(printf '%s' "$code" | esc)"
|
||||||
|
printf ' <div class="hint">apess.redclaw.dev · claim to your team</div>\n'
|
||||||
|
printf ' </div>\n'
|
||||||
|
n=$((n + 1))
|
||||||
|
done < "$CSV"
|
||||||
|
|
||||||
|
printf '</div>\n'
|
||||||
|
} > "$OUT"
|
||||||
|
|
||||||
|
echo "wrote $n sticker(s) to $OUT — open it and print (A4, 100% scale)" >&2
|
||||||
Executable
+69
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fleet onboarding provisioner. For each board in an assignments file, pushes
|
||||||
|
# its per-kit env (apess-node.env) + the self-register script over adb, then
|
||||||
|
# enables the self-register systemd timer (root boards) or an equivalent cron
|
||||||
|
# (no-root boards, the default). Run after gen-kit-codes.sh.
|
||||||
|
#
|
||||||
|
# ./provision-fleet.sh <assignments.csv> [kit-codes-dir]
|
||||||
|
#
|
||||||
|
# assignments.csv — map each kit to its board's adb serial (`adb devices`):
|
||||||
|
# kit,serial
|
||||||
|
# KIT-01,65301572
|
||||||
|
# KIT-02,88a1b2c3
|
||||||
|
# kit-codes-dir default: ./kit-codes (expects env/KIT-NN.env from gen-kit-codes.sh)
|
||||||
|
#
|
||||||
|
# Env: MODE=cron (default, no root) | systemd (needs a root-capable board).
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
ASSIGN="${1:?usage: provision-fleet.sh <assignments.csv> [kit-codes-dir]}"
|
||||||
|
KCDIR="${2:-./kit-codes}"
|
||||||
|
ENVDIR="$KCDIR/env"
|
||||||
|
MODE="${MODE:-cron}"
|
||||||
|
|
||||||
|
[ -r "$ASSIGN" ] || { echo "no assignments file: $ASSIGN" >&2; exit 1; }
|
||||||
|
[ -d "$ENVDIR" ] || { echo "no env dir: $ENVDIR (run gen-kit-codes.sh first)" >&2; exit 1; }
|
||||||
|
[ -r "$HERE/apess-selfregister.sh" ] || { echo "missing apess-selfregister.sh next to this script" >&2; exit 1; }
|
||||||
|
|
||||||
|
provision() { # kit serial -> 0 ok / 1 fail
|
||||||
|
local kit="$1" serial="$2" env="$ENVDIR/$1.env"
|
||||||
|
[ -r "$env" ] || { echo " ! no env file for $kit ($env)"; return 1; }
|
||||||
|
adb -s "$serial" shell 'mkdir -p /home/arduino/.zeroclaw' >/dev/null 2>&1 || return 1
|
||||||
|
adb -s "$serial" push "$env" /home/arduino/.zeroclaw/apess-node.env >/dev/null 2>&1 || return 1
|
||||||
|
adb -s "$serial" push "$HERE/apess-selfregister.sh" /home/arduino/ >/dev/null 2>&1 || return 1
|
||||||
|
adb -s "$serial" shell 'chmod +x /home/arduino/apess-selfregister.sh' >/dev/null 2>&1 || return 1
|
||||||
|
|
||||||
|
if [ "$MODE" = systemd ]; then
|
||||||
|
adb -s "$serial" push "$HERE/systemd/apess-selfregister.service" /tmp/ >/dev/null 2>&1
|
||||||
|
adb -s "$serial" push "$HERE/systemd/apess-selfregister.timer" /tmp/ >/dev/null 2>&1
|
||||||
|
if adb -s "$serial" shell 'sudo cp /tmp/apess-selfregister.service /tmp/apess-selfregister.timer /etc/systemd/system/ \
|
||||||
|
&& sudo systemctl enable --now apess-selfregister.timer' >/dev/null 2>&1; then
|
||||||
|
echo " ok (systemd timer)"; return 0
|
||||||
|
fi
|
||||||
|
echo " ! systemd enable failed (no root?) — falling back to cron"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# no-root: run once now (detached; it retries internally) + cron for re-announce
|
||||||
|
adb -s "$serial" shell 'setsid /home/arduino/apess-selfregister.sh >/tmp/selfreg.log 2>&1 </dev/null &' >/dev/null 2>&1 || true
|
||||||
|
adb -s "$serial" shell '(crontab -l 2>/dev/null | grep -v apess-selfregister.sh; \
|
||||||
|
echo "@reboot /home/arduino/apess-selfregister.sh"; \
|
||||||
|
echo "*/5 * * * * /home/arduino/apess-selfregister.sh") | crontab -' >/dev/null 2>&1 || true
|
||||||
|
echo " ok (cron: @reboot + every 5 min)"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ok=0 fail=0
|
||||||
|
while IFS=',' read -r kit serial _rest || [ -n "$kit" ]; do
|
||||||
|
kit="$(printf '%s' "$kit" | tr -d ' \r')"
|
||||||
|
serial="$(printf '%s' "$serial" | tr -d ' \r')"
|
||||||
|
[ -z "$kit" ] && continue
|
||||||
|
[ "$kit" = "kit" ] && continue # header
|
||||||
|
case "$kit" in \#*) continue ;; esac # comment
|
||||||
|
echo "==> $kit ($serial)"
|
||||||
|
if [ -z "$serial" ]; then echo " ! no serial — skipping"; fail=$((fail + 1)); continue; fi
|
||||||
|
if provision "$kit" "$serial"; then ok=$((ok + 1)); else echo " FAILED"; fail=$((fail + 1)); fi
|
||||||
|
done < "$ASSIGN"
|
||||||
|
|
||||||
|
echo "----"
|
||||||
|
echo "provisioned $ok board(s), $fail failed/skipped (mode: $MODE)"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
Reference in New Issue
Block a user