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
|
// Run raw command
|
||||||
// Updated 2022-05-07
|
// Updated 2026-01-26
|
||||||
exports.run_cmd = async function ({
|
exports.run_cmd = async function ({
|
||||||
cmd = null,
|
cmd = null,
|
||||||
return_stdout = null,
|
return_stdout = null,
|
||||||
@@ -1342,12 +1342,18 @@ exports.run_cmd = async function ({
|
|||||||
}) {
|
}) {
|
||||||
console.log('*** Electron framework export: run_cmd() ***');
|
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;
|
let result;
|
||||||
|
|
||||||
if (!sync) {
|
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) throw err;
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log('Error:', err);
|
console.log('Error:', err);
|
||||||
@@ -1366,7 +1372,7 @@ exports.run_cmd = async function ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
result = child_process.execSync(cmd, (err, stdout, stdin) => {
|
result = child_process.execSync(cleaned_cmd, (err, stdout, stdin) => {
|
||||||
// if (err) throw err;
|
// if (err) throw err;
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log('Error:', err);
|
console.log('Error:', err);
|
||||||
@@ -1391,16 +1397,22 @@ exports.run_cmd = async function ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Run raw command sync
|
// 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 }) {
|
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;
|
let stdout;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
stdout = child_process.execSync(cmd, { encoding: 'utf8' });
|
stdout = child_process.execSync(cleaned_cmd, { encoding: 'utf8' });
|
||||||
console.log('Std Out:', stdout);
|
console.log('Std Out:', stdout);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error:', 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');
|
console.log('Finished and returning true');
|
||||||
return 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
|
// Run raw command
|
||||||
// Updated 2022-05-25
|
// Updated 2026-01-26
|
||||||
exports.get_device_info = async function () {
|
exports.get_device_info = async function () {
|
||||||
console.log('*** Electron framework export: get_device_info() ***');
|
console.log('*** Electron framework export: get_device_info() ***');
|
||||||
|
|
||||||
@@ -1466,10 +1448,133 @@ exports.get_device_info = async function () {
|
|||||||
data['uptime'] = os.uptime();
|
data['uptime'] = os.uptime();
|
||||||
data['version'] = os.version();
|
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);
|
console.log(data);
|
||||||
return 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
|
// For loading JS file
|
||||||
function loadJS() {
|
function loadJS() {
|
||||||
// Gives -1 when the given input is not in the string
|
// 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)
|
* Specialized Presentation Launcher (Phase 5)
|
||||||
|
|
||||||
* Handles platform-specific application selection (LibreOffice on Linux,
|
* Handles platform-specific application selection (LibreOffice on Linux,
|
||||||
|
|
||||||
* PowerPoint/Keynote on macOS).
|
* PowerPoint/Keynote on macOS).
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export async function launch_presentation({
|
export async function launch_presentation({
|
||||||
|
|
||||||
path,
|
path,
|
||||||
|
|
||||||
app = 'default',
|
app = 'default',
|
||||||
|
|
||||||
os = 'auto',
|
os = 'auto',
|
||||||
|
|
||||||
log_lvl = 0
|
log_lvl = 0
|
||||||
|
|
||||||
}: {
|
}: {
|
||||||
|
|
||||||
path: string,
|
path: string,
|
||||||
|
|
||||||
app?: 'default' | 'powerpoint' | 'keynote' | 'libreoffice',
|
app?: 'default' | 'powerpoint' | 'keynote' | 'libreoffice',
|
||||||
|
|
||||||
os?: 'auto' | 'linux' | 'darwin' | 'win32',
|
os?: 'auto' | 'linux' | 'darwin' | 'win32',
|
||||||
|
|
||||||
log_lvl?: number
|
log_lvl?: number
|
||||||
|
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
if (!native) return { success: false, error: 'Native bridge not available' };
|
if (!native) return { success: false, error: 'Native bridge not available' };
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 1. Detect OS if set to auto
|
// 1. Detect OS if set to auto
|
||||||
|
|
||||||
let platform = os;
|
let platform = os;
|
||||||
|
|
||||||
if (platform === 'auto') {
|
if (platform === 'auto') {
|
||||||
|
|
||||||
const info = await get_device_info();
|
const info = await get_device_info();
|
||||||
|
|
||||||
platform = info?.platform || 'linux';
|
platform = info?.platform || 'linux';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 2. Prefer the Native Bridge implementation (Atomic Copy-and-Launch)
|
// 2. Prefer the Native Bridge implementation (Atomic Copy-and-Launch)
|
||||||
|
|
||||||
// This delegates to the hardened logic in electron_native.js
|
// This delegates to the hardened logic in electron_native.js
|
||||||
|
|
||||||
if (native.launch_presentation) {
|
if (native.launch_presentation) {
|
||||||
|
|
||||||
if (log_lvl) console.log('Relay: Using native.launch_presentation');
|
if (log_lvl) console.log('Relay: Using native.launch_presentation');
|
||||||
|
|
||||||
return await native.launch_presentation({ path, app, os_platform: platform });
|
return await native.launch_presentation({ path, app, os_platform: platform });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 3. Relay-side Fallback (Mock/Legacy)
|
// 3. Relay-side Fallback (Mock/Legacy)
|
||||||
|
|
||||||
// Manually resolve placeholders using all available context
|
// Manually resolve placeholders using all available context
|
||||||
|
|
||||||
const info = await get_device_info();
|
const info = await get_device_info();
|
||||||
|
|
||||||
const loc = get(ae_loc);
|
const loc = get(ae_loc);
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
|
||||||
// Attempt to find home/tmp from bridge info or local hydrated store
|
// 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);
|
||||||
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}`);
|
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
// 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]')) {
|
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 });
|
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' };
|
return { success: false, error: 'Could not resolve path placeholders' };
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (platform === 'linux') {
|
if (platform === 'linux') {
|
||||||
|
|
||||||
if (log_lvl) console.log(`Relay: Launching LibreOffice on Linux for path: ${cleaned_path}`);
|
if (log_lvl) console.log(`Relay: Launching LibreOffice on Linux for path: ${cleaned_path}`);
|
||||||
|
|
||||||
return await run_cmd({ cmd: `libreoffice --impress "${cleaned_path}"` });
|
return await run_cmd({ cmd: `libreoffice --impress "${cleaned_path}"` });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (platform === 'darwin') {
|
if (platform === 'darwin') {
|
||||||
|
|
||||||
if (app === 'keynote') {
|
if (app === 'keynote') {
|
||||||
|
|
||||||
return await run_osascript(`tell application "Keynote" to open POSIX file "${cleaned_path}"`);
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
event_file_open: {}, // This is from the older Launcher.
|
||||||
native: {},
|
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__title: '',
|
||||||
modal__open_event_file_id: false,
|
modal__open_event_file_id: false,
|
||||||
modal__event_file_obj: null,
|
modal__event_file_obj: null,
|
||||||
|
|||||||
@@ -43,6 +43,12 @@
|
|||||||
if (info) {
|
if (info) {
|
||||||
$ae_loc.home_directory = info.home_directory;
|
$ae_loc.home_directory = info.home_directory;
|
||||||
$ae_loc.tmp_directory = info.tmp_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 });
|
if (log_lvl) console.log('Sync: Native OS metadata hydrated.', { home: info.home_directory });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -140,85 +140,295 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Sync Loop Timers Section -->
|
<!-- Sync Loop Timers Section -->
|
||||||
<section
|
|
||||||
class:preset-outlined-warning-300-700={$events_loc.launcher.show_section__sync_timers}
|
<section
|
||||||
class="sync_timers w-full preset-outlined-surface-300-700 transition-all mb-2"
|
|
||||||
>
|
class:preset-outlined-warning-300-700={$events_loc.launcher.show_section__sync_timers}
|
||||||
<h3 class="text-center mb-2 text-sm font-semibold w-full">
|
|
||||||
<button
|
class="sync_timers w-full preset-outlined-surface-300-700 transition-all mb-2"
|
||||||
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">
|
<h3 class="text-center mb-2 text-sm font-semibold w-full">
|
||||||
<span class="grow opacity-70">Event Sync (ms):</span>
|
|
||||||
<input
|
<button
|
||||||
type="number"
|
|
||||||
bind:value={$ae_loc.native_device.check_event_loop_period}
|
onclick={() => {
|
||||||
class="input input-sm w-24 text-right preset-tonal-surface"
|
|
||||||
/>
|
$events_loc.launcher.show_section__sync_timers =
|
||||||
</label>
|
|
||||||
<label class="flex flex-row gap-1 items-center justify-start text-xs w-full">
|
!$events_loc.launcher.show_section__sync_timers;
|
||||||
<span class="grow opacity-70">Device Heartbeat (ms):</span>
|
|
||||||
<input
|
}}
|
||||||
type="number"
|
|
||||||
bind:value={$ae_loc.native_device.check_event_device_loop_period}
|
class="btn btn-sm w-full justify-between"
|
||||||
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>
|
||||||
<span class="grow opacity-70">Location Refresh (ms):</span>
|
|
||||||
<input
|
{#if $events_loc.launcher.show_section__sync_timers}
|
||||||
type="number"
|
|
||||||
bind:value={$ae_loc.native_device.check_event_location_loop_period}
|
<span class="fas fa-chevron-down"></span>
|
||||||
class="input input-sm w-24 text-right preset-tonal-surface"
|
|
||||||
/>
|
{:else}
|
||||||
</label>
|
|
||||||
<label class="flex flex-row gap-1 items-center justify-start text-xs w-full">
|
<span class="fas fa-chevron-right"></span>
|
||||||
<span class="grow opacity-70">Session Scan (ms):</span>
|
|
||||||
<input
|
{/if}
|
||||||
type="number"
|
|
||||||
bind:value={$ae_loc.native_device.check_event_session_loop_period}
|
Native Sync Timers
|
||||||
class="input input-sm w-24 text-right preset-tonal-surface"
|
|
||||||
/>
|
</span>
|
||||||
</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">
|
</button>
|
||||||
<span class="grow font-semibold text-primary-500">Hash Prefix Length:</span>
|
|
||||||
<input
|
</h3>
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="8"
|
|
||||||
bind:value={$ae_loc.native_device.hash_prefix_length}
|
<div
|
||||||
class="input input-sm w-24 text-right preset-tonal-surface font-bold"
|
|
||||||
/>
|
class="flex flex-col gap-1 items-center justify-start w-full p-2"
|
||||||
</label>
|
|
||||||
<div class="text-[9px] text-gray-500 mt-1 italic w-full text-right">
|
class:hidden={!$events_loc.launcher.show_section__sync_timers}
|
||||||
* Prefix: {($ae_loc.native_device.hash_prefix_length || 2)} chars. Reload required.
|
|
||||||
|
>
|
||||||
|
|
||||||
|
{#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>
|
</div>
|
||||||
{:else}
|
|
||||||
<div class="text-xs text-error-500 italic">No device config hydrated.</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
<!-- Basic Native Info -->
|
||||||
</section>
|
|
||||||
|
{#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}
|
{/if}
|
||||||
|
|
||||||
<!-- <hr class="w-full my-2 border-1 border-gray-200 dark:border-gray-800 " /> -->
|
<!-- <hr class="w-full my-2 border-1 border-gray-200 dark:border-gray-800 " /> -->
|
||||||
|
|||||||
Reference in New Issue
Block a user