From 76569a872fda74ca6d2e503dcf5af9721e13cf83 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Wed, 20 May 2026 16:33:33 -0400 Subject: [PATCH] feat(launcher): add display mode toggle; fix silent display layout failures - Add Extend/Mirror toggle to Native OS config section (always visible, no Technical Mode required). Default: Extend. State updates on success. - Replace .catch(() => {}) swallowing with console.warn logging on both set_display_layout call sites so failures appear in the Electron console - Remove old edit-mode-only Extend button (replaced by new toggle) - Update PROJECT doc: displayplacer install note, binary lookup order, GitHub link - Clean up TODO__Agents.md: resolve stale items, add new low-priority Electron items Co-Authored-By: Claude Sonnet 4.6 --- ...__AE_Events_Launcher_Native_integration.md | 4 +- documentation/TODO__Agents.md | 89 ++++--------------- .../launcher_cfg_native_os.svelte | 54 ++++++++--- .../(launcher)/launcher_file_cont.svelte | 10 +-- 4 files changed, 66 insertions(+), 91 deletions(-) diff --git a/documentation/PROJECT__AE_Events_Launcher_Native_integration.md b/documentation/PROJECT__AE_Events_Launcher_Native_integration.md index 693d46d1..fcb34a09 100644 --- a/documentation/PROJECT__AE_Events_Launcher_Native_integration.md +++ b/documentation/PROJECT__AE_Events_Launcher_Native_integration.md @@ -104,7 +104,7 @@ The native shell provides specialized handlers for controlling the "Podium Exper ### 5.3 Implemented Actuators (Phase 5 Complete) - **Recording:** `manage_recording({action})` — Aperture session capture (`start`, `stop`, `status`). macOS only. -- **Display Layouts:** `set_display_layout({mode, configStr?})` — Mirror / Extend via `displayplacer`. macOS only. Auto-detects displays; `configStr` is an optional manual override. +- **Display Layouts:** `set_display_layout({mode, configStr?})` — Mirror / Extend via [`displayplacer`](https://github.com/jakehilborn/displayplacer). macOS only. Auto-detects connected displays via `displayplacer list`; `configStr` is an optional per-device manual override. Requires `brew install displayplacer` on each venue Mac. Failures are logged to the Electron console but do not block file open. A **Display Mode** toggle (Extend / Mirror) is available in the Launcher config — Native OS section, visible without Technical Mode. - **Power Control:** `power_control({action})` — Shutdown, reboot, sleep. macOS + Linux. - **Window Control:** `window_control({action})` — Maximize, minimize, fullscreen, kiosk mode. - **Wallpaper:** `set_wallpaper({path?, url?, url_external?, display?, api_key?, account_id?})` — Downloads from URL (cached locally) or applies a local path. Per-display targeting (`'all'`/`'primary'`/`'external'`). macOS only in production; Linux returns a dev-mode preview payload. @@ -171,7 +171,7 @@ no-op when `window.aetherNative` is not present (i.e., in browser/non-native mod ### System Management (Phase 5) - `set_wallpaper({path?, url?, url_external?, display?, api_key?, account_id?})` — Sets desktop wallpaper. Downloads from `url` (cached to `~/Library/Caches/OSIT/wallpaper/`) or applies a local `path`. `url_external` targets the projector/second display separately. macOS only in production; Linux returns a dev-mode preview payload without applying. - `window_control({action, value?})` — Electron window management: maximize, minimize, fullscreen, kiosk. -- `set_display_layout({mode, configStr?})` — Mirror or extend displays via displayplacer. macOS only. Auto-detects connected displays; `configStr` is an optional manual override. +- `set_display_layout({mode, configStr?})` — Mirror or extend displays via [`displayplacer`](https://github.com/jakehilborn/displayplacer). macOS only. Auto-detects via `displayplacer list`; `configStr` overrides auto-detection when set. Binary lookup order: bundled `resources/bin/displayplacer` → `/opt/homebrew/bin/` (Apple Silicon) → `/usr/local/bin/` (Intel). Requires `brew install displayplacer` on each venue Mac if not bundled. - `power_control({action})` — Shutdown, reboot, or sleep the host machine. macOS + Linux. - `manage_recording({action, options?})` — Aperture capture control (`start`/`stop`/`status`). macOS only. - `open_external({url, app?})` — Opens a URL in Chrome, Firefox, or the default browser. diff --git a/documentation/TODO__Agents.md b/documentation/TODO__Agents.md index 6f18ec5a..025aa83d 100644 --- a/documentation/TODO__Agents.md +++ b/documentation/TODO__Agents.md @@ -23,6 +23,7 @@ guessing defaults. - [x] Built-in Launcher defaults refactored into canonical profile names plus extension aliases (`ae_launcher__default_launch_profiles.ts`) - [x] Device-level Launch Timing section added under Launcher Configuration → Device, with per-profile `launch_profiles[profile].post_delay_ms` overrides - [x] **URL file launch support (2026-05-13)** — `event_file.extension = 'URL'` (or filename starting with `http://`/`https://`) is treated as a non-downloaded URL. Background sync skips URL files so they are never treated as cacheable hosted files. Shared `AE_Comp_Hosted_Files_Download_Button` now hard-bypasses the download path for URL records. In native mode, `handle_open_file()` routes to `native.open_external({ url, app: 'chrome' })` with `'default'` fallback. Health section crash fixed (guarded `native_device` against undefined in preview/edit mode). +- [x] **[Launcher/Electron] Display mirroring auto-detection (2026-05-20)** — `native:set-display-layout` rewrote to auto-detect displays via `displayplacer list` when no `configStr` provided. Old code silently returned `{ success: false }` (swallowed by `.catch(() => {})` in relay). Now parses `displayplacer list` output, extracts quoted display strings, builds correct mirror/extend commands. Manual `configStr` still takes priority when provided. **Svelte-side migration — remaining before May 26:** - [x] **[Launcher] Built-in Svelte default profiles (2026-05-11)** — canonical profile constants live in `ae_launcher__default_launch_profiles.ts` with extension aliases and a `resolve_launch_profile()` 3-step fallback (device config → event config → built-in defaults). Covers macOS (`pptx`, `ppt`, `key`, `odp`, `pdf`), media (`mp4`, `mkv`, `mp3`, etc.), Windows/Parallels variants, and URL path. @@ -73,6 +74,18 @@ guessing defaults. (`aether_app_native_electron`), append a timestamp or random suffix to the temp filename on every download so macOS always sees a new path (e.g. `wallpaper_1748123456.jpg` instead of `wallpaper.jpg`). +- [ ] **[Launcher/Electron] `run_cmd`/`run_cmd_sync` — phantom `return_stdout` param (low priority)** — + `electron_relay.ts` passes `return_stdout` in the args object, but both IPC handlers ignore it + (stdout is always returned). Effectively a no-op, but creates a misleading API contract. Fix: + remove the param from relay calls, or honor it in the handlers. No behavioral impact currently. +- [ ] **[Launcher/Electron] `power_control` — sudo requirement not surfaced in UI (low priority)** — + `shutdown` and `reboot` require `sudo` on Linux. macOS works without it; Launcher only targets + macOS so this is a non-issue in production. If Linux podiums are ever deployed, `power_control` + shutdown/reboot will fail silently with a permissions error. No fix needed before CMSC. +- [ ] **[Launcher/Electron] `manage_recording` — aperture binary path on packaged builds** — + Handler resolves the aperture binary via a dev-relative path. Needs verification that the path + resolves correctly inside a packaged `.app` bundle (`app.asar` / `resources/`). Not blocking + for CMSC (recording not part of CMSC workflow). Verify on next packaged build test. --- @@ -127,57 +140,12 @@ below. The TTL + `verify_in_flight` guards are the current mitigation. --- -### [IDAA] Server-side Novi verification — 503 not auto-retried (regression, 2026-05-19) +### ~~[IDAA] Server-side Novi verification — 503 not auto-retried~~ ✅ Fixed (2026-05-20) -**File:** `src/routes/idaa/(idaa)/+layout.svelte` → `verify_novi_uuid()` - -**Bug:** After migrating to the Aether server-side proxy, a `503` response (Novi unreachable / -Novi 5xx) skips the automatic retry that exists for network-level errors. - -**Root cause:** The auto-retry branch only fires for `TypeError` and `AbortError`: -```ts -const is_network_or_timeout = - error instanceof TypeError || - (error instanceof Error && error.name === 'AbortError'); -if (is_network_or_timeout && !is_retry) { - // 3s wait → retry -} -``` - -In the old client-to-Novi flow, an unreachable Novi server produced a `TypeError: Failed to -fetch` — caught above, auto-retried. In the new flow the Aether server returns a clean HTTP -`503`, which hits `!response.ok` → `throw new Error(...)` — a plain `Error`, not `TypeError`. -`is_network_or_timeout` is `false` → no retry → `verify_failed_for_uuid` latch is set -immediately → user sees "Verification Unavailable" requiring a manual "Try Again" click for -what may be a 2-second Novi hiccup. - -**Fix:** Add a `503` check before the generic `!response.ok` throw (or after), applying the -same 3-second wait + one retry that network errors get: -```ts -if (response.status === 503) { - if (is_retry) { - throw new Error(`Novi verification: Novi unreachable (503) — retry also failed`); - } - console.warn(`IDAA Layout: Novi unreachable (503) for ${uuid}. Retrying in 3s...`); - verifying_status_msg = 'Connection issue — retrying...'; - await new Promise((resolve) => setTimeout(resolve, 3_000)); - await verify_novi_uuid(uuid, true); - return; -} - -if (!response.ok) { - throw new Error(`Novi verification returned ${response.status} for UUID ${uuid}`); -} -``` - -**Catch block:** After the retry, if 503 still fires, it throws a plain `Error` → caught → -`is_network_or_timeout = false` → `verify_error_type = 'api_error'` → "Verification -Unavailable". This is correct — after two attempts the UI should tell the user. - -**Note on `verify_error_type` reset:** The recursive retry call resets `verify_error_type = -null` at its top before checking the response. For 429 this is harmless (re-set on 429 check). -For 503 after this fix it is also harmless — 503 is caught before `!response.ok`. Worth knowing -if modifying the flow further. +Fixed in commit `42f40e990` — added `503` status check in `verify_novi_uuid()` before the +generic `!response.ok` throw. On first 503, waits 3s and auto-retries (same path as network +errors). After two consecutive 503s, surfaces "Verification Unavailable" as intended. +File: `src/routes/idaa/(idaa)/+layout.svelte`. --- @@ -261,19 +229,6 @@ Phases 1, 2a, 2b are complete (see ✅ Completed below). One phase remaining: `$ae_loc.*` auth-field read sites across 150+ files. Deferred until a Svelte runes migration of the store layer itself (touching every component anyway makes the callsite sweep cheap). -### [Backend] Join event_location_id onto event_presenter API view -The `event_presenter` object currently has `event_session_id` but not `event_location_id`. -When navigating from the Presenter View to the Launcher, the frontend has to do a secondary -session lookup to discover the location (magic redirect in launcher base `+page.svelte`). -Joining `event_session.event_location_id` into the presenter view/response would let the -frontend pass the location directly in the Launcher URL without the extra lookup. -- [x] Backend: added `event_location_id` (and `event_location_id_random`) to the `event_presenter` view or API response (2026-04-09) -- [x] Frontend: updated `ae_EventPresenter` type and `properties_to_save`; now pass as `events__launcher_id` in `presenter_page_menu.svelte` (2026-04-09) - - - - - ### [TypeScript] svelte-check hidden errors — discovered 2026-03-27 **HOW WE FOUND THIS:** The `@lucide/svelte` 0.577.0 update (2026-03-10) dropped `class` from `IconProps`. Fixing it required a `declare module '@lucide/svelte'` augmentation. That @@ -428,7 +383,7 @@ Firefox unaffected. Production unaffected (public IPs only). - `BUILD_MODE` arg can stay if needed for other build differences; just stop using it to pick the env file - Update `.gitignore` in sveltekit to un-ignore `.env.test` / remove stale entries if desired - **Do not touch before the April 21 show.** Low risk but unnecessary churn right before an event. + Low risk but unnecessary churn — defer until after the next active show. - [ ] **Branch strategy cleanup:** All environments (test, prod, bak) currently pull from the same branches. `deploy.sh` defaults are `ae_app_3x_llm` / `development` — acceptable for now but @@ -438,12 +393,6 @@ Firefox unaffected. Production unaffected (public IPs only). Linode → `deploy.sh`. Deferred until Gitea usage is more established. -### [Files] Download button — wrong ID used in `handle_click()` (2026-04-22) -- [x] **Fixed (2026-05-11):** All 5 spots in `ae_comp__hosted_files_download_button.svelte` updated - to use `hosted_file_obj?.hosted_file_id ?? hosted_file_id` instead of the old - `hosted_file_obj?.id || ...` chain that stopped at `event_file_id`. Needs live re-test to - confirm downloads still work correctly from Manage Files. - ### [Files] `db_events.file.clear()` on upload clears all cached files (2026-04-22) In `ae_comp__event_files_upload.svelte` line 114, `db_events.file.clear()` wipes the entire `file` Dexie table, not just files for the current session/presenter. Normally harmless (the diff --git a/src/routes/events/[event_id]/(launcher)/cfg_components/launcher_cfg_native_os.svelte b/src/routes/events/[event_id]/(launcher)/cfg_components/launcher_cfg_native_os.svelte index 7252fba3..1d69a68b 100644 --- a/src/routes/events/[event_id]/(launcher)/cfg_components/launcher_cfg_native_os.svelte +++ b/src/routes/events/[event_id]/(launcher)/cfg_components/launcher_cfg_native_os.svelte @@ -6,6 +6,7 @@ import Launcher_Cfg_Section from './launcher_cfg_section.svelte'; import { Code, Columns2, + Copy, FlaskConical, FolderOpen, Image, @@ -27,6 +28,7 @@ let test_cmd_result = $state(''); let remote_app: 'powerpoint' | 'keynote' = $state('powerpoint'); let remote_status = $state(''); let system_status = $state(''); +let display_mode = $state<'extend' | 'mirror'>('extend'); async function handle_remote_control( action: 'next' | 'prev' | 'start' | 'stop' @@ -55,6 +57,18 @@ async function handle_system_action(promise: Promise, label: string) { setTimeout(() => (system_status = ''), 3000); } +async function handle_display_mode(mode: 'extend' | 'mirror') { + system_status = `Setting display: ${mode}...`; + const res = await native.set_display_layout({ mode }); + if (res?.success) { + display_mode = mode; + system_status = `Display: ${mode}`; + } else { + system_status = `Display error: ${res?.error || 'No external display?'}`; + } + setTimeout(() => (system_status = ''), 4000); +} + // Modal state for dangerous actions let show_power_confirm = $state<{ action: string; label: string } | null>(null); @@ -124,6 +138,33 @@ let show_power_confirm = $state<{ action: string; label: string } | null>(null); + +
+

Display Mode

+
+ + +
+
+
@@ -186,19 +227,6 @@ let show_power_confirm = $state<{ action: string; label: string } | null>(null); System Actions

-