149 lines
12 KiB
Markdown
149 lines
12 KiB
Markdown
# Native App Agent Task List
|
|
> Use this file to track steps for complex features or bug fixes.
|
|
> **Status:** Stable - ongoing development.
|
|
|
|
## Current Investigation
|
|
- This started as an API contract review for the native Electron bootstrap path and expanded into a packaging/runtime issue after the deploy step stopped producing bundles.
|
|
- We now know the API side was not the root cause. The bootstrap request shape in `src/main/api_client.ts` was wrong and has been corrected.
|
|
- The packaging blocker has been diagnosed and fixed (see below).
|
|
|
|
## Launcher Terminology Cleanup
|
|
- Align the native docs/comments with the Svelte-side terminology: **Launch Profile** = the
|
|
Svelte config object keyed by extension; **Native Template** = the AppleScript or shell
|
|
string Electron actually executes after the file lands in temp.
|
|
- Update `src/main/file_handlers.ts` comments and any bridge-facing wording so they describe the
|
|
resolved `native_template` string accurately. `launch_profiles` is only the Svelte-side map;
|
|
the IPC payload is the single executable string. Do not reintroduce `launch_scripts` as the
|
|
public term.
|
|
- Keep the source of truth in Svelte. Electron should remain a thin executor/copy layer.
|
|
- After source/docs updates, rebuild/regenerate `dist/main/file_handlers.js` from source; do not
|
|
hand-edit the generated file.
|
|
- If any parameter names remain awkward in source, preserve runtime behavior first and rename
|
|
only when the signature ripple is understood.
|
|
|
|
## What Was Fixed
|
|
- Updated the native bootstrap flow to use the direct `site_domain/search` request body expected by API V3.
|
|
- Standardized the account-bypass header to `x-no-account-id: bypass` where that narrow bypass is intended.
|
|
- Removed a redundant `x-no-account-id` header from file download calls.
|
|
- Rewrote the device lookup smoke test so it validates the real two-step bootstrap path end to end.
|
|
- Upgraded Electron from 34.x to 42.0.1.
|
|
- Replaced deprecated `electron-packager` with `@electron/packager` 20.0.0.
|
|
- Added a `package:linux` smoke test path so packaging failures can be isolated from macOS-specific behavior.
|
|
- **Fixed packaging hang on Node 26:** `yauzl` 2.10.0 (used by `extract-zip` in `@electron/packager`) emits no `data` events on Node 26 streams, causing zip extraction to hang indefinitely. Fix: patched `@electron/packager/dist/unzip.js` to use `bsdtar` (libarchive) instead of `extract-zip`. `bsdtar` was chosen over `7z` because `7z` refuses macOS `.app` bundles with chained symlinks inside framework bundles. Patch is re-applied on every `npm install` via the `postinstall` script at `scripts/patch-packager-unzip.js`.
|
|
|
|
## Verified So Far
|
|
- `npm run dev` works once the Electron binary is present locally.
|
|
- Manual Electron cache extraction restored a runnable checkout on this machine.
|
|
- API validation confirmed the backend responds correctly for:
|
|
- `event_device/{id}` lookup
|
|
- `site_domain/search?limit=1` with the direct `SearchQuery` body
|
|
- The returned `site_domain.account_id` matches the device account context in the verified bootstrap flow.
|
|
- The SvelteKit frontend bootstrap path already follows the correct API contract and does not need the same fix.
|
|
- **`npm run package:linux` now produces `builds/aether_launcher-linux-x64/`** with a complete bundle (confirmed 2026-05-11).
|
|
- **`npm run package:mac` now produces `builds/aether_launcher-darwin-x64/` and `builds/aether_launcher-darwin-arm64/`** with `aether_launcher.app` inside each (confirmed 2026-05-11). Initial fix used `7z` but it refused chained symlinks inside macOS framework bundles; switched to `bsdtar` (libarchive) which handles both Linux and macOS zips correctly.
|
|
- `deploy/deploy.sh` output directory names (`aether_launcher-darwin-x64`, `aether_launcher-darwin-arm64`) match packager output — no script changes needed.
|
|
|
|
## Remaining Items
|
|
1. Test that the packaged Linux binary runs end-to-end against the dev API.
|
|
|
|
## Root Cause Summary (Packaging Hang)
|
|
- **Tool chain:** Node 26.1.0 + `@electron/packager` 20.0.0 + `extract-zip` 2.0.1 + `yauzl` 2.10.0
|
|
- **Symptom:** `npm run package:linux` exits 0 but produces no output. Debug log shows it starts extraction but never finishes.
|
|
- **Root cause:** `yauzl` opens a read stream for the first zip entry, but on Node 26, no `data` events are ever emitted on that stream. The `pipeline(readStream, writeStream)` call in `extract-zip` blocks forever.
|
|
- **Fix:** Replace the one-liner `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 macOS `.app` bundles with chained symlinks (e.g. `Electron Framework.framework/Libraries → Versions/Current/Libraries`). A `postinstall` npm script re-applies this patch after each `npm install`.
|
|
- **Build-time dependency:** `libarchive` (provides `bsdtar`) must be installed on the build host. On Arch: `pacman -S libarchive`; macOS: included in Xcode CLT or `brew install libarchive`; Ubuntu/Debian: `apt install libarchive-tools`.
|
|
|
|
## References
|
|
- Electron 42 release notes: https://www.electronjs.org/blog/electron-42-0
|
|
- Related Electron packaging discussion: https://github.com/aaddrick/claude-desktop-debian/pull/587
|
|
- Electron packaging/runtime change reference: https://github.com/electron/electron/pull/49328
|
|
- yauzl Node 26 stream issue: `yauzl` 2.10.0 uses legacy Node streams (streams1 style); Node 26 changed stream internal behavior so `openReadStream` returns a stream that never emits `data` without a proper pipeline consumer.
|
|
|
|
## Notes
|
|
- Was on Electron 34.
|
|
- The problem is not the backend API keys or the frontend site bootstrap flow.
|
|
- The packaging fix is a node_modules patch, not upstream. If `@electron/packager` or `extract-zip` releases a Node 26-compatible version, the `postinstall` script should be removed.
|
|
|
|
---
|
|
|
|
## set_display_layout — Setup & Status (updated 2026-05-20)
|
|
|
|
**Primary approach: `display_control` (native CoreGraphics — no Homebrew required)**
|
|
- Source: `scripts/display_control.m` (derived from OSIT MasterKey app, Ian Kohl 2019)
|
|
- Build: run `scripts/build-display-control.sh` on a Mac (requires Xcode CLT only)
|
|
- Output: `resources/bin/display_control` — commit this binary to the repo
|
|
- Uses `CGConfigureDisplayMirrorOfDisplay` — same CoreGraphics API macOS uses internally
|
|
- Supports 3+ displays; auto-detects all connected displays; no config string needed
|
|
|
|
**Fallback approach: `displayplacer` (requires `brew install displayplacer` on each venue Mac)**
|
|
- Reference: [jakehilborn/displayplacer](https://github.com/jakehilborn/displayplacer)
|
|
- Still used when `display_control` binary is not present
|
|
- Also used for per-device `configStr` overrides (displayplacer-format strings in `event_device.data_json`)
|
|
|
|
**Current state (2026-05-20):**
|
|
|
|
- ✅ Correct `mirror_of_display:<uuid>` syntax used in displayplacer fallback
|
|
- ✅ Failures logged to Electron console (`[Launcher] set_display_layout:`)
|
|
- ✅ Display Mode toggle in Launcher config (Native OS section) — always visible
|
|
- ✅ `display_control` binary built (universal x86_64 + arm64), committed to repo
|
|
- ✅ **Idempotency** — `mirror` and `extend` both no-op with a clean message if already in the requested state (no display flicker)
|
|
- ✅ **`list-modes`** — JSON array of all online displays + every usable `CGDisplayMode` (width, height, refresh, pixel size, HiDPI flag, is_current)
|
|
- ✅ **`set-mode`** — sets resolution/refresh via `CGConfigureDisplayWithDisplayMode`; supports `--refresh`, `--hidpi`, `--no-hidpi`; auto-prefers HiDPI on built-in, non-HiDPI on externals
|
|
- ✅ IPC handlers `native:list-display-modes` + `native:set-display-mode` wired through full bridge stack (system_handlers → preload → types → electron_relay)
|
|
- ✅ Remote build script (`scripts/remote-build-display-control.sh`) — compiles on laptop-01 via SSH from Linux workstation; uses `ssh cat` pipe pattern (avoids scp space-in-username bug)
|
|
|
|
**To rebuild `display_control` after source changes:**
|
|
```bash
|
|
# From repo root on workstation (laptop-01 must be reachable):
|
|
./scripts/remote-build-display-control.sh
|
|
|
|
# Or directly on a Mac:
|
|
./scripts/build-display-control.sh
|
|
|
|
# Test with a second display connected:
|
|
./resources/bin/display_control status
|
|
./resources/bin/display_control extend
|
|
./resources/bin/display_control mirror
|
|
./resources/bin/display_control list-modes
|
|
./resources/bin/display_control set-mode 0 1920 1080
|
|
./resources/bin/display_control set-mode 1 1920 1080 --refresh 60 --no-hidpi
|
|
|
|
# Commit:
|
|
git add resources/bin/display_control
|
|
git commit -m "build: update display_control binary (universal)"
|
|
```
|
|
|
|
**Optional per-device override (displayplacer format, for edge cases):**
|
|
For rooms where auto-detection produces the wrong result, store the raw configStr in `event_device.data_json`:
|
|
```json
|
|
{
|
|
"displayplacer_config_extend": "<output of displayplacer list in extended layout>",
|
|
"displayplacer_config_mirror": "<output of displayplacer list in mirrored layout>"
|
|
}
|
|
```
|
|
`configStr` is passed from the Svelte call site and uses the displayplacer fallback path directly.
|
|
|
|
---
|
|
|
|
## Future Ideas
|
|
|
|
Capabilities worth adding as the Launcher matures. Roughly ordered by venue-day impact.
|
|
|
|
### 1. Display reconfiguration events (push IPC)
|
|
`CGDisplayRegisterReconfigurationCallback` fires when a display is connected or removed. Wrapping this in a `webContents.send('native:display-changed', payload)` push event would let the Svelte UI auto-mirror the moment a projector cable lands — eliminating the most common operator action during show setup. Currently the UI must poll `status` or the operator presses Mirror manually.
|
|
|
|
### 2. Audio output routing
|
|
When mirroring to a projector the audio output should follow. CoreAudio (`AudioObjectSetPropertyData` on `kAudioHardwarePropertyDefaultOutputDevice`) can switch the default output device programmatically. Candidate bridge method: `set_audio_output({device_name?, prefer_hdmi?})`. Could be called automatically as part of the mirror flow, or exposed as a standalone control.
|
|
|
|
### 3. Battery / power status in telemetry
|
|
`get_device_info` returns CPU and RAM but nothing about power. On venue MacBook Airs this matters operationally. IOKit (`IOPSCopyPowerSourcesInfo` / `IOPSGetPowerSourceDescription`) can surface: charge %, is-charging, time-remaining, health. Low-cost addition to the existing telemetry handler.
|
|
|
|
### 4. Presentation state feedback
|
|
`control_presentation` is fire-and-forget. AppleScript can query the current slide index and total slide count from both PowerPoint (`current slide index of active presentation`) and Keynote (`slide number of current slide of front document`). A `get_presentation_state()` bridge method returning `{ app, slide, total, presenting }` would let the Launcher UI show "Slide 7 of 42" — useful for operators monitoring multiple rooms.
|
|
|
|
### 5. Push event channel (IPC renderer notifications)
|
|
All bridge calls are currently request-response. Adding a `webContents.send` channel for unsolicited Electron → renderer events would unlock: display plug/unplug (#1 above), file download progress, network state changes, "presentation ended" detection. A thin `ipcMain.on('native:subscribe', ...)` registration pattern on the Electron side and a corresponding `ipcRenderer.on` listener in the preload would cover all use cases without breaking the existing handler structure.
|
|
|
|
### 6. Kiosk / accidental-quit hardening
|
|
A speaker or operator can accidentally Cmd+Q the launcher mid-presentation. `app.on('before-quit')` with either a confirmation dialog or an API-controlled lock flag (`event_device.data_json.kiosk_locked: true`) would prevent this. Can be toggled remotely — lock before the show, unlock after.
|