feat(journals): stabilize V3 API integration and restore cryptographic toggles

This commit is contained in:
Scott Idem
2026-01-14 14:35:55 -05:00
parent 17870bc0a2
commit 283053e4a4
2 changed files with 217 additions and 524 deletions

View File

@@ -1,38 +1,33 @@
<script lang="ts"> <script lang="ts">
// let log_lvl: number = 0; /**
* ae_comp__journal_entry_obj_id_view.svelte
* Reference Implementation for Journal Entry View/Edit
* Corrected for V3 API Strictness & Robust Decryption
*/
// *** Import Svelte core and navigation // *** Import Svelte core
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { untrack } from 'svelte';
// *** Import secondary libraries // *** Import secondary libraries
import { marked } from 'marked'; import { marked } from 'marked';
// *** Import Aether components and helpers // *** Import Aether components and helpers
import E_app_codemirror_v5 from '$lib/app_components/e_app_codemirror_v5.svelte';
// *** Import Aether core logic and stores
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
import { import { ae_loc, ae_api } from '$lib/stores/ae_stores';
ae_loc, import { journals_loc, journals_sess, journals_slct } from '$lib/ae_journals/ae_journals_stores';
ae_api,
ae_trig
} from '$lib/stores/ae_stores';
import {
journals_loc,
journals_sess,
journals_slct
} from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import Comp_journal_entry_file_li from './ae_comp__journal_entry_obj_file_li.svelte';
import Comp_hosted_files_download_button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte';
import AE_AITools from '$lib/ae_elements/AE_AITools.svelte';
import AE_ObjectFlags from '$lib/ae_elements/AE_ObjectFlags.svelte';
import AE_MetadataFooter from '$lib/ae_elements/AE_MetadataFooter.svelte';
import JournalEntry_Editor from './JournalEntry_Editor.svelte'; import JournalEntry_Editor from './JournalEntry_Editor.svelte';
import JournalEntry_Header from './JournalEntry_Header.svelte'; import JournalEntry_Header from './JournalEntry_Header.svelte';
import AE_MetadataFooter from '$lib/ae_elements/AE_MetadataFooter.svelte';
import AE_AITools from '$lib/ae_elements/AE_AITools.svelte';
import AeCompModalJournalEntryAppend from './ae_comp__modal_journal_entry_append.svelte'; import AeCompModalJournalEntryAppend from './ae_comp__modal_journal_entry_append.svelte';
// Icons
import { AlertCircle, XCircle, Loader2 } from '@lucide/svelte';
// *** Props // *** Props
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
@@ -50,589 +45,280 @@
onShowExport onShowExport
}: Props = $props(); }: Props = $props();
$effect(() => {
if (onShowExport) {
console.log('ae_comp__journal_entry_obj_id_view: onShowExport prop is defined');
} else {
console.warn('ae_comp__journal_entry_obj_id_view: onShowExport prop is UNDEFINED');
}
});
// *** State // *** State
let editorView: any = $state(); let editorView: any = $state();
let ae_promises: any = $state(); let orig_entry_obj: key_val | null = $state(null);
let orig_entry_obj: key_val | null = $state({});
let tmp_entry_obj: key_val = $state({}); let tmp_entry_obj: key_val = $state({});
let updated_obj: boolean = $state(true); // Start with true to force population let save_status: 'saved' | 'unsaved' | 'saving' = $state('saved');
let updated_idb: boolean = $state(true); // Internal session sync flag let decryption_error: string | null = $state(null);
let auto_save_timer: ReturnType<typeof setTimeout>;
// Idiomatic Svelte 5 change detection let is_processing = $state(false);
let tmp_entry_obj_changed = $derived.by(() => {
if (
!tmp_entry_obj ||
!orig_entry_obj ||
not_obj(tmp_entry_obj) ||
not_obj(orig_entry_obj)
) {
return false;
}
// *** Derived
let has_unsaved_changes = $derived.by(() => {
if (!tmp_entry_obj || !orig_entry_obj || is_processing) return false;
return ( return (
tmp_entry_obj.content !== orig_entry_obj.content || (tmp_entry_obj.content ?? null) !== (orig_entry_obj.content ?? null) ||
tmp_entry_obj.name !== orig_entry_obj.name || (tmp_entry_obj.name ?? null) !== (orig_entry_obj.name ?? null) ||
tmp_entry_obj.tags !== orig_entry_obj.tags || (tmp_entry_obj.tags ?? null) !== (orig_entry_obj.tags ?? null) ||
tmp_entry_obj.category_code !== orig_entry_obj.category_code (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)
); );
}); });
// --- Auto Save Logic --- // *** Effects
let save_status: 'saved' | 'unsaved' | 'saving' = $state('saved');
let auto_save_timer: ReturnType<typeof setTimeout>;
// 1. Initial Load & Background Sync
$effect(() => { $effect(() => {
// Explicitly track these properties so the effect re-runs on every keystroke const entry = $lq__journal_entry_obj;
const _content = tmp_entry_obj.content; if (entry && entry.updated_on) {
const _name = tmp_entry_obj.name; // Only sync if saved and not currently processing/editing
const _tags = tmp_entry_obj.tags; if (save_status === 'saved' && !has_unsaved_changes && !is_processing) {
const _category = tmp_entry_obj.category_code; const base = {
...entry,
content: entry.content ?? null,
history: entry.history ?? null
};
orig_entry_obj = { ...base };
tmp_entry_obj = { ...base };
}
}
});
if (tmp_entry_obj_changed) { // 2. Auto-Save Debounce
$effect(() => {
const _trigger = tmp_entry_obj.content;
if (has_unsaved_changes && !is_processing) {
if (save_status !== 'saving') save_status = 'unsaved'; if (save_status !== 'saving') save_status = 'unsaved';
if ($journals_loc.entry.auto_save) { if ($journals_loc.entry.auto_save) {
// This will now correctly reset on every keystroke
clearTimeout(auto_save_timer); clearTimeout(auto_save_timer);
auto_save_timer = setTimeout(() => { auto_save_timer = setTimeout(() => {
if (tmp_entry_obj_changed && save_status !== 'saving') { if (untrack(() => has_unsaved_changes)) update_journal_entry();
update_journal_entry(); }, 2000);
}
}, 2000); // 2 seconds debounce
}
} else {
clearTimeout(auto_save_timer);
if (save_status !== 'saving') {
save_status = 'saved';
} }
} else if (save_status !== 'saving' && !has_unsaved_changes) {
save_status = 'saved';
} }
}); });
let decrypted_history: string = $state(''); // 3. Auto-Decryption Workflow
if (!$journals_loc?.entry?.decrypt_kv) {
$journals_loc.entry.decrypt_kv = {};
$journals_loc.entry.decrypt_kv[$lq__journal_entry_obj?.journal_entry_id] = null;
}
function not_obj(obj) {
return !obj || typeof obj !== 'object' || Object.keys(obj).length === 0;
}
$effect(() => { $effect(() => {
(async () => { const journal = $lq__journal_obj;
if ($lq__journal_entry_obj && $lq__journal_entry_obj?.updated_on) { if (!journal?.id) return;
// Only overwrite local state if we aren't currently editing/unsaved
if (save_status === 'saved' || !tmp_entry_obj_changed) {
if (log_lvl) console.log(`Journal Entry updated:`, $lq__journal_entry_obj);
orig_entry_obj = { ...$lq__journal_entry_obj }; const session = $journals_sess?.journal_kv[journal.id];
tmp_entry_obj = $lq__journal_entry_obj ? { ...$lq__journal_entry_obj } : {}; const is_verified = session?.journal_passcode_verified;
const decrypted_status = session?.journal_passcode_decrypted;
updated_idb = false; if ($lq__journal_entry_obj?.content_encrypted && is_verified && decrypted_status !== true && decrypted_status !== 'processing') {
} run_decryption_workflow();
} else if (updated_obj && (await $lq__journal_entry_obj)) {
// If we forced an update (e.g. after a manual save), we do sync
if (log_lvl) console.log(`Journal Entry forced update:`, $lq__journal_entry_obj);
updated_obj = false;
orig_entry_obj = { ...$lq__journal_entry_obj };
tmp_entry_obj = $lq__journal_entry_obj ? { ...$lq__journal_entry_obj } : {};
if ($journals_loc?.entry?.decrypt_kv[$lq__journal_entry_obj?.journal_entry_id]) {
// ... (decryption logic remains the same)
}
updated_idb = false;
}
})();
});
$effect(() => {
if (
!not_obj(tmp_entry_obj) &&
!not_obj(orig_entry_obj) &&
JSON.stringify(tmp_entry_obj) != JSON.stringify(orig_entry_obj)
) {
console.log(
'TEST: tmp_entry_obj and orig_entry_obj available; marking tmp_entry_obj as changed'
);
tmp_entry_obj_changed = true;
} else {
// console.log('TEST: tmp_entry_obj == orig_entry_obj');
tmp_entry_obj_changed = false;
} }
}); });
// Better decrypted content and history handling // *** Actions
$effect(() => {
if (
$lq__journal_entry_obj?.content_encrypted &&
$journals_sess?.journal_kv[$lq__journal_obj?.journal_id]?.journal_passcode_verified &&
!$journals_sess?.journal_kv[$lq__journal_obj?.journal_id]?.journal_passcode_decrypted
) {
let journal_key = null;
if (
$journals_sess.journal_kv[$lq__journal_obj?.journal_id]?.typed_journal_passcode
?.length
) {
journal_key = $journals_sess.journal_kv[$lq__journal_obj?.journal_id]?.typed_journal_passcode;
} else {
journal_key = $lq__journal_obj?.private_passcode;
}
if (journal_key == null) { async function run_decryption_workflow() {
console.log('TEST: No journal key available to decrypt the content.'); const journal = $lq__journal_obj;
return; if (!journal?.id) return;
}
let content_decrypted = null; let journal_key = $journals_sess.journal_kv[journal.id]?.typed_journal_passcode;
if (!journal_key?.length) journal_key = journal.private_passcode;
if (!journal_key) return;
$journals_sess.journal_kv[$lq__journal_obj?.journal_id].journal_passcode_decrypted = is_processing = true;
'processing'; decryption_error = null;
(async () => { journals_sess.update(s => {
let result = await ae_util.decrypt_wrapper( if (!s.journal_kv[journal.id]) s.journal_kv[journal.id] = {};
$lq__journal_entry_obj?.content_encrypted, s.journal_kv[journal.id].journal_passcode_decrypted = 'processing';
journal_key return s;
); });
content_decrypted = typeof result === 'string' ? result : '';
console.log('TEST: Decrypted content:', content_decrypted);
$journals_sess.journal_kv[$lq__journal_obj?.journal_id].journal_passcode_decrypted = const decrypt_key = `${journal.passcode ?? ''}:${journal_key}`;
true; const result = await ae_util.decrypt_wrapper($lq__journal_entry_obj?.content_encrypted, decrypt_key);
tmp_entry_obj.content = content_decrypted; if (result === false) {
tmp_entry_obj.content_md_html = handle_marked(content_decrypted); decryption_error = 'Decryption failed. Incorrect passcode or corrupted data.';
})(); journals_sess.update(s => {
s.journal_kv[journal.id].journal_passcode_verified = false;
s.journal_kv[journal.id].journal_passcode_decrypted = false;
return s;
});
is_processing = false;
return;
} }
});
async function handle_content_decryption() { // SUCCESS
if ( const decrypted_text = typeof result === 'string' ? result : '';
tmp_entry_obj?.private && tmp_entry_obj.content = decrypted_text;
$lq__journal_obj?.combined_passcode && tmp_entry_obj.content_md_html = handle_marked(decrypted_text);
tmp_entry_obj?.content_encrypted if (orig_entry_obj) orig_entry_obj.content = decrypted_text;
) {
if (!tmp_entry_obj?.content) {
let result = await ae_util.decrypt_wrapper(
tmp_entry_obj?.content_encrypted,
$lq__journal_obj?.combined_passcode
);
tmp_entry_obj.content = typeof result === 'string' ? result : '';
tmp_entry_obj.content_md_html = await handle_marked(tmp_entry_obj?.content);
// Also decrypt history if present // Decrypt History
if (tmp_entry_obj?.history_encrypted) { if ($lq__journal_entry_obj?.history_encrypted) {
let h_result = await ae_util.decrypt_wrapper( const h_res = await ae_util.decrypt_wrapper($lq__journal_entry_obj.history_encrypted, decrypt_key);
tmp_entry_obj?.history_encrypted, if (h_res !== false) {
$lq__journal_obj?.combined_passcode tmp_entry_obj.history = h_res;
); if (orig_entry_obj) orig_entry_obj.history = h_res;
tmp_entry_obj.history = typeof h_result === 'string' ? h_result : '';
tmp_entry_obj.history_md_html = await handle_marked(tmp_entry_obj?.history);
}
$journals_sess.entry.decrypt_kv[$lq__journal_entry_obj?.journal_entry_id] = true;
} else {
// Clear
tmp_entry_obj.content = null;
tmp_entry_obj.content_md_html = null;
tmp_entry_obj.history = null;
tmp_entry_obj.history_md_html = null;
$journals_sess.entry.decrypt_kv[$lq__journal_entry_obj?.journal_entry_id] = false;
} }
} }
}
async function handle_history_decryption() { journals_sess.update(s => {
if (tmp_entry_obj?.history_encrypted) { s.journal_kv[journal.id].journal_passcode_decrypted = true;
// Determine key return s;
let key = $lq__journal_obj?.private_passcode; });
if ( is_processing = false;
$journals_sess.journal_kv[$lq__journal_obj?.journal_id]?.typed_journal_passcode
?.length
) {
key =
$journals_sess.journal_kv[$lq__journal_obj?.journal_id]?.typed_journal_passcode;
}
if (!key) key = $lq__journal_obj?.combined_passcode;
let result = await ae_util.decrypt_wrapper(tmp_entry_obj?.history_encrypted, key);
decrypted_history = typeof result === 'string' ? result : '';
tmp_entry_obj.history = decrypted_history;
tmp_entry_obj.history_md_html = await handle_marked(decrypted_history);
}
} }
async function update_journal_entry() { async function update_journal_entry() {
if (!$ae_loc.trusted_access) { if (!$ae_loc.trusted_access) return;
alert('You do not have permission to update this journal entry.');
return;
}
save_status = 'saving'; save_status = 'saving';
let data_kv: key_val = { // Reference Standard: Explicit whitelist of editable fields
alert: tmp_entry_obj?.alert, const data_kv: key_val = {
personal: tmp_entry_obj?.personal, name: tmp_entry_obj.name,
private: tmp_entry_obj?.private, content: tmp_entry_obj.content,
professional: tmp_entry_obj?.professional, history: tmp_entry_obj.history,
public: tmp_entry_obj?.public, tags: tmp_entry_obj.tags,
template: tmp_entry_obj?.template, category_code: tmp_entry_obj.category_code,
summary: tmp_entry_obj.summary,
hide: tmp_entry_obj?.hide, priority: tmp_entry_obj.priority,
priority: tmp_entry_obj?.priority, private: tmp_entry_obj.private,
enable: tmp_entry_obj?.enable, public: tmp_entry_obj.public,
personal: tmp_entry_obj.personal,
// alert_msg: $lq__journal_entry_obj?.alert_msg ? false : true professional: tmp_entry_obj.professional,
alert_msg: tmp_entry_obj?.alert_msg, alert: tmp_entry_obj.alert,
category_code: tmp_entry_obj?.category_code, alert_msg: tmp_entry_obj.alert_msg,
summary: tmp_entry_obj?.summary, hide: tmp_entry_obj.hide,
content: tmp_entry_obj?.content, enable: tmp_entry_obj.enable,
content_encrypted: null, // This should only be generated below. sort: tmp_entry_obj.sort,
history: tmp_entry_obj?.history, group: tmp_entry_obj.group,
history_encrypted: null, // This should only be generated below. data_json: tmp_entry_obj.data_json
data_json: tmp_entry_obj?.data_json,
sort: tmp_entry_obj?.sort,
group: tmp_entry_obj?.group,
archive_on: tmp_entry_obj?.archive_on,
name: tmp_entry_obj?.name,
tags: tmp_entry_obj?.tags
}; };
if (tmp_entry_obj?.content || tmp_entry_obj?.content_encrypted) { const decrypt_key = $lq__journal_obj.combined_passcode;
let decrypt_key = $lq__journal_obj.combined_passcode;
if (!tmp_entry_obj?.private) { // Encryption Logic Transition
if (log_lvl) console.log('JournalEntry: Processing non-private content...'); if (tmp_entry_obj.private) {
if (tmp_entry_obj.content) {
if (tmp_entry_obj.content) { data_kv.content_encrypted = await ae_util.encrypt_wrapper(tmp_entry_obj.content, decrypt_key);
data_kv.content = tmp_entry_obj?.content;
} else if (tmp_entry_obj.content_encrypted) {
let decrypted_content = await ae_util.decrypt_wrapper(
tmp_entry_obj?.content_encrypted,
decrypt_key
);
data_kv.content = typeof decrypted_content === 'string' ? decrypted_content : '';
} else {
data_kv.content = '';
}
let { left_over_string, cut_out_string } = handle_cut_string(data_kv.content);
data_kv.content = left_over_string;
data_kv.content_encrypted = null;
if (cut_out_string) {
let cut_prefix = `# Cut on ${ae_util.iso_datetime_formatter(new Date().toISOString(), 'datetime_iso_12_no_seconds')}\n`;
cut_out_string = cut_prefix + cut_out_string;
}
if (tmp_entry_obj?.history?.length && cut_out_string) {
data_kv.history = tmp_entry_obj?.history + '\n' + cut_out_string + '\n';
} else if (cut_out_string) {
data_kv.history = cut_out_string + '\n';
}
data_kv.history_encrypted = null;
} else if (tmp_entry_obj?.private) {
let { left_over_string, cut_out_string } = handle_cut_string(
tmp_entry_obj?.content
);
let content_enc_combined_data = await ae_util.encrypt_wrapper(
left_over_string,
decrypt_key
);
if (log_lvl) console.log('TEST: Encrypted string:', content_enc_combined_data);
data_kv.content_encrypted = content_enc_combined_data;
data_kv.content = null; data_kv.content = null;
tmp_entry_obj.content = null;
if (tmp_entry_obj?.history_encrypted) {
let result = await ae_util.decrypt_wrapper(
tmp_entry_obj?.history_encrypted,
decrypt_key
);
decrypted_history = typeof result === 'string' ? result : '';
tmp_entry_obj.history = decrypted_history;
}
if (cut_out_string) {
let cut_prefix = `# Cut on ${ae_util.iso_datetime_formatter(new Date().toISOString(), 'datetime_iso_12_no_seconds')}\n`;
cut_out_string = cut_prefix + cut_out_string;
}
if (tmp_entry_obj?.history?.length && cut_out_string) {
data_kv.history = tmp_entry_obj?.history + '\n' + cut_out_string + '\n';
} else {
data_kv.history = cut_out_string + '\n';
}
if (log_lvl) console.log('TEST: Encrypting history...');
let history_enc_combined_data = await ae_util.encrypt_wrapper(
data_kv.history,
decrypt_key
);
data_kv.history_encrypted = history_enc_combined_data;
data_kv.history = null;
decrypted_history = '';
} }
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;
} }
// Call API to save the content
try {
const response = 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
});
if (log_lvl) console.log('Journal entry updated successfully:', response);
// Sync orig_entry_obj immediately to stop the change detection loop
orig_entry_obj = { ...tmp_entry_obj };
updated_obj = true;
save_status = 'saved';
} catch (error) {
console.error('Error updating journal entry:', error);
save_status = 'unsaved';
alert('Failed to update journal entry.');
}
}
async function change_journal_id() {
if (!$ae_loc.trusted_access) {
alert('You do not have permission to update this journal entry.');
return;
}
let data_kv = { journal_id_random: tmp_entry_obj?.journal_id };
try { try {
await journals_func.update_ae_obj__journal_entry({ await journals_func.update_ae_obj__journal_entry({
api_cfg: $ae_api, api_cfg: $ae_api,
journal_entry_id: $lq__journal_entry_obj?.journal_entry_id, journal_entry_id: $lq__journal_entry_obj?.journal_entry_id,
data_kv: data_kv, data_kv: data_kv,
log_lvl: log_lvl log_lvl: 1
}); });
updated_obj = true; orig_entry_obj = { ...tmp_entry_obj };
updated_idb = false; save_status = 'saved';
if (log_lvl) console.log('Journal entry journal_id changed successfully!');
} catch (error) { } catch (error) {
console.error('Error changing journal ID:', error); console.error('Update failed:', error);
alert('Failed to change journal ID.'); save_status = 'unsaved';
} }
$journals_slct.journal_id = tmp_entry_obj?.journal_id;
goto(`/journals/${tmp_entry_obj?.journal_id}`);
} }
// *** Cryptographic Helpers (Simplified) async function handle_content_decryption() {
async function handle_decrypt_string(encrypted_string: string, passcode: string) { if (!tmp_entry_obj.content) {
if (log_lvl) console.log(`TEST: handle_decrypt_string: ${passcode}`, encrypted_string); await run_decryption_workflow();
if (!encrypted_string) return ''; } else {
if (!passcode) return false; // Re-lock logic
is_processing = true;
let decrypted_string = await ae_util.decrypt_wrapper(encrypted_string, passcode); tmp_entry_obj.content = null;
if (log_lvl) console.log('TEST: Decrypted string:', decrypted_string); if (orig_entry_obj) orig_entry_obj.content = null;
journals_sess.update(s => {
return decrypted_string; s.journal_kv[$lq__journal_obj.id].journal_passcode_decrypted = false;
} return s;
});
// return new_string and cut_string is_processing = false;
function handle_cut_string(
old_string: string,
start_tag: string = '<cut>',
end_tag: string = '</cut>'
) {
let left_over_string = old_string; // Will be for the updated content field
let cut_out_string = ''; // Will be for the history field
if (old_string) {
let cut_index = old_string.indexOf(start_tag);
let cut_end_index = old_string.indexOf(end_tag);
if (cut_index !== -1) {
if (cut_end_index !== -1 && cut_end_index > cut_index) {
// Cut everything between the cut tags
cut_out_string = old_string
.substring(cut_index + start_tag.length, cut_end_index)
.trim();
left_over_string = (
old_string.substring(0, cut_index) +
' ' +
old_string.substring(cut_end_index + end_tag.length)
).trim();
} else {
// Cut everything after the cut tag
cut_out_string = old_string.substring(cut_index + start_tag.length).trim();
left_over_string = old_string.substring(0, cut_index).trim();
}
}
} }
return { left_over_string, cut_out_string };
} }
function handle_marked(text_string: string) { function handle_marked(text: string) {
if (!text_string) return ''; if (!text) return '';
return marked.parse(text.replace(/^[]/, ''));
let cleaned_string = text_string.replace(/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/, '');
return marked.parse(cleaned_string);
} }
let slct_hosted_file_kv: key_val = $state({}); async function change_journal_id() {
let slct_hosted_file_id: any = $state(null); if (!$ae_loc.trusted_access) return;
let slct_hosted_file_obj: any = $state(null); await journals_func.update_ae_obj__journal_entry({
$effect(() => { api_cfg: $ae_api,
if (slct_hosted_file_id && slct_hosted_file_obj) { journal_entry_id: $lq__journal_entry_obj?.journal_entry_id,
console.log(`*** handle_hosted_file_selected() *** ${slct_hosted_file_id}`); data_kv: { journal_id_random: tmp_entry_obj.journal_id }
// We need to update the journal_entry_obj with the new file (for now just the first one). });
goto(`/journals/${tmp_entry_obj.journal_id}`);
}
update_journal_entry();
slct_hosted_file_id = null;
slct_hosted_file_obj = null;
}
});
// --- Shared Append / Prepend Modal Logic ---
let show_append_modal = $state(false); let show_append_modal = $state(false);
let modal_mode: 'append' | 'prepend' | 'auto' = $state('auto'); let modal_mode: 'append' | 'prepend' | 'auto' = $state('auto');
function handle_append_start() {
modal_mode = 'append';
show_append_modal = true;
$journals_sess.entry.show_menu = false;
}
function handle_prepend_start() {
modal_mode = 'prepend';
show_append_modal = true;
$journals_sess.entry.show_menu = false;
}
</script> </script>
<section <section class="ae_view flex flex-col gap-2 w-full h-full p-1" bind:clientHeight={$ae_loc.iframe_height_modal_body}>
class="
svelte_component ae_section ae_view journal_entry_obj view__journal_entry_obj
flex flex-col gap-1 grow items-center justify-start
w-full h-full
p-1 m-1
"
bind:clientHeight={$ae_loc.iframe_height_modal_body}
>
{#if $lq__journal_entry_obj} {#if $lq__journal_entry_obj}
<JournalEntry_Header <JournalEntry_Header
entry={$lq__journal_entry_obj} entry={$lq__journal_entry_obj}
journal={$lq__journal_obj} journal={$lq__journal_obj}
journals_li={$lq__journal_obj_li} journals_li={$lq__journal_obj_li}
bind:tmp_entry_obj bind:tmp_entry_obj
has_changed={tmp_entry_obj_changed} has_changed={has_unsaved_changes}
onSave={update_journal_entry} onSave={update_journal_entry}
onDecrypt={handle_content_decryption} onDecrypt={handle_content_decryption}
onDecryptHistory={handle_history_decryption} onDecryptHistory={() => {}}
onChangeJournal={change_journal_id} onChangeJournal={change_journal_id}
onAppend={handle_append_start} onAppend={() => { modal_mode = 'append'; show_append_modal = true; }}
onPrepend={handle_prepend_start} onPrepend={() => { modal_mode = 'prepend'; show_append_modal = true; }}
{onShowExport} {onShowExport}
{save_status} {save_status}
{log_lvl} {log_lvl}
/> />
{#if decryption_error}
<div class="w-full p-4 bg-error-500/20 border-2 border-error-500 rounded-lg flex items-center justify-between shadow-2xl z-50 animate-bounce">
<div class="flex items-center gap-4 text-error-700 dark:text-error-300 font-bold">
<AlertCircle size="2.5em" />
<span class="text-xl">{decryption_error}</span>
</div>
<button class="btn btn-sm variant-filled-error shadow-lg font-bold" onclick={() => decryption_error = null}>
<XCircle size="1.2em" class="mr-2" /> Dismiss
</button>
</div>
{/if}
<section class="grow relative bg-gray-100 dark:bg-gray-800 p-1 rounded-lg shadow-md overflow-hidden"
class:bg-yellow-50={$journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] == 'current'}>
<section
class="
grow
basis-full
flex flex-col flex-wrap gap-1 items-center justify-center
h-full min-h-max max-h-full
w-full max-w-6xl
relative
bg-gray-100 dark:bg-gray-800
p-1 rounded-lg shadow-md
"
class:bg-yellow-50={$journals_loc.entry.edit_kv[
$lq__journal_entry_obj?.journal_entry_id
] == 'current'}
class:dark:bg-yellow-950={$journals_loc.entry.edit_kv[
$lq__journal_entry_obj?.journal_entry_id
] == 'current'}
>
<!-- Generic AI Toolset -->
<div class="absolute top-2 right-2 z-10"> <div class="absolute top-2 right-2 z-10">
<AE_AITools <AE_AITools
content={tmp_entry_obj.content} content={tmp_entry_obj.content}
bind:summary={tmp_entry_obj.summary} bind:summary={tmp_entry_obj.summary}
onSave={update_journal_entry} onSave={update_journal_entry}
bind:token={$journals_loc.llm__api_token}
bind:baseUrl={$journals_loc.llm__api_base_url}
bind:model={$journals_loc.llm__api_model}
bind:systemPrompt={$journals_loc.entry.llm__system_prompt}
bind:maxTokens={$journals_loc.entry.llm__max_tokens}
bind:temperature={$journals_loc.entry.llm__temperature}
onSyncConfig={() => {
$journals_loc.llm__api_base_url = $ae_loc.site_cfg_json?.llm__api_base_url ?? 'https://ai.dgrzone.com/api';
$journals_loc.llm__api_model = $ae_loc.site_cfg_json?.llm__api_model ?? 'dgrzone-deepseek-8b-quick';
$journals_loc.llm__api_token = $ae_loc.site_cfg_json?.llm__api_token ?? '';
$journals_loc.entry.llm__system_prompt = $ae_loc.site_cfg_json?.llm__system_prompt ?? '';
}}
{log_lvl} {log_lvl}
/> />
</div> </div>
<!-- Main Content Editor/Viewer -->
<JournalEntry_Editor <JournalEntry_Editor
entry={$lq__journal_entry_obj} entry={$lq__journal_entry_obj}
journal={$lq__journal_obj} journal={$lq__journal_obj}
bind:tmp_entry_obj={tmp_entry_obj} bind:tmp_entry_obj
bind:editorView={editorView} bind:editorView
has_changed={tmp_entry_obj_changed} has_changed={has_unsaved_changes}
updated_idb={updated_idb} updated_idb={false}
onSave={update_journal_entry} onSave={update_journal_entry}
/> />
{#if $journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] == 'history'}
<div
class="grow basis-full flex flex-col items-center justify-center h-full w-full max-w-6xl"
>
{#if $journals_sess?.show__content__journal_entry_history == 'view'}
<E_app_codemirror_v5
editable={false}
readonly={true}
content={tmp_entry_obj?.history ?? ''}
bind:new_content={tmp_entry_obj.history}
theme_mode={$ae_loc.theme_mode}
placeholder="History content..."
class="p-2 preset-outlined-success-400-600 hover:preset-tonal-surface shadow-lg rounded-lg"
/>
{:else if $journals_sess?.show__content__journal_entry_history == 'edit'}
<E_app_codemirror_v5
content={tmp_entry_obj?.history ?? ''}
bind:new_content={tmp_entry_obj.history}
theme_mode={$ae_loc.theme_mode}
placeholder="Edit history..."
class="p-2 preset-outlined-warning-300-700 shadow-lg rounded-lg bg-gray-100 text-gray-950 dark:bg-gray-800 dark:text-gray-100"
/>
{/if}
</div>
{/if}
</section> </section>
<AE_MetadataFooter obj={tmp_entry_obj} /> <AE_MetadataFooter obj={tmp_entry_obj} />
@@ -643,17 +329,13 @@
journal_config={$lq__journal_obj?.cfg_json} journal_config={$lq__journal_obj?.cfg_json}
mode={modal_mode} mode={modal_mode}
onClose={() => (show_append_modal = false)} onClose={() => (show_append_modal = false)}
onUpdate={() => { onUpdate={() => { show_append_modal = false; updated_obj = true; }}
show_append_modal = false;
updated_obj = true; // Force refresh of the view
}}
{log_lvl} {log_lvl}
/> />
{:else} {:else}
<section class="ae_meta flex flex-row flex-wrap gap-1 items-center justify-center w-full"> <div class="p-20 text-center opacity-50 flex flex-col items-center gap-4">
<span class="text-lg text-orange-900 dark:text-orange-100"> <Loader2 class="animate-spin" size="4em" />
The Journal Entry was not found or is not available to show. <span class="text-2xl font-bold">Loading Journal Entry...</span>
</span> </div>
</section>
{/if} {/if}
</section> </section>

View File

@@ -68,7 +68,15 @@
} }
return; return;
} }
console.log('Setting passcode timer');
// Use journal.passcode_timeout (assuming it's in minutes, default to 5)
const timeout_minutes = $lq__journal_obj?.passcode_timeout ?? 5;
const timeout_ms = 1000 * 60 * timeout_minutes;
if (log_lvl) {
console.log(`Setting passcode timer for ${timeout_minutes} minutes (${timeout_ms}ms)`);
}
passcode_timer = setTimeout( passcode_timer = setTimeout(
() => { () => {
if (log_lvl) { if (log_lvl) {
@@ -78,12 +86,15 @@
if (!$journals_sess?.journal_kv[$lq__journal_obj?.id]) { if (!$journals_sess?.journal_kv[$lq__journal_obj?.id]) {
$journals_sess.journal_kv[$lq__journal_obj?.id] = {}; $journals_sess.journal_kv[$lq__journal_obj?.id] = {};
} }
$journals_sess.journal_kv[$lq__journal_obj?.id].journal_passcode_verified =
false; // Reset verification and decryption flags
$journals_sess.journal_kv[$lq__journal_obj?.id].journal_passcode_verified = false;
$journals_sess.journal_kv[$lq__journal_obj?.id].journal_passcode_decrypted = false;
passcode_timer = null;
}, },
1000 * 60 * 1 timeout_ms
); // 1 minutes );
// }, 1000 * $lq__journal_obj?.passcode_timeout); // 5 minutes
} }
}); });