feat(deploy): add deploy script and per-device config files
deploy/deploy.sh — automated deploy: arch detection, scp .app, write seed.json, verify. Supports single laptop, list, or "all"; --seed-only flag skips .app copy for key-rotation runs. deploy/devices.conf — all 19 laptops (num / IP / event_device_id). deploy/event.env.example — template for gitignored event.env (API key). README updated: deploy/ table, script usage, manual steps moved to reference section. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,3 +27,4 @@ aether_native_app_config.json
|
||||
# package-lock.json
|
||||
builds/
|
||||
resources/seed_config.json
|
||||
event.env
|
||||
|
||||
128
README.md
128
README.md
@@ -18,6 +18,15 @@ This application serves as the "Native Mode" runtime for Aether podiums and devi
|
||||
SSH user on all laptops: **`speaker ready`**
|
||||
IP pattern: `192.168.32.1XX` (XX = zero-padded laptop number, e.g. 03 → `.103`). Find/replace this prefix for other onsite environments.
|
||||
|
||||
Deploy files live in `deploy/`:
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `deploy/deploy.sh` | Deploy script — handles arch detection, scp, and seed.json |
|
||||
| `deploy/devices.conf` | Laptop list: number, IP, `event_device_id` |
|
||||
| `deploy/event.env` | **Gitignored** — per-event API key and URLs (create from example) |
|
||||
| `deploy/event.env.example` | Template for `event.env` |
|
||||
|
||||
### Step 1 — Build the app (workstation)
|
||||
|
||||
```bash
|
||||
@@ -29,35 +38,62 @@ npm run package:mac
|
||||
Only rebuild if source code has changed. The `.app` bundle is identical for all Intel laptops —
|
||||
only `~/seed.json` differs per device.
|
||||
|
||||
### Step 2 — Determine target architecture and copy the .app
|
||||
### Step 2 — Create event.env
|
||||
|
||||
Check the target Mac's CPU if unsure:
|
||||
```bash
|
||||
ssh "speaker ready"@192.168.32.103 "uname -m"
|
||||
# x86_64 → use aether_launcher-darwin-x64 (MacBook Air 2018 and all current Intel Macs)
|
||||
# arm64 → use aether_launcher-darwin-arm64 (Apple Silicon M1/M2/M3/M4)
|
||||
cp deploy/event.env.example deploy/event.env
|
||||
# Edit deploy/event.env — fill in AETHER_API_KEY
|
||||
```
|
||||
|
||||
Copy from the workstation (replace `103` with the target IP last octet):
|
||||
Create the API key in the Aether admin panel before the show (Core → Accounts or Events →
|
||||
Devices API key section). All laptops share one key per event. Delete it after the show.
|
||||
|
||||
### Step 3 — Run the deploy script
|
||||
|
||||
```bash
|
||||
# Intel (current hardware):
|
||||
# Deploy specific laptops:
|
||||
./deploy/deploy.sh 01 02 03
|
||||
|
||||
# Deploy all laptops in devices.conf:
|
||||
./deploy/deploy.sh all
|
||||
|
||||
# Update seed.json only (no .app copy — e.g. when rotating the API key):
|
||||
./deploy/deploy.sh --seed-only all
|
||||
```
|
||||
|
||||
The script auto-detects each Mac's CPU architecture, copies the correct `.app` build, writes
|
||||
`seed.json`, and verifies. One SSH connection failure won't abort the batch — it logs and
|
||||
continues, then reports which laptops need a retry.
|
||||
|
||||
### Step 4 — Verify and launch
|
||||
|
||||
After the script completes, launch the app on each laptop and confirm it connects and shows
|
||||
the correct device name in the Launcher UI.
|
||||
|
||||
### Adding SSH key to a new laptop (first time only)
|
||||
|
||||
```bash
|
||||
ssh-copy-id "speaker ready"@192.168.32.1XX
|
||||
```
|
||||
|
||||
Run once per laptop before deploying.
|
||||
|
||||
---
|
||||
|
||||
### Manual deploy reference
|
||||
|
||||
The script covers the normal case. For one-off fixes or if the script isn't available:
|
||||
|
||||
```bash
|
||||
# Detect arch
|
||||
ssh "speaker ready"@192.168.32.103 "uname -m"
|
||||
# x86_64 → darwin-x64 | arm64 → darwin-arm64
|
||||
|
||||
# Copy .app (Intel example):
|
||||
scp -r builds/aether_launcher-darwin-x64/aether_launcher.app \
|
||||
"speaker ready"@192.168.32.103:/Applications/aether_launcher.app
|
||||
|
||||
# Apple Silicon (future hardware):
|
||||
scp -r builds/aether_launcher-darwin-arm64/aether_launcher.app \
|
||||
"speaker ready"@192.168.32.103:/Applications/aether_launcher.app
|
||||
```
|
||||
|
||||
If `/Applications/aether_launcher.app` already exists, `scp -r` overwrites it. No need to
|
||||
remove the old version first.
|
||||
|
||||
### Step 3 — Write seed.json on the target laptop
|
||||
|
||||
The seed file lives at `~/seed.json` (`/Users/speaker ready/seed.json`) on each Mac.
|
||||
It is intentionally outside the app bundle so it can be updated without redeploying.
|
||||
|
||||
```bash
|
||||
# Write seed.json:
|
||||
ssh "speaker ready"@192.168.32.103 "cat > ~/seed.json" << 'EOF'
|
||||
{
|
||||
"event_device_id": "DEVICE_ID_FOR_THIS_LAPTOP",
|
||||
@@ -67,57 +103,13 @@ ssh "speaker ready"@192.168.32.103 "cat > ~/seed.json" << 'EOF'
|
||||
"onsite_api_base_url": null
|
||||
}
|
||||
EOF
|
||||
|
||||
# Verify:
|
||||
ssh "speaker ready"@192.168.32.103 "cat ~/seed.json"
|
||||
```
|
||||
|
||||
`event_device_id` values by laptop — see the **Device Reference** table below.
|
||||
|
||||
### Step 4 — Verify
|
||||
|
||||
```bash
|
||||
# Confirm seed.json landed correctly
|
||||
ssh "speaker ready"@192.168.32.103 "cat ~/seed.json"
|
||||
|
||||
# Confirm the .app is present
|
||||
ssh "speaker ready"@192.168.32.103 "ls /Applications/aether_launcher.app"
|
||||
```
|
||||
|
||||
Then launch the app on the laptop and confirm it connects and shows the correct device name
|
||||
in the Launcher UI.
|
||||
|
||||
### Updating seed.json only (no app reinstall)
|
||||
|
||||
If only the device config needs updating (e.g. changing `event_device_id` or `onsite_api_base_url`):
|
||||
|
||||
```bash
|
||||
ssh "speaker ready"@192.168.32.103 "cat > ~/seed.json" << 'EOF'
|
||||
{ ... }
|
||||
EOF
|
||||
```
|
||||
|
||||
No need to re-copy the `.app`. Restart the Electron app after writing the new seed.
|
||||
|
||||
### Deploy to multiple laptops at once
|
||||
|
||||
Repeat Steps 2–3 for each laptop, or use a loop:
|
||||
|
||||
```bash
|
||||
for OCTET in 103 104 105 106; do
|
||||
echo "=== Deploying to 192.168.32.$OCTET ==="
|
||||
scp -r builds/aether_launcher-darwin-x64/aether_launcher.app \
|
||||
"speaker ready"@192.168.32.$OCTET:/Applications/aether_launcher.app
|
||||
done
|
||||
```
|
||||
|
||||
`seed.json` must still be written per-device (each has a unique `event_device_id`).
|
||||
|
||||
### Adding SSH key to a new laptop (first time only)
|
||||
|
||||
```bash
|
||||
ssh-copy-id "speaker ready"@192.168.32.1XX
|
||||
```
|
||||
|
||||
Run once per laptop before attempting any of the deploy steps above.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Device Reference
|
||||
|
||||
201
deploy/deploy.sh
Executable file
201
deploy/deploy.sh
Executable file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy.sh — Deploy Aether Native Launcher to onsite Mac laptops
|
||||
#
|
||||
# USAGE:
|
||||
# ./deploy.sh <num> [num ...] Deploy to one or more laptops (e.g. 03 04 05)
|
||||
# ./deploy.sh all Deploy to all laptops in devices.conf
|
||||
# ./deploy.sh --seed-only <num> Update seed.json only — skip .app copy
|
||||
# ./deploy.sh --seed-only all
|
||||
#
|
||||
# REQUIRES:
|
||||
# event.env — copy from event.env.example and fill in AETHER_API_KEY
|
||||
# builds/ — run "npm run package:mac" first if .app is missing
|
||||
#
|
||||
# SSH keys must already be installed on each target (run ssh-copy-id once per laptop).
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEVICES_FILE="$SCRIPT_DIR/devices.conf"
|
||||
EVENT_ENV="$SCRIPT_DIR/event.env"
|
||||
SSH_USER="speaker ready"
|
||||
BUILD_DIR="$SCRIPT_DIR/../builds"
|
||||
|
||||
# ── Argument parsing ──────────────────────────────────────────────────────────
|
||||
|
||||
SEED_ONLY=false
|
||||
TARGETS=()
|
||||
|
||||
usage() {
|
||||
grep '^#' "$0" | grep -v '^#!/' | sed 's/^# \{0,1\}//'
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ $# -eq 0 ]]; then usage; fi
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--seed-only) SEED_ONLY=true ;;
|
||||
--help|-h) usage ;;
|
||||
all) TARGETS+=("all") ;;
|
||||
*) TARGETS+=("$1") ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [[ ${#TARGETS[@]} -eq 0 ]]; then
|
||||
echo "ERROR: No targets specified."
|
||||
usage
|
||||
fi
|
||||
|
||||
# ── Load event config ─────────────────────────────────────────────────────────
|
||||
|
||||
if [[ ! -f "$EVENT_ENV" ]]; then
|
||||
echo "ERROR: $EVENT_ENV not found."
|
||||
echo " Copy event.env.example → event.env and fill in AETHER_API_KEY."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "$EVENT_ENV"
|
||||
|
||||
: "${AETHER_API_KEY:?event.env must set AETHER_API_KEY}"
|
||||
: "${PRIMARY_API_BASE_URL:?event.env must set PRIMARY_API_BASE_URL}"
|
||||
: "${BACKUP_API_BASE_URL:?event.env must set BACKUP_API_BASE_URL}"
|
||||
|
||||
# Render onsite URL as JSON null or quoted string
|
||||
if [[ -n "${ONSITE_API_BASE_URL:-}" ]]; then
|
||||
ONSITE_JSON="\"$ONSITE_API_BASE_URL\""
|
||||
else
|
||||
ONSITE_JSON="null"
|
||||
fi
|
||||
|
||||
# ── Device list helpers ───────────────────────────────────────────────────────
|
||||
|
||||
# Returns "ip device_id" for a laptop number, or empty if not found
|
||||
lookup_device() {
|
||||
local num="$1"
|
||||
grep -E "^[[:space:]]*${num}[[:space:]]" "$DEVICES_FILE" \
|
||||
| grep -v '^[[:space:]]*#' \
|
||||
| awk '{print $2, $3}' \
|
||||
| head -1
|
||||
}
|
||||
|
||||
# Returns all laptop numbers from devices.conf (first column, non-comment lines)
|
||||
all_device_nums() {
|
||||
grep -v '^[[:space:]]*#' "$DEVICES_FILE" \
|
||||
| grep -v '^[[:space:]]*$' \
|
||||
| awk '{print $1}'
|
||||
}
|
||||
|
||||
# ── Deploy one laptop ─────────────────────────────────────────────────────────
|
||||
|
||||
deploy_laptop() {
|
||||
local num="$1"
|
||||
|
||||
local info
|
||||
info=$(lookup_device "$num")
|
||||
if [[ -z "$info" ]]; then
|
||||
echo " ERROR: Laptop $num not found in devices.conf"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local ip device_id
|
||||
ip=$(echo "$info" | awk '{print $1}')
|
||||
device_id=$(echo "$info" | awk '{print $2}')
|
||||
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════════"
|
||||
echo " Laptop $num · $ip · $device_id"
|
||||
echo "══════════════════════════════════════════════"
|
||||
|
||||
# ── Copy .app ──────────────────────────────────────────────────────────
|
||||
if [[ "$SEED_ONLY" != "true" ]]; then
|
||||
echo " Detecting architecture..."
|
||||
local arch
|
||||
arch=$(ssh "$SSH_USER@$ip" "uname -m" 2>/dev/null) || {
|
||||
echo " ERROR: SSH failed for $ip — is the laptop on the network?"
|
||||
return 1
|
||||
}
|
||||
|
||||
local bundle
|
||||
case "$arch" in
|
||||
x86_64) bundle="$BUILD_DIR/aether_launcher-darwin-x64/aether_launcher.app" ;;
|
||||
arm64) bundle="$BUILD_DIR/aether_launcher-darwin-arm64/aether_launcher.app" ;;
|
||||
*)
|
||||
echo " ERROR: Unknown arch '$arch' on $ip"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ! -d "$bundle" ]]; then
|
||||
echo " ERROR: Build not found: $bundle"
|
||||
echo " Run: npm run package:mac"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " Arch: $arch → copying $(basename "$bundle")..."
|
||||
scp -r "$bundle" "$SSH_USER@$ip:/Applications/aether_launcher.app" || {
|
||||
echo " ERROR: scp failed."
|
||||
return 1
|
||||
}
|
||||
echo " .app copied."
|
||||
else
|
||||
echo " (--seed-only: skipping .app copy)"
|
||||
fi
|
||||
|
||||
# ── Write seed.json ────────────────────────────────────────────────────
|
||||
echo " Writing seed.json..."
|
||||
ssh "$SSH_USER@$ip" "cat > ~/seed.json" <<EOF
|
||||
{
|
||||
"event_device_id": "$device_id",
|
||||
"aether_api_key": "$AETHER_API_KEY",
|
||||
"primary_api_base_url": "$PRIMARY_API_BASE_URL",
|
||||
"backup_api_base_url": "$BACKUP_API_BASE_URL",
|
||||
"onsite_api_base_url": $ONSITE_JSON
|
||||
}
|
||||
EOF
|
||||
|
||||
# ── Verify ─────────────────────────────────────────────────────────────
|
||||
echo " Verifying..."
|
||||
ssh "$SSH_USER@$ip" "cat ~/seed.json"
|
||||
if [[ "$SEED_ONLY" != "true" ]]; then
|
||||
ssh "$SSH_USER@$ip" \
|
||||
"test -d /Applications/aether_launcher.app && echo ' ✓ .app present' || echo ' ✗ .app NOT found'"
|
||||
fi
|
||||
|
||||
echo " ✓ Laptop $num done."
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Expand "all" target ───────────────────────────────────────────────────────
|
||||
|
||||
EXPANDED_TARGETS=()
|
||||
for t in "${TARGETS[@]}"; do
|
||||
if [[ "$t" == "all" ]]; then
|
||||
while IFS= read -r num; do
|
||||
EXPANDED_TARGETS+=("$num")
|
||||
done < <(all_device_nums)
|
||||
else
|
||||
EXPANDED_TARGETS+=("$t")
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Run deploys ───────────────────────────────────────────────────────────────
|
||||
|
||||
FAILED=()
|
||||
for num in "${EXPANDED_TARGETS[@]}"; do
|
||||
if ! deploy_laptop "$num"; then
|
||||
FAILED+=("$num")
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════════"
|
||||
if [[ ${#FAILED[@]} -eq 0 ]]; then
|
||||
echo " All done. ✓"
|
||||
else
|
||||
echo " FAILED: ${FAILED[*]}"
|
||||
echo " Re-run with just those numbers to retry."
|
||||
exit 1
|
||||
fi
|
||||
28
deploy/devices.conf
Normal file
28
deploy/devices.conf
Normal file
@@ -0,0 +1,28 @@
|
||||
# Aether Native Launcher — Device List
|
||||
# Fields: laptop_num ip_address event_device_id [notes]
|
||||
# Blank lines and lines starting with # are ignored.
|
||||
#
|
||||
# IP pattern: 192.168.32.1XX (XX = zero-padded laptop number)
|
||||
# SSH user: "speaker ready" on all laptops
|
||||
# Find/replace 192.168.32 for other venue network prefixes.
|
||||
#
|
||||
# num ip event_device_id notes
|
||||
01 192.168.32.101 tFLL1fLQfnk
|
||||
02 192.168.32.102 rpbfunVPEzw
|
||||
03 192.168.32.103 1EPfPX8kfw8
|
||||
04 192.168.32.104 zvgyLM5yieU
|
||||
05 192.168.32.105 QOc046GoeSc
|
||||
06 192.168.32.106 2o8j6eb0L6s
|
||||
07 192.168.32.107 Oa1tlxPEVSQ
|
||||
08 192.168.32.108 fY4yznpUZ48
|
||||
09 192.168.32.109 YlgGCyjo9bY
|
||||
10 192.168.32.110 GcTnFsp1mHI
|
||||
11 192.168.32.111 6z88m9oEZio
|
||||
12 192.168.32.112 EggJqL2kWkA
|
||||
13 192.168.32.113 O11eckHFdVE
|
||||
14 192.168.32.114 reI0SecUEhI
|
||||
15 192.168.32.115 crozxT8mA44
|
||||
16 192.168.32.116 0nP4VZsvr2Q
|
||||
17 192.168.32.117 Gm2gNqPGzLA
|
||||
19 192.168.32.119 6tpukvRVugU
|
||||
x20 192.168.32.120 rwLYnKUNd1M old 04, spare/retired
|
||||
15
deploy/event.env.example
Normal file
15
deploy/event.env.example
Normal file
@@ -0,0 +1,15 @@
|
||||
# event.env — Per-event deployment config for deploy.sh
|
||||
# Copy this file to event.env and fill in the values before deploying.
|
||||
# event.env is gitignored — never commit it (contains the API key).
|
||||
#
|
||||
# AETHER_API_KEY: shared across all laptops for this event deployment.
|
||||
# Create in Aether admin (Core → Accounts or Events → Devices API key section)
|
||||
# before the show. Delete after the show.
|
||||
#
|
||||
# ONSITE_API_BASE_URL: set to the local onsite API if running one (e.g.
|
||||
# http://192.168.32.1/api). Leave blank to use null (cloud-only mode).
|
||||
|
||||
AETHER_API_KEY="your_api_key_here"
|
||||
PRIMARY_API_BASE_URL="https://api.oneskyit.com"
|
||||
BACKUP_API_BASE_URL="https://bak-api.oneskyit.com"
|
||||
ONSITE_API_BASE_URL=""
|
||||
Reference in New Issue
Block a user