- Add set_display_layout (+ other missing system handler methods) to AetherNativeBridge interface in types.ts - README: clarify configStr source (event_device.data_json) and no-op behaviour when absent - TODO_AGENTS: correct 7z→bsdtar throughout; mark Electron-side set_display_layout items done; remove completed bsdtar doc item Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
328 lines
15 KiB
Markdown
328 lines
15 KiB
Markdown
# Aether Native Launcher (Electron)
|
|
|
|
The Aether Native Launcher is a specialized Electron-based shell for the Aether Platform. It provides a secure bridge between the SvelteKit web UI and the local operating system, enabling features restricted by browser sandboxing.
|
|
|
|
## 🚀 Overview
|
|
|
|
This application serves as the "Native Mode" runtime for Aether podiums and devices. It handles:
|
|
- **Local File Orchestration:** Managed cache for presentation files (PPTx, Keynote, PDF).
|
|
- **Automation:** Specialized AppleScript handlers for PowerPoint and Keynote.
|
|
- **Hardware Telemetry:** Direct access to CPU, RAM, and Network interface data.
|
|
- **Remote Control:** Slide navigation and application control via WebSocket intents.
|
|
|
|
## 🖥️ Onsite Deployment
|
|
|
|
**Current hardware:** MacBook Air 2018 — Intel x64. All current deployments use `aether_launcher-darwin-x64`.
|
|
**Future hardware:** Apple Silicon Macs use `aether_launcher-darwin-arm64`. Windows and Linux builds are planned.
|
|
|
|
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
|
|
cd ~/OSIT_dev/aether_app_native_electron
|
|
npm run package:mac
|
|
# Produces builds/aether_launcher-darwin-x64/aether_launcher.app ← the one to deploy
|
|
```
|
|
|
|
Only rebuild if source code has changed. The `.app` bundle is identical for all Intel laptops —
|
|
only `~/seed.json` differs per device.
|
|
|
|
> **Note:** The build will print `WARNING: Could not find icon "..." with extension ".icon"`. This
|
|
> is cosmetic — `electron-packager` checks for several icon format variants and warns for each it
|
|
> doesn't find. The `.icns` file is correctly embedded as `electron.icns` inside the app bundle.
|
|
|
|
### Step 2 — Create event.env
|
|
|
|
```bash
|
|
cp deploy/event.env.example deploy/event.env
|
|
# Edit deploy/event.env — fill in AETHER_API_KEY
|
|
```
|
|
|
|
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
|
|
# 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
|
|
|
|
# Write seed.json:
|
|
ssh "speaker ready"@192.168.32.103 "cat > ~/seed.json" << 'EOF'
|
|
{
|
|
"event_device_id": "DEVICE_ID_FOR_THIS_LAPTOP",
|
|
"aether_api_key": "YOUR_API_KEY",
|
|
"primary_api_base_url": "https://api.oneskyit.com",
|
|
"backup_api_base_url": "https://bak-api.oneskyit.com",
|
|
"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.
|
|
|
|
---
|
|
|
|
## 📋 Device Reference
|
|
|
|
| Laptop | IP Address | 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 | (no laptop 18) |
|
|
| x20 | 192.168.32.120 | rwLYnKUNd1M | old 04, spare/retired |
|
|
|
|
`aether_api_key`: all laptops share a single key per event deployment. The key is created in
|
|
the Aether admin panel before the show and deleted after. Check `builds/seed.json` for the
|
|
current key, or create a new one in Aether (Core → Accounts or Events → Devices API key section)
|
|
before each deployment.
|
|
|
|
---
|
|
|
|
## ⚙️ Configuration
|
|
|
|
The application requires a `seed.json` file to identify the device and connect to the Aether API.
|
|
|
|
### 1. Seed Configuration
|
|
|
|
**Location: `~/seed.json`** (user's home directory — external to the app bundle by design)
|
|
|
|
This file is intentionally kept outside the application bundle so it can be edited per-device
|
|
without re-signing or repackaging the app. On macOS this is `/Users/speaker ready/seed.json`.
|
|
|
|
Seed file format:
|
|
```json
|
|
{
|
|
"event_device_id": "tFLL1fLQfnk",
|
|
"aether_api_key": "YOUR_API_KEY",
|
|
"primary_api_base_url": "https://api.oneskyit.com",
|
|
"backup_api_base_url": "https://bak-api.oneskyit.com",
|
|
"onsite_api_base_url": null
|
|
}
|
|
```
|
|
|
|
`event_device_id` is the `id_random` from the Aether `event_device` record for that physical
|
|
laptop — see the Device Reference table above. `aether_api_key` is a shared key created per
|
|
event deployment and deleted after the show.
|
|
|
|
### 2. Development Setup
|
|
```bash
|
|
npm install
|
|
npm start # Compiles TypeScript (tsc) then launches Electron
|
|
```
|
|
|
|
### 3. File Cache Layout
|
|
|
|
Presentation files are cached locally under `hash_prefix_length`-char subdirectories (default: 2):
|
|
```text
|
|
[local_file_cache_path]/
|
|
4a/
|
|
4a228ef8ac1a...sha256hash...file
|
|
1d/
|
|
1d720916a831...sha256hash...file
|
|
```
|
|
|
|
**Important:** `hash_prefix_length` must be consistent. If it changes, files in old directories
|
|
become orphaned and will be re-downloaded. The default is `2` and should not be changed unless
|
|
explicitly coordinated across all devices.
|
|
|
|
## 🌉 The Native Bridge (`aetherNative`)
|
|
|
|
The bridge is exposed to the renderer via `contextBridge`. It can be accessed in the web UI via `window.aetherNative`.
|
|
|
|
**Design principle:** The Electron app is a thin OS primitive layer. Business logic (which script
|
|
runs for which file type, how to sequence operations, etc.) belongs in the SvelteKit/Svelte side
|
|
where it can be changed without a rebuild and redeployment. Electron handlers should rarely need
|
|
to change.
|
|
|
|
### File Cache
|
|
|
|
| Method | Description |
|
|
| --- | --- |
|
|
| `check_cache({cache_root, hash, hash_prefix_length?, verify_hash?})` | Checks if a file exists in the hashed cache. `verify_hash: true` re-hashes the file to confirm integrity. |
|
|
| `download_to_cache({url, cache_root, hash, api_key, account_id, hash_prefix_length?})` | Streams a file from the API into the hashed cache. Verifies SHA-256 integrity before finalizing. |
|
|
| `copy_from_cache_to_temp({cache_root, hash, temp_root, filename, hash_prefix_length?})` | **Preferred primitive.** Copies cached file to temp dir with original filename. Returns `{ success, path }`. The Svelte caller decides what to do next. |
|
|
| `launch_from_cache({cache_root, hash, temp_root, filename, hash_prefix_length?, script_template?})` | Combines copy + launch. If `script_template` is provided, runs it (AppleScript or `shell:` prefixed command) instead of hardcoded extension logic. Falls back to built-in defaults when `null`. |
|
|
|
|
### Shell & OS
|
|
|
|
| Method | Description |
|
|
| --- | --- |
|
|
| `run_cmd({cmd, timeout?, return_stdout?})` | Async shell command execution. |
|
|
| `run_cmd_sync({cmd})` | Synchronous shell command execution. |
|
|
| `run_osascript(script)` | **Hardened.** Runs AppleScript via temp `.scpt` file — handles multi-line scripts and paths with spaces/special characters correctly. macOS only. |
|
|
| `open_folder(path)` | Opens a directory in Finder / system file manager. |
|
|
| `open_local_file_v2(path)` | Opens a file with its default OS application. |
|
|
| `open_external({url, app?})` | Opens a URL in Chrome, Firefox, or the default browser. |
|
|
| `kill_processes({process_name_li})` | Terminates processes by name. macOS/Linux: `pkill -f`. Windows: `taskkill /F`. |
|
|
|
|
### Presentations (Phase 5 — legacy specialized handlers)
|
|
|
|
| Method | Description |
|
|
| --- | --- |
|
|
| `launch_presentation({path, app?, os?})` | Platform-aware launcher. Resolves `[home]`/`[tmp]` placeholders. **Note:** uses legacy `-e` flag for AppleScript; prefer `copy_from_cache_to_temp` + `run_osascript` for new flows. |
|
|
| `control_presentation({app, action})` | Slide navigation (`next`/`prev`/`start`/`stop`) for PowerPoint or Keynote via AppleScript. macOS only. |
|
|
|
|
### System Management (Phase 5)
|
|
|
|
| Method | Description |
|
|
| --- | --- |
|
|
| `get_device_config()` | Returns hydrated device config injected at startup from `seed.json` + API. |
|
|
| `get_device_info()` | Returns OS metadata: platform, hostname, IPs, CPU count, free RAM, home/tmp paths. |
|
|
| `window_control({action, value?})` | Electron window: maximize, minimize, restore, close, fullscreen, kiosk, devtools, reload. |
|
|
| `set_wallpaper({path})` | Sets desktop wallpaper. macOS (AppleScript) + Linux (gsettings/Gnome). |
|
|
| `power_control({action})` | Shutdown, reboot, or sleep. macOS + Linux. Requires sudo for shutdown/reboot. |
|
|
| `set_display_layout({mode, configStr?})` | Mirror/extend displays via bundled `displayplacer` binary. macOS only. `configStr` is the output of `displayplacer list` for that machine, stored in `event_device.data_json.displayplacer_config_mirror` / `displayplacer_config_extend`. Required — silently no-ops without it. |
|
|
| `manage_recording({action, options?})` | Screen recording via bundled `aperture` binary. macOS only. |
|
|
| `update_app(args)` | **Stub.** Downloads update package but does not install. Not functional. |
|
|
| `list_tools()` | Returns a self-describing manifest of all available bridge functions. |
|
|
|
|
### Example Usage (preferred composable pattern)
|
|
|
|
```typescript
|
|
import * as native from '$lib/electron/electron_relay';
|
|
|
|
// Step 1: copy the cached file to temp and get the resolved path
|
|
const copy = await native.copy_from_cache_to_temp({
|
|
cache_root: $ae_loc.local_file_cache_path,
|
|
hash: event_file_obj.hash_sha256,
|
|
temp_root: $ae_loc.host_file_temp_path,
|
|
filename: event_file_obj.filename
|
|
});
|
|
if (!copy.success) { /* handle error */ return; }
|
|
|
|
// Step 2: run whatever script/command you want with that path
|
|
// Option A — AppleScript (macOS):
|
|
await native.run_osascript(`
|
|
tell application "Microsoft PowerPoint"
|
|
activate
|
|
open (POSIX file "${copy.path}")
|
|
delay 3
|
|
end tell
|
|
`);
|
|
|
|
// Option B — shell command:
|
|
await native.run_cmd({ cmd: `open "${copy.path}"` });
|
|
|
|
// Option C — use a template from device config (data-driven, no rebuild needed):
|
|
const template = $ae_loc.native_device?.launch_scripts?.pptx;
|
|
if (template) {
|
|
const script = template.replace(/\{\{path\}\}/g, copy.path);
|
|
await native.run_osascript(script);
|
|
}
|
|
```
|
|
|
|
### Configurable Launch Scripts (no rebuild needed)
|
|
|
|
`launch_from_cache` and `launcher_file_cont.svelte` support per-extension script templates
|
|
stored in `event_device.data_json.launch_scripts`. Keys are lowercase extensions (`pptx`, `key`,
|
|
`pdf`, etc.); `default` is a catch-all. Templates use `{{path}}` as the file path placeholder.
|
|
AppleScript strings run via `run_osascript`; prefix with `shell:` for shell commands.
|
|
|
|
See `documentation/PROJECT__AE_Events_Launcher_Native_integration.md` Section 8 for full details.
|
|
|
|
## 🛠️ Development
|
|
|
|
- **Preload:** Logic defined in `src/preload/index.ts`.
|
|
- **Handlers:** OS-level logic in `src/main/shell_handlers.ts`, `src/main/file_handlers.ts`, and `src/main/system_handlers.ts`.
|
|
- **Types:** Shared TypeScript interfaces in `src/shared/types.ts`.
|
|
|
|
### Build Requirements (System Dependencies)
|
|
|
|
`bsdtar` (libarchive) must be present on the build host. It is used by the `postinstall` patch
|
|
to work around a hard hang in `@electron/packager` 20 on Node 26.
|
|
|
|
| OS | Install |
|
|
| --- | --- |
|
|
| Arch Linux | `sudo pacman -S libarchive` |
|
|
| macOS | `brew install libarchive` (or use the system bsdtar included in Xcode CLT) |
|
|
| Ubuntu/Debian | `sudo apt install libarchive-tools` |
|
|
|
|
### Why bsdtar — Node 26 Packaging Bug
|
|
|
|
**Symptom:** `npm run package:mac` or `npm run package:linux` prints
|
|
`Packaging app for platform ... using electron v42.x` then hangs forever (or exits 0 with no output).
|
|
|
|
**Root cause:** `yauzl` 2.10.0 (used by `extract-zip` inside `@electron/packager` 20) creates
|
|
read streams that never emit `data` events under Node 26. The zip extraction call blocks
|
|
indefinitely.
|
|
|
|
**Fix:** `scripts/patch-packager-unzip.js` (run automatically via `postinstall`) replaces the
|
|
`extractElectronZip` function in `node_modules/@electron/packager/dist/unzip.js` with a
|
|
`child_process.execSync` call to `bsdtar -xf`. `bsdtar` was chosen over `7z` because `7z`
|
|
refuses to extract macOS `.app` bundles that contain symlinks chained through other symlinks
|
|
(e.g. `Electron Framework.framework/Libraries → Versions/Current/Libraries`, where
|
|
`Versions/Current` is itself a symlink).
|
|
|
|
**This patch is re-applied automatically on every `npm install`** via the `postinstall` hook.
|
|
If a future release of `@electron/packager` or `extract-zip` fixes Node 26 compatibility,
|
|
remove the `postinstall` line from `package.json` and delete `scripts/patch-packager-unzip.js`.
|