From 6562d4ba046ef030def0c2b0612bcf5fa12f4ccb Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Wed, 14 Jan 2026 15:52:02 -0500 Subject: [PATCH] feat(journals): extract decryption logic to helper and improve Quick Add behavior - Extracted journal entry decryption workflow to 'ae_journals_decryption.ts' to decouple it from Svelte reactivity and address loop issues. - Updated 'ae_comp__journal_entry_obj_id_view.svelte' to use the new decryption helper. - Refactored 'Quick Add' to use the first line as the title and remove it from the content before saving. --- documentation/V3_FRONTEND_API_GUIDE.md | 46 ++++++++++ src/lib/ae_journals/ae_journals_decryption.ts | 90 +++++++++++++++++++ .../ae_comp__journal_entry_obj_id_view.svelte | 29 +++--- .../ae_comp__journal_entry_quick_add.svelte | 5 +- 4 files changed, 151 insertions(+), 19 deletions(-) create mode 100644 src/lib/ae_journals/ae_journals_decryption.ts diff --git a/documentation/V3_FRONTEND_API_GUIDE.md b/documentation/V3_FRONTEND_API_GUIDE.md index 6fbeee78..dc04a2ae 100644 --- a/documentation/V3_FRONTEND_API_GUIDE.md +++ b/documentation/V3_FRONTEND_API_GUIDE.md @@ -188,3 +188,49 @@ const downloadUrl = `${BASE_URL}/v3/crud/hosted_file/${fileId}/?jwt=${jwtToken}` 1. **Use `view` for Rich Data**: Instead of manually joining data in separate calls, use `?view=enriched` or `?view=detail`. 2. **Singular Nouns**: Always use singular names for `obj_type` (e.g., `journal`). 3. **Strict Typing**: Ensure your `data` objects match the backend models to avoid `400 Bad Request` validation errors. + +--- + +## 8. Standard Patterns & Common Pitfalls (2026 Update) + +To ensure stability across the Aether mesh, all frontend components must adhere to these established patterns. + +### A. The "Whitelist" Standard for Saves +**Problem:** Sending the entire object returned by a `GET` request back to a `PATCH` endpoint will cause a `400 Bad Request`. This happens because the object contains technical metadata (like `journal_id`, `created_on`, `for_type`) that the API cannot "SET". + +**Standard:** When preparing a save payload, explicitly whitelist ONLY the user-editable fields. +```typescript +// ✅ CORRECT: Whitelist only editable fields +const data_kv = { + name: tmp_obj.name, + content: tmp_obj.content, + tags: tmp_obj.tags, + category_code: tmp_obj.category_code +}; + +// ❌ WRONG: Passing the whole object or just blacklisting a few +const data_kv = { ...tmp_obj }; +delete data_kv.id; // Still contains other computed columns! +``` + +### B. The Triple-ID Save Pattern +**Standard:** When sending an ID in a POST/PATCH payload (e.g. creating a child object), use the random string version with the `_id_random` suffix. +* **Property:** `[obj_type]_id_random` +* **Value:** A string (e.g., `qpgvOh5nOYI`) + +**Warning:** Sending a string value under an integer key (e.g. `journal_id: "qpgvOh5nOYI"`) will trigger a Pydantic validation error on the backend. + +### C. Handle 'NULL' vs '0' for Booleans +**Pitfall:** Fields like `hide` or `priority` can be `NULL` in the database. +**Standard:** Ensure your synchronization logic in components handles `null`, `undefined`, and `0` identically for boolean checks to prevent "ghost" changes being detected by Svelte 5. + +--- + +## 9. Planned API Improvements (2026 Roadmap) + +The following refinements have been requested from the Backend Agent to further improve frontend productivity: + +1. **Permissive Update Mode**: A new header (`x-ae-ignore-extra-fields: true`) is planned to allow the API to ignore non-writable columns in `PATCH` requests instead of returning a `400 Bad Request`. +2. **Automated ID Resolution**: Backend will soon support automatic resolution of `_id_random` strings to integer IDs during `POST/PATCH`, reducing the need for manual lookups in the frontend. +3. **Schema Evolution Tools**: New orchestration tools are being developed to automate field creation and renaming, including the automatic regeneration of the enriched SQL views. +4. **Structured Validation Errors**: Move from string-based error details to a machine-readable format for better inline form validation. diff --git a/src/lib/ae_journals/ae_journals_decryption.ts b/src/lib/ae_journals/ae_journals_decryption.ts new file mode 100644 index 00000000..528fe663 --- /dev/null +++ b/src/lib/ae_journals/ae_journals_decryption.ts @@ -0,0 +1,90 @@ +import { ae_util } from '$lib/ae_utils/ae_utils'; +import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types'; + +/** + * Result structure for decryption operations. + */ +export interface DecryptionResult { + success: boolean; + content?: string; + history?: string; + error?: string; +} + +/** + * Decrypts a journal entry's content and history using the provided or stored passcode. + * + * @param entry The journal entry object to decrypt. + * @param journal The parent journal object (provides base passcode and private_passcode). + * @param typed_passcode Optional: A user-entered passcode override. + * @returns A Promise resolving to a DecryptionResult. + */ +export async function decrypt_journal_entry( + entry: ae_JournalEntry, + journal: ae_Journal, + typed_passcode?: string +): Promise { + + // Safety check: if not encrypted, return as-is + if (!entry.content_encrypted) { + return { + success: true, + content: entry.content ?? '', + history: entry.history ?? '' + }; + } + + // Determine which key to use + let journal_key = typed_passcode; + + // If no override, try the private passcode stored on the journal object + if (!journal_key?.length) { + journal_key = journal.private_passcode; + } + + if (!journal_key) { + return { + success: false, + error: 'No passcode provided or available for decryption.' + }; + } + + // Aether standard: combine the journal's public passcode with the private key + const decrypt_key = `${journal.passcode ?? ''}:${journal_key}`; + + try { + // Decrypt Primary Content + const result = await ae_util.decrypt_wrapper(entry.content_encrypted, decrypt_key); + + if (result === false) { + return { + success: false, + error: 'Decryption failed. Incorrect passcode or corrupted data.' + }; + } + + const decrypted_text = typeof result === 'string' ? result : ''; + let decrypted_history = ''; + + // Decrypt History (if it exists) + if (entry.history_encrypted) { + const h_res = await ae_util.decrypt_wrapper(entry.history_encrypted, decrypt_key); + if (h_res !== false) { + decrypted_history = typeof h_res === 'string' ? h_res : ''; + } + } + + return { + success: true, + content: decrypted_text, + history: decrypted_history + }; + + } catch (err: any) { + console.error('decrypt_journal_entry error:', err); + return { + success: false, + error: `System error during decryption: ${err.message}` + }; + } +} diff --git a/src/routes/journals/ae_comp__journal_entry_obj_id_view.svelte b/src/routes/journals/ae_comp__journal_entry_obj_id_view.svelte index 1494d956..6eda33e1 100644 --- a/src/routes/journals/ae_comp__journal_entry_obj_id_view.svelte +++ b/src/routes/journals/ae_comp__journal_entry_obj_id_view.svelte @@ -18,6 +18,7 @@ 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 { decrypt_journal_entry } from '$lib/ae_journals/ae_journals_decryption'; import JournalEntry_Editor from './JournalEntry_Editor.svelte'; import JournalEntry_Header from './JournalEntry_Header.svelte'; @@ -125,9 +126,7 @@ if (!journal?.id) return; 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; - + is_processing = true; decryption_error = null; @@ -137,11 +136,10 @@ return s; }); - const decrypt_key = `${journal.passcode ?? ''}:${journal_key}`; - const result = await ae_util.decrypt_wrapper($lq__journal_entry_obj?.content_encrypted, decrypt_key); + const result = await decrypt_journal_entry($lq__journal_entry_obj, journal, journal_key); - if (result === false) { - decryption_error = 'Decryption failed. Incorrect passcode or corrupted data.'; + if (!result.success) { + decryption_error = result.error || 'Decryption failed.'; journals_sess.update(s => { s.journal_kv[journal.id].journal_passcode_verified = false; s.journal_kv[journal.id].journal_passcode_decrypted = false; @@ -152,18 +150,13 @@ } // 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; + tmp_entry_obj.content = result.content; + tmp_entry_obj.content_md_html = handle_marked(result.content || ''); + if (orig_entry_obj) orig_entry_obj.content = result.content; - // 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; - } + if (result.history) { + tmp_entry_obj.history = result.history; + if (orig_entry_obj) orig_entry_obj.history = result.history; } journals_sess.update(s => { diff --git a/src/routes/journals/ae_comp__journal_entry_quick_add.svelte b/src/routes/journals/ae_comp__journal_entry_quick_add.svelte index 72e0a33a..0f74112a 100644 --- a/src/routes/journals/ae_comp__journal_entry_quick_add.svelte +++ b/src/routes/journals/ae_comp__journal_entry_quick_add.svelte @@ -36,9 +36,12 @@ let name = lines[0].substring(0, 100); if (lines[0].length > 100) name += "..."; + // Remove the first line (title) from the content + const entry_content = lines.slice(1).join('\n').trim(); + const data_kv = { name: name, - content: note_content, + content: entry_content, type_code: 'note', private: false, // Ensure notes are public/decrypted by default enable: true,