4 Commits

Author SHA1 Message Date
Scott Idem
054775b0f8 feat(launcher): skip wallpaper gsettings on Linux, show dev popup instead
Running gsettings on the dev workstation resets monitors on every test cycle.
Linux Electron handler now returns linux_test_mode:true with would_run details
instead of applying. Svelte cfg component shows a debug popup (mirrors Native
Test Mode style). Background sync logs to console and leaves applied-URL unset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 18:54:56 -04:00
Scott Idem
af28fba263 feat(launcher): restore macOS default wallpaper + external-only apply fix
- electron_relay: add restore_macos_default_wallpaper() — uses run_osascript
  to find first .heic in /System/Library/Desktop Pictures/ (version-agnostic)
- wallpaper cfg: Restore macOS Default button (native or edit_mode); clears
  applied-URL tracking so next configured URL re-applies correctly
- wallpaper cfg: fix Apply Now / Save & Apply enabled when only external URL
  is filled; display target becomes 'external' to leave built-in unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 18:37:15 -04:00
Scott Idem
17e522f826 feat(launcher): wallpaper auto-apply from device config
Stores wallpaper URL(s) in event_device.other_json.launcher.wallpaper and
auto-applies on all native Launcher instances within one heartbeat cycle (~60s),
eliminating manual per-Mac setup at events.

- electron_relay: typed set_wallpaper wrapper (url, url_external, display, auth headers)
- launcher_defaults: wallpaper_applied_url tracking + section_state__wallpaper
- launcher_cfg_wallpaper: new config section — save to device config + apply now
- launcher_cfg: add wallpaper section to device tab
- launcher_background_sync: auto-apply if config URL changed since last apply;
  external-only config targets only the secondary display, leaving built-in unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 18:31:39 -04:00
Scott Idem
324f3a97ac Doc update 2026-05-13 17:47:49 -04:00
6 changed files with 559 additions and 14 deletions

View File

