refactor(launcher): modularize launcher config and implement Phase 5 actuators

- Broke down the massive launcher_cfg.svelte into 7 modular sub-components.
- Updated electron_relay.ts with Phase 5 presentation controls and manifest tools.
- Updated architecture documentation to reflect the new TypeScript-based native bridge.
This commit is contained in:
Scott Idem
2026-01-26 16:18:00 -05:00
parent 7c14b1e3a2
commit 5f2ccf8823
12 changed files with 1306 additions and 1027 deletions

View File

@@ -7,39 +7,37 @@ The Aether Native App serves as the OS-level bridge for the SvelteKit frontend.
## 2. The Three-Layer Architecture
### 2.1 The Engine (`electron_native.js`)
* **Process:** Main Process (Node.js)
### 2.1 The Engine (`src/main/*.ts`)
* **Process:** Main Process (Node.js/TypeScript)
* **Role:** Performs the actual OS-level operations.
* **Key Responsibilities:**
* Executing shell commands via `child_process`.
* Managing the permanent Hashed Cache (`file_cache/`).
* Performing atomic "Safe Handover" copies to the operational `temp/` directory.
* Resolving global path placeholders like `[home]` and `[tmp]`.
* **Shell Handlers (`shell_handlers.ts`)**: Executing shell commands, AppleScripts, and process management.
* **File Handlers (`file_handlers.ts`)**: Managing the permanent Hashed Cache (`file_cache/`) and atomic Safe Handover.
* Resolving global path placeholders like `[home]` and `[tmp]` via `file_utils.ts`.
### 2.2 The Bridge (`preload.js`)
### 2.2 The Bridge (`src/preload/index.ts`)
* **Process:** Preload Script
* **Role:** The security gatekeeper.
* **Key Responsibilities:**
* Uses Electron's `contextBridge` to securely expose specific Main Process functions to the UI.
* Uses Electron's `contextBridge` to securely expose Main Process functions to the UI.
* Exposes the `window.aetherNative` object.
* Ensures that only whitelisted IPC channels can be triggered by the renderer.
### 2.3 The Messenger (`electron_relay.ts`)
### 2.3 The Messenger (`src/lib/electron/electron_relay.ts`)
* **Process:** Renderer Process (SvelteKit)
* **Role:** The TypeScript API used by Svelte components.
* **Key Responsibilities:**
* Detecting if the app is running in "Native Mode".
* Providing a clean, typed interface for native calls.
* Implementing "Smart Fallbacks" (e.g., resolving placeholders UI-side if the bridge is outdated).
* Standardizing IPC calls to snake_case.
* Implementing "Smart Fallbacks" (e.g., UI-side placeholder resolution if the bridge is inactive).
---
## 3. Core Lifecycle
1. **Seed Phase:** The Electron shell reads `~/seed.json` to identify the device.
2. **Hydration Phase:** The shell authenticates with the Aether V3 API to pull device-specific settings (room assignment, sync timers).
3. **Injection Phase:** The shell injects the **JWT (Auth Token)** and **Native Config** into the SvelteKit environment.
4. **Observation Phase:** `LauncherBackgroundSync.svelte` force-hydrates absolute OS paths (Home/Tmp) into the global `ae_loc` store for immediate use.
1. **Seed Phase:** The Electron shell reads `resources/seed_config.json` to identify the device.
2. **Hydration Phase:** The shell authenticates with the Aether V3 API to pull device-specific settings.
3. **Injection Phase:** The shell injects the **JWT** and **Native Config** into the SvelteKit environment.
4. **Observation Phase:** Background sync loops manage file warming and heartbeat telemetry.
---
@@ -48,17 +46,20 @@ The Aether Native App serves as the OS-level bridge for the SvelteKit frontend.
### File & Cache Management
- `check_cache({hash})`: Verify if a file exists in the hidden hashed storage.
- `download_to_cache({url, hash})`: Secure background download using the native API key.
- `launch_from_cache({hash, filename})`: **Safe Handover** — copies from cache to temp with the original name and triggers the launcher.
- `launch_from_cache({hash, filename})`: **Safe Handover** — copies from cache to temp with the original name and triggers the specialized launcher.
### OS Execution (The "Actuators")
- `launch_presentation({path, app})`: Intelligent application selection.
- `launch_presentation({path, app})`: Phase 5 specialized launcher with auto-focus (`activate`) and slideshow start.
- **Linux:** Triggers `libreoffice --impress`.
- **macOS:** Triggers AppleScript for Keynote/PowerPoint.
- `run_cmd({cmd})`: Executes raw shell commands with automatic `[home]` and `[tmp]` resolution.
- `kill_processes([names])`: Force-closes presentation apps to clean the podium between sessions.
- **macOS:** Triggers AppleScript for Keynote or PowerPoint.
- `control_presentation({app, action})`: Remote navigation for active decks.
- **Actions:** `next`, `prev`, `start`, `stop`.
- `run_cmd({cmd})`: Executes raw shell commands with automatic path resolution.
- `kill_processes([names])`: Force-closes apps to clean the podium.
### System Metadata
- `get_device_info()`: Provides OS version, CPU/RAM stats, and absolute system paths.
### Metadata & Discovery
- `get_device_info()`: Provides CPU/RAM stats and absolute system paths.
- `list_tools()`: Returns a JSON manifest of all available native functions for self-documentation.
---

View File

