feat(journals): stabilize V3 API integration and restore cryptographic toggles
This commit is contained in:
@@ -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>
|
||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user