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 8a68421a..bec4f5d0 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 @@ -58,16 +58,20 @@ // *** Derived let has_unsaved_changes = $derived.by(() => { if (!tmp_entry_obj || !orig_entry_obj || is_processing) return false; - return ( - (tmp_entry_obj.content ?? null) !== (orig_entry_obj.content ?? null) || - (tmp_entry_obj.name ?? null) !== (orig_entry_obj.name ?? null) || + + // Use normalization to avoid loop on whitespace/null differences + const normalize = (val: any) => (val === null || val === undefined) ? '' : String(val).trim(); + + const content_changed = normalize(tmp_entry_obj.content) !== normalize(orig_entry_obj.content); + 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.private ?? false) !== (orig_entry_obj.private ?? false) || (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) - ); + (tmp_entry_obj.professional ?? false) !== (orig_entry_obj.professional ?? false); }); // *** Effects @@ -77,21 +81,24 @@ const entry = $lq__journal_entry_obj; const journal = $lq__journal_obj; if (entry && (entry.updated_on || entry.created_on)) { - // Determine if we are in a decrypted state for this journal - const session_kv = $journals_sess?.journal_kv[journal?.id]; - const is_decrypted = session_kv?.journal_passcode_decrypted === true; + + // ISOLATE Store checks to avoid re-triggering this effect when stores change + const is_decrypted = untrack(() => { + const session_kv = $journals_sess?.journal_kv[journal?.id]; + return session_kv?.journal_passcode_decrypted === true; + }); - console.log(`ae_view: Effect[Sync] trigger. Entry updated_on: ${entry.updated_on}. Decrypted in session: ${is_decrypted}`); + console.log(`ae_view: Effect[Sync] trigger. Decrypted: ${is_decrypted}. Status: ${save_status}`); // Only sync if saved and not currently processing/editing if (save_status === 'saved' && !has_unsaved_changes && !is_processing) { - // If the journal is already decrypted, we MUST NOT overwrite the decrypted content with null from DB + // If decrypted, don't let DB's null content overwrite local recovered text if (is_decrypted && tmp_entry_obj.content && !entry.content) { - console.log('ae_view: Effect[Sync] SKIPPED to preserve decrypted content.'); + console.log('ae_view: Effect[Sync] preserving decrypted content.'); return; } - console.log('ae_view: Effect[Sync] PROCEEDING. Updating local state from DB.'); + console.log('ae_view: Effect[Sync] update local state.'); const base = { ...entry, content: entry.content ?? null, @@ -99,23 +106,25 @@ }; orig_entry_obj = { ...base }; tmp_entry_obj = { ...base }; - } else { - console.log(`ae_view: Effect[Sync] BLOCKED. status: ${save_status}, has_changes: ${has_unsaved_changes}, processing: ${is_processing}`); } } }); // 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; + if (has_unsaved_changes && !is_processing) { - console.log('ae_view: Effect[AutoSave] trigger. Changes detected.'); + console.log('ae_view: Effect[AutoSave] trigger.'); if (save_status !== 'saving') save_status = 'unsaved'; - if ($journals_loc.entry.auto_save) { + + const auto_save_enabled = untrack(() => $journals_loc.entry.auto_save); + if (auto_save_enabled) { clearTimeout(auto_save_timer); auto_save_timer = setTimeout(() => { - if (untrack(() => has_unsaved_changes)) { - console.log('ae_view: Effect[AutoSave] Timer fired. Updating...'); + if (untrack(() => has_unsaved_changes && save_status !== 'saving')) { update_journal_entry(); } }, 2000); @@ -135,10 +144,8 @@ const decrypted_status = session?.journal_passcode_decrypted; const has_encrypted_content = !!$lq__journal_entry_obj?.content_encrypted; - console.log(`ae_view: Effect[Decryption] check. Encrypted: ${has_encrypted_content}, Verified: ${is_verified}, Status: ${decrypted_status}`); - if (has_encrypted_content && is_verified && decrypted_status !== true && decrypted_status !== 'processing') { - console.log('ae_view: Effect[Decryption] PROCEEDING to run workflow.'); + console.log('ae_view: Effect[Decryption] auto-running.'); run_decryption_workflow(); } }); @@ -147,7 +154,7 @@ async function run_decryption_workflow() { const journal = $lq__journal_obj; - if (!journal?.id) return; + if (!journal?.id || is_processing) return; let journal_key = $journals_sess.journal_kv[journal.id]?.typed_journal_passcode; @@ -174,16 +181,17 @@ } // SUCCESS - console.log(`ae_view: Decryption SUCCESS. Updating state.`); - tmp_entry_obj.content = result.content; - tmp_entry_obj.content_md_html = handle_marked(result.content || ''); + console.log(`ae_view: Decryption SUCCESS.`); - // CRITICAL: Clear encrypted fields in local state so UI doesn't think it's still locked + // Update both to prevent "unsaved changes" loop + const content = result.content || ''; + tmp_entry_obj.content = content; + tmp_entry_obj.content_md_html = handle_marked(content); tmp_entry_obj.content_encrypted = null; tmp_entry_obj.history_encrypted = null; if (orig_entry_obj) { - orig_entry_obj.content = result.content; + orig_entry_obj.content = content; orig_entry_obj.content_encrypted = null; orig_entry_obj.history_encrypted = null; } @@ -201,7 +209,7 @@ } async function update_journal_entry() { - if (!$ae_loc.trusted_access) return; + if (!$ae_loc.trusted_access || save_status === 'saving') return; save_status = 'saving'; // Reference Standard: Explicit whitelist of editable fields @@ -251,7 +259,8 @@ data_kv: data_kv, log_lvl: 1 }); - orig_entry_obj = { ...tmp_entry_obj }; + // CRITICAL: Sync ORIG after save to clear unsaved changes + orig_entry_obj = JSON.parse(JSON.stringify(tmp_entry_obj)); save_status = 'saved'; } catch (error) { console.error('Update failed:', error);