From b9fc4addfc119e3c01a479cd4e7ec046d98c33dc Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Wed, 14 Jan 2026 16:31:27 -0500 Subject: [PATCH] fix(journals): resolve browser hang and integrate Metadata component - Implemented manual 'deep_copy' to handle reactive proxies safely. - Added concurrency locks ('is_processing') to prevent PATCH loops. - Standardized metadata display by switching to 'JournalEntry_Metadata'. --- .../ae_comp__journal_entry_obj_id_view.svelte | 94 ++++++++++++------- 1 file changed, 61 insertions(+), 33 deletions(-) diff --git a/src/routes/journals/ae_comp__journal_entry_obj_id_view.svelte b/src/routes/journals/ae_comp__journal_entry_obj_id_view.svelte index bec4f5d0..8be42771 100644 --- a/src/routes/journals/ae_comp__journal_entry_obj_id_view.svelte +++ b/src/routes/journals/ae_comp__journal_entry_obj_id_view.svelte @@ -22,7 +22,7 @@ import JournalEntry_Editor from './JournalEntry_Editor.svelte'; import JournalEntry_Header from './JournalEntry_Header.svelte'; - import AE_MetadataFooter from '$lib/ae_elements/AE_MetadataFooter.svelte'; + import JournalEntry_Metadata from './JournalEntry_Metadata.svelte'; import AE_AITools from '$lib/ae_elements/AE_AITools.svelte'; import AeCompModalJournalEntryAppend from './ae_comp__modal_journal_entry_append.svelte'; @@ -55,6 +55,32 @@ let auto_save_timer: ReturnType; let is_processing = $state(false); + // *** Helpers + function deep_copy(obj: any) { + if (!obj) return null; + try { + // Manual selective copy to avoid proxy issues and handle Dates + const copy: key_val = {}; + for (const key in obj) { + const val = obj[key]; + if (val instanceof Date) { + copy[key] = new Date(val.getTime()); + } else if (Array.isArray(val)) { + copy[key] = [...val]; + } else if (typeof val === 'object' && val !== null) { + // Simple object copy (shallow for nested to avoid recursion) + copy[key] = { ...val }; + } else { + copy[key] = val; + } + } + return copy; + } catch (e) { + console.error('deep_copy failed:', e); + return { ...obj }; + } + } + // *** Derived let has_unsaved_changes = $derived.by(() => { if (!tmp_entry_obj || !orig_entry_obj || is_processing) return false; @@ -66,12 +92,14 @@ const name_changed = normalize(tmp_entry_obj.name) !== normalize(orig_entry_obj.name); const private_changed = (tmp_entry_obj.private ?? false) !== (orig_entry_obj.private ?? false); - return content_changed || name_changed || private_changed || - (tmp_entry_obj.tags ?? null) !== (orig_entry_obj.tags ?? null) || - (tmp_entry_obj.category_code ?? null) !== (orig_entry_obj.category_code ?? null) || - (tmp_entry_obj.public ?? false) !== (orig_entry_obj.public ?? false) || - (tmp_entry_obj.personal ?? false) !== (orig_entry_obj.personal ?? false) || - (tmp_entry_obj.professional ?? false) !== (orig_entry_obj.professional ?? false); + if (content_changed || name_changed || private_changed) return true; + + const other_fields = ['tags', 'category_code', 'public', 'personal', 'professional']; + for (const field of other_fields) { + if (normalize(tmp_entry_obj[field]) !== normalize(orig_entry_obj[field])) return true; + } + + return false; }); // *** Effects @@ -99,22 +127,22 @@ } console.log('ae_view: Effect[Sync] update local state.'); - const base = { - ...entry, - content: entry.content ?? null, - history: entry.history ?? null - }; - orig_entry_obj = { ...base }; - tmp_entry_obj = { ...base }; + const base = deep_copy(entry); + if (base) { + base.content = base.content ?? null; + base.history = base.history ?? null; + orig_entry_obj = { ...base }; + tmp_entry_obj = { ...base }; + } } } }); // 2. Auto-Save Debounce $effect(() => { - // Explicitly depend on content but DO NOT react to other tmp_entry_obj properties here - const _trigger = tmp_entry_obj.content; - const _trigger_priv = tmp_entry_obj.private; + // Dependency tracking + const _content = tmp_entry_obj.content; + const _private = tmp_entry_obj.private; if (has_unsaved_changes && !is_processing) { console.log('ae_view: Effect[AutoSave] trigger.'); @@ -145,7 +173,7 @@ const has_encrypted_content = !!$lq__journal_entry_obj?.content_encrypted; if (has_encrypted_content && is_verified && decrypted_status !== true && decrypted_status !== 'processing') { - console.log('ae_view: Effect[Decryption] auto-running.'); + console.log('ae_view: Auto-decrypt triggering.'); run_decryption_workflow(); } }); @@ -210,9 +238,12 @@ async function update_journal_entry() { if (!$ae_loc.trusted_access || save_status === 'saving') return; + + // Prevent concurrent saves + if (is_processing) return; + is_processing = true; save_status = 'saving'; - // Reference Standard: Explicit whitelist of editable fields const data_kv: key_val = { name: tmp_entry_obj.name, content: tmp_entry_obj.content, @@ -236,48 +267,45 @@ const decrypt_key = $lq__journal_obj.combined_passcode; - // Encryption Logic Transition if (tmp_entry_obj.private) { if (tmp_entry_obj.content) { data_kv.content_encrypted = await ae_util.encrypt_wrapper(tmp_entry_obj.content, decrypt_key); data_kv.content = null; } - if (tmp_entry_obj.history) { - data_kv.history_encrypted = await ae_util.encrypt_wrapper(tmp_entry_obj.history, decrypt_key); - data_kv.history = null; - } } else { - // Ensure we clear encrypted fields if privacy is disabled data_kv.content_encrypted = null; data_kv.history_encrypted = null; } try { + console.log('ae_view: Starting PATCH update...'); await journals_func.update_ae_obj__journal_entry({ api_cfg: $ae_api, journal_entry_id: $lq__journal_entry_obj?.journal_entry_id, data_kv: data_kv, log_lvl: 1 }); - // CRITICAL: Sync ORIG after save to clear unsaved changes - orig_entry_obj = JSON.parse(JSON.stringify(tmp_entry_obj)); + + // Re-sync after successful save + orig_entry_obj = deep_copy(tmp_entry_obj); save_status = 'saved'; + console.log('ae_view: PATCH complete and state synced.'); } catch (error) { console.error('Update failed:', error); save_status = 'unsaved'; + } finally { + is_processing = false; } } async function handle_content_decryption() { const journal = $lq__journal_obj; const entry = $lq__journal_entry_obj; - if (!journal?.id || !entry) return; + if (!journal?.id || !entry || is_processing) return; - // If encrypted and not currently decrypted, run the decryption workflow if (entry.content_encrypted && !tmp_entry_obj.content) { await run_decryption_workflow(); } else { - // Toggle logic: if already decrypted, "lock" it by clearing local state is_processing = true; tmp_entry_obj.content = null; tmp_entry_obj.history = null; @@ -299,7 +327,7 @@ } async function change_journal_id() { - if (!$ae_loc.trusted_access) return; + if (!$ae_loc.trusted_access || is_processing) return; await journals_func.update_ae_obj__journal_entry({ api_cfg: $ae_api, journal_entry_id: $lq__journal_entry_obj?.journal_entry_id, @@ -366,7 +394,7 @@ /> - + Loading Journal Entry... {/if} - \ No newline at end of file +