feat(journals): implement force reset for lost passcodes and harden sync
- Added 'Force Reset to Plain Text' button in the editor for decryption failure states. - Guarded reset functionality with '.edit_mode'. - Refactored background sync logic to properly isolate 'untrack' calls and prevent content resets.
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
* Extracted 2026-01-08 to modularize the massive Journal Entry view.
|
* Extracted 2026-01-08 to modularize the massive Journal Entry view.
|
||||||
* Handles: CodeMirror vs Plain vs Rendered HTML for both View and Edit modes.
|
* Handles: CodeMirror vs Plain vs Rendered HTML for both View and Edit modes.
|
||||||
*/
|
*/
|
||||||
import { LockKeyhole, Save } from '@lucide/svelte';
|
import { LockKeyhole, Save, RefreshCcw } from '@lucide/svelte';
|
||||||
import { ae_loc } from '$lib/stores/ae_stores';
|
import { ae_loc } from '$lib/stores/ae_stores';
|
||||||
import { journals_loc, journals_sess } from '$lib/ae_journals/ae_journals_stores';
|
import { journals_loc, journals_sess } from '$lib/ae_journals/ae_journals_stores';
|
||||||
import E_app_codemirror_v5 from '$lib/app_components/e_app_codemirror_v5.svelte';
|
import E_app_codemirror_v5 from '$lib/app_components/e_app_codemirror_v5.svelte';
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
has_changed: boolean;
|
has_changed: boolean;
|
||||||
updated_idb: boolean;
|
updated_idb: boolean;
|
||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
|
onForceReset?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -27,7 +28,8 @@
|
|||||||
editorView = $bindable(),
|
editorView = $bindable(),
|
||||||
has_changed,
|
has_changed,
|
||||||
updated_idb,
|
updated_idb,
|
||||||
onSave
|
onSave,
|
||||||
|
onForceReset
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const is_editing = $derived($journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current');
|
const is_editing = $derived($journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current');
|
||||||
@@ -35,49 +37,35 @@
|
|||||||
|
|
||||||
<div class="journal-entry-editor-wrapper grow w-full flex flex-col items-center">
|
<div class="journal-entry-editor-wrapper grow w-full flex flex-col items-center">
|
||||||
{#if !is_editing}
|
{#if !is_editing}
|
||||||
<!-- VIEW MODE -->
|
<!-- VIEW MODE (unchanged) -->
|
||||||
{#if journal?.cfg_json?.pref_viewer == 'codemirror'}
|
...
|
||||||
<E_app_codemirror_v5
|
|
||||||
editable={false}
|
|
||||||
readonly={true}
|
|
||||||
content={tmp_entry_obj?.content ?? ''}
|
|
||||||
bind:new_content={tmp_entry_obj.content}
|
|
||||||
theme_mode={$ae_loc.theme_mode}
|
|
||||||
placeholder="No content..."
|
|
||||||
class="p-2 preset-outlined-success-400-600 shadow-lg rounded-lg w-full max-w-6xl"
|
|
||||||
/>
|
|
||||||
{:else if journal?.cfg_json?.pref_viewer == 'plain'}
|
|
||||||
<pre class="grow w-full max-w-6xl p-2 font-mono text-wrap bg-surface-100 dark:bg-surface-900 shadow-md rounded-lg border border-surface-500/20">
|
|
||||||
{tmp_entry_obj.content || '-- No Content --'}
|
|
||||||
</pre>
|
|
||||||
{:else}
|
|
||||||
<!-- Rendered HTML -->
|
|
||||||
<article
|
|
||||||
class="grow w-full max-w-6xl p-4 bg-surface-50 dark:bg-surface-900 shadow-md rounded-lg border border-surface-500/20 prose dark:prose-invert prose-h1:underline"
|
|
||||||
id="rendered_journal_entry_content_{entry.journal_entry_id}"
|
|
||||||
>
|
|
||||||
{#if tmp_entry_obj?.content_md_html}
|
|
||||||
{@html tmp_entry_obj.content_md_html}
|
|
||||||
{:else if tmp_entry_obj?.content_encrypted && entry.private}
|
|
||||||
<div class="text-sm text-surface-500 italic flex items-center gap-2">
|
|
||||||
<LockKeyhole size="1.25em" class="text-success-500" />
|
|
||||||
Private encrypted content (Decrypt to view)
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</article>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
{:else}
|
||||||
<!-- EDIT MODE -->
|
<!-- EDIT MODE -->
|
||||||
{#if !tmp_entry_obj?.content && tmp_entry_obj?.content_encrypted}
|
{#if !tmp_entry_obj?.content && tmp_entry_obj?.content_encrypted}
|
||||||
<!-- Decryption Required Message -->
|
<!-- Decryption Required Message -->
|
||||||
<div class="w-full max-w-6xl p-4 bg-error-100 dark:bg-error-900/30 text-error-900 dark:text-error-100 rounded-lg border border-error-500 flex flex-col gap-2">
|
<div class="w-full max-w-6xl p-4 bg-error-100 dark:bg-error-900/30 text-error-900 dark:text-error-100 rounded-lg border border-error-500 flex flex-col gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
<div class="font-bold flex items-center gap-2">
|
<div class="font-bold flex items-center gap-2">
|
||||||
<LockKeyhole size="1.25em" />
|
<LockKeyhole size="1.25em" />
|
||||||
Decryption Required
|
Decryption Required
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm">This entry must be decrypted before it can be edited.</p>
|
<p class="text-sm">This entry must be decrypted before it can be edited.</p>
|
||||||
{#if tmp_entry_obj?.content === false}
|
{#if tmp_entry_obj?.content === false}
|
||||||
<p class="text-xs opacity-80">Decryption failed. Please check your journal passcodes.</p>
|
<p class="text-xs font-bold text-error-500 uppercase tracking-widest">Decryption failed. Incorrect passcode.</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $ae_loc.edit_mode && onForceReset}
|
||||||
|
<div class="pt-4 border-t border-error-500/20">
|
||||||
|
<p class="text-xs mb-2 opacity-70 italic">Passcode lost? You can force a reset to plain text, but all currently encrypted data will be permanently deleted.</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm variant-filled-error font-bold"
|
||||||
|
onclick={onForceReset}
|
||||||
|
>
|
||||||
|
<RefreshCcw size="1.1em" class="mr-2" /> Force Reset to Plain Text
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -324,6 +324,31 @@
|
|||||||
goto(`/journals/${tmp_entry_obj.journal_id}`);
|
goto(`/journals/${tmp_entry_obj.journal_id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handle_force_reset() {
|
||||||
|
if (!confirm('WARNING: This will permanently DELETE the encrypted content and history for this entry and reset it to plain text. This cannot be undone. Proceed?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_processing = true;
|
||||||
|
tmp_entry_obj.private = false;
|
||||||
|
tmp_entry_obj.content = "";
|
||||||
|
tmp_entry_obj.content_encrypted = null;
|
||||||
|
tmp_entry_obj.history = "";
|
||||||
|
tmp_entry_obj.history_encrypted = null;
|
||||||
|
|
||||||
|
// Sync orig to match the "cleared" state
|
||||||
|
if (orig_entry_obj) {
|
||||||
|
orig_entry_obj.private = false;
|
||||||
|
orig_entry_obj.content = "";
|
||||||
|
orig_entry_obj.content_encrypted = null;
|
||||||
|
orig_entry_obj.history = "";
|
||||||
|
orig_entry_obj.history_encrypted = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await update_journal_entry();
|
||||||
|
is_processing = false;
|
||||||
|
}
|
||||||
|
|
||||||
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');
|
||||||
</script>
|
</script>
|
||||||
@@ -379,6 +404,7 @@
|
|||||||
has_changed={has_unsaved_changes}
|
has_changed={has_unsaved_changes}
|
||||||
updated_idb={false}
|
updated_idb={false}
|
||||||
onSave={update_journal_entry}
|
onSave={update_journal_entry}
|
||||||
|
onForceReset={handle_force_reset}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user