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

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