10 KiB
Aether Journals UI Update (2026)
Status: 🚧 Phase 4 Active (Security/Encryption Blockers remain; Style pass complete) Last Updated: 2026-03-06 Primary Agent: Frontend SvelteKit Agent
1. Project Overview
This document outlines the modernization of the Journals module UI in the SvelteKit frontend (aether_app_sveltekit). The primary goals are to fully leverage the generic V3 API architecture and introduce high-velocity productivity features for journal management.
Context: The backend transition to the generic api_crud router is complete. Custom legacy routers have been removed. The frontend must now fully align with this pattern and provide a frictionless user experience.
2. Core Objectives
🎯 Primary Goals
- V3 API Verification: Ensure all CRUD operations utilize the generic
api_crudendpoints (Verified). - Quick Add UI: Implement a specialized interface for rapid, friction-free entry creation.
- Append/Prepend UI: Allow users to quickly add text to the beginning or end of existing entries without full edit mode.
- Interop & Portability: Robust import/export logic for Markdown/HTML (Nextcloud Notes compatibility).
- Security Hardening: Review and harden client-side encryption logic (BLOCKED).
3. Technical Architecture
Backend (Completed)
- Router:
api_crud(Generic) - Definitions:
app/ae_obj_types_def.py->app/object_definitions/journals.py - Endpoints:
/v3/crud/journal/...and/v3/crud/journal_entry/...
Frontend (In Progress)
- State Management:
src/lib/ae_journals/ae_journals_stores.ts - Local Storage: Dexie.js (
db_journals) - API Client:
src/lib/api/api.ts->get_ae_obj - Export Engine: Centralized templates in
src/lib/ae_journals/ae_journals_export_templates.ts.
4. Feature Specifications
⚡ Quick Add (Complete)
- Component:
src/routes/journals/ae_comp__journal_entry_quick_add.svelte - Behavior: Creates a new
journal_entryattached to the active journal without leaving the list view.
📝 Append / Prepend (Complete)
- Interaction: Fast text injection via
AeCompModalJournalEntryAppend. - Logic: Updates entry content without full editor state overhead.
🔄 Interop (Markdown/HTML) (Complete)
- Goal: Bulk export/import for data portability.
- Templates: Standard, Personal Log, Amazon Vine.
5. Implementation Plan
Phase 1: Foundation (Done)
- Backend cleanup (remove legacy routers).
- Verify frontend uses V3 API (
ae_journals__journal.ts).
Phase 2: Rapid Entry (Complete)
- Create
ae_comp__journal_entry_quick_add.svelte. - Integrate Quick Add into
+page.svelte.
Phase 3: Content Manipulation & Portability (Complete)
- Implement Append/Prepend logic.
- Implement Bulk Export/Import system.
- Establish centralized Export Template engine.
Phase 4: Polish & Security (ACTIVE)
- Implement Auto-Save toggle and visual status indicators.
- Extract decryption workflow to non-reactive helper.
- Standardize Configuration Modals: Refactored Module, Journal, and Entry configuration into a unified tabbed UI.
- RESOLVED: Decryption workflow stability (Fixed via dependency isolation).
- Style Standardization (2026-03-06): Full Skeleton v4
preset-*class pass across all 17 journal components. See style token table in Lessons Learned below. - Dark mode fixes: Entry content hover, journal view section/description background and text colors.
- Modal close button: All 3 config modals use
dismissable={false}+ explicit<X>button in header snippet for correct right-aligned placement. - Global select padding: Added
padding-inline: 0.5remto@layer baseinapp.css(safe — utilitypx-*classes override it where intentional). - Solidify E2EE passcode system for Journals and Entries.
- Audit encryption flow for Quick Added and Imported entries.
- Integrate Outbound Email sharing.
🧠 Lessons Learned: Solving the Svelte 5 Reactivity Hang
During the implementation of the Privacy/Decryption toggle and the new Configuration Modals, we encountered critical browser hangs caused by infinite reactivity loops. Here is how we resolved them:
1. Rigorous Dependency Isolation (untrack)
Svelte 5 runes ($effect, $derived) automatically track every reactive variable read inside them.
- The Problem: An effect would read
save_statusortmp_entry_obj.contentto decide if it should sync, but the act of syncing would update those same variables, re-triggering the effect. - The Fix: Wrap any "check-only" state or store reads in
untrack(() => ... ). This allows the effect to use the value without becoming a dependency of it. This is CRITICAL when initializing local state from props inside an effect.
2. Standardized Modal UI ("Aether Orange") & Style Token Conventions
We have established a unified design language for configuration interfaces and all Journals UI. Use these as the module template.
Modal Chrome
- Header/Footer:
bg-orange-100 dark:bg-orange-900with consistent orange borders. - Close button: Always use
dismissable={false}on the<Modal>and add an explicit<button>with<X>inside the{#snippet header()}so placement is fully in our control. Theflex-1class on the<h3>pushes it right. - Tabs: Center-aligned
btn btn-smwithpreset-filled-primary(active) /preset-tonal-surface(inactive). - Icons: Every tab and primary action should have a Lucide icon for better scannability.
- Explicit Persistence: Follow "Edit working copy → Save Changes" pattern to prevent accidental store/API churn.
Skeleton v4 Style Token Reference (Journals = canonical example)
| Intent | Class |
|---|---|
| Primary CTA (save, create, open) | btn preset-filled-primary |
| Neutral / cancel / close | btn preset-tonal-surface |
| Secondary action | btn preset-tonal-secondary |
| Success (confirmed save) | btn preset-filled-success |
| Warning (caution action) | btn preset-tonal-warning hover:preset-filled-warning-500 |
| Error / danger (delete, force reset) | btn preset-tonal-error hover:preset-filled-error-500 |
| Active tab | preset-filled-primary |
| Inactive tab | preset-tonal-surface |
| Icon button | btn-icon btn-icon-sm preset-tonal-surface |
| Input (base) | input |
| Input (small) | input input-sm |
| Select (base) | select |
| Select (small) | select select-sm |
| Textarea | textarea |
| Badge (info/neutral) | badge preset-tonal-surface |
| Badge (success) | badge preset-tonal-success |
| Badge (error) | badge preset-tonal-error |
Removed Patterns (never use)
- All
variant-*classes are now fully removed from the codebase. Use onlypreset-*classes for all buttons and interactive elements. variant-form-material— Skeleton v2, removed from all inputs/selects/textareasinput-bordered— non-standard, removed- DaisyUI
modal/modal-box/modal-actionwrapper divs inside Flowbite<Modal>— removed
Dark Mode Rules
- Any
bg-{color}-100dynamic background must have adark:bg-gray-800(or similar) override — light shades are unreadable in dark mode. - Hover states on content areas need both light and dark variants:
hover:bg-blue-100 dark:hover:bg-blue-950. - Text locked to
dark:text-gray-900is almost always wrong — usedark:text-gray-100.
3. Dexie LiveQuery Subscriptions
- The Problem: Accessing
liveQueryobservables directly in templates results in[object Object]orundefinedproperty errors. - The Mandate: ALWAYS use the
$prefix (e.g.$lq__obj) when passing or using data from a DexieliveQuery.
4. Manual Deep Copying vs. Proxies
Svelte 5 state is backed by Proxies.
- The Problem: Using
JSON.parse(JSON.stringify(proxy))can sometimes trigger unexpected behavior or loops when used inside a reactive context. - The Fix: Implement a manual
deep_copyhelper or selective property assignment when syncing "Original" vs "Temporary" state. This ensuresorig_entry_objis a plain JS object, making thehas_unsaved_changescheck stable.
3. Concurrency Locking (is_processing)
- The Problem: Decryption (Async) and Auto-Save (Debounced Async) can fire nearly simultaneously.
- The Fix: Use a simple
is_processingboolean flag. If any async workflow is active, block others from starting and prevent thehas_unsaved_changesderived rune from reportingtrue.
4. Comparison Normalization
- The Problem: Trivial differences (e.g.,
nullvs""or trailing whitespace) would trigger "unsaved changes" and fire the save loop. - The Fix: Use a
normalize()function in thehas_unsaved_changesderived rune to trim strings and treatnull/undefinedas empty strings during comparison.
⚠️ Technical Blocker: The "Decryption-Sync" Loop (Resolved)
The Issue
The component is suffering from a Reactive Feedback Loop between decryption, auto-save, and background IDB refreshes.
- Decrypting content triggers a change in
tmp_entry_obj.content. - The Auto-Save effect sees this as a manual user edit and saves to the database.
- Dexie LiveQuery detects the DB update and refreshes the object.
- The Sync effect resets the entry to its encrypted state (from DB).
- The Auto-Decryption effect fires again, starting the loop over.
What we tried:
is_processingflags: Attempted to block reactivity during decryption.untrack(): Attempted to isolate store updates.- Reference Sync: Attempted to update
orig_entry_objsimultaneously with decryption to fool the change detector. - Direct Store Updates: Switching from property assignment to
journals_sess.update()to fix Svelte 5 notification failures.
Future Fix Ideas:
- Native Svelte 5 State: Refactor
journals_sessfrom a Svelte 4Writableto a class using$state. - Logic Extraction: Move decryption logic into a non-reactive class/helper to isolate side effects from the UI render cycle.
- Hash Comparison: Use content hashes for change detection instead of string comparisons to avoid whitespace/normalization loops.