@@ -0,0 +1,187 @@
# Aether Svelte 5 App — Performance Review
Date: 2026-01-26
This document collects findings from a quick code review of the Aether Svelte 5 app (focused on `src/routes/+layout.svelte`, `src/routes/+page.svelte`, and the events launcher layout). It lists concrete performance issues observed, their likely impact, remediation recommendations (quick wins first), code examples for safe refactors, measurement steps, and a prioritized action plan.
**Scope**
- Files inspected: `src/routes/+layout.svelte`, `src/routes/+page.svelte`, event launcher `+layout.svelte` and related store initialization logic.
- Platform: SvelteKit with Vite (assumed from repo files). Builds use Tailwind CSS and manual global CSS.
---
## Summary of Key Findings
- Large synchronous imports at module evaluation time (notably `highlight.js` and its stylesheet) increase initial bundle size and run before first paint.
- Many components and icon imports are static at top-level even when rendered conditionally (`Analytics`, `MyClipboard`, `E_app_debug_menu`, `E_app_sys_menu`, multiple Lucide icons). This inflates the client bundle.
- Significant initialization and cache logic runs synchronously during module evaluation: store writes, cache expiration checks, IndexedDB operations, localStorage population, calls to `invalidateAll()` and conditional `window.location.reload()` paths — these can block initial render and cause reload loops or extra round trips.
- Dexie `liveQuery` usages in nested layouts are eager and may execute multiple DB queries on route entry, causing CPU and I/O work up-front.
- Multiple synchronous CSS imports (global CSS + highlight CSS) can delay first paint and increase transfer size.
- Many localStorage/sessionStorage writes and store updates are done immediately and synchronously; iterating many keys on startup can stall the main thread.
- Analytics, WebSocket connections, and external network resource usage may be triggered too early.
Impact: Slow Time to First Paint (TTFP), increased Time to Interactive (TTI), larger JS bundles, higher CPU main-thread blocking, worse Lighthouse/perceived performance.
---
## Concrete, Prioritized Recommendations
Priority ordering: Quick wins (low code risk) → Short-term (moderate refactor) → Long-term (architectural).
### Quick Wins (apply first)
- Lazy-load `highlight.js` and its CSS in `onMount` only when highlighting is needed. This removes a heavy library and stylesheet from the initial bundle.
- Dynamically import non-critical components (`Analytics`, debug menus, clipboard UI, large icon modules) when they will actually render.
- Move non-UI, non-blocking initialization to `onMount` and/or `requestIdleCallback` so SSR rendering and first paint aren't blocked by client-only logic.
- Guard `invalidateAll()` / `window.location.reload()` so they run only after a user-visible prompt or on explicit action; avoid automatic reload on subtle mismatches.
### Short-Term Changes (next 12 days)
- Defer expensive Dexie `liveQuery` and database queries until their containing route/section mounts, or add limits/pagination to reduce initial query size.
- Batch localStorage writes (use a single write or `requestIdleCallback`/`setTimeout(…,0)`), or store a single object rather than many keys.
- Replace heavy icon imports with an icon-on-demand strategy (SVG sprite, `unplugin-icons`, or inlining only icons used on first paint).
- Lazy-load large CSS (e.g., highlight theme) with dynamic import or by adding/removing link elements.
### Long-Term / Architectural
- Split routes to ensure route-level code-splitting is effective — verify that `+layout` doesn't import many route-specific modules. Keep `+layout.svelte` as thin as possible.
- Use a bundle analyzer to identify largest modules and tune dependencies.
- Serve production build with proper HTTP caching, compression (Brotli), and HTTP/2 or HTTP/3.
- Consider server-side rendering vs. partial hydration boundaries (islands) for heavy interactive regions.
---
## Concrete Code Examples
These are safe, copy-pasteable snippets for common quick fixes.
1) Lazy-load `highlight.js` in `src/routes/+layout.svelte` (move work to `onMount`):
```svelte
<script lang="ts">
import { onMount } from 'svelte';
let hljs: any;
onMount(async () => {
const mod = await import('highlight.js/lib/core');
hljs = mod.default || mod;
// register languages lazily
const xml = (await import('highlight.js/lib/languages/xml')).default;
const css = (await import('highlight.js/lib/languages/css')).default;
const js = (await import('highlight.js/lib/languages/javascript')).default;
const ts = (await import('highlight.js/lib/languages/typescript')).default;
hljs.registerLanguage('xml', xml);
hljs.registerLanguage('css', css);
hljs.registerLanguage('javascript', js);
hljs.registerLanguage('typescript', ts);
// load stylesheet dynamically if needed
await import('highlight.js/styles/github-dark.css');
});
</script>
```
2) Dynamic component import for `Analytics`:
```svelte
<script lang="ts">
import { onMount } from 'svelte';
let Analytics: any = null;
let showAnalytics = false;
onMount(async () => {
if (/* condition to show analytics, or simply true after idle */) {
const mod = await import('$lib/app_components/analytics.svelte');
Analytics = mod.default;
showAnalytics = true;
}
});
</script>
{#if showAnalytics && Analytics}
<svelte:component this={Analytics} bind:site_google_tracking_id={$ae_loc.site_google_tracking_id} />
{/if}
```
3) Batch localStorage writes using `requestIdleCallback` fallback:
```ts
function batchSetLocalStorage(obj: Record<string, any>) {
const work = () => {
for (const [k, v] of Object.entries(obj)) {
try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {}
}
};
if ('requestIdleCallback' in window) {
(window as any).requestIdleCallback(work);
} else {
setTimeout(work, 0);
}
}
```
4) Avoid automatic reloads — example guard:
```ts
// Only force reload after user consent or if critical mismatch
if (flag_reload) {
const reloadKey = 'aether_reload_shown_v' + ($ae_loc?.ver ?? 'unknown');
if (!localStorage.getItem(reloadKey)) {
if (confirm('A new version is available. Reload now?')) {
localStorage.setItem(reloadKey, '1');
location.reload();
}
}
}
```
---
## Measurement & Tooling
Run these locally to find concrete hotspots and bundle contributors.
- Bundle analysis (Vite):
```bash
# Build and open analyzer (if plugin configured)
npm run build
npx vite build --sourcemap
# Use source-map-explorer or webpack-bundle-analyzer (adapt if using Rollup)
npx source-map-explorer dist/assets/*.js
```
- Vite visualizer (install `rollup-plugin-visualizer` or `vite-plugin-visualizer`):
```bash
# add plugin and run build with report
# then open .html output from the plugin
```
- Lighthouse / DevTools:
- Run Lighthouse (Performance, TTI, Largest Contentful Paint, Main thread) in Chrome.
- In Performance panel record page load to find long main-thread tasks (JS parsing/execution) and identify blocking code paths.
- Runtime marks: add `performance.mark('init-start')` / `performance.mark('init-end')` to measure areas (e.g., store population, Dexie queries) then `performance.measure()`.
---
## Prioritized Action Plan (Checklist)
- [ ] Move `highlight.js` imports and theme stylesheet to `onMount` (quick win).
- [ ] Convert `Analytics` and other developer tools to dynamic imports (quick win).
- [ ] Move non-critical store population, IndexedDB maintenance, and cache-expiration logic to `onMount` or `requestIdleCallback`.
- [ ] Add guard & confirmation around `invalidateAll()` and `location.reload()` to avoid reload loops.
- [ ] Defer Dexie `liveQuery` initialization to the route or component that needs it (avoid running all live queries globally).
- [ ] Batch localStorage writes and avoid iterating thousands of keys synchronously.
- [ ] Run bundle analysis and produce a top-10 biggest modules list.
- [ ] Replace heavy icon imports with icon-on-demand solution.
- [ ] Re-run Lighthouse and validate improvements.
---
## Appendix: Quick Checklist for PRs
- Ensure client-only code is inside `onMount` or guarded by `if (browser)`.
- Where dynamic import used, use `<svelte:component this={Comp} />` pattern.
- Add performance marks around expensive blocks and log durations in dev mode.
- Avoid unconditional network or socket connections in top-level layout; open them after user interaction or when component mounts.
---
If you want, I can implement the first quick wins now: lazy-load `highlight.js` and convert `Analytics` import in `src/routes/+layout.svelte`, then run a local bundle analysis. Tell me which of the quick wins you'd like me to patch first and I'll start.

