Fix: Persist native sync timers and implement monitor UI
- Implemented safe merge in root layout to preserve local timer overrides. - Added Native Sync Monitor UI to background sync component. - Exposed loop periods and hash prefix length in Launcher Config. - Ensured manual adjustments survive page refreshes.
This commit is contained in:
@@ -140,13 +140,32 @@ export async function load({ fetch, params, parent, route, url }) {
|
|||||||
site_domain_id: native_device_config.site_domain_id || native_device_config.site_domain_id_random,
|
site_domain_id: native_device_config.site_domain_id || native_device_config.site_domain_id_random,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Inject native device metadata into the location state
|
// Inject native device metadata into the location state with SAFE MERGE
|
||||||
if (native_device_config.native_device) {
|
if (native_device_config.native_device) {
|
||||||
ae_loc_init['native_device'] = native_device_config.native_device;
|
const incoming_dev = native_device_config.native_device;
|
||||||
|
|
||||||
|
// 1. Recover existing user overrides from localStorage
|
||||||
|
let existing_dev = {};
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem('ae_loc');
|
||||||
|
if (raw) existing_dev = JSON.parse(raw).native_device || {};
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// 2. Merge: Priority to EXISTING overrides for specific timers
|
||||||
|
ae_loc_init['native_device'] = {
|
||||||
|
...incoming_dev,
|
||||||
|
// Persist these specific user-controlled fields
|
||||||
|
check_event_loop_period: (existing_dev as any).check_event_loop_period || incoming_dev.check_event_loop_period,
|
||||||
|
check_event_device_loop_period: (existing_dev as any).check_event_device_loop_period || incoming_dev.check_event_device_loop_period,
|
||||||
|
check_event_location_loop_period: (existing_dev as any).check_event_location_loop_period || incoming_dev.check_event_location_loop_period,
|
||||||
|
check_event_session_loop_period: (existing_dev as any).check_event_session_loop_period || incoming_dev.check_event_session_loop_period,
|
||||||
|
hash_prefix_length: (existing_dev as any).hash_prefix_length || incoming_dev.hash_prefix_length
|
||||||
|
};
|
||||||
|
|
||||||
// Map specific operational paths
|
// Map specific operational paths
|
||||||
ae_loc_init['local_file_cache_path'] = native_device_config.native_device.local_file_cache_path;
|
ae_loc_init['local_file_cache_path'] = incoming_dev.local_file_cache_path;
|
||||||
ae_loc_init['host_file_temp_path'] = native_device_config.native_device.host_file_temp_path;
|
ae_loc_init['host_file_temp_path'] = incoming_dev.host_file_temp_path;
|
||||||
ae_loc_init['recording_path'] = native_device_config.native_device.recording_path;
|
ae_loc_init['recording_path'] = incoming_dev.recording_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
// IMPORTANT: Update API settings with the native-authorized key if present
|
// IMPORTANT: Update API settings with the native-authorized key if present
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
/**
|
/**
|
||||||
* launcher_background_sync.svelte
|
* launcher_background_sync.svelte
|
||||||
* Exhaustive Background Pre-Caching Engine.
|
* Exhaustive Background Pre-Caching Engine with Loop Monitoring.
|
||||||
* Scans Sessions, Presentations, and Presenters to ensure 100% of room files are cached.
|
|
||||||
*/
|
*/
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
|
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
|
||||||
@@ -16,46 +15,59 @@
|
|||||||
let sync_results: Record<string, 'success' | 'error' | 'pending'> = $state({});
|
let sync_results: Record<string, 'success' | 'error' | 'pending'> = $state({});
|
||||||
let sync_stats = $state({ total: 0, cached: 0, missing: 0 });
|
let sync_stats = $state({ total: 0, cached: 0, missing: 0 });
|
||||||
|
|
||||||
let main_sync_interval: any = null;
|
// Loop Timings (Visible in UI)
|
||||||
|
let loop_info = $state({
|
||||||
|
event: 90000,
|
||||||
|
device: 60000,
|
||||||
|
location: 30000,
|
||||||
|
session: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timer Handles
|
||||||
|
let timer__event: any = null;
|
||||||
|
let timer__device: any = null;
|
||||||
|
let timer__location: any = null;
|
||||||
|
let timer__session: any = null;
|
||||||
|
|
||||||
let is_syncing = false;
|
let is_syncing = false;
|
||||||
|
let show_monitor = $state(false);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!$ae_loc.is_native) return;
|
if (!$ae_loc.is_native) return;
|
||||||
|
|
||||||
if (log_lvl) console.log('Sync: Starting exhaustive background caching engine.');
|
const dev = $ae_loc.native_device || {};
|
||||||
|
|
||||||
// Setup persistent loop (Check every 10s for new data/missing files)
|
// Load timings from device config
|
||||||
const sync_period = 10000;
|
loop_info.event = dev.check_event_loop_period || 90000;
|
||||||
|
loop_info.device = dev.check_event_device_loop_period || 60000;
|
||||||
|
loop_info.location = dev.check_event_location_loop_period || 30000;
|
||||||
|
loop_info.session = dev.check_event_session_loop_period || 10000;
|
||||||
|
|
||||||
main_sync_interval = setInterval(() => {
|
timer__event = setInterval(() => run_sync_cycle(), loop_info.event);
|
||||||
if (!is_syncing) {
|
timer__device = setInterval(() => { /* TODO: Heartbeat */ }, loop_info.device);
|
||||||
run_sync_cycle();
|
timer__location = setInterval(() => { /* TODO: Refresh Loc */ }, loop_info.location);
|
||||||
}
|
timer__session = setInterval(() => run_sync_cycle(), loop_info.session);
|
||||||
}, sync_period);
|
|
||||||
|
|
||||||
// Immediate first run
|
// Immediate first run
|
||||||
run_sync_cycle();
|
run_sync_cycle();
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (main_sync_interval) clearInterval(main_sync_interval);
|
if (timer__event) clearInterval(timer__event);
|
||||||
|
if (timer__device) clearInterval(timer__device);
|
||||||
|
if (timer__location) clearInterval(timer__location);
|
||||||
|
if (timer__session) clearInterval(timer__session);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Primary Sync Cycle
|
|
||||||
* Recursively identifies ALL files in the room hierarchy.
|
|
||||||
*/
|
|
||||||
async function run_sync_cycle() {
|
async function run_sync_cycle() {
|
||||||
const location_id = $events_slct.event_location_id;
|
const location_id = $events_slct.event_location_id;
|
||||||
const cache_root = $ae_loc.local_file_cache_path;
|
const cache_root = $ae_loc.local_file_cache_path;
|
||||||
|
const prefix_len = $ae_loc.native_device?.hash_prefix_length || 2;
|
||||||
|
|
||||||
if (!location_id || !cache_root) return;
|
if (!location_id || !cache_root || is_syncing) return;
|
||||||
|
|
||||||
is_syncing = true;
|
is_syncing = true;
|
||||||
if (log_lvl > 1) console.log(`Sync Engine: Scanning room ${location_id}...`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Gather all IDs in this room's hierarchy
|
|
||||||
const sessions = await db_events.session.where('event_location_id').equals(location_id).toArray();
|
const sessions = await db_events.session.where('event_location_id').equals(location_id).toArray();
|
||||||
const session_ids = sessions.map(s => s.event_session_id);
|
const session_ids = sessions.map(s => s.event_session_id);
|
||||||
if (session_ids.length === 0) return;
|
if (session_ids.length === 0) return;
|
||||||
@@ -66,31 +78,21 @@
|
|||||||
const presenters = await db_events.presenter.where('event_session_id').anyOf(session_ids).toArray();
|
const presenters = await db_events.presenter.where('event_session_id').anyOf(session_ids).toArray();
|
||||||
const presenter_ids = presenters.map(p => p.event_presenter_id);
|
const presenter_ids = presenters.map(p => p.event_presenter_id);
|
||||||
|
|
||||||
// 2. Collect every ID that could have a file attached
|
|
||||||
const all_for_ids = [...session_ids, ...presentation_ids, ...presenter_ids, $events_slct.event_id];
|
const all_for_ids = [...session_ids, ...presentation_ids, ...presenter_ids, $events_slct.event_id];
|
||||||
|
|
||||||
// 3. Find ALL files linked to these IDs
|
|
||||||
const files = await db_events.file.where('for_id').anyOf(all_for_ids).toArray();
|
const files = await db_events.file.where('for_id').anyOf(all_for_ids).toArray();
|
||||||
|
|
||||||
sync_stats.total = files.length;
|
sync_stats.total = files.length;
|
||||||
let cached_count = 0;
|
let cached_count = 0;
|
||||||
let missing_count = 0;
|
let missing_count = 0;
|
||||||
|
|
||||||
if (log_lvl > 1) console.log(`Sync Engine: Found ${files.length} potential files for this room.`);
|
|
||||||
|
|
||||||
// 4. Verify and Download
|
|
||||||
for (const file_obj of files) {
|
for (const file_obj of files) {
|
||||||
if (!file_obj.hash_sha256) continue;
|
if (!file_obj.hash_sha256) continue;
|
||||||
|
if (sync_results[file_obj.event_file_id] === 'success') { cached_count++; continue; }
|
||||||
// Fast-track if already confirmed this session
|
|
||||||
if (sync_results[file_obj.event_file_id] === 'success') {
|
|
||||||
cached_count++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const exists = await native.check_hash_file_cache({
|
const exists = await native.check_hash_file_cache({
|
||||||
cache_root,
|
cache_root,
|
||||||
hash: file_obj.hash_sha256
|
hash: file_obj.hash_sha256,
|
||||||
|
hash_prefix_length: prefix_len
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
@@ -99,35 +101,22 @@
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Download missing file
|
|
||||||
missing_count++;
|
missing_count++;
|
||||||
if (log_lvl) console.log(`Sync: Pulling missing file -> ${file_obj.filename}`);
|
|
||||||
currently_syncing = file_obj.filename;
|
currently_syncing = file_obj.filename;
|
||||||
|
|
||||||
const url = `${$ae_api.base_url}/hosted_file/${file_obj.hosted_file_id}/download?return_file=true&filename=${encodeURIComponent(file_obj.filename)}`;
|
const url = `${$ae_api.base_url}/hosted_file/${file_obj.hosted_file_id}/download?return_file=true&filename=${encodeURIComponent(file_obj.filename)}`;
|
||||||
|
|
||||||
const result = await native.download_to_cache({
|
const result = await native.download_to_cache({
|
||||||
url,
|
url, cache_root, hash: file_obj.hash_sha256,
|
||||||
cache_root,
|
api_key: $ae_api.api_secret_key, account_id: $ae_api.account_id,
|
||||||
hash: file_obj.hash_sha256,
|
hash_prefix_length: prefix_len
|
||||||
api_key: $ae_api.api_secret_key,
|
|
||||||
account_id: $ae_api.account_id
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) sync_results[file_obj.event_file_id] = 'success';
|
||||||
sync_results[file_obj.event_file_id] = 'success';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sync_stats.cached = cached_count;
|
sync_stats.cached = cached_count;
|
||||||
sync_stats.missing = missing_count;
|
sync_stats.missing = missing_count;
|
||||||
|
|
||||||
if (log_lvl && missing_count > 0) {
|
|
||||||
console.log(`Sync Engine: Room scan complete. Cached: ${cached_count}, Downloaded: ${missing_count}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Sync Engine: Critical failure in room scan:', err);
|
console.error('Sync Engine Error:', err);
|
||||||
} finally {
|
} finally {
|
||||||
currently_syncing = null;
|
currently_syncing = null;
|
||||||
is_syncing = false;
|
is_syncing = false;
|
||||||
@@ -135,15 +124,66 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if currently_syncing && log_lvl}
|
<!-- Monitor Overlay (Hidden by default, triggered by config modal or secret gesture) -->
|
||||||
<div class="fixed bottom-4 right-4 bg-black/80 text-white p-2 rounded-lg text-[10px] z-[9999] border border-primary-500 animate-pulse shadow-2xl">
|
<div class="fixed bottom-4 right-4 z-[9999] flex flex-col items-end gap-2 pointer-events-none">
|
||||||
<div class="flex items-center gap-2">
|
{#if show_monitor}
|
||||||
<span class="fas fa-sync fa-spin text-primary-500"></span>
|
<div class="bg-black/90 text-white p-3 rounded-lg border border-primary-500 shadow-2xl text-[10px] font-mono min-w-48 pointer-events-auto">
|
||||||
<div class="flex flex-col">
|
<div class="flex justify-between border-b border-primary-500 pb-1 mb-2">
|
||||||
<span class="font-bold">Syncing Room Files...</span>
|
<span class="font-bold text-primary-400">NATIVE SYNC MONITOR</span>
|
||||||
<span class="opacity-70 truncate max-w-48">{currently_syncing}</span>
|
<button onclick={() => show_monitor = false} class="text-error-500 hover:text-error-400">×</button>
|
||||||
<span class="text-[8px] mt-1 text-primary-300">Room Status: {sync_stats.cached}/{sync_stats.total} Ready</span>
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-x-4 gap-y-1 mb-2">
|
||||||
|
<span class="opacity-70 text-primary-300">Room Status:</span>
|
||||||
|
<span class="text-right">{sync_stats.cached} / {sync_stats.total} Files</span>
|
||||||
|
|
||||||
|
<span class="opacity-70 text-primary-300">Prefix Len:</span>
|
||||||
|
<span class="text-right">{$ae_loc.native_device?.hash_prefix_length || 2} chars</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-white/10 pt-2 flex flex-col gap-1">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class:text-primary-500={timer__event}>Event Loop:</span>
|
||||||
|
<span>{loop_info.event / 1000}s</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class:text-primary-500={timer__device}>Device Loop:</span>
|
||||||
|
<span>{loop_info.device / 1000}s</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class:text-primary-500={timer__location}>Location Loop:</span>
|
||||||
|
<span>{loop_info.location / 1000}s</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class:text-primary-500={timer__session}>Session Loop:</span>
|
||||||
|
<span>{loop_info.session / 1000}s</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
|
||||||
|
{#if currently_syncing}
|
||||||
|
<button
|
||||||
|
onclick={() => show_monitor = !show_monitor}
|
||||||
|
class="bg-black/80 text-white p-2 rounded-lg text-[10px] border border-primary-500 animate-pulse shadow-2xl pointer-events-auto transition-transform active:scale-95"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 text-left">
|
||||||
|
<span class="fas fa-sync fa-spin text-primary-500 text-sm"></span>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-bold">Syncing Room Files...</span>
|
||||||
|
<span class="opacity-70 truncate max-w-48">{currently_syncing}</span>
|
||||||
|
<span class="text-[8px] mt-1 text-primary-300">Monitor Ready (Click to View)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<!-- Secret button area to toggle monitor when not syncing -->
|
||||||
|
<button
|
||||||
|
onclick={() => show_monitor = !show_monitor}
|
||||||
|
class="w-8 h-8 opacity-0 hover:opacity-20 bg-primary-500 rounded-full pointer-events-auto flex items-center justify-center transition-opacity"
|
||||||
|
title="Toggle Sync Monitor"
|
||||||
|
>
|
||||||
|
<span class="fas fa-microchip text-white text-[10px]"></span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -116,6 +116,86 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
||||||
|
|
||||||
|
<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}
|
{/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