# 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. | | `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`.