6 Commits

Author SHA1 Message Date
Scott Idem
a3d229c803 docs(launcher): CMSC Charlotte task breakdown + doc date fix
- TODO__Agents: expand CMSC Charlotte launcher task into done/remaining
  sections; enumerate Svelte-side migration items (default templates,
  composable open flow, error fallback, slide control config, kill list
  config, Launcher config UI editor, end-to-end Mac test gate)
- PROJECT__AE_Events_Launcher_Native_integration: update Last Updated to 2026-05-11
2026-05-11 17:34:48 -04:00
Scott Idem
f72454f379 docs(launcher): sync native integration doc with current Electron implementation
- Bootstrap lifecycle now reflects actual two-step flow (event_device → site_domain/search).
  Removes stale /v3/data_store/code reference and corrects the JWT claim — auth is
  x-aether-api-key + x-account-id throughout; no JWT is issued or used.
- check_hash_file_cache corrected to check_cache (actual method name in preload bridge).
- cleanup_tmp_files removed from relay list — stale .tmp cleanup is handled inline inside
  download_to_cache, not via a standalone IPC channel.
- Known Issue section replaced: all AppleScript handlers are now hardened (launch_presentation
  converted to temp-.scpt approach in the companion Electron commit ca4fddd).
- Markdown lint fixes: blank lines around tables, table pipe spacing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 17:16:01 -04:00
Scott Idem
c5c5292715 feat(launcher): configurable launch scripts + composable native primitives
- electron_relay: type launch_from_cache with script_template param;
  add copy_from_cache_to_temp export; add JSDoc for run_osascript hardening
- launcher_file_cont: add get_launch_script_template() helper reading from
  device-level (event_device.data_json.launch_scripts) and event-level
  (events_loc.launcher.launch_scripts) config; wire into handle_open_file()
- PROJECT__AE_Events_Launcher_Native_integration.md: add Section 8
  (Configurable Launch Scripts); update IPC reference for new/changed handlers
- MODULE__AE_Events_PressMgmt_Launcher.md: add configurable launch behavior
  note to Native Mode Safe Handover section
2026-05-11 13:48:54 -04:00
Scott Idem
8ed7e0f8d7 Remove _random field handling from event_session and event_file modules
Drop the _random key copy loop from _process_generic_props in both files —
V3 API returns {obj_type}_id directly as the random string ID, so copying
from {obj_type}_id_random is a no-op. Simplify .id assignment to use
baseIdKey only. Remove commented-out _random entries from properties_to_save.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 12:31:27 -04:00
Scott Idem
68e5e01df1 A quick backup of the todo before removing things. 2026-05-11 12:26:51 -04:00
Scott Idem
611b1e6b51 Remove _random ID references and fix hosted file download ID
- Fix download button to use hosted_file_id instead of id (which resolved
  to event_file_id via _process_generic_props, hitting the wrong endpoint)
- Fix Dexie file table query in event_file_obj_tbl_wrapper to use _id
  fields (the indexed ones) instead of _id_random variants
- Remove _random fields from properties_to_save in event, event_session
- Drop _id_random fallbacks from launcher device ID resolution and
  background sync heartbeat
- Clean up dead comments and old FA anchor in post edit component
- Update TODO__Agents.md: BGH section removed, CMSC/Axonius shows added,
  download button fix marked complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 12:26:21 -04:00
16 changed files with 589 additions and 140 deletions

View File

@@ -9,6 +9,7 @@
"autofetch", "autofetch",
"Axonius", "Axonius",
"displayplacer", "displayplacer",
"elif",
"filelist", "filelist",
"gsettings", "gsettings",
"onsave" "onsave"

View File

@@ -311,6 +311,13 @@ The Electron app zero-configs itself:
3. Rename to original filename (e.g., `Abstract_101.pptx`) 3. Rename to original filename (e.g., `Abstract_101.pptx`)
4. OS opens the file (Keynote, PowerPoint, Preview, etc.) 4. OS opens the file (Keynote, PowerPoint, Preview, etc.)
**Configurable launch behavior:** The open/launch command in step 4 can be overridden
per file extension via `event_device.data_json.launch_scripts` (device-level config) or
`event.launcher.launch_scripts` (event-level fallback). Templates use `{{path}}` as the
file path placeholder; AppleScript or `shell:` prefixed commands are both supported. No
Electron rebuild required to change how files open — edit config in Aether and it applies
immediately. See `PROJECT__AE_Events_Launcher_Native_integration.md` Section 8.
Versioning is handled automatically: when a presenter uploads an updated file, the new Versioning is handled automatically: when a presenter uploads an updated file, the new
hash is cached separately and the old one remains intact. hash is cached separately and the old one remains intact.

View File