@@ -20,19 +20,12 @@ guessing defaults.
- [x] `native:copy-from-cache-to-temp` primitive added — copy to tmp, caller decides launch
- [x] `native:launch-from-cache` executes a provided `native_template` string — AppleScript or `shell:` prefix; no Electron-side fallback
- [x] `get_launch_profile()` in `launcher_file_cont.svelte` reads from device config then event config; resolves to a `native_template` string and passes it to `launch_from_cache`
- [x] Built-in Launcher defaults refactored into canonical profile names plus extension aliases
- [x] Built-in Launcher defaults refactored into canonical profile names plus extension aliases (`ae_launcher__default_launch_profiles.ts`)
- [x] Device-level Launch Timing section added under Launcher Configuration → Device, with per-profile `launch_profiles[profile].post_delay_ms` overrides
- [x] **URL file launch support (2026-05-13)**`event_file.extension = 'URL'` (or filename starting with `http://`/`https://`) is treated as a non-downloaded URL. Background sync skips URL files so they are never treated as cacheable hosted files. Shared `AE_Comp_Hosted_Files_Download_Button` now hard-bypasses the download path for URL records. In native mode, `handle_open_file()` routes to `native.open_external({ url, app: 'chrome' })` with `'default'` fallback. Health section crash fixed (guarded `native_device` against undefined in preview/edit mode).
**Svelte-side migration — remaining before May 26:**
- [ ] **[Launcher] Built-in Svelte default profiles** — move the built-in presentation/media
policy objects into a Svelte constants file (e.g. `ae_launcher__default_launch_profiles.ts`).
Use canonical profile names plus extension aliases so the media family does not repeat the
same VLC config for every file type. Cover the core macOS set (`pptx`, `ppt`, `key`, `odp`,
`pdf`), the media set (`mp4`, `mkv`, `mp3`, and related media types), the Windows /
Parallels variants (`pptxwin`, `pptwin`, `odpwin`, `pdfwin`), and the URL/web-based
presentation path. Priority: `get_launch_profile()` already checks device config and event
config; add a 3rd fallback to these Svelte defaults before returning `null`. Keep the
fallback in Svelte, not in Electron.
- [x] **[Launcher] Built-in Svelte default profiles (2026-05-11)** — canonical profile constants live in `ae_launcher__default_launch_profiles.ts` with extension aliases and a `resolve_launch_profile()` 3-step fallback (device config → event config → built-in defaults). Covers macOS (`pptx`, `ppt`, `key`, `odp`, `pdf`), media (`mp4`, `mkv`, `mp3`, etc.), Windows/Parallels variants, and URL path.
- [ ] **[Launcher] Composable open flow** — refactor `handle_open_file()` to use
`copy_from_cache_to_temp` + `run_osascript` / `run_cmd` directly instead of the all-in-one
`launch_from_cache`. Finer error handling at each step (verify copy succeeded before

View File

@@ -384,13 +384,65 @@ export async function control_presentation({
// 4. System Management (Phase 5+)
export async function set_wallpaper({ path }: { path: string }) {
export async function set_wallpaper({
path,
url,
url_external,
display = 'all',
api_key,
account_id
}: {
/** Local file path (existing behavior). */
path?: string;
/** HTTPS URL — downloaded to ~/Library/Caches/OSIT/wallpaper/ before applying. */
url?: string;
/** Optional separate URL for the external/projector display only. */
url_external?: string;
/** Which display(s) to target. Defaults to 'all'. */
display?: 'all' | 'primary' | 'external';
/** Aether API key passed as x-aether-api-key header on the download request. */
api_key?: string;
/** Aether account ID passed as x-account-id header on the download request. */
account_id?: string;
}) {
if (!native || !native.set_wallpaper)
return {
success: false,
error: 'Native handler set_wallpaper not available'
};
return await native.set_wallpaper({ path });
return await native.set_wallpaper({ path, url, url_external, display, api_key, account_id });
}
/**
* Restores the macOS default wallpaper on all displays.
* Scans /System/Library/Desktop Pictures/ for the first .heic file — works across
* all recent macOS versions without needing to know the version name.
* No-op on non-macOS (Linux/Windows return success:false from run_osascript).
*/
export async function restore_macos_default_wallpaper(
display: 'all' | 'primary' | 'external' = 'all'
): Promise<{ success: boolean; error?: string }> {
const display_target =
display === 'primary'
? 'tell desktop 1'
: display === 'external'
? 'tell desktop 2'
: 'tell every desktop';
const script = `
set pic_path to do shell script "ls '/System/Library/Desktop Pictures/'*.heic 2>/dev/null | head -1"
if pic_path is "" then
error "No default macOS wallpaper (.heic) found in /System/Library/Desktop Pictures/"
end if
tell application "System Events"
${display_target}
set picture to pic_path
end tell
end tell
`.trim();
const result = await run_osascript(script);
return result ?? { success: false, error: 'Native bridge not available' };
}
export async function update_app(args: {

View File

@@ -39,12 +39,17 @@ export interface LauncherLocState {
section_state__health: SectionState;
section_state__native_os: SectionState;
section_state__launch_timing: SectionState;
section_state__wallpaper: SectionState;
section_state__sync_timers: SectionState;
section_state__updates: SectionState;
section_state__controller: SectionState;
section_state__screen_saver: SectionState;
section_state__app_modes: SectionState;
section_state__local_actions: SectionState;
/** URL last successfully applied as wallpaper on this device. Used to detect remote config changes. */
wallpaper_applied_url: string | null;
/** URL last applied to the external display (when url_external is configured). */
wallpaper_applied_url_external: string | null;
datetime_format: string;
time_format: string;
time_hours: 12 | 24;
@@ -160,6 +165,7 @@ export const launcher_loc_defaults: LauncherLocState = {
section_state__health: 'auto',
section_state__native_os: 'collapsed',
section_state__launch_timing: 'collapsed',
section_state__wallpaper: 'collapsed',
section_state__sync_timers: 'collapsed',
section_state__updates: 'collapsed',
section_state__controller: 'auto',
@@ -208,7 +214,9 @@ export const launcher_loc_defaults: LauncherLocState = {
controller: 'local',
controller_group_code: 'launcher-00',
controller_client_id: null,
native_test_mode: false
native_test_mode: false,
wallpaper_applied_url: null,
wallpaper_applied_url_external: null
// controller_cmd: null,
// controller_trigger_send: null,
};

View File

@@ -0,0 +1,410 @@
<script lang="ts">
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import { events_loc } from '$lib/stores/ae_events_stores';
import * as native from '$lib/electron/electron_relay';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { FlaskConical, Image, Monitor, RotateCcw, Save, Zap } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
type NativeDeviceLike = {
event_device_id?: string;
id?: string;
other_json?: string | Record<string, unknown> | null;
[key: string]: unknown;
};
let url_input = $state('');
let url_external_input = $state('');
let save_status = $state('');
let apply_status = $state('');
let restore_status = $state('');
let last_device_id: string | null = null;
let linux_test_popup_open = $state(false);
let linux_test_popup_data = $state<Record<string, unknown> | null>(null);
function get_native_device(): NativeDeviceLike | null {
const store_loc = $ae_loc as { native_device?: NativeDeviceLike };
return store_loc.native_device ?? null;
}
function get_device_id(): string | null {
const native_device = get_native_device();
return native_device?.event_device_id ?? native_device?.id ?? null;
}
function parse_other_json(raw_json: unknown): Record<string, unknown> {
if (!raw_json) return {};
if (typeof raw_json === 'object') return { ...(raw_json as Record<string, unknown>) };
if (typeof raw_json !== 'string') return {};
try {
const parsed = JSON.parse(raw_json);
return parsed && typeof parsed === 'object' ? { ...parsed } : {};
} catch {
return {};
}
}
function get_configured_wallpaper(): { url?: string; url_external?: string } {
const native_device = get_native_device();
const other_json = parse_other_json(native_device?.other_json);
const launcher_cfg = (other_json.launcher as Record<string, unknown> | undefined) ?? {};
return (launcher_cfg.wallpaper as { url?: string; url_external?: string } | undefined) ?? {};
}
function sync_from_device() {
const device_id = get_device_id();
if (device_id === last_device_id) return;
last_device_id = device_id;
const configured = get_configured_wallpaper();
url_input = configured.url ?? '';
url_external_input = configured.url_external ?? '';
}
sync_from_device();
$effect(() => {
sync_from_device();
});
function set_save_status(msg: string) {
save_status = msg;
setTimeout(() => { if (save_status === msg) save_status = ''; }, 3000);
}
function set_apply_status(msg: string) {
apply_status = msg;
setTimeout(() => { if (apply_status === msg) apply_status = ''; }, 4000);
}
async function handle_save() {
const device_id = get_device_id();
if (!device_id) { set_save_status('No device loaded'); return; }
const native_device = get_native_device();
const other_json_obj = parse_other_json(native_device?.other_json);
const launcher_cfg: Record<string, unknown> = {
...((other_json_obj.launcher as Record<string, unknown> | undefined) ?? {})
};
const wallpaper: Record<string, string> = {};
if (url_input.trim()) wallpaper.url = url_input.trim();
if (url_external_input.trim()) wallpaper.url_external = url_external_input.trim();
if (Object.keys(wallpaper).length > 0) {
launcher_cfg.wallpaper = wallpaper;
} else {
delete launcher_cfg.wallpaper;
}
if (Object.keys(launcher_cfg).length > 0) {
other_json_obj.launcher = launcher_cfg;
} else {
delete other_json_obj.launcher;
}
const other_json = JSON.stringify(other_json_obj);
set_save_status('Saving...');
const result = await events_func.update_ae_obj__event_device({
api_cfg: $ae_api,
event_device_id: device_id,
data_kv: { other_json },
log_lvl: 0
});
if (!result) { set_save_status('Save failed'); return; }
const store_loc = $ae_loc as { native_device?: NativeDeviceLike };
store_loc.native_device = { ...get_native_device(), ...result, other_json };
set_save_status('Saved');
}
async function handle_apply() {
const url = url_input.trim();
const url_ext = url_external_input.trim();
if (!url && !url_ext) { set_apply_status('Enter a URL first'); return; }
// If only external is set, target only that display so the built-in stays unchanged.
const display = !url && url_ext ? 'external' : 'all';
set_apply_status('Downloading & applying...');
const result = await native.set_wallpaper({
url: url || undefined,
url_external: url_ext || undefined,
display,
api_key: $ae_api.api_secret_key,
account_id: String($ae_api.account_id ?? '')
});
if (result?.success && (result as any).linux_test_mode) {
linux_test_popup_data = result as Record<string, unknown>;
linux_test_popup_open = true;
set_apply_status('Linux dev mode — see popup');
} else if (result?.success) {
$events_loc.launcher.wallpaper_applied_url = url || null;
$events_loc.launcher.wallpaper_applied_url_external = url_ext || null;
set_apply_status('Applied ✓');
} else {
set_apply_status(`Error: ${(result as any)?.error ?? 'Unknown error'}`);
}
}
async function handle_restore_default() {
restore_status = 'Restoring...';
const result = await native.restore_macos_default_wallpaper('all');
if (result?.success) {
// Clear tracked applied URL so the next config URL re-applies correctly.
$events_loc.launcher.wallpaper_applied_url = null;
$events_loc.launcher.wallpaper_applied_url_external = null;
restore_status = 'Restored ✓';
} else {
restore_status = `Error: ${(result as any)?.error ?? 'Unknown error'}`;
}
setTimeout(() => { if (restore_status.startsWith('Restored') || restore_status.startsWith('Error')) restore_status = ''; }, 4000);
}
async function handle_save_and_apply() {
await handle_save();
if (save_status !== 'Save failed' && save_status !== 'No device loaded') {
await handle_apply();
}
}
const configured_url = $derived(get_configured_wallpaper().url ?? '');
const is_applied = $derived(
!!url_input.trim() &&
$events_loc.launcher.wallpaper_applied_url === url_input.trim()
);
const section_description = $derived(
configured_url
? is_applied
? 'Configured • Applied ✓'
: 'Configured • Pending'
: 'Not configured'
);
</script>
<Launcher_Cfg_Section
title="Wallpaper"
icon={Image}
bind:state={$events_loc.launcher.section_state__wallpaper}
{on_expand}
description={section_description}>
{#if $ae_loc.edit_mode && !$ae_loc.is_native}
<div
class="bg-warning-500/10 border-warning-500/30 mb-1 flex items-center gap-2 rounded-lg border px-2 py-1.5">
<FlaskConical size="0.75em" class="text-warning-500" />
<span class="text-warning-500 text-[9px] font-bold tracking-wide uppercase">
Dev Preview — Apply requires Electron; Save works from any device
</span>
</div>
{/if}
<div class="flex flex-col gap-3">
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Desktop Background Images
</p>
<p class="ml-1 text-[10px] leading-snug opacity-60">
Paste an HTTPS image URL. Save writes it to this device's config so all
devices auto-apply on their next heartbeat. Apply sets it immediately on
this machine.
</p>
{#if !get_device_id()}
<div
class="bg-warning-500/10 border-warning-500/20 rounded-lg border px-2 py-2 text-[9px] opacity-80">
No device record loaded — Save requires a native device config.
</div>
{/if}
<!-- All Displays (primary URL) -->
<section class="border-surface-500/10 rounded-lg border p-2">
<div class="mb-1.5 flex items-center gap-1.5">
<Monitor size="0.85em" class="opacity-50" />
<p class="text-[10px] font-bold uppercase tracking-wide">
All Displays
</p>
{#if is_applied}
<span
class="text-success-500 ml-auto text-[9px] font-bold">Applied ✓</span>
{:else if $events_loc.launcher.wallpaper_applied_url}
<span class="text-warning-500 ml-auto text-[9px] italic"
>Pending apply</span>
{/if}
</div>
<input
type="url"
bind:value={url_input}
placeholder="https://example.com/wallpaper.jpg"
class="input input-sm preset-tonal-surface mb-1.5 h-8 w-full text-[10px]" />
{#if $events_loc.launcher.wallpaper_applied_url && $events_loc.launcher.wallpaper_applied_url !== url_input.trim()}
<p class="text-[9px] italic opacity-40 truncate">
Applied: {$events_loc.launcher.wallpaper_applied_url}
</p>
{/if}
</section>
<!-- External Display (optional override) -->
<section class="border-surface-500/10 rounded-lg border p-2">
<div class="mb-1.5 flex items-center gap-1.5">
<Monitor size="0.85em" class="opacity-40" />
<p class="text-[10px] font-bold uppercase tracking-wide">
External / Projector
<span class="ml-1 font-normal normal-case opacity-50">(optional)</span>
</p>
</div>
<p class="mb-1.5 text-[9px] leading-snug opacity-50">
Leave blank to use the same image on all displays.
</p>
<input
type="url"
bind:value={url_external_input}
placeholder="https://example.com/projector-bg.jpg"
class="input input-sm preset-tonal-surface h-8 w-full text-[10px]" />
</section>
<!-- Action Buttons -->
<div class="grid grid-cols-2 gap-2">
<button
type="button"
onclick={handle_save}
disabled={!get_device_id()}
class="btn btn-sm preset-tonal-surface h-8 text-[10px]">
<Save size="0.8em" class="mr-1" /> Save Config
</button>
<button
type="button"
onclick={handle_apply}
disabled={!url_input.trim() && !url_external_input.trim()}
class="btn btn-sm preset-tonal-primary h-8 text-[10px]">
<Zap size="0.8em" class="mr-1" /> Apply Now
</button>
</div>
<button
type="button"
onclick={handle_save_and_apply}
disabled={!get_device_id() || (!url_input.trim() && !url_external_input.trim())}
class="btn btn-sm preset-filled-primary h-8 w-full text-[10px] font-bold">
<Save size="0.8em" class="mr-1" />
<Zap size="0.8em" class="mr-1" />
Save & Apply
</button>
<!-- Restore macOS default — safety valve for end-of-show cleanup -->
{#if $ae_loc.is_native || $ae_loc.edit_mode}
<button
type="button"
onclick={handle_restore_default}
class="btn btn-sm preset-tonal-surface h-8 w-full text-[10px] opacity-60 hover:opacity-100">
<RotateCcw size="0.8em" class="mr-1" /> Restore macOS Default
</button>
{/if}
{#if save_status}
<div
class="text-primary-500 text-center text-[9px] italic"
class:text-error-500={save_status.includes('failed') || save_status.includes('No device')}>
{save_status}
</div>
{/if}
{#if apply_status}
<div
class="text-[9px] italic text-center"
class:text-success-500={apply_status.includes('✓')}
class:text-primary-500={apply_status.includes('Downloading')}
class:text-error-500={apply_status.includes('Error')}>
{apply_status}
</div>
{/if}
{#if restore_status}
<div
class="text-[9px] italic text-center"
class:text-success-500={restore_status.includes('✓')}
class:text-primary-500={restore_status === 'Restoring...'}
class:text-error-500={restore_status.includes('Error')}>
{restore_status}
</div>
{/if}
</div>
</Launcher_Cfg_Section>
<!-- Linux Dev Mode Popup — mirrors Native Test Mode style -->
<!-- Shows what WOULD have been applied; actual gsettings call is skipped to avoid
resetting the dev workstation monitors on every test cycle. -->
{#if linux_test_popup_open && linux_test_popup_data}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-9999 flex items-center justify-center bg-black/70 p-4"
role="presentation"
onclick={() => (linux_test_popup_open = false)}>
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="bg-surface-50/95 dark:bg-surface-900/95 border-warning-500/40 relative flex max-h-[90vh] w-full max-w-lg flex-col gap-0 overflow-hidden rounded-xl border shadow-2xl"
role="dialog"
aria-modal="true"
aria-label="Linux Dev Mode — Wallpaper not applied"
tabindex="-1"
onclick={(e) => e.stopPropagation()}>
<div class="bg-warning-500/10 border-warning-500/30 flex items-center gap-2 border-b px-4 py-3">
<span class="text-warning-600 dark:text-warning-400 font-mono text-xs font-bold uppercase tracking-wider">
🧪 Linux Dev Mode — Wallpaper not applied
</span>
<button
type="button"
onclick={() => (linux_test_popup_open = false)}
class="btn btn-xs preset-tonal-surface ml-auto">
Close
</button>
</div>
<div class="flex flex-col gap-3 overflow-y-auto p-4 font-mono text-xs">
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">Display Target</span>
<div class="rounded bg-surface-500/10 px-3 py-2">
{linux_test_popup_data.display ?? 'all'}
</div>
</div>
{#if linux_test_popup_data.url}
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">Primary URL</span>
<div class="text-primary-500 rounded bg-surface-500/10 px-3 py-2 break-all">
{linux_test_popup_data.url}
</div>
</div>
{/if}
{#if linux_test_popup_data.url_external}
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">External / Projector URL</span>
<div class="text-primary-500 rounded bg-surface-500/10 px-3 py-2 break-all">
{linux_test_popup_data.url_external}
</div>
</div>
{/if}
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">Would Have Run</span>
<div class="rounded bg-surface-500/10 px-3 py-2 leading-relaxed whitespace-pre-wrap text-success-600 dark:text-success-400">
{#each (linux_test_popup_data.would_run as string[]) as cmd}
<div class="mb-1">{cmd}</div>
{/each}
</div>
</div>
<p class="text-[9px] italic opacity-40">
gsettings is skipped on Linux to avoid resetting dev monitors. On macOS this applies via osascript.
</p>
</div>
</div>
</div>
{/if}

View File

@@ -17,6 +17,73 @@ import { db_events } from '$lib/ae_events/db_events';
import * as native from '$lib/electron/electron_relay';
const { cleanup_tmp_files } = native;
/**
* Parses other_json (string or object) into a plain Record.
* Duplicated from launcher_cfg_wallpaper to keep this component self-contained.
*/
function parse_other_json(raw: unknown): Record<string, unknown> {
if (!raw) return {};
if (typeof raw === 'object') return { ...(raw as Record<string, unknown>) };
if (typeof raw !== 'string') return {};
try {
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? { ...parsed } : {};
} catch {
return {};
}
}
/**
* Auto-applies the configured wallpaper if the URL in device config differs from
* what was last applied on this machine. Called at startup and after each heartbeat.
*
* WHY: staff updates the wallpaper URL in the device config once (from any machine);
* all native Launcher instances pick it up within one heartbeat cycle (~60s) without
* anyone needing to visit each podium Mac physically.
*/
async function apply_wallpaper_if_changed(device_other_json: unknown) {
if (!$ae_loc.is_native) return;
const other = parse_other_json(device_other_json);
const launcher_cfg = (other.launcher as Record<string, unknown> | undefined) ?? {};
const wallpaper = (launcher_cfg.wallpaper as { url?: string; url_external?: string } | undefined) ?? {};
const configured_url = wallpaper.url ?? '';
const configured_url_external = wallpaper.url_external ?? '';
const applied_url = $events_loc.launcher.wallpaper_applied_url ?? '';
const applied_url_external = $events_loc.launcher.wallpaper_applied_url_external ?? '';
const url_changed = configured_url && configured_url !== applied_url;
const ext_changed = configured_url_external !== applied_url_external;
if (!url_changed && !ext_changed) return;
if (log_lvl) console.log('Sync: Wallpaper config changed — applying.', { configured_url, applied_url });
// If only the external URL is configured, leave the built-in display unchanged.
const display = !configured_url && configured_url_external ? 'external' : 'all';
const result = await native.set_wallpaper({
url: configured_url || undefined,
url_external: configured_url_external || undefined,
display,
api_key: $ae_api.api_secret_key,
account_id: String($ae_api.account_id ?? '')
});
if (result?.success && (result as any).linux_test_mode) {
// Linux dev mode: gsettings was skipped. Log and leave applied URL unset so
// the cfg component popup can show the details when triggered manually.
console.info('Sync: Wallpaper linux_test_mode — would have applied:', (result as any).would_run);
} else if (result?.success) {
$events_loc.launcher.wallpaper_applied_url = configured_url || null;
$events_loc.launcher.wallpaper_applied_url_external = configured_url_external || null;
if (log_lvl) console.log('Sync: Wallpaper applied.');
} else {
console.warn('Sync: Wallpaper apply failed.', (result as any)?.error);
}
}
let { log_lvl = 1 } = $props();
let currently_syncing: string | null = $state(null);
@@ -134,6 +201,12 @@ onMount(async () => {
run_device_heartbeat();
refresh_location_config();
// On startup: apply wallpaper if the device config has one that differs from
// what was last applied (covers reboots and first-time device setup).
if ($ae_loc.is_native) {
apply_wallpaper_if_changed($ae_loc.native_device?.other_json);
}
// Stagger initial data fetches
setTimeout(() => refresh_session_data(), 1000);
setTimeout(() => refresh_presentation_data(), 3000);
@@ -432,13 +505,19 @@ async function run_device_heartbeat() {
update_payload.notes = 'Heartbeat from non-native web session.';
}
await events_func.update_ae_obj__event_device({
const heartbeat_result = await events_func.update_ae_obj__event_device({
api_cfg: $ae_api,
event_device_id: String(device_id),
data_kv: update_payload,
log_lvl: 0
});
// V3 PATCH returns the full updated record. Use it to detect wallpaper
// config changes made remotely (e.g. staff updated other_json from another device).
if (heartbeat_result) {
apply_wallpaper_if_changed((heartbeat_result as any).other_json);
}
last_heartbeat = new Date().toLocaleTimeString();
$events_sess.launcher.heartbeat_info = {
last_timestamp: last_heartbeat,

View File

@@ -33,6 +33,7 @@ import Launcher_Cfg_Screen_Saver from './cfg_components/launcher_cfg_screen_save
import Launcher_Cfg_App_Modes from './cfg_components/launcher_cfg_app_modes.svelte';
import Launcher_Cfg_Local_Actions from './cfg_components/launcher_cfg_local_actions.svelte';
import Launcher_Cfg_Launch_Timing from './cfg_components/launcher_cfg_launch_timing.svelte';
import Launcher_Cfg_Wallpaper from './cfg_components/launcher_cfg_wallpaper.svelte';
import {
Bug,
Code,
@@ -181,6 +182,8 @@ function handle_section_expand(current_key: string) {
on_expand={() => handle_section_expand('health')} />
<Launcher_Cfg_Native_OS
on_expand={() => handle_section_expand('native_os')} />
<Launcher_Cfg_Wallpaper
on_expand={() => handle_section_expand('wallpaper')} />
<Launcher_Cfg_Launch_Timing
on_expand={() => handle_section_expand('launch_timing')} />
{#if $ae_loc.is_native}