Files
OSIT-AE-App-Svelte/src/routes/events/[event_id]/(launcher)/launcher_background_sync.svelte
Scott Idem efdf1188a6 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.
2026-01-23 16:56:02 -05:00

189 lines
8.3 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
/**
* launcher_background_sync.svelte
* Exhaustive Background Pre-Caching Engine with Loop Monitoring.
*/
import { onMount, onDestroy } from 'svelte';
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_slct } from '$lib/stores/ae_events_stores';
import { db_events } from '$lib/ae_events/db_events';
import * as native from '$lib/electron/electron_relay';
let { log_lvl = 1 } = $props();
let currently_syncing: string | null = $state(null);
let sync_results: Record<string, 'success' | 'error' | 'pending'> = $state({});
let sync_stats = $state({ total: 0, cached: 0, missing: 0 });
// 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 show_monitor = $state(false);
onMount(() => {
if (!$ae_loc.is_native) return;
const dev = $ae_loc.native_device || {};
// Load timings from device config
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;
timer__event = setInterval(() => run_sync_cycle(), loop_info.event);
timer__device = setInterval(() => { /* TODO: Heartbeat */ }, loop_info.device);
timer__location = setInterval(() => { /* TODO: Refresh Loc */ }, loop_info.location);
timer__session = setInterval(() => run_sync_cycle(), loop_info.session);
// Immediate first run
run_sync_cycle();
});
onDestroy(() => {
if (timer__event) clearInterval(timer__event);
if (timer__device) clearInterval(timer__device);
if (timer__location) clearInterval(timer__location);
if (timer__session) clearInterval(timer__session);
});
async function run_sync_cycle() {
const location_id = $events_slct.event_location_id;
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 || is_syncing) return;
is_syncing = true;
try {
const sessions = await db_events.session.where('event_location_id').equals(location_id).toArray();
const session_ids = sessions.map(s => s.event_session_id);
if (session_ids.length === 0) return;
const presentations = await db_events.presentation.where('event_session_id').anyOf(session_ids).toArray();
const presentation_ids = presentations.map(p => p.event_presentation_id);
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 all_for_ids = [...session_ids, ...presentation_ids, ...presenter_ids, $events_slct.event_id];
const files = await db_events.file.where('for_id').anyOf(all_for_ids).toArray();
sync_stats.total = files.length;
let cached_count = 0;
let missing_count = 0;
for (const file_obj of files) {
if (!file_obj.hash_sha256) continue;
if (sync_results[file_obj.event_file_id] === 'success') { cached_count++; continue; }
const exists = await native.check_hash_file_cache({
cache_root,
hash: file_obj.hash_sha256,
hash_prefix_length: prefix_len
});
if (exists) {
sync_results[file_obj.event_file_id] = 'success';
cached_count++;
continue;
}
missing_count++;
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 result = await native.download_to_cache({
url, cache_root, hash: file_obj.hash_sha256,
api_key: $ae_api.api_secret_key, account_id: $ae_api.account_id,
hash_prefix_length: prefix_len
});
if (result.success) sync_results[file_obj.event_file_id] = 'success';
}
sync_stats.cached = cached_count;
sync_stats.missing = missing_count;
} catch (err) {
console.error('Sync Engine Error:', err);
} finally {
currently_syncing = null;
is_syncing = false;
}
}
</script>
<!-- Monitor Overlay (Hidden by default, triggered by config modal or secret gesture) -->
<div class="fixed bottom-4 right-4 z-[9999] flex flex-col items-end gap-2 pointer-events-none">
{#if show_monitor}
<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 justify-between border-b border-primary-500 pb-1 mb-2">
<span class="font-bold text-primary-400">NATIVE SYNC MONITOR</span>
<button onclick={() => show_monitor = false} class="text-error-500 hover:text-error-400">×</button>
</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>
{/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>