Files
OSIT-AE-App-Native-Electron/documentation/TODO_AGENTS.md

12 KiB

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

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
  • 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
  • Idempotencymirror 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:

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

{
  "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.