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'.
This commit is contained in:
Scott Idem
2026-01-14 16:31:27 -05:00
parent 58a41e093b
commit b9fc4addfc

View File

@@ -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<typeof setTimeout>;
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 @@
/>
</section>
<AE_MetadataFooter obj={tmp_entry_obj} />
<JournalEntry_Metadata entry={tmp_entry_obj} />
<AeCompModalJournalEntryAppend
bind:open={show_append_modal}
@@ -383,4 +411,4 @@
<span class="text-2xl font-bold">Loading Journal Entry...</span>
</div>
{/if}
</section>
</section>