feat(native): harden launcher bridge and implement presentation-aware handover
- Upgraded LauncherBackgroundSync to force-hydrate OS metadata (home/tmp) on mount. - Hardened electron_relay.ts with robust placeholder resolution and global regex. - Restored safe handover by making native.launch_from_cache presentation-aware. - Integrated heartbeat and sync status into the formal Launcher Config UI. - Added comprehensive technical documentation for the 2026 native architecture.
This commit is contained in:
70
documentation/AETHER_NATIVE_APP_ELECTRON_NEW_2026.md
Normal file
70
documentation/AETHER_NATIVE_APP_ELECTRON_NEW_2026.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Aether Native Electron App - V3 Architecture (2026)
|
||||
|
||||
## 1. Overview
|
||||
The Aether Native App serves as the OS-level bridge for the SvelteKit frontend. It enables functionality that is normally restricted by browser sandboxing, such as local filesystem management, hardware telemetry, and direct control of third-party presentation software (PowerPoint, Keynote, LibreOffice).
|
||||
|
||||
---
|
||||
|
||||
## 2. The Three-Layer Architecture
|
||||
|
||||
### 2.1 The Engine (`electron_native.js`)
|
||||
* **Process:** Main Process (Node.js)
|
||||
* **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]`.
|
||||
|
||||
### 2.2 The Bridge (`preload.js`)
|
||||
* **Process:** Preload Script
|
||||
* **Role:** The security gatekeeper.
|
||||
* **Key Responsibilities:**
|
||||
* Uses Electron's `contextBridge` to securely expose specific 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`)
|
||||
* **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).
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
## 4. Native Tool Library (`window.aetherNative`)
|
||||
|
||||
### 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.
|
||||
|
||||
### OS Execution (The "Actuators")
|
||||
- `launch_presentation({path, app})`: Intelligent application selection.
|
||||
- **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.
|
||||
|
||||
### System Metadata
|
||||
- `get_device_info()`: Provides OS version, CPU/RAM stats, and absolute system paths.
|
||||
|
||||
---
|
||||
|
||||
## 5. Path Placeholder System
|
||||
To maintain cross-platform compatibility, the system uses a placeholder syntax for all paths:
|
||||
- `[home]`: Resolves to the user's home directory (e.g., `/home/scott` or `/Users/scott`).
|
||||
- `[tmp]`: Resolves to the system temporary directory.
|
||||
|
||||
**Resolution Logic:** All native handlers utilize a global regex (`/\[home\]/g`) to ensure these are converted to absolute paths before any disk or shell operation occurs.
|
||||
@@ -1333,7 +1333,7 @@ exports.run_osascript = async function ({
|
||||
};
|
||||
|
||||
// Run raw command
|
||||
// Updated 2022-05-07
|
||||
// Updated 2026-01-26
|
||||
exports.run_cmd = async function ({
|
||||
cmd = null,
|
||||
return_stdout = null,
|
||||
@@ -1342,12 +1342,18 @@ exports.run_cmd = async function ({
|
||||
}) {
|
||||
console.log('*** Electron framework export: run_cmd() ***');
|
||||
|
||||
console.log(`Command String: ${cmd}`);
|
||||
// Resolve placeholders in the command string
|
||||
let cleaned_cmd = cmd;
|
||||
if (cmd && typeof cmd === 'string') {
|
||||
cleaned_cmd = cmd.replace(/\[home\]/g, home_directory).replace(/\[tmp\]/g, tmp_directory);
|
||||
}
|
||||
|
||||
console.log(`Command String: ${cleaned_cmd}`);
|
||||
|
||||
let result;
|
||||
|
||||
if (!sync) {
|
||||
result = child_process.exec(cmd, (err, stdout, stdin) => {
|
||||
result = child_process.exec(cleaned_cmd, (err, stdout, stdin) => {
|
||||
// if (err) throw err;
|
||||
if (err) {
|
||||
console.log('Error:', err);
|
||||
@@ -1366,7 +1372,7 @@ exports.run_cmd = async function ({
|
||||
}
|
||||
});
|
||||
} else {
|
||||
result = child_process.execSync(cmd, (err, stdout, stdin) => {
|
||||
result = child_process.execSync(cleaned_cmd, (err, stdout, stdin) => {
|
||||
// if (err) throw err;
|
||||
if (err) {
|
||||
console.log('Error:', err);
|
||||
@@ -1391,16 +1397,22 @@ exports.run_cmd = async function ({
|
||||
};
|
||||
|
||||
// Run raw command sync
|
||||
// Updated 2022-05-07
|
||||
// Updated 2026-01-26
|
||||
exports.run_cmd_sync = function ({ cmd = null, return_stdout = null, return_stdin = null }) {
|
||||
console.log('*** Electron framework export: run_cmd() ***');
|
||||
console.log('*** Electron framework export: run_cmd_sync() ***');
|
||||
|
||||
console.log(`Command String: ${cmd}`);
|
||||
// Resolve placeholders in the command string
|
||||
let cleaned_cmd = cmd;
|
||||
if (cmd && typeof cmd === 'string') {
|
||||
cleaned_cmd = cmd.replace(/\[home\]/g, home_directory).replace(/\[tmp\]/g, tmp_directory);
|
||||
}
|
||||
|
||||
console.log(`Command String: ${cleaned_cmd}`);
|
||||
|
||||
let stdout;
|
||||
|
||||
try {
|
||||
stdout = child_process.execSync(cmd, { encoding: 'utf8' });
|
||||
stdout = child_process.execSync(cleaned_cmd, { encoding: 'utf8' });
|
||||
console.log('Std Out:', stdout);
|
||||
} catch (err) {
|
||||
console.error('Error:', err);
|
||||
@@ -1414,40 +1426,10 @@ exports.run_cmd_sync = function ({ cmd = null, return_stdout = null, return_stdi
|
||||
console.log('Finished and returning true');
|
||||
return true;
|
||||
}
|
||||
|
||||
// let result;
|
||||
// let stdout;
|
||||
// let stderr;
|
||||
// try {
|
||||
// let { stdout, stderr } = child_process.execSync(cmd, (err, stdout, stdin) => {
|
||||
// // if (err) throw err;
|
||||
// if (err) {
|
||||
// console.log('Error:', err);
|
||||
// return false;
|
||||
// };
|
||||
|
||||
// console.log('stdout:', stdout);
|
||||
// // console.log('stdin:', stdin);
|
||||
|
||||
// if (return_stdout) {
|
||||
// console.log('Finished and returning stdout');
|
||||
// return stdout;
|
||||
// } else {
|
||||
// console.log('Finished and returning true');
|
||||
// return true;
|
||||
// }
|
||||
// });
|
||||
// } catch (err) {
|
||||
// console.error(err);
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// console.log('Result:', result);
|
||||
// return result;
|
||||
};
|
||||
|
||||
// Run raw command
|
||||
// Updated 2022-05-25
|
||||
// Updated 2026-01-26
|
||||
exports.get_device_info = async function () {
|
||||
console.log('*** Electron framework export: get_device_info() ***');
|
||||
|
||||
@@ -1465,11 +1447,134 @@ exports.get_device_info = async function () {
|
||||
data['release'] = os.release();
|
||||
data['uptime'] = os.uptime();
|
||||
data['version'] = os.version();
|
||||
|
||||
// Add directory info for placeholder resolution in UI
|
||||
data['home_directory'] = home_directory;
|
||||
data['tmp_directory'] = tmp_directory;
|
||||
|
||||
console.log(data);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Atomic Copy-and-Launch (Phase 2/5)
|
||||
* Moves a file from the hashed cache to the operational temp directory
|
||||
* and triggers the system launcher.
|
||||
*/
|
||||
exports.launch_from_cache = async function ({
|
||||
cache_root,
|
||||
hash,
|
||||
temp_root,
|
||||
filename,
|
||||
hash_prefix_length = 2
|
||||
}) {
|
||||
console.log('*** Aether App Native export: launch_from_cache() ***');
|
||||
|
||||
// 1. Resolve Path Placeholders (using global regex)
|
||||
const clean_cache_root = cache_root.replace(/\[home\]/g, home_directory).replace(/\[tmp\]/g, tmp_directory);
|
||||
const clean_temp_root = temp_root.replace(/\[home\]/g, home_directory).replace(/\[tmp\]/g, tmp_directory);
|
||||
|
||||
const hash_filename = `${hash}.file`;
|
||||
const prefix = hash.substring(0, hash_prefix_length);
|
||||
const source_path = path.join(clean_cache_root, prefix, hash_filename);
|
||||
const dest_path = path.join(clean_temp_root, filename);
|
||||
|
||||
console.log(`Source: ${source_path}`);
|
||||
console.log(`Dest: ${dest_path}`);
|
||||
|
||||
try {
|
||||
// 2. Ensure temp directory exists
|
||||
if (!fs.existsSync(clean_temp_root)) {
|
||||
fs.mkdirSync(clean_temp_root, { recursive: true });
|
||||
}
|
||||
|
||||
// 3. Verify Source
|
||||
if (!fs.existsSync(source_path)) {
|
||||
throw new Error(`Source file not found in cache: ${source_path}`);
|
||||
}
|
||||
|
||||
// 4. Perform atomic copy
|
||||
fs.copyFileSync(source_path, dest_path);
|
||||
console.log('File copied to temp successfully.');
|
||||
|
||||
// 5. Trigger Specialized Launcher (if presentation)
|
||||
const ext = path.extname(filename).toLowerCase().replace('.', '');
|
||||
const is_pres = ['pptx', 'ppt', 'key', 'pdf', 'odp'].includes(ext);
|
||||
|
||||
if (is_pres) {
|
||||
return await exports.launch_presentation({
|
||||
path: dest_path,
|
||||
app: ext === 'key' ? 'keynote' : 'default'
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Default Fallback
|
||||
return await ipcRenderer.invoke('open_local_file', '', dest_path);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Launch Error:', err);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Specialized Presentation Launcher (Phase 5)
|
||||
* Handles platform-specific application selection (LibreOffice on Linux,
|
||||
* PowerPoint/Keynote on macOS).
|
||||
* Updated 2026-01-26
|
||||
*/
|
||||
exports.launch_presentation = async function ({
|
||||
path: raw_path,
|
||||
app = 'default',
|
||||
os_platform = 'auto'
|
||||
}) {
|
||||
console.log('*** Aether App Native export: launch_presentation() ***');
|
||||
|
||||
// Resolve placeholders if they exist in the incoming path (using global regex)
|
||||
let cleaned_path = raw_path
|
||||
.replace(/\[home\]/g, home_directory)
|
||||
.replace(/\[tmp\]/g, tmp_directory);
|
||||
|
||||
console.log(`Raw Path: ${raw_path}; Cleaned Path: ${cleaned_path}; App: ${app}; OS: ${os_platform}`);
|
||||
|
||||
// 1. Detect OS
|
||||
let platform = os_platform;
|
||||
if (platform === 'auto') {
|
||||
platform = os.platform();
|
||||
}
|
||||
|
||||
// 2. Handle Linux (LibreOffice Testing)
|
||||
if (platform === 'linux') {
|
||||
console.log(`Native: Launching LibreOffice on Linux for path: ${cleaned_path}`);
|
||||
const cmd = `libreoffice --impress "${cleaned_path}"`;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
child_process.exec(cmd, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error('LibreOffice Launch Error:', err);
|
||||
resolve({ success: false, error: err.message });
|
||||
} else {
|
||||
resolve({ success: true, stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Handle macOS (Production)
|
||||
if (platform === 'darwin') {
|
||||
if (app === 'keynote') {
|
||||
const script = `tell application "Keynote" to open POSIX file "${cleaned_path}"`;
|
||||
return exports.run_osascript({ cmd: script });
|
||||
}
|
||||
|
||||
// Default to shell open
|
||||
return ipcRenderer.invoke('open_local_file', '', cleaned_path);
|
||||
}
|
||||
|
||||
// 4. Default Fallback (Windows/Others)
|
||||
return ipcRenderer.invoke('open_local_file', '', cleaned_path);
|
||||
};
|
||||
|
||||
// For loading JS file
|
||||
function loadJS() {
|
||||
// Gives -1 when the given input is not in the string
|
||||
|
||||
@@ -70,165 +70,72 @@ export async function open_local_file_v2(path: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
* Specialized Presentation Launcher (Phase 5)
|
||||
|
||||
* Handles platform-specific application selection (LibreOffice on Linux,
|
||||
|
||||
* PowerPoint/Keynote on macOS).
|
||||
|
||||
*/
|
||||
|
||||
export async function launch_presentation({
|
||||
|
||||
path,
|
||||
|
||||
app = 'default',
|
||||
|
||||
os = 'auto',
|
||||
|
||||
log_lvl = 0
|
||||
|
||||
}: {
|
||||
|
||||
path: string,
|
||||
|
||||
app?: 'default' | 'powerpoint' | 'keynote' | 'libreoffice',
|
||||
|
||||
os?: 'auto' | 'linux' | 'darwin' | 'win32',
|
||||
|
||||
log_lvl?: number
|
||||
|
||||
}) {
|
||||
|
||||
if (!native) return { success: false, error: 'Native bridge not available' };
|
||||
|
||||
|
||||
|
||||
// 1. Detect OS if set to auto
|
||||
|
||||
let platform = os;
|
||||
|
||||
if (platform === 'auto') {
|
||||
|
||||
const info = await get_device_info();
|
||||
|
||||
platform = info?.platform || 'linux';
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 2. Prefer the Native Bridge implementation (Atomic Copy-and-Launch)
|
||||
|
||||
// This delegates to the hardened logic in electron_native.js
|
||||
|
||||
if (native.launch_presentation) {
|
||||
|
||||
if (log_lvl) console.log('Relay: Using native.launch_presentation');
|
||||
|
||||
return await native.launch_presentation({ path, app, os_platform: platform });
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 3. Relay-side Fallback (Mock/Legacy)
|
||||
|
||||
// Manually resolve placeholders using all available context
|
||||
|
||||
const info = await get_device_info();
|
||||
|
||||
const loc = get(ae_loc);
|
||||
|
||||
|
||||
|
||||
// Attempt to find home/tmp from bridge info or local hydrated store
|
||||
|
||||
// Attempt to find home/tmp from bridge info OR local hydrated store
|
||||
const home = info?.home_directory || loc.home_directory || loc.native_device?.home_directory || '';
|
||||
const tmp = info?.tmp_directory || loc.tmp_directory || loc.native_device?.tmp_directory || '';
|
||||
|
||||
if (log_lvl) console.log('Relay Debug:', { home, tmp, raw_path: path });
|
||||
|
||||
const home = info?.home_directory || loc.home_directory || loc.native_device?.home_directory || '';
|
||||
|
||||
|
||||
|
||||
const tmp = info?.tmp_directory || loc.tmp_directory || loc.native_device?.tmp_directory || '';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (log_lvl) console.log('Relay Debug:', { home, tmp, raw_path: path });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// CRITICAL: Resolve all instances of placeholders using global regex (fixed pattern)
|
||||
|
||||
|
||||
|
||||
let cleaned_path = path;
|
||||
|
||||
|
||||
|
||||
if (home) cleaned_path = cleaned_path.replace(/\[home\]/g, home);
|
||||
|
||||
|
||||
|
||||
if (tmp) cleaned_path = cleaned_path.replace(/\[tmp\]/g, tmp);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (log_lvl) console.log(`Relay Fallback: Resolving ${path} -> ${cleaned_path}`);
|
||||
|
||||
// CRITICAL: Resolve all instances of placeholders using global regex
|
||||
let cleaned_path = path;
|
||||
if (home) cleaned_path = cleaned_path.replace(/\[home\]/g, home);
|
||||
if (tmp) cleaned_path = cleaned_path.replace(/\[tmp\]/g, tmp);
|
||||
|
||||
if (log_lvl) console.log(`Relay Fallback: Resolving ${path} -> ${cleaned_path}`);
|
||||
|
||||
// If path still contains [home] or [tmp], it means we failed to resolve it.
|
||||
|
||||
// Stop here to prevent sending a broken path like /temp/... to the OS.
|
||||
|
||||
if (cleaned_path.includes('[home]') || cleaned_path.includes('[tmp]')) {
|
||||
|
||||
console.error('Relay Error: Could not resolve path placeholders. Home or Tmp directory unknown.', { home, tmp });
|
||||
|
||||
return { success: false, error: 'Could not resolve path placeholders' };
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (platform === 'linux') {
|
||||
|
||||
if (log_lvl) console.log(`Relay: Launching LibreOffice on Linux for path: ${cleaned_path}`);
|
||||
|
||||
return await run_cmd({ cmd: `libreoffice --impress "${cleaned_path}"` });
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (platform === 'darwin') {
|
||||
|
||||
if (app === 'keynote') {
|
||||
|
||||
return await run_osascript(`tell application "Keynote" to open POSIX file "${cleaned_path}"`);
|
||||
|
||||
}
|
||||
|
||||
return await open_local_file_v2(cleaned_path);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
return await open_local_file_v2(cleaned_path);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,6 +400,18 @@ const events_session_data_struct: key_val = {
|
||||
event_file_open: {}, // This is from the older Launcher.
|
||||
native: {},
|
||||
|
||||
// Shared observability for Sync Monitor and Config Drawer
|
||||
sync_stats: {
|
||||
total: 0,
|
||||
cached: 0,
|
||||
missing: 0,
|
||||
currently_syncing: null
|
||||
},
|
||||
heartbeat_info: {
|
||||
last_timestamp: null,
|
||||
status: 'pending'
|
||||
},
|
||||
|
||||
modal__title: '',
|
||||
modal__open_event_file_id: false,
|
||||
modal__event_file_obj: null,
|
||||
|
||||
@@ -43,6 +43,12 @@
|
||||
if (info) {
|
||||
$ae_loc.home_directory = info.home_directory;
|
||||
$ae_loc.tmp_directory = info.tmp_directory;
|
||||
|
||||
// Also sync into native_device for redundancy
|
||||
if (!$ae_loc.native_device) $ae_loc.native_device = {};
|
||||
$ae_loc.native_device.home_directory = info.home_directory;
|
||||
$ae_loc.native_device.tmp_directory = info.tmp_directory;
|
||||
|
||||
if (log_lvl) console.log('Sync: Native OS metadata hydrated.', { home: info.home_directory });
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -140,85 +140,295 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Sync Loop Timers Section -->
|
||||
<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>
|
||||
<!-- Sync Loop Timers Section -->
|
||||
|
||||
<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"
|
||||
|
||||
<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.
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
<!-- System Health Section -->
|
||||
|
||||
<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>
|
||||
{:else}
|
||||
<div class="text-xs text-error-500 italic">No device config hydrated.</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
<!-- 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}
|
||||
|
||||
<!-- <hr class="w-full my-2 border-1 border-gray-200 dark:border-gray-800 " /> -->
|
||||
|
||||
Reference in New Issue
Block a user