feat(journals): stabilize V3 API integration and restore cryptographic toggles
This commit is contained in:
@@ -1,37 +1,32 @@
|
||||
<script lang="ts">
|
||||
// let log_lvl: number = 0;
|
||||
|
||||
// *** Import Svelte core and navigation
|
||||
/**
|
||||
* 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
|
||||
import { goto } from '$app/navigation';
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
// *** Import secondary libraries
|
||||
import { marked } from 'marked';
|
||||
|
||||
// *** 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 { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
import {
|
||||
ae_loc,
|
||||
ae_api,
|
||||
ae_trig
|
||||
} from '$lib/stores/ae_stores';
|
||||
import {
|
||||
journals_loc,
|
||||
journals_sess,
|
||||
journals_slct
|
||||
} from '$lib/ae_journals/ae_journals_stores';
|
||||
import { ae_loc, ae_api } 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 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_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';
|
||||
|
||||
// Icons
|
||||
import { AlertCircle, XCircle, Loader2 } from '@lucide/svelte';
|
||||
|
||||
// *** Props
|
||||
interface Props {
|
||||
@@ -50,589 +45,280 @@
|
||||
onShowExport
|
||||
}: 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
|
||||
let editorView: any = $state();
|
||||
let ae_promises: any = $state();
|
||||
|
||||
let orig_entry_obj: key_val | null = $state({});
|
||||
let orig_entry_obj: key_val | null = $state(null);
|
||||
let tmp_entry_obj: key_val = $state({});
|
||||
let updated_obj: boolean = $state(true); // Start with true to force population
|
||||
let updated_idb: boolean = $state(true); // Internal session sync flag
|
||||
|
||||
// Idiomatic Svelte 5 change detection
|
||||
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;
|
||||
}
|
||||
let save_status: 'saved' | 'unsaved' | 'saving' = $state('saved');
|
||||
let decryption_error: string | null = $state(null);
|
||||
let auto_save_timer: ReturnType<typeof setTimeout>;
|
||||
let is_processing = $state(false);
|
||||
|
||||
// *** Derived
|
||||
let has_unsaved_changes = $derived.by(() => {
|
||||
if (!tmp_entry_obj || !orig_entry_obj || is_processing) return false;
|
||||
return (
|
||||
tmp_entry_obj.content !== orig_entry_obj.content ||
|
||||
tmp_entry_obj.name !== orig_entry_obj.name ||
|
||||
tmp_entry_obj.tags !== orig_entry_obj.tags ||
|
||||
tmp_entry_obj.category_code !== orig_entry_obj.category_code
|
||||
(tmp_entry_obj.content ?? null) !== (orig_entry_obj.content ?? null) ||
|
||||
(tmp_entry_obj.name ?? null) !== (orig_entry_obj.name ?? null) ||
|
||||
(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)
|
||||
);
|
||||
});
|
||||
|
||||
// --- Auto Save Logic ---
|
||||
let save_status: 'saved' | 'unsaved' | 'saving' = $state('saved');
|
||||
let auto_save_timer: ReturnType<typeof setTimeout>;
|
||||
// *** Effects
|
||||
|
||||
// 1. Initial Load & Background Sync
|
||||
$effect(() => {
|
||||
// Explicitly track these properties so the effect re-runs on every keystroke
|
||||
const _content = tmp_entry_obj.content;
|
||||
const _name = tmp_entry_obj.name;
|
||||
const _tags = tmp_entry_obj.tags;
|
||||
const _category = tmp_entry_obj.category_code;
|
||||
const entry = $lq__journal_entry_obj;
|
||||
if (entry && entry.updated_on) {
|
||||
// Only sync if saved and not currently processing/editing
|
||||
if (save_status === 'saved' && !has_unsaved_changes && !is_processing) {
|
||||
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 ($journals_loc.entry.auto_save) {
|
||||
// This will now correctly reset on every keystroke
|
||||
clearTimeout(auto_save_timer);
|
||||
auto_save_timer = setTimeout(() => {
|
||||
if (tmp_entry_obj_changed && save_status !== 'saving') {
|
||||
update_journal_entry();
|
||||
}
|
||||
}, 2000); // 2 seconds debounce
|
||||
}
|
||||
} else {
|
||||
clearTimeout(auto_save_timer);
|
||||
if (save_status !== 'saving') {
|
||||
save_status = 'saved';
|
||||
if (untrack(() => has_unsaved_changes)) update_journal_entry();
|
||||
}, 2000);
|
||||
}
|
||||
} else if (save_status !== 'saving' && !has_unsaved_changes) {
|
||||
save_status = 'saved';
|
||||
}
|
||||
});
|
||||
|
||||
let decrypted_history: string = $state('');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 3. Auto-Decryption Workflow
|
||||
$effect(() => {
|
||||
(async () => {
|
||||
if ($lq__journal_entry_obj && $lq__journal_entry_obj?.updated_on) {
|
||||
// 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);
|
||||
const journal = $lq__journal_obj;
|
||||
if (!journal?.id) return;
|
||||
|
||||
orig_entry_obj = { ...$lq__journal_entry_obj };
|
||||
tmp_entry_obj = $lq__journal_entry_obj ? { ...$lq__journal_entry_obj } : {};
|
||||
|
||||
updated_idb = false;
|
||||
}
|
||||
} 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;
|
||||
const session = $journals_sess?.journal_kv[journal.id];
|
||||
const is_verified = session?.journal_passcode_verified;
|
||||
const decrypted_status = session?.journal_passcode_decrypted;
|
||||
|
||||
if ($lq__journal_entry_obj?.content_encrypted && is_verified && decrypted_status !== true && decrypted_status !== 'processing') {
|
||||
run_decryption_workflow();
|
||||
}
|
||||
});
|
||||
|
||||
// Better decrypted content and history handling
|
||||
$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;
|
||||
}
|
||||
// *** Actions
|
||||
|
||||
if (journal_key == null) {
|
||||
console.log('TEST: No journal key available to decrypt the content.');
|
||||
return;
|
||||
}
|
||||
async function run_decryption_workflow() {
|
||||
const journal = $lq__journal_obj;
|
||||
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 =
|
||||
'processing';
|
||||
is_processing = true;
|
||||
decryption_error = null;
|
||||
|
||||
journals_sess.update(s => {
|
||||
if (!s.journal_kv[journal.id]) s.journal_kv[journal.id] = {};
|
||||
s.journal_kv[journal.id].journal_passcode_decrypted = 'processing';
|
||||
return s;
|
||||
});
|
||||
|
||||
(async () => {
|
||||
let result = await ae_util.decrypt_wrapper(
|
||||
$lq__journal_entry_obj?.content_encrypted,
|
||||
journal_key
|
||||
);
|
||||
content_decrypted = typeof result === 'string' ? result : '';
|
||||
console.log('TEST: Decrypted content:', content_decrypted);
|
||||
const decrypt_key = `${journal.passcode ?? ''}:${journal_key}`;
|
||||
const result = await ae_util.decrypt_wrapper($lq__journal_entry_obj?.content_encrypted, decrypt_key);
|
||||
|
||||
$journals_sess.journal_kv[$lq__journal_obj?.journal_id].journal_passcode_decrypted =
|
||||
true;
|
||||
|
||||
tmp_entry_obj.content = content_decrypted;
|
||||
tmp_entry_obj.content_md_html = handle_marked(content_decrypted);
|
||||
})();
|
||||
if (result === false) {
|
||||
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() {
|
||||
if (
|
||||
tmp_entry_obj?.private &&
|
||||
$lq__journal_obj?.combined_passcode &&
|
||||
tmp_entry_obj?.content_encrypted
|
||||
) {
|
||||
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);
|
||||
// SUCCESS
|
||||
const decrypted_text = typeof result === 'string' ? result : '';
|
||||
tmp_entry_obj.content = decrypted_text;
|
||||
tmp_entry_obj.content_md_html = handle_marked(decrypted_text);
|
||||
if (orig_entry_obj) orig_entry_obj.content = decrypted_text;
|
||||
|
||||
// Also decrypt history if present
|
||||
if (tmp_entry_obj?.history_encrypted) {
|
||||
let h_result = await ae_util.decrypt_wrapper(
|
||||
tmp_entry_obj?.history_encrypted,
|
||||
$lq__journal_obj?.combined_passcode
|
||||
);
|
||||
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;
|
||||
// Decrypt History
|
||||
if ($lq__journal_entry_obj?.history_encrypted) {
|
||||
const h_res = await ae_util.decrypt_wrapper($lq__journal_entry_obj.history_encrypted, decrypt_key);
|
||||
if (h_res !== false) {
|
||||
tmp_entry_obj.history = h_res;
|
||||
if (orig_entry_obj) orig_entry_obj.history = h_res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handle_history_decryption() {
|
||||
if (tmp_entry_obj?.history_encrypted) {
|
||||
// Determine key
|
||||
let key = $lq__journal_obj?.private_passcode;
|
||||
if (
|
||||
$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);
|
||||
}
|
||||
journals_sess.update(s => {
|
||||
s.journal_kv[journal.id].journal_passcode_decrypted = true;
|
||||
return s;
|
||||
});
|
||||
is_processing = false;
|
||||
}
|
||||
|
||||
async function update_journal_entry() {
|
||||
if (!$ae_loc.trusted_access) {
|
||||
alert('You do not have permission to update this journal entry.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$ae_loc.trusted_access) return;
|
||||
save_status = 'saving';
|
||||
|
||||
let data_kv: key_val = {
|
||||
alert: tmp_entry_obj?.alert,
|
||||
personal: tmp_entry_obj?.personal,
|
||||
private: tmp_entry_obj?.private,
|
||||
professional: tmp_entry_obj?.professional,
|
||||
public: tmp_entry_obj?.public,
|
||||
template: tmp_entry_obj?.template,
|
||||
|
||||
hide: tmp_entry_obj?.hide,
|
||||
priority: tmp_entry_obj?.priority,
|
||||
enable: tmp_entry_obj?.enable,
|
||||
|
||||
// alert_msg: $lq__journal_entry_obj?.alert_msg ? false : true
|
||||
alert_msg: tmp_entry_obj?.alert_msg,
|
||||
category_code: tmp_entry_obj?.category_code,
|
||||
summary: tmp_entry_obj?.summary,
|
||||
content: tmp_entry_obj?.content,
|
||||
content_encrypted: null, // This should only be generated below.
|
||||
history: tmp_entry_obj?.history,
|
||||
history_encrypted: null, // This should only be generated below.
|
||||
|
||||
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
|
||||
// Reference Standard: Explicit whitelist of editable fields
|
||||
const data_kv: key_val = {
|
||||
name: tmp_entry_obj.name,
|
||||
content: tmp_entry_obj.content,
|
||||
history: tmp_entry_obj.history,
|
||||
tags: tmp_entry_obj.tags,
|
||||
category_code: tmp_entry_obj.category_code,
|
||||
summary: tmp_entry_obj.summary,
|
||||
priority: tmp_entry_obj.priority,
|
||||
private: tmp_entry_obj.private,
|
||||
public: tmp_entry_obj.public,
|
||||
personal: tmp_entry_obj.personal,
|
||||
professional: tmp_entry_obj.professional,
|
||||
alert: tmp_entry_obj.alert,
|
||||
alert_msg: tmp_entry_obj.alert_msg,
|
||||
hide: tmp_entry_obj.hide,
|
||||
enable: tmp_entry_obj.enable,
|
||||
sort: tmp_entry_obj.sort,
|
||||
group: tmp_entry_obj.group,
|
||||
data_json: tmp_entry_obj.data_json
|
||||
};
|
||||
|
||||
if (tmp_entry_obj?.content || tmp_entry_obj?.content_encrypted) {
|
||||
let decrypt_key = $lq__journal_obj.combined_passcode;
|
||||
const decrypt_key = $lq__journal_obj.combined_passcode;
|
||||
|
||||
if (!tmp_entry_obj?.private) {
|
||||
if (log_lvl) console.log('JournalEntry: Processing non-private content...');
|
||||
|
||||
if (tmp_entry_obj.content) {
|
||||
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;
|
||||
// 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;
|
||||
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 {
|
||||
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: log_lvl
|
||||
log_lvl: 1
|
||||
});
|
||||
updated_obj = true;
|
||||
updated_idb = false;
|
||||
if (log_lvl) console.log('Journal entry journal_id changed successfully!');
|
||||
orig_entry_obj = { ...tmp_entry_obj };
|
||||
save_status = 'saved';
|
||||
} catch (error) {
|
||||
console.error('Error changing journal ID:', error);
|
||||
alert('Failed to change journal ID.');
|
||||
console.error('Update failed:', error);
|
||||
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_decrypt_string(encrypted_string: string, passcode: string) {
|
||||
if (log_lvl) console.log(`TEST: handle_decrypt_string: ${passcode}`, encrypted_string);
|
||||
if (!encrypted_string) return '';
|
||||
if (!passcode) return false;
|
||||
|
||||
let decrypted_string = await ae_util.decrypt_wrapper(encrypted_string, passcode);
|
||||
if (log_lvl) console.log('TEST: Decrypted string:', decrypted_string);
|
||||
|
||||
return decrypted_string;
|
||||
}
|
||||
|
||||
// return new_string and cut_string
|
||||
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();
|
||||
}
|
||||
}
|
||||
async function handle_content_decryption() {
|
||||
if (!tmp_entry_obj.content) {
|
||||
await run_decryption_workflow();
|
||||
} else {
|
||||
// Re-lock logic
|
||||
is_processing = true;
|
||||
tmp_entry_obj.content = null;
|
||||
if (orig_entry_obj) orig_entry_obj.content = null;
|
||||
journals_sess.update(s => {
|
||||
s.journal_kv[$lq__journal_obj.id].journal_passcode_decrypted = false;
|
||||
return s;
|
||||
});
|
||||
is_processing = false;
|
||||
}
|
||||
return { left_over_string, cut_out_string };
|
||||
}
|
||||
|
||||
function handle_marked(text_string: string) {
|
||||
if (!text_string) return '';
|
||||
|
||||
let cleaned_string = text_string.replace(/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/, '');
|
||||
return marked.parse(cleaned_string);
|
||||
function handle_marked(text: string) {
|
||||
if (!text) return '';
|
||||
return marked.parse(text.replace(/^[]/, ''));
|
||||
}
|
||||
|
||||
let slct_hosted_file_kv: key_val = $state({});
|
||||
let slct_hosted_file_id: any = $state(null);
|
||||
let slct_hosted_file_obj: any = $state(null);
|
||||
$effect(() => {
|
||||
if (slct_hosted_file_id && slct_hosted_file_obj) {
|
||||
console.log(`*** handle_hosted_file_selected() *** ${slct_hosted_file_id}`);
|
||||
// We need to update the journal_entry_obj with the new file (for now just the first one).
|
||||
async function change_journal_id() {
|
||||
if (!$ae_loc.trusted_access) return;
|
||||
await journals_func.update_ae_obj__journal_entry({
|
||||
api_cfg: $ae_api,
|
||||
journal_entry_id: $lq__journal_entry_obj?.journal_entry_id,
|
||||
data_kv: { journal_id_random: tmp_entry_obj.journal_id }
|
||||
});
|
||||
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 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>
|
||||
|
||||
<section
|
||||
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}
|
||||
>
|
||||
<section class="ae_view flex flex-col gap-2 w-full h-full p-1" bind:clientHeight={$ae_loc.iframe_height_modal_body}>
|
||||
{#if $lq__journal_entry_obj}
|
||||
<JournalEntry_Header
|
||||
entry={$lq__journal_entry_obj}
|
||||
journal={$lq__journal_obj}
|
||||
journals_li={$lq__journal_obj_li}
|
||||
bind:tmp_entry_obj
|
||||
has_changed={tmp_entry_obj_changed}
|
||||
has_changed={has_unsaved_changes}
|
||||
onSave={update_journal_entry}
|
||||
onDecrypt={handle_content_decryption}
|
||||
onDecryptHistory={handle_history_decryption}
|
||||
onDecryptHistory={() => {}}
|
||||
onChangeJournal={change_journal_id}
|
||||
onAppend={handle_append_start}
|
||||
onPrepend={handle_prepend_start}
|
||||
onAppend={() => { modal_mode = 'append'; show_append_modal = true; }}
|
||||
onPrepend={() => { modal_mode = 'prepend'; show_append_modal = true; }}
|
||||
{onShowExport}
|
||||
{save_status}
|
||||
{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
|
||||
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 -->
|
||||
<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'}>
|
||||
|
||||
<div class="absolute top-2 right-2 z-10">
|
||||
<AE_AITools
|
||||
content={tmp_entry_obj.content}
|
||||
bind:summary={tmp_entry_obj.summary}
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Main Content Editor/Viewer -->
|
||||
<JournalEntry_Editor
|
||||
entry={$lq__journal_entry_obj}
|
||||
journal={$lq__journal_obj}
|
||||
bind:tmp_entry_obj={tmp_entry_obj}
|
||||
bind:editorView={editorView}
|
||||
has_changed={tmp_entry_obj_changed}
|
||||
updated_idb={updated_idb}
|
||||
bind:tmp_entry_obj
|
||||
bind:editorView
|
||||
has_changed={has_unsaved_changes}
|
||||
updated_idb={false}
|
||||
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>
|
||||
|
||||
<AE_MetadataFooter obj={tmp_entry_obj} />
|
||||
@@ -643,17 +329,13 @@
|
||||
journal_config={$lq__journal_obj?.cfg_json}
|
||||
mode={modal_mode}
|
||||
onClose={() => (show_append_modal = false)}
|
||||
onUpdate={() => {
|
||||
show_append_modal = false;
|
||||
updated_obj = true; // Force refresh of the view
|
||||
}}
|
||||
onUpdate={() => { show_append_modal = false; updated_obj = true; }}
|
||||
{log_lvl}
|
||||
/>
|
||||
{:else}
|
||||
<section class="ae_meta flex flex-row flex-wrap gap-1 items-center justify-center w-full">
|
||||
<span class="text-lg text-orange-900 dark:text-orange-100">
|
||||
The Journal Entry was not found or is not available to show.
|
||||
</span>
|
||||
</section>
|
||||
<div class="p-20 text-center opacity-50 flex flex-col items-center gap-4">
|
||||
<Loader2 class="animate-spin" size="4em" />
|
||||
<span class="text-2xl font-bold">Loading Journal Entry...</span>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</section>
|
||||
@@ -68,7 +68,15 @@
|
||||
}
|
||||
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(
|
||||
() => {
|
||||
if (log_lvl) {
|
||||
@@ -78,12 +86,15 @@
|
||||
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].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
|
||||
); // 1 minutes
|
||||
// }, 1000 * $lq__journal_obj?.passcode_timeout); // 5 minutes
|
||||
timeout_ms
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user