@@ -1,7 +1,7 @@
# Aether Events Launcher: Native Electron Integration # Aether Events Launcher: Native Electron Integration
> **Status:** Operational / Phase 5 Implementation > **Status:** Operational / Phase 5 Implementation
> **Last Updated:** 2026-03-11 > **Last Updated:** 2026-05-11
> **Primary Platform:** macOS (Darwin) > **Primary Platform:** macOS (Darwin)
> **Fallback Platform:** Linux / Windows > **Fallback Platform:** Linux / Windows
@@ -9,6 +9,7 @@
The Aether Events Launcher utilizes an Electron-based "Native Shell" to provide OS-level capabilities that are normally restricted by browser sandboxing. This enables persistent file caching, direct control of presentation software (Keynote, PowerPoint), and hardware telemetry. The Aether Events Launcher utilizes an Electron-based "Native Shell" to provide OS-level capabilities that are normally restricted by browser sandboxing. This enables persistent file caching, direct control of presentation software (Keynote, PowerPoint), and hardware telemetry.
### Operational Modes ### Operational Modes
| Mode | Purpose | File Handling | | Mode | Purpose | File Handling |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| **Default** | Standard web browser access. | Direct downloads; no local caching. | | **Default** | Standard web browser access. | Direct downloads; no local caching. |
@@ -49,9 +50,9 @@ The integration is built on a decoupled three-layer communication model to ensur
To support rapid onsite deployment, the native app requires zero manual setup. To support rapid onsite deployment, the native app requires zero manual setup.
1. **Seed:** On launch, the Main process reads a local `seed.json` (Device ID + API Key). 1. **Seed:** On launch, the Main process reads a local `seed.json` (Device ID + API Key).
2. **Identity:** Calls `GET /v3/data_store/code/{device_code}` or `GET /v3/crud/event_device/{id}` to pull operational context. 2. **Identity:** Calls `GET /v3/crud/event_device/{id}` to pull device config and extract `app_base_url` (the event FQDN) and `account_id`.
3. **Hydrate:** Authenticates with the Aether V3 API and injects the **JWT** and **Device Config** into the UI environment. 3. **Site Context:** POSTs to `/v3/crud/site_domain/search?limit=1` with the FQDN to resolve the correct site. No JWT — auth is `x-aether-api-key` + `x-account-id` throughout.
4. **Launch:** Navigates the SvelteKit frontend directly to the assigned Event Launcher route. 4. **Launch:** Navigates the SvelteKit frontend directly to the assigned Event Launcher route (`/events/{eventId}/launcher/{locationId}`).
--- ---
@@ -85,11 +86,12 @@ When a user clicks "Open", the system follows a non-destructive sequence:
The native shell provides specialized handlers for controlling the "Podium Experience." The native shell provides specialized handlers for controlling the "Podium Experience."
### 5.1 Presentation Acts ### 5.1 Presentation Acts
| Action | Handler | Actuator (macOS) | | Action | Handler | Actuator (macOS) |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| **Launch** | `launch_presentation` | `open` or `osascript` (slideshow start) | | **Launch** | `launch_presentation` | `open` or `osascript` (slideshow start) |
| **Control** | `control_presentation` | `osascript` (next/prev slide) | | **Control** | `control_presentation` | `osascript` (next/prev slide) |
| **Clean Up**| `kill_processes` | `killall -INT` (graceful exit) | | **Clean Up** | `kill_processes` | `killall -INT` (graceful exit) |
### 5.2 System Management ### 5.2 System Management
- **Telemetry:** Pushes `cpu_usage`, `memory_free_gb`, and `foreground_app` via heartbeats using the `get_device_info` relay. - **Telemetry:** Pushes `cpu_usage`, `memory_free_gb`, and `foreground_app` via heartbeats using the `get_device_info` relay.
@@ -142,10 +144,10 @@ no-op when `window.aetherNative` is not present (i.e., in browser/non-native mod
- `get_device_info()` — Returns OS metadata, IP list, hostname, and path placeholders (`[home]`, `[tmp]`). - `get_device_info()` — Returns OS metadata, IP list, hostname, and path placeholders (`[home]`, `[tmp]`).
### File Cache ### File Cache
- `check_hash_file_cache({cache_root, hash, hash_prefix_length?})` — Verifies a file exists in the local hashed cache. - `check_cache({cache_root, hash, hash_prefix_length?, verify_hash?})` — Verifies a file exists in the local hashed cache. `verify_hash: true` re-hashes to confirm integrity.
- `download_to_cache({url, cache_root, hash, api_key, account_id, hash_prefix_length?})` — Streams a file download to the hashed cache with SHA-256 integrity check. - `download_to_cache({url, cache_root, hash, api_key, account_id, hash_prefix_length?})` — Streams a file download to the hashed cache with SHA-256 integrity check. Stale `.tmp` files (older than 5 min) from crashed downloads are cleaned up automatically on each call.
- `launch_from_cache({cache_root, hash, temp_root, filename, hash_prefix_length?})`Atomic "Safe Handover": copy from cache → tmp → rename → execute. - `copy_from_cache_to_temp({cache_root, hash, temp_root, filename, hash_prefix_length?})`**Preferred primitive.** Copies a cached file to temp and returns `{ success, path }`. The Svelte caller decides what to do next (run a script, open it, etc.).
- `cleanup_tmp_files({cache_root, max_age_minutes?})` — Removes stale `*.tmp` download artifacts. Default: 1440 min (24h). Called at launcher startup. - `launch_from_cache({cache_root, hash, temp_root, filename, hash_prefix_length?, script_template?})` — Combines copy + launch in one call. Uses `script_template` if provided, otherwise falls back to hardcoded extension logic. See **Configurable Launch Scripts** below.
> `hash_prefix_length` defaults to `2` throughout. Do not change without coordinating all devices — mismatched values create orphaned cache subdirectories. > `hash_prefix_length` defaults to `2` throughout. Do not change without coordinating all devices — mismatched values create orphaned cache subdirectories.
@@ -153,8 +155,8 @@ no-op when `window.aetherNative` is not present (i.e., in browser/non-native mod
- `open_folder(path)` — Opens a path in the OS file manager. - `open_folder(path)` — Opens a path in the OS file manager.
- `run_cmd({cmd, timeout?, return_stdout?})` — Async shell command execution. - `run_cmd({cmd, timeout?, return_stdout?})` — Async shell command execution.
- `run_cmd_sync({cmd, return_stdout?})` — Synchronous shell command execution. - `run_cmd_sync({cmd, return_stdout?})` — Synchronous shell command execution.
- `run_osascript(script)` — Executes an AppleScript string. macOS only. - `run_osascript(script)` — Executes an AppleScript string. macOS only. **Hardened (2026-05-11):** writes script to a temp `.scpt` file; multi-line scripts and paths with special characters now work correctly. No shell escaping needed in the passed string.
- `kill_processes({process_name_li})`Gracefully terminates processes by name. - `kill_processes({process_name_li})`Terminates processes by name. macOS/Linux: `pkill -f`. Windows: `taskkill /F`.
- `open_local_file_v2(path)` — Opens a file with its default OS application. - `open_local_file_v2(path)` — Opens a file with its default OS application.
### Presentations (Phase 5) ### Presentations (Phase 5)
@@ -176,5 +178,66 @@ All paths passed to native handlers should use tokens rather than hardcoded OS p
- `[home]` — Resolved to the user's home directory by the native bridge. - `[home]` — Resolved to the user's home directory by the native bridge.
- `[tmp]` — Resolved to the system temporary directory. - `[tmp]` — Resolved to the system temporary directory.
---
## 8. Configurable Launch Scripts (No-Rebuild File Handling)
To avoid requiring a full Electron rebuild for changes to how files are opened, `launch_from_cache`
supports an optional `script_template` parameter. When provided, Electron runs the template
instead of its built-in hardcoded logic. The hardcoded logic remains intact as the fallback
when no template is configured.
### Template Formats
| Format | Example |
| :--- | :--- |
| **AppleScript** (macOS) | Multi-line AppleScript string with `{{path}}` placeholder |
| **Shell command** | String prefixed with `shell:` — e.g. `shell:open "{{path}}"` |
The placeholder `{{path}}` is replaced with the full resolved path to the file in the
temp directory (after the atomic copy from cache).
### Where to Configure
Templates are resolved in priority order by `get_launch_script_template()` in
`launcher_file_cont.svelte`:
1. **`event_device.data_json.launch_scripts`** — API-driven, per-device. Highest priority.
Set via the `event_device` record (Pres Mgmt → Device Management or direct DB edit).
2. **`$events_loc.launcher.launch_scripts`** — Local persistent config. Editable via the
Launcher config UI (planned) or direct `localStorage` manipulation.
If neither is set, `script_template` is `null` and Electron uses its built-in hardcoded defaults.
### Key Format
Keys are lowercase file extensions without the dot. A `"default"` key catches all
unrecognised extensions.
```json
// event_device.data_json.launch_scripts example
{
"launch_scripts": {
"pptx": "tell application \"Microsoft PowerPoint\"\n activate\n open (POSIX file \"{{path}}\")\n delay 3\nend tell\ntell application \"System Events\"\n keystroke return using command down\nend tell",
"key": "tell application \"Keynote\"\n activate\n open (POSIX file \"{{path}}\")\n delay 1\n start (front document)\nend tell",
"pdf": "shell:open \"{{path}}\"",
"default": "shell:open \"{{path}}\""
}
}
```
### AppleScript Execution — All Handlers Hardened (2026-05-11)
All AppleScript execution in the native shell now writes scripts to a temp `.scpt` file and
runs `osascript "<path>"` rather than the old `osascript -e "<inline>"` approach.
- **`run_osascript`** — hardened (2026-05-11, earlier batch)
- **`launch_from_cache`** — hardened (same batch)
- **`launch_presentation`** — hardened (2026-05-11, follow-up fix; was the last handler still using `-e`)
- **`control_presentation`** — uses single-line scripts with no path interpolation; `-e` is safe here and retained for simplicity
The `-e` approach breaks on (1) multi-line scripts and (2) file paths containing spaces,
quotes, or parentheses — common in conference presentation filenames.
### Not Exposed via Relay (intentional) ### Not Exposed via Relay (intentional)
- `get_seed_config` / `get_jwt` — Exposed in the preload but not relayed to the UI. The JWT and seed are injected into the environment at startup; components should not call these directly. - `get_seed_config` / `get_jwt` — Exposed in the preload but not relayed to the UI. The JWT and seed are injected into the environment at startup; components should not call these directly.

View File

@@ -3,38 +3,56 @@
> **Status:** Stable — ongoing development. > **Status:** Stable — ongoing development.
## 🔴 BGH Conference — April 21 (Must Fix Before Event) ## 🔴 CMSC Charlotte — May 27 (Presentation Management)
**Drive down:** May 25 | **Setup:** May 26 morning | **Show:** May 27+
- [x] **[Locations] Event Locations list does not auto-load** — added `+page.ts` to trigger **[Electron/Launcher] Clean up presentation file launch scripts** — BGH show revealed issues
`load_ae_obj_li__event_location` on page load. Also fixed session query using stale with the scripts used to open/launch presentation files. Architecture decision: move launch
`event_location_id_random` index (should be `event_location_id`). (2026-04-19) logic to the Svelte side so it can be changed without an Electron rebuild. Electron becomes a
thin OS primitive layer; all business logic lives in Svelte and device config.
- [x] **[Files] Warn/error on `.ppt`/`.doc` upload** — warning rows shown per-file in upload table; **Electron groundwork (2026-05-11) — DONE:**
non-trusted users are fully blocked (`file_list_status = 'blocked_legacy'`); trusted users see - [x] `run_osascript` hardened — temp `.scpt` file approach; handles multi-line + special chars
warnings but can still upload. Covers `.ppt`, `.doc` (block) and other legacy exts (warn-only). - [x] `native:copy-from-cache-to-temp` primitive added — copy to tmp, caller decides launch
(2026-04-19) - [x] `native:launch-from-cache` accepts optional `script_template` — AppleScript or `shell:` prefix; falls back to hardcoded defaults when null
- [x] `get_launch_script_template()` in `launcher_file_cont.svelte` reads from device config then event config; passes result to `launch_from_cache`
- [x] **[Files] Hide internal-purpose files from Launcher by default** — renamed `hide_draft` prop **Svelte-side migration — remaining before May 26:**
to `show_internal_purpose_files` in `launcher_file_cont.svelte`; logic flipped so `false` (the - [ ] **[Launcher] Built-in Svelte default templates** — move the "known good" pptx/key/pdf
default) hides files with `file_purpose == 'outline'`, `'draft'`, or `'admin'`. Store key renamed AppleScript strings out of Electron hardcode and into a Svelte constants file (e.g.
from `hide_content__draft_files` (inverted, misleading) to `show_content__internal_files: false` `ae_launcher__default_launch_scripts.ts`). Priority: `get_launch_script_template()` already
(show-on-opt-in, consistent with all other `show_content__*` flags). Updated across all 8 Launcher checks device config and event config; add a 3rd fallback to these Svelte defaults before
templates that pass this prop. (2026-04-19, revised 2026-04-20) returning `null`. This means Electron's hardcoded defaults become the last-resort only.
- [ ] **[Launcher] Composable open flow** — refactor `handle_open_file()` to use
`copy_from_cache_to_temp` + `run_osascript` / `run_cmd` directly instead of the all-in-one
`launch_from_cache`. Finer error handling at each step (verify copy succeeded before
attempting script; surface failure clearly in UI).
- [ ] **[Launcher] Error handling + fallback** — when the launch script fails, offer fallback
to `open_local_file_v2` (OS default handler) rather than silently failing. Show error detail
in the launcher file row so staff can diagnose onsite.
- [ ] **[Launcher] Slide control scripts in Svelte config** — `control_presentation` AppleScript
one-liners are hardcoded in Electron. Move to device config (`data_json.control_scripts`) or
Svelte constants so slide nav behavior (e.g. keystroke vs. AppleScript command) can be adjusted
without a rebuild. Wire through `run_osascript` directly.
- [ ] **[Launcher] `kill_processes` target list in config** — process names to kill on cleanup
are currently caller-hardcoded. Allow device config to specify the process name list per
file type / app, so adding a new presentation app doesn't require a Svelte code change.
- [ ] **[Launcher] Launcher config UI — launch_scripts editor** — add a Technical Mode panel
in the Launcher config (tabbed settings) to view and edit `launch_scripts` entries on the
active device record. PATCH via `event_device` V3 CRUD. Lets OSIT staff tune scripts onsite
without needing phpMyAdmin or a code deploy.
- [ ] **[Launcher] End-to-end test on macOS** — test pptx and key opens on a real podium Mac
before May 26 setup day. Verify: file copies to tmp correctly, script fires, app opens in
slideshow mode, error fallback works.
- [x] **[Launcher] Remove duplicate session API call on session select** — `menu_session_list.svelte` ---
was calling `load_ae_obj_id__event_session` directly AND then `goto()` triggered `+page.ts` which
also called it — two concurrent calls per session click. Removed the direct call entirely;
`+page.ts` is now the sole owner of session data loading. `goto()` promise assigned to
`ae_promises.slct__event_session_id` to drive the existing `{#await}` spinner. (2026-04-20)
- [ ] **[Electron/Launcher] Deploy + test Aether Native Electron app on Mac laptops** — build, ## 🔴 Axonius DC — June 9 (Badge Printing)
deploy, and verify on onsite Mac laptops. Additional testing of cache/launch flow still needed **Setup/Registration:** June 8 | **Show:** June 9
before April 21.
- [x] **[Pres Mgmt] POC column shown in "Sessions at this Location"** — wired - [ ] **[Badges] Epson C3500 fanfold badge layout** — Axonius is using Epson C3500 printers
`hide__session_poc={!pres_mgmt_loc.current.show__session_li_poc_field}` in with fanfold (continuous) badge stock. Create/configure a fanfold badge layout compatible
`ae_comp__event_location_obj_li.svelte`; also set `hide__session_location={true}` since with the C3500 format. Must be ready before the June 8 setup/registration day.
location is implicit in that context. (2026-04-19)
--- ---
@@ -268,28 +286,10 @@ Firefox unaffected. Production unaffected (public IPs only).
### [Files] Download button — wrong ID used in `handle_click()` (2026-04-22) ### [Files] Download button — wrong ID used in `handle_click()` (2026-04-22)
`ae_comp__hosted_files_download_button.svelte` resolves `file_id` for the download call as - [x] **Fixed (2026-05-11):** All 5 spots in `ae_comp__hosted_files_download_button.svelte` updated
`hosted_file_obj?.id || hosted_file_obj?.hosted_file_id || hosted_file_id`. When called from to use `hosted_file_obj?.hosted_file_id ?? hosted_file_id` instead of the old
Manage Files with an `event_file_obj`, `hosted_file_obj.id` = `event_file_id` (set by `hosted_file_obj?.id || ...` chain that stopped at `event_file_id`. Needs live re-test to
`_process_generic_props` via the `_random` strip logic), so the chain stops at the wrong value. confirm downloads still work correctly from Manage Files.
The download call goes to `/v3/action/hosted_file/{event_file_id}/download` instead of using the
correct `hosted_file_id`. May work if the backend accepts event_file_id at that endpoint —
needs live verification.
**Status (2026-04-22):** Tested — downloads ARE working despite the wrong ID. The backend
V3 action endpoint appears to silently accept `event_file_id` at the `hosted_file` download
path (or maps between the two). Working by accident, not by design. Needs proper fix before
it breaks — if the backend ever tightens that endpoint, all Manage Files downloads will 404.
**Fix:** In `handle_click()` and both `$effect` blocks and the `content` snippet, replace:
```ts
const file_id = hosted_file_obj?.id || hosted_file_obj?.hosted_file_id || hosted_file_id;
```
with:
```ts
const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id;
```
The direct-download `<a>` path is unaffected (already uses `event_file_id` → correct endpoint).
### [Files] `db_events.file.clear()` on upload clears all cached files (2026-04-22) ### [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 In `ae_comp__event_files_upload.svelte` line 114, `db_events.file.clear()` wipes the entire

View File

@@ -0,0 +1,326 @@
# Frontend Agent Task List
> Use this file to track steps for complex features or bug fixes.
> **Status:** Stable — ongoing development.
## 🔴 BGH Conference — April 21 (Must Fix Before Event)
- [x] **[Locations] Event Locations list does not auto-load** — added `+page.ts` to trigger
`load_ae_obj_li__event_location` on page load. Also fixed session query using stale
`event_location_id_random` index (should be `event_location_id`). (2026-04-19)
- [x] **[Files] Warn/error on `.ppt`/`.doc` upload** — warning rows shown per-file in upload table;
non-trusted users are fully blocked (`file_list_status = 'blocked_legacy'`); trusted users see
warnings but can still upload. Covers `.ppt`, `.doc` (block) and other legacy exts (warn-only).
(2026-04-19)
- [x] **[Files] Hide internal-purpose files from Launcher by default** — renamed `hide_draft` prop
to `show_internal_purpose_files` in `launcher_file_cont.svelte`; logic flipped so `false` (the
default) hides files with `file_purpose == 'outline'`, `'draft'`, or `'admin'`. Store key renamed
from `hide_content__draft_files` (inverted, misleading) to `show_content__internal_files: false`
(show-on-opt-in, consistent with all other `show_content__*` flags). Updated across all 8 Launcher
templates that pass this prop. (2026-04-19, revised 2026-04-20)
- [x] **[Launcher] Remove duplicate session API call on session select** — `menu_session_list.svelte`
was calling `load_ae_obj_id__event_session` directly AND then `goto()` triggered `+page.ts` which
also called it — two concurrent calls per session click. Removed the direct call entirely;
`+page.ts` is now the sole owner of session data loading. `goto()` promise assigned to
`ae_promises.slct__event_session_id` to drive the existing `{#await}` spinner. (2026-04-20)
- [ ] **[Electron/Launcher] Deploy + test Aether Native Electron app on Mac laptops** — build,
deploy, and verify on onsite Mac laptops. Additional testing of cache/launch flow still needed
before April 21.
- [x] **[Pres Mgmt] POC column shown in "Sessions at this Location"** — wired
`hide__session_poc={!pres_mgmt_loc.current.show__session_li_poc_field}` in
`ae_comp__event_location_obj_li.svelte`; also set `hide__session_location={true}` since
location is implicit in that context. (2026-04-19)
---
## 🔴 CMSC Charlotte — May 27 (Presentation Management)
**Drive down:** May 25 | **Setup:** May 26 morning | **Show:** May 27+
- [ ] **[Electron/Launcher] Clean up presentation file launch scripts** — BGH show revealed
issues with the scripts used to open/launch presentation files through the Electron Launcher.
Audit and improve the launch/open flow (script reliability, error handling, file path resolution).
Must be tested and ready before May 26 setup day.
---
## 🔴 Axonius DC — June 9 (Badge Printing)
**Setup/Registration:** June 8 | **Show:** June 9
- [ ] **[Badges] Epson C3500 fanfold badge layout** — Axonius is using Epson C3500 printers
with fanfold (continuous) badge stock. Create/configure a fanfold badge layout compatible
with the C3500 format. Must be ready before the June 8 setup/registration day.
---
## 🚧 Upcoming High Priority
### [Stores] Svelte 4 → Svelte 5 State Migration (prerequisite for Phase 2c)
The app uses `svelte-persisted-store` (Svelte 4 store contract) for all core persisted state
(`ae_loc`, `idaa_loc`, `ae_api`, `ae_sess`, etc.). In Svelte 5 `$effect`, reading **any field**
of a Svelte 4 store subscribes to the **entire store** — coarse-grained reactivity. This is the
root cause of the IDAA Novi re-auth bug (2026-03-30): unrelated `$ae_loc` writes (e.g. iframe
height, SWR cfg reload) triggered the Novi verification effect repeatedly.
Migration target: replace `svelte-persisted-store` with Svelte 5 `$state`-based persistence
(e.g. `runed` `PersistedState`, or a lightweight custom wrapper). This gives fine-grained
reactivity — only effects that actually read a changed field re-run.
**Phased approach (do NOT do all at once):**
- [ ] **Phase A — Project plan + wrapper decision:** Write `PROJECT__Stores_Svelte5_Migration.md`.
Decide: `runed` library vs. custom `$state` + localStorage wrapper. Audit all store consumers.
Identify stores in priority order. Estimate blast radius per store.
- [ ] **Phase B — Core auth stores (highest impact, start here):**
- `ae_loc` (persisted) — auth flags, site cfg, UI state; ~471 consumer sites across 150+ files
- `idaa_loc` (persisted) — Novi auth, IDAA query prefs
These two cause the most reactive noise. Migrating them also unlocks Phase 2c (separate `ae_auth`
store) since the callsite sweep is now required anyway.
- [ ] **Phase C — Remaining persisted stores:**
- `ae_api` (persisted) — API config / JWT
- `ae_events_stores` persisted entries (badges, launcher, leads, pres_mgmt loc stores)
- [ ] **Phase D — Non-persisted writable stores:**
- `ae_sess`, `idaa_sess`, `slct`, `slct_trigger`, `ae_auth_error`, `ae_trig`, `ae_snip`, etc.
- Lower urgency (no localStorage churn), but fine-grained reactivity still beneficial.
- [ ] **Phase E — Phase 2c (unblocked after B):** Split `ae_loc` into `ae_auth` + `ae_app`
(see entry below — ~471 callsites, but sweep is cheap once already touching every consumer).
**Project plan doc needed:** Yes — scope is app-wide. Do NOT start Phase B without Phase A.
---
### [Stores] Refactor — Phase 2c (deferred)
Phases 1, 2a, 2b are complete (see ✅ Completed below). One phase remaining:
- [ ] **Phase 2c — Actual separate stores (`ae_auth`, `ae_app`):** Requires touching ~471
`$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
augmentation was mistakenly placed in `app.d.ts`, which is a *script-context* declaration file
(no `export {}`). In that context, `declare module` is an **ambient replacement**, not a merge —
it wiped all icon exports from svelte-check's view, surfacing 1368 previously hidden errors.
Once moved to `src/lucide-augment.d.ts` (a proper module file with `export {}`), the masking
lifted and the real pre-existing errors became visible.
**Lesson:** A broken ambient declaration can silently hide unrelated errors. If svelte-check
suddenly jumps to 0 errors, verify it's not because a bad `.d.ts` replaced a package's types.
**Current state (2026-03-31):** 32 errors, 0 warnings — all `ModalProps.children`.
- [ ] **[flowbite-svelte] `ModalProps.children` — 31 errors across 26 files.** The flowbite-svelte
`Modal` component API changed; `children` is no longer a direct prop (now Svelte snippet-based).
Affected files span journals, pres_mgmt, events/settings, and IDAA archives.
Run `npx svelte-check 2>&1 | grep ModalProps` to get the current list.
Fix pattern: replace `children` prop binding with Svelte snippet syntax per flowbite-svelte docs.
### [Journals] Journal Entry Config follow-ups
- [ ] **[Journals] Visibility / audience toggle contrast** — the flag buttons need a clearer
selected state in both light and dark mode.
- [ ] **[Journals] Footer button style** — the actual `Done` button should read like a real button,
not a seamless footer spacer.
- [ ] **[Journals] Entry passcode secondary auth** — `passcode_hash` stores a hash; compare the
entered passcode hash to the stored hash, gate entry loading, and honor the TTL-based access
window. This is secondary entry auth, not a plain-text passcode field.
- [ ] **[Journals] Summary AI shortcut** — add an AI summarize button next to Entry Details
Summary so staff can generate a summary directly from the modal.
- [ ] **[Journals] Archive On sizing** — constrain the Archive On control to a reasonable width
instead of letting it expand to full width.
- [ ] **[Journals] Archive On behavior** — define what Archive On actually means and wire the
behavior; it is currently just a UI field with no live effect.
- [x] **[IDAA] Do not cache IDAA data in IDB when access is denied (2026-04-19, audited 2026-04-28)**
Full audit confirmed all protection layers are in place. No code changes required.
- All `+page.ts` / `+layout.ts` under `src/routes/idaa/` are clean — no SWR loads run before auth resolves.
- All `$effect` SWR calls in IDAA `+page.svelte` files are gated on `$idaa_loc.novi_verified || $ae_loc.trusted_access`.
- `(idaa)/+layout.svelte` purges `db_posts`, `db_archives`, `db_events` on auth failure, no-UUID/no-session, and inconsistent state.
- `sign_out()` calls `indexedDB.deleteDatabase()` on all IDAA databases.
- API 401/403 responses fail-fast in `api_get_object.ts` (throw before any IDB write).
- `idaa_trig` is in-memory `writable()` only — cannot carry stale trigger state across sessions.
- `$effect` auth guards in IDAA page components are reactivity guards (prevent spurious SWR calls on coarse `$ae_loc` writes), NOT auth-bypass guards. SvelteKit layout hierarchy already prevents child components from mounting when `(idaa)/+layout.svelte` blocks rendering.
- Doc: SvelteKit layout hierarchy security model captured in `GUIDE__SvelteKit2_Svelte5_DexieJS.md` and `BOOTSTRAP__AI_Agent_Quickstart.md` (Mistake #7).
- [ ] **[IDAA] Make `contact_li_json_ext` searchable — Recovery Meeting contact search (2026-04-08)**
Members cannot search for meetings by contact name or email. `contact_li_json` data is not
included in `default_qry_str` and MariaDB cannot substring-search a JSON longtext directly.
The `event` table already has `contact_li_json_ext` (STORED GENERATED, indexed) to work around this.
**Backend (blocked on this first):** Add `contact_li_json_ext` to the searchable fields
whitelist for the `event` object type — likely a one-line change in `ae_obj_types_def.py`
or the event object definition. Message sent to backend agent 2026-04-08.
**Frontend (after backend ships):**
- `src/lib/ae_events/ae_events__event.ts``search__event()`: add `contact_li_json_ext`
as an OR condition alongside `default_qry_str` when `qry_str` is present.
- `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte` fast-path IDB filter: parse
`contact_li_json` and include contact names/emails in the local text match check.
- [ ] **[IDAA / Events] Audit `default_qry_str` coverage in other event search pages.**
The backend was updated 2026-03-31 to expose `default_qry_str` in API responses.
Frontend fix applied to Recovery Meetings (`+page.svelte` + `properties_to_save`).
Check all other event search pages that use `db_events.event.filter()` or a secondary
post-API text filter — they may have the same mismatch (local searches `name`/`description`
only while server uses `default_qry_str`). Start with: any route under `/events/` or `/idaa/`
that has a full-text search input.
### [IDAA] Jitsi config editor + live site fix
- [ ] **Fix live site (id=17) `jitsi_token_endpoint` pointing to dev-api:** DB has
`https://dev-api.oneskyit.com/api/jitsi_token` for both site 10 and site 17 (IDAA live).
Need to update site 17 in **production** to `https://api.oneskyit.com/api/jitsi_token`.
SQL: `UPDATE site SET cfg_json = JSON_SET(cfg_json, '$.jitsi_token_endpoint', 'https://api.oneskyit.com/api/jitsi_token') WHERE id = 17;`
- [ ] **Add IDAA Jitsi config editor UI** to the jitsi_reports page (administrator_access only),
alongside the existing Jitsi URL Builder section. Should allow editing key fields in
`site_cfg_json` without needing phpMyAdmin:
- `jitsi_token_endpoint` — the JWT signing endpoint (needs to point to prod)
- Jitsi domain default (currently hardcoded as `jitsi.dgrzone.com` fallback in the page)
- `novi_jitsi_mod_li` — list of Novi UUIDs who get moderator privileges
Read from `$ae_loc.site_cfg_json`, PATCH the site record via V3 CRUD
(`PATCH /v3/crud/site/{id}/`), reload `$ae_loc.site_cfg_json` on save so it takes
effect without re-login.
### [IDAA] Jitsi Reports still incomplete
- [x] **Finish Jitsi Reports filters** — added Novi UUID exclusion plus meeting-name whitelist
filtering, with room-level unique counts based on Novi UUID when present. (2026-05-06)
### [PWA] Service worker ignoring `chrome-extension://` requests
Browser console shows repeated errors:
```text
TypeError: Failed to execute 'put' on 'Cache': Request scheme 'chrome-extension' is unsupported
```
The service worker's fetch/install handler is trying to cache requests with `chrome-extension://`
URLs (injected by browser extensions), which the Cache API rejects. Fix: filter out non-`http`/`https`
requests before attempting to cache. In the service worker fetch handler, add a guard:
```js
if (!event.request.url.startsWith('http')) return; // skip chrome-extension:// etc.
```
Locate in `static/service-worker.js` or the Vite PWA plugin config. Low severity — doesn't break
functionality, but pollutes the console and may cause unhandled promise rejections.
### [CSS] Global placeholder text color — too dark in light mode
Placeholder text inherits full input text color in light mode (Tailwind CSS default), making
placeholders indistinguishable from filled-in values. Most visible in badge print controls
where placeholders show the actual badge value (e.g. "John Smith").
Workaround: scoped `::placeholder` rule added to `ae_comp__badge_print_controls.svelte`
(gray-400 light / gray-500 dark) — `commit 7733ef8`.
**Long-term fix:** Add a global rule to the main CSS (e.g. `src/app.css` or a theme file):
```css
::placeholder {
color: #9ca3af; /* gray-400 */
opacity: 1; /* overrides Firefox's 0.54 default */
}
.dark ::placeholder {
color: #6b7280; /* gray-500 */
}
```
Once the global rule is in place, remove the scoped workaround from the badge controls.
### [Backend/DevOps] Re-add `Access-Control-Allow-Private-Network: true` CORS header
Chrome's Private Network Access (PNA) policy blocks public-origin iframes from fetching
private-network addresses. Symptom: when `dev-api.oneskyit.com` resolves to a LAN IP
(testing from home), Chrome blocks the site domain lookup → ghost account → `site_cfg_json`
never loads → `novi_idaa_api_key` is null → IDAA Novi verifier spins forever → timeout banner.
Firefox unaffected. Production unaffected (public IPs only).
- [ ] **Re-add PNA header to API CORS config**`dev-api` Nginx or FastAPI CORS middleware
must respond with `Access-Control-Allow-Private-Network: true` when Chrome sends
`Access-Control-Request-Private-Network: true` in the preflight. This was fixed ~1 month
ago and regressed. Check Nginx site config and FastAPI `CORSMiddleware` settings.
Low urgency (dev-only, Firefox workaround available), but blocks home-network iframe testing.
### [DevOps] Remaining deployment items
- [ ] **Simplify Dockerfile env file selection** — Currently the Dockerfile uses a `BUILD_MODE` arg to
select between `.env.dev`, `.env.test`, `.env.prod` during the Docker build. This is unnecessary
complexity: each server (test Linode, prod Linode, workstation) only ever runs one environment, so
there will only ever be one env file present in that server's app directory.
**The fix:** Each server's app dir (`/srv/apps/test_aether_app_sveltekit/`, etc.) should have a
plain `.env` file (gitignored, placed manually during server setup). The Dockerfile should just
`COPY . .` and `cp .env .env.runtime` unconditionally — no `if prod / elif test / else dev`
branching for env file selection.
**What this changes:**
- `aether_app_sveltekit/Dockerfile` — remove the `BUILD_MODE`-driven `cp` block; always use `.env`
- Each Linode app dir gets a plain `.env` instead of `.env.test` / `.env.prod`
- Workstation keeps `.env.local` (for `npm run dev`) and `.env.dev` (for `build:docker:dev`) —
those stay as-is since they legitimately coexist locally
- `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.
- [ ] **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
should establish proper branch separation (e.g. `main`/`master` for prod).
- [ ] **Tier 2 deploy (Gitea webhook):** Push-triggered deploys via Gitea webhook → listener on
Linode → `deploy.sh`. Deferred until Gitea usage is more established.
### [Files] Download button — wrong ID used in `handle_click()` (2026-04-22)
`ae_comp__hosted_files_download_button.svelte` resolves `file_id` for the download call as
`hosted_file_obj?.id || hosted_file_obj?.hosted_file_id || hosted_file_id`. When called from
Manage Files with an `event_file_obj`, `hosted_file_obj.id` = `event_file_id` (set by
`_process_generic_props` via the `_random` strip logic), so the chain stops at the wrong value.
The download call goes to `/v3/action/hosted_file/{event_file_id}/download` instead of using the
correct `hosted_file_id`. May work if the backend accepts event_file_id at that endpoint —
needs live verification.
**Status (2026-04-22):** Tested — downloads ARE working despite the wrong ID. The backend
V3 action endpoint appears to silently accept `event_file_id` at the `hosted_file` download
path (or maps between the two). Working by accident, not by design. Needs proper fix before
it breaks — if the backend ever tightens that endpoint, all Manage Files downloads will 404.
**Fix:** In `handle_click()` and both `$effect` blocks and the `content` snippet, replace:
```ts
const file_id = hosted_file_obj?.id || hosted_file_obj?.hosted_file_id || hosted_file_id;
```
with:
```ts
const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id;
```
The direct-download `<a>` path is unaffected (already uses `event_file_id` → correct endpoint).
### [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
reload right after repopulates), but if multiple sessions' file lists are open simultaneously
they'd briefly flash empty. Low priority — only noticeable in multi-panel workflows.
### [General]
- **Input Field Audit:** Several input fields are missing `name`/`id` attributes or `data-testid`. Known examples: badge override fields in `ae_comp__badge_obj_view.svelte`; template name input in `ae_comp__badge_template_form.svelte`. Matters for: accessibility, autofill, label associations, and test targeting. (For tests, use `getByLabel()` rather than `input[value*=...]` which only checks the HTML attribute, not the Svelte-bound DOM property.)
## ✅ Completed (2026-04)
## ✅ Completed (archived)
See the full completed history in:
[documentation/archive/TODO__Agents__ARCHIVE_2026-03.md](documentation/archive/TODO__Agents__ARCHIVE_2026-03.md)
[documentation/archive/TODO__Agents__ARCHIVE_2026-04.md](documentation/archive/TODO__Agents__ARCHIVE_2026-04.md)

View File

@@ -128,10 +128,7 @@ $effect(() => {
let ae_promises: key_val = $state({}); let ae_promises: key_val = $state({});
$effect(() => { $effect(() => {
const file_id = const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id;
hosted_file_obj?.id ||
hosted_file_obj?.hosted_file_id ||
hosted_file_id;
if (file_id && $ae_sess?.api_download_kv[file_id]?.percent_completed) { if (file_id && $ae_sess?.api_download_kv[file_id]?.percent_completed) {
download_percent = $ae_sess.api_download_kv[file_id].percent_completed; download_percent = $ae_sess.api_download_kv[file_id].percent_completed;
} }
@@ -139,10 +136,7 @@ $effect(() => {
// Reactive timer to alternate views during active download // Reactive timer to alternate views during active download
$effect(() => { $effect(() => {
const file_id = const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id;
hosted_file_obj?.id ||
hosted_file_obj?.hosted_file_id ||
hosted_file_id;
const is_actively_downloading = const is_actively_downloading =
ae_promises[file_id] && download_complete === undefined; ae_promises[file_id] && download_complete === undefined;
@@ -193,10 +187,7 @@ let direct_download_url = $derived.by(() => {
}); });
async function handle_click() { async function handle_click() {
const file_id = const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id;
hosted_file_obj?.id ||
hosted_file_obj?.hosted_file_id ||
hosted_file_id;
download_complete = undefined; download_complete = undefined;
download_status_msg = 'Downloading...'; download_status_msg = 'Downloading...';
@@ -238,10 +229,7 @@ async function handle_click() {
</script> </script>
{#snippet content()} {#snippet content()}
{@const file_id = {@const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id}
hosted_file_obj?.id ||
hosted_file_obj?.hosted_file_id ||
hosted_file_id}
{#await ae_promises[file_id]} {#await ae_promises[file_id]}
<div class="flex min-h-[1.5rem] w-full items-center"> <div class="flex min-h-[1.5rem] w-full items-center">
<div <div
@@ -316,8 +304,7 @@ async function handle_click() {
{/snippet} {/snippet}
{#if hosted_file_id && hosted_file_obj} {#if hosted_file_id && hosted_file_obj}
{@const file_id = {@const file_id = hosted_file_obj.hosted_file_id ?? hosted_file_id}
hosted_file_obj.id || hosted_file_obj.hosted_file_id || hosted_file_id}
{#if show_direct_download} {#if show_direct_download}
<a <a

View File

@@ -787,7 +787,7 @@ export const properties_to_save = [
'event_id', 'event_id',
'code', 'code',
'account_id', 'account_id',
'account_id_random', // 'account_id_random',
'conference', 'conference',
'type', 'type',
'name', 'name',

View File

@@ -359,7 +359,7 @@ export async function create_event_file_obj_from_hosted_file_async({
}); });
if (return_obj) return result; if (return_obj) return result;
return result?.event_file_id || result?.id || result?.event_file_id_random; return result?.event_file_id || result?.id;
} }
export async function delete_ae_obj_id__event_file({ export async function delete_ae_obj_id__event_file({
@@ -527,15 +527,11 @@ export const qry__event_file = search__event_file;
export const properties_to_save = [ export const properties_to_save = [
'id', 'id',
'event_file_id', 'event_file_id',
// 'event_file_id_random', // DO NOT UNCOMMENT
'hosted_file_id', 'hosted_file_id',
// 'hosted_file_id_random', // DO NOT UNCOMMENT
'hash_sha256', 'hash_sha256',
'for_type', 'for_type',
'for_id', 'for_id',
// 'for_id_random', // DO NOT UNCOMMENT
'event_id', 'event_id',
// 'event_id_random', // DO NOT UNCOMMENT
'event_session_id', 'event_session_id',
'event_presentation_id', 'event_presentation_id',
'event_presenter_id', 'event_presenter_id',
@@ -598,22 +594,9 @@ async function _process_generic_props<T extends Record<string, any>>({
const processed_obj_li: T[] = []; const processed_obj_li: T[] = [];
for (const original_obj of obj_li) { for (const original_obj of obj_li) {
let processed_obj = { ...original_obj }; let processed_obj = { ...original_obj };
for (const key in processed_obj) { const base_id_key = `${obj_type}_id`;
if (key.endsWith('_random')) { if (processed_obj[base_id_key])
const newKey = key.slice(0, -7); (processed_obj as any).id = processed_obj[base_id_key];
// ONLY overwrite if the random variant has a valid value
if (
processed_obj[key] !== null &&
processed_obj[key] !== undefined &&
processed_obj[key] !== ''
) {
(processed_obj as any)[newKey] = processed_obj[key];
}
}
}
const random_id_key = `${obj_type}_id_random`;
if (processed_obj[random_id_key])
(processed_obj as any).id = processed_obj[random_id_key];
const group = processed_obj.group ?? '0'; const group = processed_obj.group ?? '0';
const priority = processed_obj.priority ? 1 : 0; const priority = processed_obj.priority ? 1 : 0;
const sort = processed_obj.sort ?? '0'; const sort = processed_obj.sort ?? '0';

View File

@@ -836,12 +836,10 @@ export async function email_sign_in__event_session({
export const properties_to_save = [ export const properties_to_save = [
'id', 'id',
'event_session_id', 'event_session_id',
'event_session_id_random',
'external_id', 'external_id',
'code', 'code',
'for_type', 'for_type',
'for_id', 'for_id',
'for_id_random',
'type_code', 'type_code',
'event_id', 'event_id',
'event_location_id', 'event_location_id',
@@ -898,18 +896,8 @@ async function _process_generic_props<T extends Record<string, any>>({
const processed_obj_li: T[] = []; const processed_obj_li: T[] = [];
for (const original_obj of obj_li) { for (const original_obj of obj_li) {
let processed_obj = { ...original_obj }; let processed_obj = { ...original_obj };
for (const key in processed_obj) {
if (key.endsWith('_random')) {
const newKey = key.slice(0, -7);
(processed_obj as any)[newKey] = processed_obj[key];
}
}
const randomIdKey = `${obj_type}_id_random`;
const baseIdKey = `${obj_type}_id`; const baseIdKey = `${obj_type}_id`;
if (processed_obj[randomIdKey]) { if (processed_obj[baseIdKey])
(processed_obj as any).id = processed_obj[randomIdKey];
(processed_obj as any)[baseIdKey] = processed_obj[randomIdKey];
} else if (processed_obj[baseIdKey])
(processed_obj as any).id = processed_obj[baseIdKey]; (processed_obj as any).id = processed_obj[baseIdKey];
const group = processed_obj.group ?? '0'; const group = processed_obj.group ?? '0';

View File

@@ -64,11 +64,68 @@ export async function launch_from_cache({
hash, hash,
temp_root, temp_root,
filename, filename,
hash_prefix_length = 2 hash_prefix_length = 2,
}: any) { script_template = null
}: {
cache_root: string;
hash: string;
temp_root: string;
filename: string;
hash_prefix_length?: number;
/**
* Optional data-driven launch script. If provided, Electron runs this instead of
* its hardcoded extension-based logic — no app rebuild needed for script changes.
*
* Two formats:
* - AppleScript: multi-line string with {{path}} placeholder (macOS only)
* - Shell command: prefix with "shell:" → e.g. "shell:open \"{{path}}\""
*
* Configure via event_device.data_json.launch_scripts or $events_loc.launcher.launch_scripts.
* If null, Electron falls through to its built-in hardcoded defaults.
*/
script_template?: string | null;
}) {
if (!native) if (!native)
return { success: false, error: 'Native bridge not available' }; return { success: false, error: 'Native bridge not available' };
return await native.launch_from_cache({ return await native.launch_from_cache({
cache_root,
hash,
temp_root,
filename,
hash_prefix_length,
script_template
});
}
/**
* Thin cache primitive — copies a cached file to the temp directory and returns
* the resolved path. The caller decides what happens next.
*
* Preferred building block for composable launch flows on the Svelte side:
* 1. copy_from_cache_to_temp(...) → { path }
* 2. run_osascript(template.replace('{{path}}', path))
* OR run_cmd(`open "${path}"`)
* OR whatever you need
*
* Use launch_from_cache when the built-in hardcoded logic is sufficient.
* Use this when you want full control over what happens after the file lands in temp.
*/
export async function copy_from_cache_to_temp({
cache_root,
hash,
temp_root,
filename,
hash_prefix_length = 2
}: {
cache_root: string;
hash: string;
temp_root: string;
filename: string;
hash_prefix_length?: number;
}): Promise<{ success: boolean; path?: string; error?: string }> {
if (!native)
return { success: false, error: 'Native bridge not available' };
return await native.copy_from_cache_to_temp({
cache_root, cache_root,
hash, hash,
temp_root, temp_root,
@@ -129,6 +186,18 @@ export async function cleanup_tmp_files({
return await native.run_cmd({ cmd, timeout: 30000, return_stdout: false }); return await native.run_cmd({ cmd, timeout: 30000, return_stdout: false });
} }
/**
* Executes an AppleScript string. macOS only.
*
* HARDENED (2026-05-11): The Electron handler now writes the script to a temp .scpt
* file and runs `osascript "/path/to/file.scpt"` rather than passing it inline via
* the -e flag. This means:
* - Multi-line scripts work correctly
* - Paths with spaces or special characters work correctly
* - No shell escaping required in the script string you pass here
*
* The .scpt file is deleted immediately after execution.
*/
export async function run_osascript(script: string) { export async function run_osascript(script: string) {
if (!native) if (!native)
return { success: false, error: 'Native bridge not available' }; return { success: false, error: 'Native bridge not available' };

View File

@@ -197,9 +197,7 @@ $effect(() => {
untrack(() => { untrack(() => {
$events_slct.event_device_id = $events_slct.event_device_id =
native_dev.event_device_id || native_dev.event_device_id ||
native_dev.id || native_dev.id;
native_dev.event_device_id_random ||
native_dev.id_random;
}); });
} }
}); });

View File

@@ -370,9 +370,7 @@ async function run_device_heartbeat() {
// String-Only ID Vision: Prioritize semantic string IDs, then generic, then legacy random strings // String-Only ID Vision: Prioritize semantic string IDs, then generic, then legacy random strings
const device_id = const device_id =
dev?.event_device_id || dev?.event_device_id ||
dev?.id || dev?.id;
dev?.event_device_id_random ||
dev?.id_random;
if (!device_id) { if (!device_id) {
// Only log warning if we are actually supposed to be in native mode // Only log warning if we are actually supposed to be in native mode

View File

@@ -88,6 +88,41 @@ let open_file_status_message: null | string = $state(null);
let screen_saver_exts = ['jpg', 'png', 'PNG', 'webp']; let screen_saver_exts = ['jpg', 'png', 'PNG', 'webp'];
/**
* Resolves a data-driven launch script template for a given file extension.
* Checked in priority order:
* 1. event_device.data_json.launch_scripts (API-driven, per-device, most specific)
* 2. $events_loc.launcher.launch_scripts (local persistent override)
* Keys are lowercase extensions without the dot ("pptx", "key", "pdf", etc.).
* A "default" key acts as a catch-all for unrecognised extensions.
*
* Returns null when no config is found → Electron falls back to its hardcoded defaults.
*
* Template formats:
* - AppleScript (macOS): plain string with {{path}} placeholder
* - Shell command: prefix with "shell:" → "shell:open \"{{path}}\""
*/
function get_launch_script_template(extension: string): string | null {
const ext = (extension || '').toLowerCase().replace('.', '');
// 1. Device-level config (from API, per device — highest priority)
const device_scripts = ($ae_loc as any).native_device?.launch_scripts;
if (device_scripts) {
if (device_scripts[ext]) return device_scripts[ext];
if (device_scripts['default']) return device_scripts['default'];
}
// 2. Launcher local config override (set manually via Launcher config UI)
const local_scripts = ($events_loc as any).launcher?.launch_scripts;
if (local_scripts) {
if (local_scripts[ext]) return local_scripts[ext];
if (local_scripts['default']) return local_scripts['default'];
}
// 3. No override — let Electron use its built-in hardcoded defaults
return null;
}
onMount(() => { onMount(() => {
if (screen_saver_exts.includes(event_file_obj.extension)) { if (screen_saver_exts.includes(event_file_obj.extension)) {
if (!$events_loc.launcher.screen_saver_img_kv) if (!$events_loc.launcher.screen_saver_img_kv)
@@ -149,11 +184,14 @@ async function handle_open_file() {
// Phase 2/5: Use the atomic copy-and-launch operation. // Phase 2/5: Use the atomic copy-and-launch operation.
// The main process handler (file_handlers.ts) now handles the // The main process handler (file_handlers.ts) now handles the
// specialized LibreOffice/AppleScript logic internally after copying. // specialized LibreOffice/AppleScript logic internally after copying.
// script_template is null when no device/local config exists → Electron uses hardcoded defaults.
const script_template = get_launch_script_template(event_file_obj.extension);
const launch_result = await native.launch_from_cache({ const launch_result = await native.launch_from_cache({
cache_root, cache_root,
hash: event_file_obj.hash_sha256, hash: event_file_obj.hash_sha256,
temp_root, temp_root,
filename: event_file_obj.filename filename: event_file_obj.filename,
script_template
}); });
if (!launch_result.success) { if (!launch_result.success) {

View File

@@ -77,10 +77,10 @@ let lq__event_obj = $derived(
); );
// It is important that these not be set to a value! It messes with the Dexie LiveQuery. // It is important that these not be set to a value! It messes with the Dexie LiveQuery.
// let event_file_id_random_li: Array<string> = $state(); // let event_file_id_li: Array<string> = $state();
// let event_session_id_random_li: Array<string> = $state(); // let event_session_id_li: Array<string> = $state();
// let event_presentation_id_random_li: Array<string>; // let event_presentation_id_li: Array<string>;
// let event_presenter_id_random_li: Array<string> = $state(); // let event_presenter_id_li: Array<string> = $state();
// let load_obj_li_results: Promise<any>|key_val; // let load_obj_li_results: Promise<any>|key_val;
// let search_submit_results: Promise<any>|key_val; // let search_submit_results: Promise<any>|key_val;
@@ -142,9 +142,11 @@ $effect(() => {
</span> </span>
<!-- Reports for: --> <!-- Reports for: -->
{#if $lq__event_obj?.cfg_json?.short_name} {#if $lq__event_obj?.cfg_json?.short_name}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html $lq__event_obj?.cfg_json.short_name ?? {@html $lq__event_obj?.cfg_json.short_name ??
ae_snip.html__not_set} ae_snip.html__not_set}
{:else} {:else}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html $lq__event_obj?.name ?? ae_snip.html__not_set} {@html $lq__event_obj?.name ?? ae_snip.html__not_set}
{/if} {/if}
</h2> </h2>

View File

@@ -18,7 +18,7 @@ interface Props {
let { let {
container_class_li = [], container_class_li = [],
// display_mode = 'default', // display_mode = 'default',
// event_file_id_random_li = $bindable(), // event_file_id_li = $bindable(),
event_file_obj_li = $bindable(), event_file_obj_li = $bindable(),
link_to_type, link_to_type,
link_to_id, link_to_id,
@@ -55,9 +55,9 @@ $effect(() => {
// let ae_tmp: key_val = {}; // let ae_tmp: key_val = {};
// let ae_triggers: key_val = {}; // let ae_triggers: key_val = {};
let event_file_id_random_li: Array<string> = $state([]); // let event_file_id_li: Array<string> = $state([]);
let dq__where_type_id_val = $derived(`${link_to_type}_id_random`); let dq__where_type_id_val = $derived(`${link_to_type}_id`);
let dq__where_eq_id_val = $derived(link_to_id ?? ''); let dq__where_eq_id_val = $derived(link_to_id ?? '');
// *** Functions and Logic // *** Functions and Logic
@@ -122,7 +122,7 @@ let lq__event_file_obj_li = $derived(
</script> </script>
{#if event_file_obj_li && event_file_obj_li?.length} {#if event_file_obj_li && event_file_obj_li?.length}
<!-- {#if event_file_id_random_li && event_file_id_random_li?.length} --> <!-- {#if event_file_id_li && event_file_id_li?.length} -->
<Comp_event_file_obj_tbl <Comp_event_file_obj_tbl
{container_class_li} {container_class_li}
{lq__event_file_obj_li} {lq__event_file_obj_li}

View File

@@ -595,15 +595,6 @@ $effect(() => {
{#each $idaa_slct.post_obj.linked_li_json as linked_obj, index (linked_obj.hosted_file_id ?? index)} {#each $idaa_slct.post_obj.linked_li_json as linked_obj, index (linked_obj.hosted_file_id ?? index)}
<span <span
class="flex flex-col items-center gap-1 rounded-lg border bg-white/50 p-1"> class="flex flex-col items-center gap-1 rounded-lg border bg-white/50 p-1">
<!-- <a
href={linked_obj.url}
target="_blank"
class="badge badge-info variant-filled-info"
>
<span class="fas fa-paperclip m-1"></span>
{linked_obj.filename}
({linked_obj.hosted_file_id})
</a> -->
{#if $ae_loc.authenticated_access} {#if $ae_loc.authenticated_access}
{@const file_id = {@const file_id =
@@ -650,8 +641,6 @@ $effect(() => {
return false; return false;
} }
// ae_promises[linked_obj.event_file_id] = handle_delete__event_file({event_file_id: linked_obj.event_file_id});
// First - Attempt to delete the hosted file // First - Attempt to delete the hosted file
ae_promises.delete__linked_obj = ae_promises.delete__linked_obj =
await core_func await core_func