View File

@@ -139,3 +139,55 @@ export async function launch_presentation({
return await open_local_file_v2(cleaned_path);
}
/**
* Control Presentation (Phase 5)
* Sends navigation commands to the active presentation (Next, Prev, Stop).
*/
export async function control_presentation({
app,
action
}: {
app: 'powerpoint' | 'keynote',
action: 'next' | 'prev' | 'start' | 'stop'
}) {
if (!native) return { success: false, error: 'Native bridge not available' };
// Check if the native bridge has the direct implementation
if (native.control_presentation) {
return await native.control_presentation({ app, action });
}
// Fallback to generic osascript if direct handler is missing
let script = '';
if (app === 'powerpoint') {
switch (action) {
case 'next': script = 'tell application "Microsoft PowerPoint" to next slide of slide show view of active presentation'; break;
case 'prev': script = 'tell application "Microsoft PowerPoint" to previous slide of slide show view of active presentation'; break;
case 'start': script = 'tell application "Microsoft PowerPoint" to run slide show of active presentation'; break;
case 'stop': script = 'tell application "Microsoft PowerPoint" to stop slide show of active presentation'; break;
}
} else if (app === 'keynote') {
switch (action) {
case 'next': script = 'tell application "Keynote" to show next'; break;
case 'prev': script = 'tell application "Keynote" to show previous'; break;
case 'start': script = 'tell application "Keynote" to start (front document)'; break;
case 'stop': script = 'tell application "Keynote" to stop'; break;
}
}
if (script) {
return await run_osascript(script);
}
return { success: false, error: `Unsupported app or action: ${app}/${action}` };
}
/**
* List Tools (Self-Documentation)
* Returns a JSON manifest of all available native bridge functions.
*/
export async function list_tools() {
if (!native || !native.list_tools) return [];
return await native.list_tools();
}

View File

@@ -0,0 +1,211 @@
import { ipcMain, shell } from 'electron';
import { exec, execSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import { expandPath } from './file_utils';
export function registerShellHandlers() {
ipcMain.handle('native:open-folder', async (event, folderPath: string) => {
const cleanPath = expandPath(folderPath);
const error = await shell.openPath(cleanPath);
return { success: !error, error };
});
ipcMain.handle('native:run-cmd', async (event, { cmd, timeout = 30000 }) => {
const cleanCmd = expandPath(cmd);
return new Promise((resolve) => {
exec(cleanCmd, { timeout }, (error, stdout, stderr) => {
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
});
});
});
ipcMain.handle('native:run-cmd-sync', async (event, { cmd }) => {
const cleanCmd = expandPath(cmd);
try {
const stdout = execSync(cleanCmd).toString();
return { success: true, stdout: stdout.trim() };
} catch (error: any) {
return { success: false, error: error.message, stderr: error.stderr?.toString() };
}
});
ipcMain.handle('native:run-osascript', async (event, script: string) => {
if (os.platform() !== 'darwin') return { success: false, error: 'AppleScript is only available on macOS' };
const escapedScript = script.replace(/"/g, '\"');
const cmd = `osascript -e "${escapedScript}"`;
return new Promise((resolve) => {
exec(cmd, (error, stdout, stderr) => {
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
});
});
});
ipcMain.handle('native:kill-processes', async (event, { process_name_li = [] }) => {
console.log(`Native: Killing processes -> `, process_name_li);
const results = [];
for (const name of process_name_li) {
const cmd = os.platform() === 'win32'
? `taskkill /F /IM ${name} /T`
: `pkill -f ${name}`;
try {
execSync(cmd);
results.push({ name, success: true });
} catch (e: any) {
results.push({ name, success: false, error: e.message });
}
}
return { success: true, results };
});
ipcMain.handle('native:open-local-file-v2', async (event, filePath: string) => {
const cleanPath = expandPath(filePath);
const error = await shell.openPath(cleanPath);
return { success: !error, error };
});
ipcMain.handle('native:launch-presentation', async (event, { path: rawPath, app: appType = 'default' }) => {
const cleanedPath = expandPath(rawPath);
console.log(`Native: Launching Presentation -> ${cleanedPath} (App: ${appType})`);
if (os.platform() === 'linux') {
const cmd = `libreoffice --impress "${cleanedPath}"`;
return new Promise((resolve) => {
exec(cmd, (err, stdout, stderr) => {
if (err) resolve({ success: false, error: err.message });
else resolve({ success: true, stdout, stderr });
});
});
}
if (os.platform() === 'darwin') {
let script = '';
if (appType === 'keynote') {
script = `
tell application "Keynote"
activate
open (POSIX file "${cleanedPath}")
delay 1
start (front document)
end tell
`;
} else if (appType === 'powerpoint') {
script = `
tell application "Microsoft PowerPoint"
activate
open (POSIX file "${cleanedPath}")
delay 1
run slide show of active presentation
end tell
`;
}
if (script) {
return new Promise((resolve) => {
const escapedScript = script.replace(/"/g, '\\"');
exec(`osascript -e "${escapedScript}"`, (err, stdout, stderr) => {
if (err) resolve({ success: false, error: err.message });
else resolve({ success: true });
});
});
}
}
const error = await shell.openPath(cleanedPath);
return { success: !error, error };
});
ipcMain.handle('native:control-presentation', async (event, { app, action }) => {
if (os.platform() !== 'darwin') return { success: false, error: 'Presentation control is only available on macOS' };
let script = '';
if (app === 'powerpoint') {
switch (action) {
case 'next': script = 'tell application "Microsoft PowerPoint" to next slide of slide show view of active presentation'; break;
case 'prev': script = 'tell application "Microsoft PowerPoint" to previous slide of slide show view of active presentation'; break;
case 'start': script = 'tell application "Microsoft PowerPoint" to run slide show of active presentation'; break;
case 'stop': script = 'tell application "Microsoft PowerPoint" to stop slide show of active presentation'; break;
}
} else if (app === 'keynote') {
switch (action) {
case 'next': script = 'tell application "Keynote" to show next'; break;
case 'prev': script = 'tell application "Keynote" to show previous'; break;
case 'start': script = 'tell application "Keynote" to start (front document)'; break;
case 'stop': script = 'tell application "Keynote" to stop'; break;
}
}
if (!script) return { success: false, error: `Unsupported app or action: ${app}/${action}` };
return new Promise((resolve) => {
exec(`osascript -e "${script.replace(/"/g, '\\"')}"`, (error, stdout, stderr) => {
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
});
});
});
ipcMain.handle('native:list-tools', async () => {
return [
{
name: 'open_folder',
description: 'Opens a directory in the OS file explorer (Finder/Files/Explorer).',
params: { path: 'string' }
},
{
name: 'run_cmd',
description: 'Executes an asynchronous shell command with a timeout.',
params: { cmd: 'string', timeout: 'number (optional)' }
},
{
name: 'run_cmd_sync',
description: 'Executes a synchronous shell command.',
params: { cmd: 'string' }
},
{
name: 'run_osascript',
description: 'Executes a raw AppleScript string (macOS only).',
params: { script: 'string' }
},
{
name: 'kill_processes',
description: 'Forcefully terminates processes by name.',
params: { process_name_li: 'string[]' }
},
{
name: 'open_local_file_v2',
description: 'Opens a local file using the default OS handler.',
params: { filePath: 'string' }
},
{
name: 'launch_presentation',
description: 'Phase 5: Specialized launcher for PowerPoint, Keynote, and LibreOffice with auto-focus.',
params: { path: 'string', app: 'default|powerpoint|keynote' }
},
{
name: 'control_presentation',
description: 'Phase 5: Remote navigation for active slideshows.',
params: { app: 'powerpoint|keynote', action: 'next|prev|start|stop' }
},
{
name: 'check_cache',
description: 'Checks if a file exists in the local organized cache.',
params: { cache_root: 'string', hash: 'string', hash_prefix_length: 'number' }
},
{
name: 'download_to_cache',
description: 'Downloads a file from the API directly into the native cache.',
params: { url: 'string', cache_root: 'string', hash: 'string', api_key: 'string', account_id: 'string' }
},
{
name: 'launch_from_cache',
description: 'Atomic operation: Copies file from cache to temp with original name and launches via specialized handler.',
params: { cache_root: 'string', hash: 'string', temp_root: 'string', filename: 'string' }
},
{
name: 'get_device_info',
description: 'Returns hardware and OS metadata (CPUs, RAM, IP addresses, Hostname).',
params: {}
}
];
});
}

View File

@@ -0,0 +1,229 @@
<script lang="ts">
import { events_loc } from '$lib/stores/ae_events_stores';
</script>
<section
class:preset-outlined-warning-300-700={$events_loc.launcher.show_section__app_modes}
class="app_modes w-full preset-outlined-surface-300-700 transition-all"
>
<h3 class="text-center mb-2 text-sm font-semibold">
<button
onclick={() => {
$events_loc.launcher.show_section__app_modes =
!$events_loc.launcher.show_section__app_modes;
}}
class="btn btn-sm w-full justify-between"
>
<span>
{#if $events_loc.launcher.show_section__app_modes}
<span class="fas fa-chevron-down"></span>
{:else}
<span class="fas fa-chevron-right"></span>
{/if}
App Modes
</span>
{$events_loc.launcher.app_mode ?? '-- not set --'}
</button>
</h3>
<div
class="flex flex-col gap-1 items-center justify-start w-full"
class:hidden={!$events_loc.launcher.show_section__app_modes}
>
<div class="flex flex-row flex-wrap gap-1 items-center justify-center w-full">
{#if !$events_loc.launcher.app_mode || $events_loc.launcher.app_mode != 'default'}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.app_mode = 'default';
}}
title="Switch to default web browser mode"
>
Change to Default Mode
</button>
{/if}
{#if $events_loc.launcher.app_mode != 'native'}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.app_mode = 'native';
}}
title="Switch to native app mode"
>
Change to App Mode
</button>
{/if}
{#if $events_loc.launcher.app_mode != 'onsite'}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.app_mode = 'onsite';
}}
title="Switch to onsite mode"
>
Change to Onsite Mode
</button>
{/if}
</div>
<div class="flex flex-row flex-wrap gap-1 items-center justify-center w-full">
{#if $events_loc.launcher.hide__launcher_menu}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__launcher_menu = false;
}}
title="Show launcher menu"
>
Show Launcher Menu
</button>
{/if}
{#if !$events_loc.launcher.hide__launcher_header}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__launcher_header = true;
}}
title="Hide launcher header"
>
Hide Launcher Header
</button>
{/if}
{#if $events_loc.launcher.hide__launcher_header}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__launcher_header = false;
}}
title="Show launcher header"
>
Show Launcher Header
</button>
{/if}
{#if !$events_loc.launcher.hide__launcher_footer}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__launcher_footer = true;
}}
title="Hide launcher footer"
>
Hide Launcher Footer
</button>
{/if}
{#if $events_loc.launcher.hide__launcher_footer}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__launcher_footer = false;
}}
title="Show launcher footer"
>
Show Launcher Footer
</button>
{/if}
{#if !$events_loc.launcher.hide__launcher_menu}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__launcher_menu = true;
}}
title="Hide launcher menu"
>
Hide Launcher Menu
</button>
{/if}
{#if !$events_loc.launcher.hide__session_datetimes}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__session_datetimes = true;
}}
title="Hide session start/end datetimes"
>
Hide Session Datetimes
</button>
{/if}
{#if $events_loc.launcher.hide__session_datetimes}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__session_datetimes = false;
}}
title="Show session start/end datetimes"
>
Show Session Datetimes
</button>
{/if}
<button
type="button"
onclick={() => {
if ($events_loc.launcher.time_format == 'time_12_short') {
$events_loc.launcher.time_format = 'time_short';
$events_loc.launcher.time_hours = 24;
} else {
$events_loc.launcher.time_format = 'time_12_short';
$events_loc.launcher.time_hours = 12;
}
}}
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
>
Time Format:
{#if $events_loc.launcher.time_format == 'time_12_short'}
12-hour
{:else}
24-hour
{/if}
</button>
{#if !$events_loc.launcher.hide__ws_element}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__ws_element = true;
}}
title="Hide WebSocket element"
>
Hide WebSocket Element
</button>
{/if}
{#if $events_loc.launcher.hide__ws_element}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__ws_element = false;
}}
title="Show WebSocket element"
>
Show WebSocket Element
</button>
{/if}
{#if !$events_loc.launcher.hide__modal_header_title}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__modal_header_title = true;
}}
title="Hide modal header title"
>
Hide Modal Header Title
</button>
{/if}
{#if $events_loc.launcher.hide__modal_header_title}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__modal_header_title = false;
}}
title="Show modal header title"
>
Show Modal Header Title
</button>
{/if}
</div>
</div>
</section>

View File

@@ -0,0 +1,105 @@
<script lang="ts">
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
</script>
<section
class:preset-outlined-warning-300-700={$events_loc.launcher.show_section__controller}
class="controller w-full preset-outlined-surface-300-700 transition-all"
>
<h3 class="text-center mb-2 text-sm font-semibold">
<button
onclick={() => {
$events_loc.launcher.show_section__controller =
!$events_loc.launcher.show_section__controller;
}}
class="btn btn-sm w-full justify-between"
>
{#if $events_loc.launcher.show_section__controller}
<span class="fas fa-chevron-down"></span>
{:else}
<span class="fas fa-chevron-right"></span>
{/if}
Controller:
{$events_loc.launcher?.controller ?? '-- not set --'}
({$events_loc.launcher.controller_group_code ?? '-- not set --'})
{#if $events_sess.launcher.ws_connect_status == 'connected'}
<span>
<span class="fas fa-sitemap m-1 text-green-700"></span>
WS
</span>
{:else}
<span>
<span class="fas fa-times m-1 text-red-700"></span>
<span class="fas fa-plug"></span>
WS
</span>
{/if}
</button>
</h3>
<div class:hidden={!$events_loc.launcher.show_section__controller}>
<div class="flex flex-row gap-1 p-0.5">
<select
bind:value={$events_loc.launcher.controller}
class="input select text-sm preset-tonal-surface"
>
<option value="local">Local Only</option>
<option value="remote">Remotely WS Controlled</option>
<option value="local_push">Local and WS Controller</option>
</select>
</div>
<div class="flex flex-row gap-1 p-0.5">
<input
bind:value={$events_loc.launcher.controller_group_code}
placeholder="Controller group code"
class="input preset-tonal-surface text-sm"
ondblclick={() => {
$events_sess.launcher.controller_unlock_group_code =
!$events_sess.launcher.controller_unlock_group_code;
if ($events_loc.launcher.ws_connect) {
$events_sess.launcher.trigger__ws_disconnect = true;
}
}}
readonly={!$events_sess.launcher.controller_unlock_group_code}
/>
<button
onclick={() => {
if ($events_loc.launcher.ws_connect) {
$events_sess.launcher.trigger__ws_disconnect = true;
} else {
$events_loc.launcher.ws_connect = true;
$events_sess.launcher.trigger__ws_connect = true;
}
$events_sess.launcher.controller_unlock_group_code = false;
(($events_sess.launcher.controller_cmd = null),
($events_sess.launcher.controller_trigger_send = null));
}}
class="btn btn-sm hover:preset-filled-primary-500"
class:preset-tonal-warning={!$events_loc.launcher.ws_connect}
class:preset-tonal-success={$events_loc.launcher.ws_connect}
>
{#if $events_loc.launcher.ws_connect}
Disconnect?
{:else}
Connect?
{/if}
</button>
{#if $events_loc.launcher.ws_connect}
<button
onclick={() => {
$events_sess.launcher.controller_unlock_group_code = false;
$events_sess.launcher.controller_cmd = 'ae_refresh:now';
$events_sess.launcher.controller_trigger_send = 'trigger';
}}
class="btn btn-sm preset-tonal-secondary border border-secondary-500 hover:preset-filled-secondary-500"
>
Send Group Reload
</button>
{/if}
</div>
</div>
</section>

View File

@@ -0,0 +1,70 @@
<script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
</script>
{#if $ae_loc.is_native}
<section
class:preset-outlined-primary-300-700={$events_loc.launcher.show_section__health}
class="system_health w-full preset-outlined-surface-300-700 transition-all mb-2"
>
<h3 class="text-center mb-2 text-sm font-semibold w-full">
<button
onclick={() => {
$events_loc.launcher.show_section__health =
!$events_loc.launcher.show_section__health;
}}
class="btn btn-sm w-full justify-between"
>
<span>
{#if $events_loc.launcher.show_section__health}
<span class="fas fa-chevron-down"></span>
{:else}
<span class="fas fa-chevron-right"></span>
{/if}
System & Sync Health
</span>
<span class="flex gap-1 items-center">
{#if $events_sess.launcher.heartbeat_info.status === 'success'}
<span class="w-2 h-2 rounded-full bg-success-500 animate-pulse"></span>
{:else}
<span class="w-2 h-2 rounded-full bg-error-500"></span>
{/if}
</span>
</button>
</h3>
<div
class="flex flex-col gap-2 p-2 items-center justify-start w-full"
class:hidden={!$events_loc.launcher.show_section__health}
>
<!-- Heartbeat Info -->
<div class="grid grid-cols-2 gap-x-2 gap-y-1 w-full text-[10px] bg-surface-500/5 p-2 rounded border border-surface-500/10">
<span class="opacity-70">Last Heartbeat:</span>
<span class="text-right font-mono {$events_sess.launcher.heartbeat_info.status === 'success' ? 'text-success-500' : 'text-error-500'}">
{$events_sess.launcher.heartbeat_info.last_timestamp || 'Pending...'}
</span>
<span class="opacity-70">Room Sync Status:</span>
<span class="text-right font-mono">
{$events_sess.launcher.sync_stats.cached} / {$events_sess.launcher.sync_stats.total} Files
</span>
{#if $events_sess.launcher.sync_stats.currently_syncing}
<span class="col-span-2 text-center text-primary-500 animate-pulse mt-1 border-t border-primary-500/20 pt-1">
<span class="fas fa-sync fa-spin mr-1"></span>
Syncing: {$events_sess.launcher.sync_stats.currently_syncing}
</span>
{/if}
</div>
<!-- Basic Native Info -->
{#if $ae_loc.is_native && $ae_loc.native_device}
<div class="w-full mt-1 flex flex-col gap-1 text-[10px] opacity-80 pl-1 italic">
<div>Host: {$ae_loc.native_device.info_hostname || 'Loading...'}</div>
<div class="truncate">IPs: {$ae_loc.native_device.info_ip_list || '...'}</div>
</div>
{/if}
</div>
</section>
{/if}

View File

@@ -0,0 +1,117 @@
<script lang="ts">
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
</script>
<section
class:preset-outlined-warning-300-700={$events_loc.launcher.show_section__local_cfg_refresh}
class="local_cfg_refresh w-full preset-outlined-surface-300-700 transition-all"
>
<h3 class="text-center mb-2 text-sm font-semibold">
<button
onclick={() => {
$events_loc.launcher.show_section__local_cfg_refresh =
!$events_loc.launcher.show_section__local_cfg_refresh;
}}
class="btn btn-sm w-full justify-between"
>
<span>
{#if $events_loc.launcher.show_section__local_cfg_refresh}
<span class="fas fa-chevron-down"></span>
{:else}
<span class="fas fa-chevron-right"></span>
{/if}
Other Local Config and Caches
</span>
</button>
</h3>
<div
class="flex flex-col gap-1 items-center justify-start w-full"
class:hidden={!$events_loc.launcher.show_section__local_cfg_refresh}
>
<div class="flex flex-col gap-1 items-center justify-start w-full">
<select
class="input w-full preset-tonal-surface text-sm"
onchange={(event) => {
const val = (event.target as HTMLSelectElement).value;
if (val && val != '') {
if (val == 'delete_idbs') {
if (confirm('Are you sure you want to delete ALL IndexedDB databases?')) {
indexedDB.deleteDatabase('ae_archives_db');
indexedDB.deleteDatabase('ae_core_db');
indexedDB.deleteDatabase('ae_events_db');
indexedDB.deleteDatabase('ae_journals_db');
indexedDB.deleteDatabase('ae_posts_db');
indexedDB.deleteDatabase('ae_sponsorships_db');
alert('All IndexedDB databases deleted. Please reload the app.');
}
} else if (val == 'delete_idbs_events') {
if (confirm('Are you sure you want to delete ONLY the Events IndexedDB database?')) {
indexedDB.deleteDatabase('ae_events_db');
alert('Events IndexedDB database deleted. Please reload the app.');
}
} else if (val == 'delete_local') {
if (confirm('Are you sure you want to delete ALL local config?')) {
localStorage.removeItem('ae_loc');
localStorage.removeItem('ae_events_loc');
localStorage.removeItem('ae_idaa_loc');
localStorage.removeItem('ae_journals_loc');
location.reload();
}
} else if (val == 'delete_local_events') {
if (confirm('Are you sure you want to delete ONLY the Events local config?')) {
localStorage.removeItem('ae_events_loc');
location.reload();
}
}
(event.target as HTMLSelectElement).value = '';
}
}}
>
<option value="">-- select an option --</option>
<option value="delete_idbs">Delete all IDB tables</option>
<option value="delete_idbs_events">Delete Events IDB tables</option>
<option value="delete_local">Delete all local config</option>
<option value="delete_local_events">Delete local config for Events</option>
</select>
<span class="text-xs text-gray-500 dark:text-gray-400">
The action happens when the option is selected
</span>
</div>
<div class="flex flex-row gap-1 items-center justify-center w-full">
<button
type="button"
onclick={() => ($ae_loc.sys_menu.hide = !$ae_loc.sys_menu.hide)}
class="btn btn-sm p-1 preset-tonal-error hover:preset-filled-error-500"
title="Show or hide the Aether system menu (global)"
>
{#if !$ae_loc.sys_menu.hide}
<span class="fas fa-times"></span>
Hide Sys Menu
{:else}
<span class="fas fa-cog"></span>
Show Sys Menu
{/if}
</button>
<button
type="button"
onclick={() => ($ae_loc.debug_menu.hide = !$ae_loc.debug_menu.hide)}
class="btn btn-sm p-1 preset-tonal-error hover:preset-filled-error-500"
title="Show or hide the Aether debug menu (global)"
>
{#if !$ae_loc.debug_menu.hide}
<span class="fas fa-times"></span>
Hide Debug Menu
{:else}
<span class="fas fa-bug"></span>
Show Debug Menu
{/if}
</button>
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">API: {$ae_api.base_url}</div>
</div>
</section>

View File

@@ -0,0 +1,143 @@
<script lang="ts">
import { ae_loc, ae_api, ae_sess } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import * as native from '$lib/electron/electron_relay';
let test_cmd_result = $state('');
let remote_app: 'powerpoint' | 'keynote' = $state('powerpoint');
let remote_status = $state('');
async function handle_remote_control(action: 'next' | 'prev' | 'start' | 'stop') {
remote_status = `Sending ${action}...`;
const res = await native.control_presentation({ app: remote_app, action });
if (res.success) {
remote_status = `Success: ${action}`;
} else {
remote_status = `Error: ${res.error}`;
}
setTimeout(() => remote_status = '', 3000);
}
</script>
{#if $ae_loc.is_native}
<section
class:preset-outlined-warning-300-700={$events_loc.launcher.show_section__native_os}
class="native_os w-full preset-outlined-surface-300-700 transition-all mb-2"
>
<h3 class="text-center mb-2 text-sm font-semibold w-full">
<button
onclick={() => {
$events_loc.launcher.show_section__native_os =
!$events_loc.launcher.show_section__native_os;
}}
class="btn btn-sm w-full justify-between"
>
<span>
{#if $events_loc.launcher.show_section__native_os}
<span class="fas fa-chevron-down"></span>
{:else}
<span class="fas fa-chevron-right"></span>
{/if}
Native OS Handlers & Folders
</span>
<span class="badge variant-filled-success">Active</span>
</button>
</h3>
<div
class="flex flex-col gap-2 p-2 items-center justify-start w-full"
class:hidden={!$events_loc.launcher.show_section__native_os}
>
<div class="grid grid-cols-1 gap-2 w-full">
<button
onclick={() => native.open_folder($ae_loc.local_file_cache_path)}
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500 justify-start"
>
<span class="fas fa-folder-open mr-2"></span> Open Local Cache
</button>
<button
onclick={() => native.open_folder($ae_loc.host_file_temp_path)}
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500 justify-start"
>
<span class="fas fa-folder-open mr-2"></span> Open Host Temp
</button>
<button
onclick={() => native.open_folder($ae_loc.recording_path)}
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500 justify-start"
>
<span class="fas fa-folder-open mr-2"></span> Open Recording Path
</button>
</div>
<!-- Presentation Remote Control (Phase 5) -->
<div class="w-full border-t border-surface-500/30 pt-2 mt-2 flex flex-col gap-2">
<div class="flex flex-row justify-between items-center px-1">
<label class="text-[10px] font-bold uppercase opacity-70">Presentation Remote:</label>
<select bind:value={remote_app} class="select select-sm py-0 h-6 text-[10px] w-24 preset-tonal-surface">
<option value="powerpoint">PowerPoint</option>
<option value="keynote">Keynote</option>
</select>
</div>
<div class="grid grid-cols-4 gap-1">
<button onclick={() => handle_remote_control('prev')} class="btn btn-sm preset-tonal-secondary" title="Previous Slide">
<span class="fas fa-step-backward"></span>
</button>
<button onclick={() => handle_remote_control('start')} class="btn btn-sm preset-tonal-success" title="Start/Resume Slideshow">
<span class="fas fa-play"></span>
</button>
<button onclick={() => handle_remote_control('stop')} class="btn btn-sm preset-tonal-error" title="Stop Slideshow">
<span class="fas fa-stop"></span>
</button>
<button onclick={() => handle_remote_control('next')} class="btn btn-sm preset-tonal-secondary" title="Next Slide">
<span class="fas fa-step-forward"></span>
</button>
</div>
{#if remote_status}
<div class="text-[9px] text-center italic animate-pulse">{remote_status}</div>
{/if}
</div>
<div class="w-full border-t border-surface-500/30 pt-2 mt-2 flex flex-col gap-2">
<div class="flex flex-col gap-1">
<label class="text-[10px] opacity-70 ml-1">Run Manual Command:</label>
<div class="flex gap-1">
<input
type="text"
bind:value={$events_sess.launcher.manual_cmd}
placeholder="e.g. ls -la or whoami"
class="input input-sm grow text-[10px] preset-tonal-surface"
/>
<button
onclick={async () => {
test_cmd_result = 'Running...';
const res = await native.run_cmd({ cmd: $events_sess.launcher.manual_cmd || 'whoami && uptime' });
if (res && typeof res === 'object') {
test_cmd_result = (res as any).stdout || (res as any).error || 'No Output';
if ((res as any).stderr) test_cmd_result += `\nStderr: ${(res as any).stderr}`;
} else {
test_cmd_result = String(res);
}
}}
class="btn btn-sm preset-filled-secondary hover:preset-filled-primary-500 text-[10px]"
>
Run
</button>
</div>
</div>
{#if test_cmd_result}
<div class="relative">
<pre class="text-[9px] bg-black text-green-500 p-2 mt-1 overflow-x-auto rounded w-full border border-surface-500/50 min-h-12 max-h-32 shadow-inner">{test_cmd_result}</pre>
<button
onclick={() => test_cmd_result = ''}
class="absolute top-2 right-2 text-white/30 hover:text-white text-[8px]"
>Clear</button>
</div>
{/if}
</div>
</div>
</section>
{/if}

View File

@@ -0,0 +1,65 @@
<script lang="ts">
import { events_loc } from '$lib/stores/ae_events_stores';
</script>
<section
class:preset-outlined-warning-300-700={$events_loc.launcher.show_section__screen_saver}
class="screen_saver w-full preset-outlined-surface-300-700 transition-all"
>
<h3 class="text-center mb-2 text-sm font-semibold w-full">
<button
onclick={() => {
$events_loc.launcher.show_section__screen_saver =
!$events_loc.launcher.show_section__screen_saver;
}}
class="btn btn-sm w-full justify-between"
>
<span>
{#if $events_loc.launcher.show_section__screen_saver}
<span class="fas fa-chevron-down"></span>
{:else}
<span class="fas fa-chevron-right"></span>
{/if}
Screen Saver
</span>
{$events_loc.launcher.idle_timer
? ($events_loc.launcher.idle_timer / 60000).toPrecision(4) + ' min'
: '-- not set --'}
</button>
</h3>
<div
class="flex flex-col gap-1 items-center justify-start w-full"
class:hidden={!$events_loc.launcher.show_section__screen_saver}
>
<label class="flex flex-row gap-1 items-center justify-start text-sm">
<span class="w-36">Idle Time (ms):</span>
<input
type="number"
min={3000}
bind:value={$events_loc.launcher.idle_timer}
class="input input-sm w-28 text-right preset-tonal-surface"
/>
</label>
<label class="flex flex-row gap-1 items-center justify-start text-sm">
<span class="w-36">Cycle Check (ms):</span>
<input
type="number"
min={500}
bind:value={$events_loc.launcher.idle_cycle}
class="input input-sm w-28 text-right preset-tonal-surface"
/>
</label>
<label class="flex flex-row gap-1 items-center justify-start text-sm">
<span class="w-36">Image Change Period (ms):</span>
<input
type="number"
min={750}
bind:value={$events_loc.launcher.idle_loop_period}
class="input input-sm w-28 text-right preset-tonal-surface"
/>
</label>
</div>
</section>

View File

@@ -0,0 +1,85 @@
<script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
</script>
{#if $ae_loc.is_native}
<section
class:preset-outlined-warning-300-700={$events_loc.launcher.show_section__sync_timers}
class="sync_timers w-full preset-outlined-surface-300-700 transition-all mb-2"
>
<h3 class="text-center mb-2 text-sm font-semibold w-full">
<button
onclick={() => {
$events_loc.launcher.show_section__sync_timers =
!$events_loc.launcher.show_section__sync_timers;
}}
class="btn btn-sm w-full justify-between"
>
<span>
{#if $events_loc.launcher.show_section__sync_timers}
<span class="fas fa-chevron-down"></span>
{:else}
<span class="fas fa-chevron-right"></span>
{/if}
Native Sync Timers
</span>
</button>
</h3>
<div
class="flex flex-col gap-1 items-center justify-start w-full p-2"
class:hidden={!$events_loc.launcher.show_section__sync_timers}
>
{#if $ae_loc.native_device}
<label class="flex flex-row gap-1 items-center justify-start text-xs w-full">
<span class="grow opacity-70">Event Sync (ms):</span>
<input
type="number"
bind:value={$ae_loc.native_device.check_event_loop_period}
class="input input-sm w-24 text-right preset-tonal-surface"
/>
</label>
<label class="flex flex-row gap-1 items-center justify-start text-xs w-full">
<span class="grow opacity-70">Device Heartbeat (ms):</span>
<input
type="number"
bind:value={$ae_loc.native_device.check_event_device_loop_period}
class="input input-sm w-24 text-right preset-tonal-surface"
/>
</label>
<label class="flex flex-row gap-1 items-center justify-start text-xs w-full">
<span class="grow opacity-70">Location Refresh (ms):</span>
<input
type="number"
bind:value={$ae_loc.native_device.check_event_location_loop_period}
class="input input-sm w-24 text-right preset-tonal-surface"
/>
</label>
<label class="flex flex-row gap-1 items-center justify-start text-xs w-full">
<span class="grow opacity-70">Session Scan (ms):</span>
<input
type="number"
bind:value={$ae_loc.native_device.check_event_session_loop_period}
class="input input-sm w-24 text-right preset-tonal-surface"
/>
</label>
<label class="flex flex-row gap-1 items-center justify-start text-xs w-full border-t border-surface-500/20 pt-1 mt-1">
<span class="grow font-semibold text-primary-500">Hash Prefix Length:</span>
<input
type="number"
min="1"
max="8"
bind:value={$ae_loc.native_device.hash_prefix_length}
class="input input-sm w-24 text-right preset-tonal-surface font-bold"
/>
</label>
<div class="text-[9px] text-gray-500 mt-1 italic w-full text-right">
* Prefix: {($ae_loc.native_device.hash_prefix_length || 2)} chars. Reload required.
</div>
{:else}
<div class="text-xs text-error-500 italic">No device config hydrated.</div>
{/if}
</div>
</section>
{/if}

File diff suppressed because it is too large Load Diff